diff --git a/src/addrmap.rs b/src/addrmap.rs index 0db0a67..74eea7b 100644 --- a/src/addrmap.rs +++ b/src/addrmap.rs @@ -1,5 +1,4 @@ use std::rc::Rc; -use std::sync::Arc; use crate::btrfs_structs::{ParseBin, Key, ChunkItem, Value, Superblock, ParseError, NODE_SIZE}; use crate::btrfs_lookup::Tree; diff --git a/src/btrfs_lookup.rs b/src/btrfs_lookup.rs index 184887a..7c52f3e 100644 --- a/src/btrfs_lookup.rs +++ b/src/btrfs_lookup.rs @@ -2,8 +2,7 @@ use std::convert::identity; use std::rc::Rc; use std::ops::{Deref, RangeBounds, Bound}; -use crate::btrfs_structs::{Leaf, Key, Item, InteriorNode, Node, ParseError, ParseBin, Value, Superblock, ItemType, ZERO_KEY}; -use crate::addrmap::{AddressMap, LogToPhys}; +use crate::btrfs_structs::{Leaf, Key, Item, InteriorNode, Node, ParseError, ParseBin, Value, Superblock, ItemType, ZERO_KEY, LAST_KEY}; use crate::nodereader::NodeReader; /// Represents a B-Tree inside a filesystem image. Can be used to look up keys, @@ -16,7 +15,6 @@ pub struct Tree<'a> { impl<'a> Tree<'a> { pub fn new>(image: &'a [u8], tree_id: T) -> Result, ParseError> { -// let addr_map = Arc::new(AddressMap::new(image)?); let superblock = Superblock::parse(&image[0x10000..])?; let reader = Rc::new(NodeReader::new(image)?); @@ -25,11 +23,17 @@ impl<'a> Tree<'a> { reader: Rc::clone(&reader), root_addr_log: superblock.root }; - let tree_root_item = root_tree.find_key(Key::new(tree_id.into(), ItemType::Root, 0))?; - let root_addr_log = match tree_root_item.value { - Value::Root(root) => root.bytenr, - _ => return Err("root item invalid".into()) + // let tree_root_item = root_tree.find_key(Key::new(tree_id.into(), ItemType::Root, 0))?; + let tree_id = tree_id.into(); + let root_item_key = Key::new(tree_id, ItemType::Root, 0); + let tree_root_item = root_tree.range(root_item_key..) + .next() + .filter(|x| x.key.key_id == tree_id && x.key.key_type == ItemType::Root); + + let root_addr_log = match tree_root_item { + Some(Item { key: _, value: Value::Root(root)}) => root.bytenr, + _ => return Err("root item not found or invalid".into()) }; Ok(Tree { image, reader: Rc::clone(&reader), root_addr_log }) @@ -37,7 +41,6 @@ impl<'a> Tree<'a> { pub fn root(image: &'a [u8]) -> Result, ParseError> { let reader = Rc::new(NodeReader::new(image)?); -// let addr_map = Arc::new(AddressMap::new(image)?); let superblock = Superblock::parse(&image[0x10000..])?; Ok(Tree { image, reader, root_addr_log: superblock.root }) @@ -45,7 +48,6 @@ impl<'a> Tree<'a> { pub fn chunk(image: &'a [u8]) -> Result, ParseError> { let reader = Rc::new(NodeReader::new(image)?); -// let addr_map = Arc::new(AddressMap::new(image)?); let superblock = Superblock::parse(&image[0x10000..])?; Ok(Tree { image, reader, root_addr_log: superblock.chunk_root }) @@ -78,12 +80,12 @@ impl InteriorNode { /// branch which contains `key` if it exists. Returns `None` if all children are greater than /// `key`, which guarantees that `key` is not among the descendants of `self`. pub fn find_key_or_previous(&self, key: Key) -> Option { - self.children - .iter() - .take_while(|x|x.key <= key) - .enumerate() - .last() - .map(|x|x.0) + // if the key is not exactly matched, binary_search returns the next index, but we want the previous one + match self.children.binary_search_by_key(&key, |x|x.key) { + Ok(idx) => Some(idx), + Err(idx) if idx == 0 => None, + Err(idx) => Some(idx-1), + } } } @@ -92,7 +94,7 @@ impl Tree<'_> { /// Recursively traverse a tree to find a key, given they key and logical address /// of the tree root. Internal function, `Tree::find_key` is the public interface. fn find_key_in_node(&self, addr: u64, key: Key) -> Result { - let node = self.reader.get_node(self.root_addr_log)?; + let node = self.reader.get_node(addr)?; match node.deref() { Node::Interior(interior_node) => { @@ -178,12 +180,8 @@ impl<'a> Tree<'a> { /// This function panics if there are no items fn get_first_item(tree: &Tree, addr: u64) -> Result { match tree.reader.get_node(addr)?.deref() { - Node::Interior(intnode) => { - get_first_item(tree, intnode.children[0].ptr) - }, - Node::Leaf(leafnode) => { - Ok(leafnode.items[0].clone()) - }, + Node::Interior(intnode) => get_first_item(tree, intnode.children[0].ptr), + Node::Leaf(leafnode) => Ok(leafnode.items[0].clone()), } } @@ -191,12 +189,8 @@ fn get_first_item(tree: &Tree, addr: u64) -> Result { /// This function panics if there are no items fn get_last_item(tree: &Tree, addr: u64) -> Result { match tree.reader.get_node(addr)?.deref() { - Node::Interior(intnode) => { - get_last_item(tree, intnode.children.last().unwrap().ptr) - }, - Node::Leaf(leafnode) => { - Ok(leafnode.items.last().unwrap().clone()) - }, + Node::Interior(intnode) => get_last_item(tree, intnode.children.last().unwrap().ptr), + Node::Leaf(leafnode) => Ok(leafnode.items.last().unwrap().clone()), } } @@ -313,7 +307,7 @@ impl<'a, 'b> Iterator for RangeIter<'a, 'b> { return None; } - let (start_key, mode) : (Key, FindKeyMode) = match &self.start { + let (start_key, mode): (Key, FindKeyMode) = match &self.start { &Bound::Included(x) => (x, FindKeyMode::GE), &Bound::Excluded(x) => (x, FindKeyMode::GT), &Bound::Unbounded => (ZERO_KEY, FindKeyMode::GE), @@ -327,7 +321,7 @@ impl<'a, 'b> Iterator for RangeIter<'a, 'b> { self.start = Bound::Excluded((self.forward_skip_fn)(item.key)); } - let end_filter = |item : &Item| { + let end_filter = |item: &Item| { match &self.end { &Bound::Included(x) => item.key <= x, &Bound::Excluded(x) => item.key < x, @@ -340,3 +334,36 @@ impl<'a, 'b> Iterator for RangeIter<'a, 'b> { .map(|item|item.clone()) } } + +impl<'a, 'b> DoubleEndedIterator for RangeIter<'a, 'b> { + fn next_back(&mut self) -> Option { + if !range_valid(self.start.as_ref(), self.end.as_ref()) { + return None; + } + + let (start_key, mode): (Key, FindKeyMode) = match &self.end { + &Bound::Included(x) => (x, FindKeyMode::LE), + &Bound::Excluded(x) => (x, FindKeyMode::LT), + &Bound::Unbounded => (LAST_KEY, FindKeyMode::LE), + }; + + let result = find_closest_key(self.tree, start_key, mode) + .expect("file system should be consistent (or this is a bug)"); + + if let Some(item) = &result { + self.end = Bound::Excluded((self.backward_skip_fn)(item.key)); + } + + let start_filter = |item: &Item| { + match &self.start { + &Bound::Included(x) => item.key >= x, + &Bound::Excluded(x) => item.key > x, + &Bound::Unbounded => true, + } + }; + + result + .filter(start_filter) + .map(|item|item.clone()) + } +} diff --git a/src/btrfs_structs.rs b/src/btrfs_structs.rs index 29ad183..02ca1eb 100644 --- a/src/btrfs_structs.rs +++ b/src/btrfs_structs.rs @@ -53,6 +53,7 @@ pub enum ItemType { UUIDSubvol = 0xfb, // implemented UUIDReceivedSubvol = 0xfc, String = 0xfd, + InvalidMax = 0xff, } #[allow(unused)] @@ -74,6 +75,7 @@ impl Key { } pub const ZERO_KEY: Key = Key {key_id: 0, key_type: ItemType::Invalid, key_offset: 0}; +pub const LAST_KEY: Key = Key {key_id: 0xffff_ffff_ffff_ffff, key_type: ItemType::InvalidMax, key_offset: 0xffff_ffff_ffff_ffff}; #[allow(unused)] #[derive(Debug,Clone)] @@ -545,7 +547,10 @@ impl From for ItemType { let variants = ItemType::all_variants(); match variants.binary_search_by_key(&value, |x|u8::from(*x)) { Ok(idx) => variants[idx], - Err(_) => ItemType::Invalid, + Err(_) => { + println!("Unknown item type: {}", value); + ItemType::Invalid + }, } } } diff --git a/src/http_tree.rs b/src/http_tree.rs new file mode 100644 index 0000000..a344bcb --- /dev/null +++ b/src/http_tree.rs @@ -0,0 +1,136 @@ +use std::str::FromStr; +use rouille::{Request, Response}; +use crate::{ + btrfs_structs::{ItemType, Item, Key, ZERO_KEY, LAST_KEY}, + btrfs_lookup::Tree, + render_tree::{render_table, TableResult}, + main_error::MainError, +}; + +enum TreeDisplayMode { + // (x,y,z): Highlight key_id x, show y keys before (excluding x*), show z keys after (including x*) + Highlight(u64, usize, usize), + // (x, y): Show y keys starting at x, including x + From(Key, usize), + // (x, y): Show y keys before y, excluding y + To(Key, usize), +} + + +fn http_tree_internal(tree: &Tree, tree_id: u64, mode: TreeDisplayMode) -> Response { + let mut items: Vec; + let mut highlighted_key_id: Option = None; + + match mode { + TreeDisplayMode::Highlight(key_id, before, after) => { + let key = Key {key_id, key_type: ItemType::Invalid, key_offset: 0 }; + items = tree.range(..key).rev().take(before).collect(); + items.reverse(); + items.extend(tree.range(key..).take(after)); + highlighted_key_id = Some(key_id); + }, + TreeDisplayMode::From(key, num_lines) => { + items = tree.range(key..).take(num_lines).collect(); + if items.len() < num_lines { + items.reverse(); + items.extend(tree.range(..key).rev().take(num_lines - items.len())); + items.reverse(); + } + }, + TreeDisplayMode::To(key, num_lines) => { + items = tree.range(..key).rev().take(num_lines).collect(); + items.reverse(); + if items.len() < num_lines { + items.extend(tree.range(key..).take(num_lines - items.len())); + } + } + }; + + let table_result = TableResult { + tree_id, + tree_desc: root_key_desc(tree_id).map(|x|x.to_string()), + key_id: highlighted_key_id, + items: items.iter().map(|it|(it,&[] as &[u8])).collect(), + first_key: items.first().map(|it|it.key).unwrap_or(LAST_KEY), + last_key: items.last().map(|it|it.key).unwrap_or(ZERO_KEY), + }; + + Response::html(render_table(table_result)) +} + +fn root_key_desc(id: u64) -> Option<&'static str> { + match id { + 1 => Some("root"), + 2 => Some("extent"), + 3 => Some("chunk"), + 4 => Some("device"), + 5 => Some("filesystem"), + 6 => Some("root directory"), + 7 => Some("checksum"), + 8 => Some("quota"), + 9 => Some("UUID"), + 10 => Some("free space"), + 11 => Some("block group"), + 0xffff_ffff_ffff_fff7 => Some("data reloc"), + _ => None, + } +} + +fn http_tree_parse_parameters(method: Option<&str>, key: Option<&str>) -> Result { + let result = match key { + None => TreeDisplayMode::From(ZERO_KEY, 50), + Some(key) => { + let components: Vec<&str> = key.split('-').collect(); + + match method { + None => { + if components.len() < 1 { + return Err(MainError(format!("Invalid key: {key}"))) + } + let key_id = u64::from_str_radix(components[0], 16)?; + TreeDisplayMode::Highlight(key_id, 10, 40) + }, + Some(method) => { + if components.len() < 3 { + return Err(MainError(format!("Invalid key: {key}"))) + } + + let key_id = u64::from_str_radix(components[0], 16)?; + let key_type: ItemType = u8::from_str_radix(components[1], 16)?.into(); + let key_offset = u64::from_str_radix(components[2], 16)?; + let key = Key {key_id, key_type, key_offset }; + + if method == "from" { + TreeDisplayMode::From(key, 50) + } else if method == "to" { + TreeDisplayMode::To(key, 50) + } else { + return Err(MainError(format!("not a valid method: {method}"))) + } + } + } + } + }; + + Ok(result) +} + +pub fn http_tree(image: &[u8], tree_id: &str, method: Option<&str>, key: Option<&str>, _req: &Request) -> Result { + let tree_display_mode = http_tree_parse_parameters(method, key)?; + + let tree_id = u64::from_str(tree_id).unwrap(); + let tree = if tree_id == 1 { + Tree::root(image).unwrap() + } else if tree_id == 3 { + Tree::chunk(image).unwrap() + } else { + Tree::new(image, tree_id).unwrap() + }; + + Ok(http_tree_internal(&tree, tree_id, tree_display_mode)) +} + +pub fn http_root(image: &[u8], _key: Option<&str>, _req: &Request) -> Response { + let tree = Tree::root(image).unwrap(); + http_tree_internal(&tree, 1, TreeDisplayMode::From(ZERO_KEY, 100)) +} diff --git a/src/lib.rs b/src/lib.rs index a3fe97f..1564679 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,9 @@ pub mod btrfs_structs; pub mod btrfs_lookup; pub mod addrmap; pub mod nodereader; +pub mod http_tree; +pub mod render_tree; +pub mod main_error; #[cfg(test)] mod test; diff --git a/src/main.rs b/src/main.rs index b624f62..d41f10f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,13 @@ use std::{ - iter, - env, - fs::OpenOptions, - collections::HashMap, - str::FromStr, - fs::File, + collections::HashMap, env, fs::{File, OpenOptions}, iter, }; -use memmap2::Mmap; +use memmap2::{Mmap, MmapOptions}; use rouille::{Request, Response, router}; use parsebtrfs::{ - btrfs_structs::{TreeID, Value::Extent, Value::BlockGroup, ParseError, NODE_SIZE, ItemType, Item, RootItem, Value, Key, ExtentDataBody}, + btrfs_structs::{TreeID, Value::Extent, Value::BlockGroup, NODE_SIZE, ItemType}, btrfs_lookup::Tree, addrmap::AddressMap, + main_error::MainError, }; const COLORS: &[&str] = &["#e6194b", "#3cb44b", "#ffe119", "#4363d8", "#f58231", "#911eb4", "#46f0f0", "#f032e6", "#bcf60c", "#fabebe", "#008080", "#e6beff", "#9a6324", "#fffac8", "#800000", "#aaffc3", "#808000", "#ffd8b1", "#000075", "#808080", "#000000"]; @@ -19,12 +15,14 @@ const COLORS: &[&str] = &["#e6194b", "#3cb44b", "#ffe119", "#4363d8", "#f58231", fn main() -> Result<(), MainError> { let filename = env::args().skip(1).next().ok_or("Argument required")?; + /* let file = OpenOptions::new().read(true).open(filename)?; let image = unsafe { Mmap::map(&file)? }; + */ const O_DIRECT: i32 = 0x4000; -// let file = OpenOptions::new().read(true).custom_flags(O_DIRECT).open(filename)?; -// let image = unsafe { MmapOptions::new().len(493921239040usize).map(&file)? }; + let file = OpenOptions::new().read(true).open(filename)?; + let image = unsafe { MmapOptions::new().len(493921239040usize).map(&file)? }; // return Ok(()); @@ -40,12 +38,21 @@ fn main() -> Result<(), MainError> { rouille::start_server("127.0.0.1:8080", move |request| { router!( request, - (GET) ["/"] => http_main_boxes(&image, request), - (GET) ["/root"] => http_root(&image, None, request), - (GET) ["/tree/{tree}", tree: String] => http_tree(&image, &tree, None, request), - (GET) ["/tree/{tree}/{key}", tree: String, key: String] => http_tree(&image, &tree, Some(&key), request), + (GET) ["/"] => + http_main_boxes(&image, request), + (GET) ["/root"] => + parsebtrfs::http_tree::http_root(&image, None, request), + (GET) ["/tree/{tree}", tree: String] => + parsebtrfs::http_tree::http_tree(&image, &tree, None, request.get_param("key").as_deref(), request).unwrap(), + (GET) ["/tree/{tree}/{key}", tree: String, key: String] => + parsebtrfs::http_tree::http_tree(&image, &tree, None, Some(&key), request).unwrap(), + (GET) ["/tree/{tree}?key={key}", tree: String, key: String] => + parsebtrfs::http_tree::http_tree(&image, &tree, None, Some(&key), request).unwrap(), + (GET) ["/tree/{tree}/{method}/{key}", tree: String, method: String, key: String] => + parsebtrfs::http_tree::http_tree(&image, &tree, Some(&method), Some(&key), request).unwrap(), (GET) ["/favicon.ico"] => Response::empty_404(), (GET) ["/style.css"] => Response::from_file("text/css", File::open("style.css").unwrap()), + (GET) ["/htmx.min.js"] => Response::from_file("text/css", File::open("htmx.min.js").unwrap()), _ => Response::empty_404(), ) }); @@ -57,52 +64,12 @@ static CIRCLE_IMAGE: &str = ////DRRP+v//vzw2hZP+Y4JJ2BS+waLwDUyeiVinIStchkV+GfmeoRoAAJqLWnEf4UboAAAAAElF\ TkSuQmCC"; -static HTML_HEADER: &str = r###" - - - - - -"###; - -static HTML_FOOTER: &str = r###" - -"###; - static EXPLANATION_TEXT: &str = "\

