diff --git a/Cargo.toml b/Cargo.toml index c0425a6..a1423e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,5 +5,6 @@ edition = "2021" [dependencies] binparse_derive = { path = "../binparse_derive" } +maud = "0.26.0" memmap2 = "0.7.1" rouille = "3.6.2" diff --git a/src/lib.rs b/src/lib.rs index 1564679..9a186b6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ pub mod btrfs_lookup; pub mod addrmap; pub mod nodereader; pub mod http_tree; +pub mod render_common; pub mod render_tree; pub mod main_error; diff --git a/src/main.rs b/src/main.rs index d41f10f..d183d47 100644 --- a/src/main.rs +++ b/src/main.rs @@ -272,31 +272,3 @@ fn http_main_boxes(image: &[u8], _req: &Request) -> Response { Response::html(result) } - -/* -fn html_item_row(item: &Item, index: usize, tree_id: u64, highlight: bool, extend: bool) -> String { - let value_string = item_value_string(tree_id, item.key, &item.value); - let raw_string = format!("{:#?}", item); - let even = if index % 2 == 0 { "even" } else { "odd" }; - let id_desc = row_id_desc(item.key, tree_id); - format!("{}{}{}{}\n{}\n", - id_desc.0, id_desc.1, id_desc.2, &value_string, &raw_string) -} - -fn html_item_row_highlighted(key: Key, value: String, _index: usize, tree_id: u64) -> String { - let id_desc = row_id_desc(key, tree_id); - format!("{}{}{}{}\n", - id_desc.0, id_desc.1, id_desc.2, value) -} - -fn row_id_desc(key: Key, tree_id: u64) -> (String, String, String) { - let x = format!("{:X}", key.key_id); - let y = format!("{:?}", key.key_type); - let z = if key.key_type == ItemType::RootRef || key.key_type == ItemType::Ref { - format!("{:X}", tree_id, key.key_offset, key.key_offset) - } else { - format!("{:X}", key.key_offset) - }; - (x,y,z) -} -*/ diff --git a/src/render_common.rs b/src/render_common.rs new file mode 100644 index 0000000..d36c425 --- /dev/null +++ b/src/render_common.rs @@ -0,0 +1,38 @@ +use maud::Render; +use std::fmt::{Debug, UpperHex}; + +pub struct DebugRender(pub T); + +impl Render for DebugRender { + fn render_to(&self, w: &mut String) { + format_args!("{0:#?}", self.0).render_to(w); + } +} + +pub struct Hex(pub T); + +impl Render for Hex { + fn render_to(&self, w: &mut String) { + format_args!("{0:X}", self.0).render_to(w); + } +} + +pub fn size_name(x: u64) -> String { + if x == 0 { + format!("0 B") + } else if x % (1<<10) != 0 { + format!("{} B", x) + } else if x % (1<<20) != 0 { + format!("{} KiB", x / (1<<10)) + } else if x % (1<<30) != 0 { + format!("{} MiB", x / (1<<20)) + } else if x % (1<<40) != 0 { + format!("{} GiB", x / (1<<30)) + } else if x % (1<<50) != 0 { + format!("{} TiB", x / (1<<40)) + } else if x % (1<<60) != 0 { + format!("{} PiB", x / (1<<50)) + } else { + format!("{} EiB", x / (1<<60)) + } +} diff --git a/src/render_tree.rs b/src/render_tree.rs index b849780..6c2f206 100644 --- a/src/render_tree.rs +++ b/src/render_tree.rs @@ -1,4 +1,6 @@ use crate::btrfs_structs::{Item, Key, ItemType, Value, ExtentDataBody}; +use crate::render_common::{DebugRender, Hex, size_name}; +use maud::{Markup, html, DOCTYPE, PreEscaped}; #[derive(Debug)] pub struct TableResult<'a> { @@ -10,92 +12,124 @@ pub struct TableResult<'a> { pub last_key: Key, } -static HTML_HEADER: &str = r###" - - - - - -"###; +pub fn render_table(table: TableResult) -> Markup { -static HTML_FOOTER: &str = r###" - - - -"###; - -pub fn render_table(table: TableResult) -> String { - let mut result = String::new(); - - result.push_str(HTML_HEADER); - - // header - if let Some(desc) = table.tree_desc { - result.push_str(&format!("

Tree {} ({})

", table.tree_id, desc)); + let header: String = if let Some(desc) = table.tree_desc { + format!("Tree {} ({})", table.tree_id, desc) } else { - result.push_str(&format!("

Tree {}