Chunks

On the highest level, btrfs splits the disk into chunks (also called block groups). They can have different sizes, with 1GiB being typical in a large file system. Each chunk can either contain data or metadata.

Here we look at the metadata chunks. They contain the B-treesm which btrfs gets its name from. They are key-value stores for different kinds of information. For example, the filesystem tree stores which files and directories are in the filesystem, and the extent tree stores which areas of the disk are in use. Each B-tree consists of a number of 16KiB nodes, here symbolized by colorful boxes, with the color indicating which tree the node belongs to. Most of the nodes are leaves, which contain the actual key-value pairs. The others are interior nodes, and we indicate them with a little white circle. They are important to find the leaf a key is stored in.

"; -fn root_key_desc(id: u64) -> Option<&'static str> { - match id { - 1 => Some("root"), - 2 => Some("extent"), - 3 => Some("chunk"), - 4 => Some("device"), - 5 => Some("filesystem"), - 6 => Some("root directory"), - 7 => Some("checksum"), - 8 => Some("quota"), - 9 => Some("UUID"), - 10 => Some("free space"), - 11 => Some("block group"), - 0xffff_ffff_ffff_fff7 => Some("data reloc"), - _ => None, - } -} - fn http_main_boxes(image: &[u8], _req: &Request) -> Response { let mut treecolors: HashMap = HashMap::new(); @@ -306,279 +273,30 @@ fn http_main_boxes(image: &[u8], _req: &Request) -> Response { Response::html(result) } -fn html_item_row(key: Key, value: String, index: usize) -> String { +/* +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" }; - format!("{:X}{:?}{:X}{}\n", - key.key_id, key.key_type, key.key_offset, value) + 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_id_desc(key: Key, value: String, index: usize, desc: &str) -> String { - let even = if index % 2 == 0 { "even" } else { "odd" }; - format!("{}{:?}{:X}{}\n", - desc, key.key_type, key.key_offset, value) +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 item_value_string(tree_id: u64, key: Key, val: &Value) -> String { - match val { - Value::Root(_) => { - format!("go", key.key_id) - }, - Value::Dir(dir_item) => - format!("name: {:?}, location: {:X}", - &dir_item.name, - tree_id, - dir_item.location.key_id, - dir_item.location.key_id), - Value::DirIndex(dir_item) => - format!("name: {:?}, location: {:X}", - &dir_item.name, - tree_id, - dir_item.location.key_id, - dir_item.location.key_id), - Value::Inode(inode_item) => - format!("mode: {:o}, ctime: {}, mtime: {}, otime: {}", - inode_item.mode, - inode_item.ctime.sec, - inode_item.mtime.sec, - inode_item.otime.sec), - Value::ExtentData(extent_data_item) => - match &extent_data_item.data { - ExtentDataBody::Inline(data) => format!("inline, len: {}", data.len()), - ExtentDataBody::External(ext_extent) => - format!("external, disk_bytenr: {}, disk_num_bytes: {}, offset: {}, num_bytes: {}", - ext_extent.disk_bytenr, - ext_extent.disk_num_bytes, - ext_extent.offset, - ext_extent.num_bytes), - }, - Value::Ref(ref_item) => - format!("name: {:?}, index: {}", - &ref_item.name, - ref_item.index), - Value::Extent(extent_item) => - format!("flags: {}, block_refs: {:?}", extent_item.flags, extent_item.block_refs), - Value::BlockGroup(blockgroup_item) => - format!("used: {} bytes", 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), - Value::UUIDSubvol(uuid_subvol_item) => - 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), - Value::Dev(dev_item) => - format!("total_bytes: {}", dev_item.total_bytes), - Value::Chunk(chunk_item) => - format!("size: {}", chunk_item.size), - _ => { - println!("{:?}", val); - String::new() - }, - } -} - -fn http_tree_internal(tree: &Tree, tree_id: u64, key: Option) -> Response { - let mut table: Vec = Vec::new(); - - table.push(String::from("")); - - let k = Key {key_id: key.unwrap_or(0), key_type: ItemType::Invalid, key_offset: 0}; - let mut last_key_id: Option = None; - let mut key_index = 0; - - for item in tree.range(k..).take(3000) { - if last_key_id.replace(item.key.key_id).filter(|x| *x != item.key.key_id).is_some() { - key_index += 1; - } - - match &item.value { - Value::Root(_) => { - let id = match root_key_desc(item.key.key_id) { - Some(desc) => format!("{:X} ({})", item.key.key_id, desc), - None => format!("{:X}", item.key.key_id), - }; - let value = item_value_string(tree_id, item.key, &item.value); - table.push(html_item_row_id_desc(item.key, value, key_index, &id)); - }, - _ => { - table.push(html_item_row(item.key, item_value_string(tree_id, item.key, &item.value), key_index)); - }, - } - } - - table.push(String::from("
")); - - let mut result = String::new(); - - result.push_str(HTML_HEADER); - result.extend(table.iter().map(>::as_ref)); - result.push_str(HTML_FOOTER); - - Response::html(result) - -} - -fn http_tree(image: &[u8], tree_id: &str, key: Option<&str>, _req: &Request) -> Response { - let key_id = key.and_then(|x|u64::from_str_radix(x, 16).ok()); - let tree_id = u64::from_str(tree_id).unwrap(); - let tree = if tree_id == 1 { - Tree::root(image).unwrap() - } else if tree_id == 3 { - Tree::chunk(image).unwrap() +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 { - Tree::new(image, tree_id).unwrap() + format!("{:X}", key.key_offset) }; - http_tree_internal(&tree, tree_id, key_id) -} - -fn http_root(image: &[u8], key: Option<&str>, _req: &Request) -> Response { - let key_id = key.and_then(|x|u64::from_str_radix(x, 16).ok()); - let tree = Tree::root(image).unwrap(); - http_tree_internal(&tree, 1, key_id) -} - - -// ----- Error handling ----- - -pub struct MainError(String); - -impl std::error::Error for MainError {} - -impl std::fmt::Debug for MainError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", &self.0) - } -} - -impl std::fmt::Display for MainError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", &self.0) - } -} - -impl From for MainError { - fn from(value: String) -> MainError { - MainError(value) - } -} - -impl From<&str> for MainError { - fn from(value: &str) -> MainError { - MainError::from(String::from(value)) - } -} - -impl From for MainError { - fn from(value: ParseError) -> MainError { - MainError::from(format!("BTRFS format error: {value}")) - } -} - -impl From for MainError { - fn from(value: std::io::Error) -> MainError { - MainError::from(format!("IO error: {value}")) - } -} - -/* -fn main() -> Result<(), std::io::Error> { - let file = File::open("../image")?; - let image = unsafe { Mmap::map(&file)? }; - - let addr = AddressTranslation::new(&image); - - rouille::start_server("127.0.0.1:8080", move |request| { - http_main_list(&image, &addr, request) - }); -} - - -fn http_main_list(image: &[u8], addr: &AddressTranslation, req: &Request) -> Response { - let chunk_offset = 0x02500000; - let nodes_in_chunk = 2048; - let mut result = String::new(); - - result.push_str("\n"); - - for i in 0..nodes_in_chunk { - let node = read_node(&image, chunk_offset + i*0x4000); - - let active = ACTIVE_NODES.contains(&(i*0x4000)); - let style = if active { "color:black;" } else { "color:lightgray;" }; - - let newline = format!("