", table.tree_id)); - } + format!("Tree {}", table.tree_id) + }; - // link to root tree - if table.tree_id != 1 { - result.push_str(&format!("go back to root tree")); - } - - // search field let key_input_value = table.key_id.map_or(String::new(), |x| format!("{:X}", x)); - result.push_str(&format!("
\n", table.tree_id)); - result.push_str(&format!("\n", key_input_value)); - result.push_str("\n"); - result.push_str("
\n"); - // navigation links - // technically, adding one to offset in "next" is not correct if offset is -1 - // it would show an entry twice in that case, but who cares, that never actually happens anyway - let first = table.first_key; - let last = table.last_key; - result.push_str(&format!("first\n", - table.tree_id)); - result.push_str(&format!("prev\n", - table.tree_id, first.key_id, u8::from(first.key_type), first.key_offset)); - result.push_str(&format!("next\n", - table.tree_id, last.key_id, u8::from(last.key_type), last.key_offset)); - result.push_str(&format!("last\n", - table.tree_id, u64::wrapping_sub(0,1), u8::wrapping_sub(0,1), u64::wrapping_sub(0,1))); + let first_key_url = format!("/tree/{}", + table.tree_id); + let prev_key_url = format!("/tree/{}/to/{:016X}-{:02X}-{:016X}", + table.tree_id, + table.first_key.key_id, + u8::from(table.first_key.key_type), + table.first_key.key_offset); + let next_key_url = format!("/tree/{}/from/{:016X}-{:02X}-{:016X}", + table.tree_id, + table.last_key.key_id, + u8::from(table.last_key.key_type), + table.first_key.key_offset); + let last_key_url = format!("/tree/{}/to/{:016X}-{:02X}-{:016X}", + table.tree_id, + u64::wrapping_sub(0,1), + u8::wrapping_sub(0,1), + u64::wrapping_sub(0,1)); - // the actual table - result.push_str("\n"); - for (idx, (it, _it_data)) in table.items.iter().enumerate() { - let highlighted = table.key_id.filter(|x|*x == it.key.key_id).is_some(); - let value_string = item_value_string(table.tree_id, it.key, &it.value); - let raw_string = format!("{:#?}", it); + let mut rows: Vec = Vec::new(); + + for &(it, _it_data) in table.items.iter() { + let highlighted = if table.key_id.filter(|x|*x == it.key.key_id).is_some() { "highlight" } else { "" }; + let value_string = item_value_string(table.tree_id, it); + let details_string = item_details_string(table.tree_id, it); + let raw_string = format!("{:#?}", &it.value); let id_desc = row_id_desc(it.key, table.tree_id); - let key_type_class = match it.key.key_type { - ItemType::Inode => "inode", - ItemType::Ref => "ref", - ItemType::RootRef => "ref", - ItemType::RootBackRef => "ref", - ItemType::ExtentData => "extent", - ItemType::Dir => "dir", - ItemType::DirIndex => "dir", - ItemType::Root => "root", - _ => "", - }; - let row = format!("{}{}{}{}
{}
\n", - if highlighted { " class = \"highlight\"" } else { "" }, - id_desc.0, id_desc.1, id_desc.2, &value_string, &raw_string); + rows.push(html! { + details.item.(highlighted) { + summary { + span.key.key_id.(key_type_class(it.key)) { + (id_desc.0) + } + span.key.key_type.(key_type_class(it.key)) { + (id_desc.1) + } + span.key.key_offset.(key_type_class(it.key)) { + (id_desc.2) + } + span.itemvalue.(key_type_class(it.key)) { + (&value_string) + } + } + div.details { + (&details_string) - result.push_str(&row); + details { + summary { + "show full value" + } + pre { + (&raw_string) + } + } + } + } + }); } - result.push_str("
\n"); - result.push_str(HTML_FOOTER); + // the complete page + html! { + (DOCTYPE) + head { + link rel="stylesheet" href="/style.css"; + } + body { + h1 { + (header) + } - result + @if table.tree_id != 1 { + a href="/tree/1" { + "go back to root tree" + } + } + + form method="get" action={"/tree/" (table.tree_id)} { + input type="text" name="key" value=(key_input_value); + input type="submit" value="Search"; + } + + a.nav href=(first_key_url) { div.nav { "first" } } + a.nav href=(prev_key_url) { div.nav { "prev" } } + + @for row in &rows { (row) } + + a.nav href=(next_key_url) { div.nav { "next" } } + a.nav href=(last_key_url) { div.nav { "last" } } + } + } } -fn row_id_desc(key: Key, tree_id: u64) -> (String, String, String) { +fn key_type_class(key: Key) -> &'static str { + match key.key_type { + ItemType::Inode => "inode", + ItemType::Ref => "ref", + ItemType::RootRef => "ref", + ItemType::RootBackRef => "ref", + ItemType::ExtentData => "extent", + ItemType::Dir => "dir", + ItemType::DirIndex => "dir", + ItemType::Root => "root", + _ => "", + } +} + +fn row_id_desc(key: Key, tree_id: u64) -> (Markup, Markup, Markup) { let x = format!("{:X}", key.key_id); let y = format!("{:?} ({:02X})", key.key_type, u8::from(key.key_type)); let z = if key.key_type == ItemType::RootRef || key.key_type == ItemType::Ref { @@ -103,59 +137,131 @@ fn row_id_desc(key: Key, tree_id: u64) -> (String, String, String) { } else { format!("{:X}", key.key_offset) }; - (x,y,z) + (PreEscaped(x),PreEscaped(y),PreEscaped(z)) } -fn item_value_string(tree_id: u64, key: Key, val: &Value) -> String { - match val { +fn item_value_string(tree_id: u64, item: &Item) -> Markup { + match &item.value { Value::Root(_) => { - format!("go to tree {}", key.key_id, key.key_id) + html! { a href={"/tree/" (item.key.key_id)} { "go to tree " (item.key.key_id) } } }, - Value::Dir(dir_item) => - format!("{:?} @ {:X}", - &dir_item.name, - tree_id, - dir_item.location.key_id, - dir_item.location.key_id), - Value::DirIndex(dir_item) => - format!("{:?} @ {:X}", - &dir_item.name, - tree_id, - dir_item.location.key_id, - dir_item.location.key_id), - Value::Inode(inode_item) => - String::new(), -/* format!("mode: {:o}, ctime: {}, mtime: {}, otime: {}", - inode_item.mode, - inode_item.ctime.sec, - inode_item.mtime.sec, - inode_item.otime.sec), */ + Value::Dir(dir_item) | Value::DirIndex(dir_item) => { + let name = format!("{:?}", &dir_item.name); + let id = dir_item.location.key_id; + html! { + (name) + " @ " + a href=(format!("/tree/{tree_id}/{id:x}")) { + (Hex(id)) + } + } + }, + Value::Inode(_) => + html! { "" }, Value::ExtentData(extent_data_item) => match &extent_data_item.data { - ExtentDataBody::Inline(data) => format!("inline, length {}", data.len()), + ExtentDataBody::Inline(data) => + PreEscaped(format!("inline, length {}", size_name(data.len() as u64))), ExtentDataBody::External(ext_extent) => - format!("external, length {}", - ext_extent.num_bytes), + PreEscaped(format!("external, length {}", size_name(ext_extent.num_bytes))), }, Value::Ref(ref_item) => - format!("{:?}", &ref_item.name), + html! { (format!("{:?}", &ref_item.name)) }, Value::Extent(extent_item) => - format!("flags: {}, block_refs: {:?}", extent_item.flags, extent_item.block_refs), + PreEscaped(format!("flags: {}, block_refs: {:?}", extent_item.flags, extent_item.block_refs)), Value::BlockGroup(blockgroup_item) => - format!("{} bytes used", blockgroup_item.used), + PreEscaped(format!("{} used", size_name(blockgroup_item.used))), Value::DevExtent(dev_extent_item) => - format!("chunk_tree: {}, chunk_offset: {:x}, length: {} bytes", dev_extent_item.chunk_tree, dev_extent_item.chunk_offset, dev_extent_item.length), + PreEscaped(format!("chunk_tree: {}, chunk_offset: {:x}, length: {}", dev_extent_item.chunk_tree, dev_extent_item.chunk_offset, size_name(dev_extent_item.length))), Value::UUIDSubvol(uuid_subvol_item) => - format!("subvolume id: {}", uuid_subvol_item.subvol_id), + PreEscaped(format!("subvolume id: {}", uuid_subvol_item.subvol_id)), Value::FreeSpaceInfo(free_space_info) => - format!("extent_count: {}, flags: {}", free_space_info.extent_count, free_space_info.flags), + PreEscaped(format!("extent_count: {}, flags: {}", free_space_info.extent_count, free_space_info.flags)), Value::Dev(dev_item) => - format!("total_bytes: {}", dev_item.total_bytes), + PreEscaped(format!("total_bytes: {}", size_name(dev_item.total_bytes))), Value::Chunk(chunk_item) => - format!("size: {}", chunk_item.size), + PreEscaped(format!("size: {}", chunk_item.size)), _ => { - println!("{:?} {:?}", key, val); - String::new() +// println!("{:?} {:?}", item.key, item.valu); + PreEscaped(String::new()) + }, + } +} + +fn item_details_string(tree_id: u64, item: &Item) -> Markup { + match &item.value { + Value::Inode(inode_item) => { + html! { table { tbody { + tr { td { "size" } td { (inode_item.size) } } + tr { td { "mode" } td { (inode_item.mode) } } + tr { td { "uid" } td { (inode_item.uid) } } + tr { td { "gid" } td { (inode_item.gid) } } + tr { td { "nlink" } td { (inode_item.nlink) } } + tr { td { "atime" } td { (inode_item.atime.sec) } } + tr { td { "ctime" } td { (inode_item.ctime.sec) } } + tr { td { "mtime" } td { (inode_item.mtime.sec) } } + tr { td { "otime" } td { (inode_item.otime.sec) } } + }}} + }, + Value::ExtentData(extent_item) => { + match &extent_item.data { + ExtentDataBody::Inline(data) => { + html! {} // we really want data as string / hex + }, + ExtentDataBody::External(ext_extent) => { + html! { + p { + @if ext_extent.disk_bytenr == 0 { + (size_name(ext_extent.num_bytes)) " of zeros." + } @ else { + (format!("{} on disk, starting at offset {:X} within the extent at address {:X}; {} in the file starting from offset {:X}.", size_name(ext_extent.disk_num_bytes), ext_extent.offset, ext_extent.disk_bytenr, size_name(ext_extent.num_bytes), item.key.key_offset)) + } + } + table { tbody { + tr { td { "compression" } td { (extent_item.header.compression) } } + tr { td { "encryption" } td { (extent_item.header.encryption) } } + tr { td { "other_encoding" } td { (extent_item.header.other_encoding) } } + }} + } + }, + } + }, + Value::Ref(ref_item) => { + html! { table { tbody { + tr { td { "name" } td { (format!("{:?}", ref_item.name)) } } + tr { td { "index" } td { (ref_item.index) } } + }}} + }, + Value::Dir(dir_item) | Value::DirIndex(dir_item) => { + html! { table { tbody { + tr { td { "name" } td { (format!("{:?}", dir_item.name)) } } + }}} + }, + Value::Root(root_item) => { + let inode = &root_item.inode; + html! { table { tbody { + tr { td { "root dir id" } td { (format!("{:X}", root_item.root_dirid)) } } + tr { td { "logical address" } td { (format!("{:X}", root_item.bytenr)) } } + tr { td { "bytes used" } td { (size_name(root_item.bytes_used)) } } + tr { td { "last snapshot" } td { (root_item.last_snapshot) } } + tr { td { "flags" } td { (root_item.flags) } } + tr { td { "refs" } td { (root_item.refs) } } + tr { td { "level" } td { (root_item.level) } } + tr { td { "UUID" } td { (format!("{:?}", root_item.uuid)) } } + tr { td { "parent UUID" } td { (format!("{:?}", root_item.parent_uuid)) } } + tr { td { "received UUID" } td { (format!("{:?}", root_item.received_uuid)) } } + tr { td { "ctransid" } td { (root_item.ctransid) } } + tr { td { "otransid" } td { (root_item.otransid) } } + tr { td { "stransid" } td { (root_item.stransid) } } + tr { td { "rtransid" } td { (root_item.rtransid) } } + tr { td { "ctime" } td { (root_item.ctime.sec) } } + tr { td { "otime" } td { (root_item.otime.sec) } } + tr { td { "stime" } td { (root_item.stime.sec) } } + tr { td { "rtime" } td { (root_item.rtime.sec) } } + }}} + }, + _ => { + html! {} }, } } diff --git a/style.css b/style.css index fd3591a..66dd743 100644 --- a/style.css +++ b/style.css @@ -55,7 +55,20 @@ table > tbody > tr.view.open td:first-child:before { } */ -details { +div.nav { + padding: 5px; + background-color: #dde; + border-radius: 5px; + margin: 5px 0; + overflow: hidden; + text-align: center; +} + +a.nav { + text-decoration: none; +} + +details.item { padding: 5px; background-color: #dde; border-radius: 5px; @@ -103,16 +116,16 @@ details .key a { } span.key_id { - width: 160px; + min-width: 160px; text-align: right; } span.key_type { - width: 160px; + min-width: 160px; } span.key_offset { - width: 160px; + min-width: 160px; text-align: right; } @@ -135,3 +148,22 @@ span.key_type.dir { span.key_type.root { background-color: #111; } + +.details table { + border-collapse: collapse; + margin-bottom: 10px; +} + +.details td { + border: 1px solid white; +} + +.details td:first-child { + border: 1px solid white; + width: 160px; +} + +.details p { + padding: 0; + margin: 5px 0; +}