{:x} {} {} {}\n

    \n", - style, - chunk_offset + i*0x4000, - node.level, - node.items.len(), - node.generation); - result.push_str(&newline); - - for item in &node.items { - let newline = format!("
  • {:016x} {:?} {:x}
  • \n", - style, - item.key.key_id, - item.key.key_type, - item.key.key_offset); - result.push_str(&newline); - } - - result.push_str("

\n"); - } - - Response::html(result) -} -*/ - -/* -fn read_node_log(image: &[u8], trans: &AddressTranslation, log: u64) -> Option> { - let phys = trans.to_phys(log)?; - Some(read_node(image, phys as usize)) -} - -fn read_node(image: &[u8], offset: usize) -> Box { - let mut result = Box::new(BtrfsNode { - csum: FromBytes::get(image, offset), - fs_uid: FromBytes::get(image, offset + 0x20), - bytenr: FromBytes::get(image, offset + 0x30), - flags: FromBytes::get(image, offset + 0x38), - chunk_tree_uid: FromBytes::get(image, offset + 0x40), - generation: FromBytes::get(image, offset + 0x50), - owner: FromBytes::get(image, offset + 0x58), - nritems: FromBytes::get(image, offset + 0x60), - level: FromBytes::get(image, offset + 0x64), - items: Vec::new(), - }); - - // assuming leaf for now - - for i in 0..result.nritems as usize { - let key_id: u64 = FromBytes::get(image, offset + 0x65 + i*0x19); - let key_type_code: u8 = FromBytes::get(image, offset + 0x65 + i*0x19 + 0x08); - let key_offset: u64 = FromBytes::get(image, offset + 0x65 + i*0x19 + 0x09); - let data_offset: u32 = FromBytes::get(image, offset + 0x65 + i*0x19 + 0x11); - let data_size: u32 = FromBytes::get(image, offset + 0x65 + i*0x19 + 0x15); - - let key_type = itemtype_from_code(key_type_code); - let data_slice = &image[(offset + 0x65 + data_offset as usize) .. (offset + 0x65 + data_offset as usize + data_size as usize)]; - - let value = match key_type { - BtrfsItemType::BlockGroup => BtrfsValue::BlockGroup(FromBytes::get(data_slice, 0)), - BtrfsItemType::Metadata => BtrfsValue::Extent(FromBytes::get(data_slice, 0)), - BtrfsItemType::Chunk => BtrfsValue::Chunk(FromBytes::get(data_slice, 0)), - BtrfsItemType::Root => BtrfsValue::Root(FromBytes::get(data_slice, 0)), - _ => BtrfsValue::Unknown(Vec::from(data_slice)), - }; - - result.items.push(BtrfsItem { - key: BtrfsKey { - key_id: key_id, - key_type: key_type, - key_offset: key_offset, - }, - value: value, - }); - } - - result + (x,y,z) } */ diff --git a/src/main_error.rs b/src/main_error.rs new file mode 100644 index 0000000..8b72df2 --- /dev/null +++ b/src/main_error.rs @@ -0,0 +1,45 @@ +pub struct MainError(pub String); + +impl std::error::Error for MainError {} + +impl std::fmt::Debug for MainError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", &self.0) + } +} + +impl std::fmt::Display for MainError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", &self.0) + } +} + +impl From for MainError { + fn from(value: String) -> MainError { + MainError(value) + } +} + +impl From<&str> for MainError { + fn from(value: &str) -> MainError { + MainError::from(String::from(value)) + } +} + +impl From for MainError { + fn from(value: crate::btrfs_structs::ParseError) -> MainError { + MainError::from(format!("BTRFS format error: {value}")) + } +} + +impl From for MainError { + fn from(value: std::io::Error) -> MainError { + MainError::from(format!("IO error: {value}")) + } +} + +impl From for MainError { + fn from(value: std::num::ParseIntError) -> MainError { + MainError::from(format!("Not an integer: {value}")) + } +} diff --git a/src/nodereader.rs b/src/nodereader.rs new file mode 100644 index 0000000..f139262 --- /dev/null +++ b/src/nodereader.rs @@ -0,0 +1,43 @@ +use std::{ + collections::HashMap, + sync::Arc, + cell::RefCell, +}; + +use crate::btrfs_structs::{Node, ParseError, ParseBin}; +use crate::addrmap::{LogToPhys, AddressMap}; + +pub struct NodeReader<'a> { + image: &'a [u8], + addr_map: AddressMap, + cache: RefCell>>, +} + +impl<'a> NodeReader<'a> { + pub fn new(image: &'a [u8]) -> Result, ParseError> { + let addr_map = AddressMap::new(image)?; + Ok(NodeReader {image, addr_map, cache: RefCell::new(HashMap::new())}) + } + + pub fn with_addrmap(image: &'a [u8], addr_map: AddressMap) -> Result, ParseError> { + Ok(NodeReader {image, addr_map, cache: RefCell::new(HashMap::new())}) + } + + /// Read a node given its logical address + pub fn get_node(&self, addr: u64) -> Result, ParseError> { + if let Some(node) = self.cache.borrow().get(&addr) { + return Ok(Arc::clone(node)) + } + + let node_data = self.addr_map.node_at_log(self.image, addr)?; + let node = Arc::new(Node::parse(node_data)?); + + self.cache.borrow_mut().insert(addr, Arc::clone(&node)); + + Ok(node) + } + + pub fn addr_map(&self) -> &AddressMap { + &self.addr_map + } +} diff --git a/src/render_tree.rs b/src/render_tree.rs new file mode 100644 index 0000000..b849780 --- /dev/null +++ b/src/render_tree.rs @@ -0,0 +1,161 @@ +use crate::btrfs_structs::{Item, Key, ItemType, Value, ExtentDataBody}; + +#[derive(Debug)] +pub struct TableResult<'a> { + pub tree_id: u64, + pub tree_desc: Option, + pub key_id: Option, + pub items: Vec<(&'a Item, &'a [u8])>, + pub first_key: Key, + pub last_key: Key, +} + +static HTML_HEADER: &str = r###" + + + + + +"###; + +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)); + } else { + result.push_str(&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))); + + // 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 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); + + + result.push_str(&row); + } + result.push_str("
\n"); + + result.push_str(HTML_FOOTER); + + result +} + +fn row_id_desc(key: Key, tree_id: u64) -> (String, String, String) { + 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 { + format!("{:X}", tree_id, key.key_offset, key.key_offset) + } else { + format!("{:X}", key.key_offset) + }; + (x,y,z) +} + +fn item_value_string(tree_id: u64, key: Key, val: &Value) -> String { + match val { + Value::Root(_) => { + format!("go to tree {}", key.key_id, 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::ExtentData(extent_data_item) => + match &extent_data_item.data { + ExtentDataBody::Inline(data) => format!("inline, length {}", data.len()), + ExtentDataBody::External(ext_extent) => + format!("external, length {}", + ext_extent.num_bytes), + }, + Value::Ref(ref_item) => + format!("{:?}", &ref_item.name), + Value::Extent(extent_item) => + format!("flags: {}, block_refs: {:?}", extent_item.flags, extent_item.block_refs), + Value::BlockGroup(blockgroup_item) => + format!("{} bytes used", 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), + Value::UUIDSubvol(uuid_subvol_item) => + 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), + Value::Dev(dev_item) => + format!("total_bytes: {}", dev_item.total_bytes), + Value::Chunk(chunk_item) => + format!("size: {}", chunk_item.size), + _ => { + println!("{:?} {:?}", key, val); + String::new() + }, + } +} diff --git a/style.css b/style.css index 29da254..fd3591a 100644 --- a/style.css +++ b/style.css @@ -26,6 +26,10 @@ table > tbody > tr.even { background: #eee; } +table > tbody > tr.highlight { + background: #0cc; +} + table > tbody > tr.fold { display: none; } @@ -50,3 +54,84 @@ table > tbody > tr.view.open td:first-child:before { color: #333; } */ + +details { + padding: 5px; + background-color: #dde; + border-radius: 5px; + margin: 5px 0; + overflow: hidden; +} + +a { + color: black; +} + +details.highlight { + background-color: #abc; +} + +details .details { + color: white; + background-color: #222; + padding: 10px; + margin-top: 5px; + border-radius: 5px; +} + +details .itemvalue { + color: black; + padding: 3px; + margin: 1px 2px; + width: auto; + display: inline-block; +} + +details .key { + color: white; + background-color: #999; + border-radius: 5px; + padding: 3px; + margin: 1px 2px; + display: inline-block; + font-family: monospace; + font-size: 12pt; +} + +details .key a { + color: white; +} + +span.key_id { + width: 160px; + text-align: right; +} + +span.key_type { + width: 160px; +} + +span.key_offset { + width: 160px; + text-align: right; +} + +span.key_type.inode { + background-color: #c22; +} + +span.key_type.ref { + background-color: #aa5; +} + +span.key_type.extent { + background-color: #151; +} + +span.key_type.dir { + background-color: #33c; +} + +span.key_type.root { + background-color: #111; +}