refactor and add nice styling

This commit is contained in:
Florian Stecker 2024-02-24 11:24:25 -05:00
parent ad3f782c67
commit 38b1f3d040
10 changed files with 575 additions and 353 deletions

View File

@ -1,5 +1,4 @@
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc;
use crate::btrfs_structs::{ParseBin, Key, ChunkItem, Value, Superblock, ParseError, NODE_SIZE}; use crate::btrfs_structs::{ParseBin, Key, ChunkItem, Value, Superblock, ParseError, NODE_SIZE};
use crate::btrfs_lookup::Tree; use crate::btrfs_lookup::Tree;

View File

@ -2,8 +2,7 @@ use std::convert::identity;
use std::rc::Rc; use std::rc::Rc;
use std::ops::{Deref, RangeBounds, Bound}; use std::ops::{Deref, RangeBounds, Bound};
use crate::btrfs_structs::{Leaf, Key, Item, InteriorNode, Node, ParseError, ParseBin, Value, Superblock, ItemType, ZERO_KEY}; use crate::btrfs_structs::{Leaf, Key, Item, InteriorNode, Node, ParseError, ParseBin, Value, Superblock, ItemType, ZERO_KEY, LAST_KEY};
use crate::addrmap::{AddressMap, LogToPhys};
use crate::nodereader::NodeReader; use crate::nodereader::NodeReader;
/// Represents a B-Tree inside a filesystem image. Can be used to look up keys, /// 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> { impl<'a> Tree<'a> {
pub fn new<T: Into<u64>>(image: &'a [u8], tree_id: T) -> Result<Tree<'a>, ParseError> { pub fn new<T: Into<u64>>(image: &'a [u8], tree_id: T) -> Result<Tree<'a>, ParseError> {
// let addr_map = Arc::new(AddressMap::new(image)?);
let superblock = Superblock::parse(&image[0x10000..])?; let superblock = Superblock::parse(&image[0x10000..])?;
let reader = Rc::new(NodeReader::new(image)?); let reader = Rc::new(NodeReader::new(image)?);
@ -25,11 +23,17 @@ impl<'a> Tree<'a> {
reader: Rc::clone(&reader), reader: Rc::clone(&reader),
root_addr_log: superblock.root 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 { // let tree_root_item = root_tree.find_key(Key::new(tree_id.into(), ItemType::Root, 0))?;
Value::Root(root) => root.bytenr, let tree_id = tree_id.into();
_ => return Err("root item invalid".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 }) 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<Tree<'a>, ParseError> { pub fn root(image: &'a [u8]) -> Result<Tree<'a>, ParseError> {
let reader = Rc::new(NodeReader::new(image)?); let reader = Rc::new(NodeReader::new(image)?);
// let addr_map = Arc::new(AddressMap::new(image)?);
let superblock = Superblock::parse(&image[0x10000..])?; let superblock = Superblock::parse(&image[0x10000..])?;
Ok(Tree { image, reader, root_addr_log: superblock.root }) Ok(Tree { image, reader, root_addr_log: superblock.root })
@ -45,7 +48,6 @@ impl<'a> Tree<'a> {
pub fn chunk(image: &'a [u8]) -> Result<Tree<'a>, ParseError> { pub fn chunk(image: &'a [u8]) -> Result<Tree<'a>, ParseError> {
let reader = Rc::new(NodeReader::new(image)?); let reader = Rc::new(NodeReader::new(image)?);
// let addr_map = Arc::new(AddressMap::new(image)?);
let superblock = Superblock::parse(&image[0x10000..])?; let superblock = Superblock::parse(&image[0x10000..])?;
Ok(Tree { image, reader, root_addr_log: superblock.chunk_root }) 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 /// 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`. /// `key`, which guarantees that `key` is not among the descendants of `self`.
pub fn find_key_or_previous(&self, key: Key) -> Option<usize> { pub fn find_key_or_previous(&self, key: Key) -> Option<usize> {
self.children // if the key is not exactly matched, binary_search returns the next index, but we want the previous one
.iter() match self.children.binary_search_by_key(&key, |x|x.key) {
.take_while(|x|x.key <= key) Ok(idx) => Some(idx),
.enumerate() Err(idx) if idx == 0 => None,
.last() Err(idx) => Some(idx-1),
.map(|x|x.0) }
} }
} }
@ -92,7 +94,7 @@ impl Tree<'_> {
/// Recursively traverse a tree to find a key, given they key and logical address /// 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. /// of the tree root. Internal function, `Tree::find_key` is the public interface.
fn find_key_in_node(&self, addr: u64, key: Key) -> Result<Item, ParseError> { fn find_key_in_node(&self, addr: u64, key: Key) -> Result<Item, ParseError> {
let node = self.reader.get_node(self.root_addr_log)?; let node = self.reader.get_node(addr)?;
match node.deref() { match node.deref() {
Node::Interior(interior_node) => { Node::Interior(interior_node) => {
@ -178,12 +180,8 @@ impl<'a> Tree<'a> {
/// This function panics if there are no items /// This function panics if there are no items
fn get_first_item(tree: &Tree, addr: u64) -> Result<Item, ParseError> { fn get_first_item(tree: &Tree, addr: u64) -> Result<Item, ParseError> {
match tree.reader.get_node(addr)?.deref() { match tree.reader.get_node(addr)?.deref() {
Node::Interior(intnode) => { Node::Interior(intnode) => get_first_item(tree, intnode.children[0].ptr),
get_first_item(tree, intnode.children[0].ptr) Node::Leaf(leafnode) => Ok(leafnode.items[0].clone()),
},
Node::Leaf(leafnode) => {
Ok(leafnode.items[0].clone())
},
} }
} }
@ -191,12 +189,8 @@ fn get_first_item(tree: &Tree, addr: u64) -> Result<Item, ParseError> {
/// This function panics if there are no items /// This function panics if there are no items
fn get_last_item(tree: &Tree, addr: u64) -> Result<Item, ParseError> { fn get_last_item(tree: &Tree, addr: u64) -> Result<Item, ParseError> {
match tree.reader.get_node(addr)?.deref() { match tree.reader.get_node(addr)?.deref() {
Node::Interior(intnode) => { Node::Interior(intnode) => get_last_item(tree, intnode.children.last().unwrap().ptr),
get_last_item(tree, intnode.children.last().unwrap().ptr) Node::Leaf(leafnode) => Ok(leafnode.items.last().unwrap().clone()),
},
Node::Leaf(leafnode) => {
Ok(leafnode.items.last().unwrap().clone())
},
} }
} }
@ -340,3 +334,36 @@ impl<'a, 'b> Iterator for RangeIter<'a, 'b> {
.map(|item|item.clone()) .map(|item|item.clone())
} }
} }
impl<'a, 'b> DoubleEndedIterator for RangeIter<'a, 'b> {
fn next_back(&mut self) -> Option<Item> {
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())
}
}

View File

@ -53,6 +53,7 @@ pub enum ItemType {
UUIDSubvol = 0xfb, // implemented UUIDSubvol = 0xfb, // implemented
UUIDReceivedSubvol = 0xfc, UUIDReceivedSubvol = 0xfc,
String = 0xfd, String = 0xfd,
InvalidMax = 0xff,
} }
#[allow(unused)] #[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 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)] #[allow(unused)]
#[derive(Debug,Clone)] #[derive(Debug,Clone)]
@ -545,7 +547,10 @@ impl From<u8> for ItemType {
let variants = ItemType::all_variants(); let variants = ItemType::all_variants();
match variants.binary_search_by_key(&value, |x|u8::from(*x)) { match variants.binary_search_by_key(&value, |x|u8::from(*x)) {
Ok(idx) => variants[idx], Ok(idx) => variants[idx],
Err(_) => ItemType::Invalid, Err(_) => {
println!("Unknown item type: {}", value);
ItemType::Invalid
},
} }
} }
} }

136
src/http_tree.rs Normal file
View File

@ -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<Item>;
let mut highlighted_key_id: Option<u64> = 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<TreeDisplayMode, MainError> {
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<Response, MainError> {
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))
}

View File

@ -4,6 +4,9 @@ pub mod btrfs_structs;
pub mod btrfs_lookup; pub mod btrfs_lookup;
pub mod addrmap; pub mod addrmap;
pub mod nodereader; pub mod nodereader;
pub mod http_tree;
pub mod render_tree;
pub mod main_error;
#[cfg(test)] #[cfg(test)]
mod test; mod test;

View File

@ -1,17 +1,13 @@
use std::{ use std::{
iter, collections::HashMap, env, fs::{File, OpenOptions}, iter,
env,
fs::OpenOptions,
collections::HashMap,
str::FromStr,
fs::File,
}; };
use memmap2::Mmap; use memmap2::{Mmap, MmapOptions};
use rouille::{Request, Response, router}; use rouille::{Request, Response, router};
use parsebtrfs::{ 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, btrfs_lookup::Tree,
addrmap::AddressMap, 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"]; 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> { fn main() -> Result<(), MainError> {
let filename = env::args().skip(1).next().ok_or("Argument required")?; let filename = env::args().skip(1).next().ok_or("Argument required")?;
/*
let file = OpenOptions::new().read(true).open(filename)?; let file = OpenOptions::new().read(true).open(filename)?;
let image = unsafe { Mmap::map(&file)? }; let image = unsafe { Mmap::map(&file)? };
*/
const O_DIRECT: i32 = 0x4000; const O_DIRECT: i32 = 0x4000;
// let file = OpenOptions::new().read(true).custom_flags(O_DIRECT).open(filename)?; let file = OpenOptions::new().read(true).open(filename)?;
// let image = unsafe { MmapOptions::new().len(493921239040usize).map(&file)? }; let image = unsafe { MmapOptions::new().len(493921239040usize).map(&file)? };
// return Ok(()); // return Ok(());
@ -40,12 +38,21 @@ fn main() -> Result<(), MainError> {
rouille::start_server("127.0.0.1:8080", move |request| { rouille::start_server("127.0.0.1:8080", move |request| {
router!( router!(
request, request,
(GET) ["/"] => http_main_boxes(&image, request), (GET) ["/"] =>
(GET) ["/root"] => http_root(&image, None, request), http_main_boxes(&image, request),
(GET) ["/tree/{tree}", tree: String] => http_tree(&image, &tree, None, request), (GET) ["/root"] =>
(GET) ["/tree/{tree}/{key}", tree: String, key: String] => http_tree(&image, &tree, Some(&key), request), 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) ["/favicon.ico"] => Response::empty_404(),
(GET) ["/style.css"] => Response::from_file("text/css", File::open("style.css").unwrap()), (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(), _ => Response::empty_404(),
) )
}); });
@ -57,52 +64,12 @@ static CIRCLE_IMAGE: &str =
////DRRP+v//vzw2hZP+Y4JJ2BS+waLwDUyeiVinIStchkV+GfmeoRoAAJqLWnEf4UboAAAAAElF\ ////DRRP+v//vzw2hZP+Y4JJ2BS+waLwDUyeiVinIStchkV+GfmeoRoAAJqLWnEf4UboAAAAAElF\
TkSuQmCC"; TkSuQmCC";
static HTML_HEADER: &str = r###"<html>
<head>
<link rel="stylesheet" href="/style.css">
<script>
document.addEventListener("DOMContentLoaded", function() {
folds = document.getElementsByClassName("fold");
for(i = 0; i < folds.length; i++) {
// folds[i].classList.add("open");
folds[i].addEventListener("click", (event) => {
folds[i].classList.add("open");
});
}
});
</script>
</head>
<body>
"###;
static HTML_FOOTER: &str = r###"</body>
</html>
"###;
static EXPLANATION_TEXT: &str = "\ static EXPLANATION_TEXT: &str = "\
<h3>Chunks</h3> <h3>Chunks</h3>
<p>On the highest level, btrfs splits the disk into <b>chunks</b> (also called <b>block groups</b>). They can have different sizes, with 1GiB being typical in a large file system. Each chunk can either contain data or metadata.<p> <p>On the highest level, btrfs splits the disk into <b>chunks</b> (also called <b>block groups</b>). They can have different sizes, with 1GiB being typical in a large file system. Each chunk can either contain data or metadata.<p>
<p>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 <b>nodes</b>, here symbolized by colorful boxes, with the color indicating which tree the node belongs to. Most of the nodes are <b>leaves</b>, which contain the actual key-value pairs. The others are <b>interior nodes</b>, and we indicate them with a little white circle. They are important to find the leaf a key is stored in.</p>"; <p>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 <b>nodes</b>, here symbolized by colorful boxes, with the color indicating which tree the node belongs to. Most of the nodes are <b>leaves</b>, which contain the actual key-value pairs. The others are <b>interior nodes</b>, and we indicate them with a little white circle. They are important to find the leaf a key is stored in.</p>";
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 { fn http_main_boxes(image: &[u8], _req: &Request) -> Response {
let mut treecolors: HashMap<u64, &str> = HashMap::new(); let mut treecolors: HashMap<u64, &str> = HashMap::new();
@ -306,279 +273,30 @@ fn http_main_boxes(image: &[u8], _req: &Request) -> Response {
Response::html(result) 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" }; let even = if index % 2 == 0 { "even" } else { "odd" };
format!("<tr class = \"{even}\"><td>{:X}</td><td>{:?}</td><td>{:X}</td><td>{}</td></tr>\n", let id_desc = row_id_desc(item.key, tree_id);
key.key_id, key.key_type, key.key_offset, value) format!("<tr class = \"{even}\"><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>\n<tr class=\"{even}\"><td colspan=4>{}</td></tr>\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 { fn html_item_row_highlighted(key: Key, value: String, _index: usize, tree_id: u64) -> String {
let even = if index % 2 == 0 { "even" } else { "odd" }; let id_desc = row_id_desc(key, tree_id);
format!("<tr class = \"{even}\"><td>{}</td><td>{:?}</td><td>{:X}</td><td>{}</td></tr>\n", format!("<tr class = \"highlight\"><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>\n",
desc, key.key_type, key.key_offset, value) id_desc.0, id_desc.1, id_desc.2, value)
} }
fn item_value_string(tree_id: u64, key: Key, val: &Value) -> String { fn row_id_desc(key: Key, tree_id: u64) -> (String, String, String) {
match val { let x = format!("{:X}", key.key_id);
Value::Root(_) => { let y = format!("{:?}", key.key_type);
format!("<a href=\"/tree/{}\">go</a>", key.key_id) let z = if key.key_type == ItemType::RootRef || key.key_type == ItemType::Ref {
}, format!("<a href=\"/tree/{}/{:X}\">{:X}</a>", tree_id, key.key_offset, key.key_offset)
Value::Dir(dir_item) =>
format!("name: {:?}, location: <a href=\"/tree/{}/{:x}\">{:X}<a>",
&dir_item.name,
tree_id,
dir_item.location.key_id,
dir_item.location.key_id),
Value::DirIndex(dir_item) =>
format!("name: {:?}, location: <a href=\"/tree/{}/{:x}\">{:X}<a>",
&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<u64>) -> Response {
let mut table: Vec<String> = Vec::new();
table.push(String::from("<table><tbody>"));
let k = Key {key_id: key.unwrap_or(0), key_type: ItemType::Invalid, key_offset: 0};
let mut last_key_id: Option<u64> = 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("</tbody></table>"));
let mut result = String::new();
result.push_str(HTML_HEADER);
result.extend(table.iter().map(<String as AsRef<str>>::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()
} else { } else {
Tree::new(image, tree_id).unwrap() format!("{:X}", key.key_offset)
}; };
http_tree_internal(&tree, tree_id, key_id) (x,y,z)
}
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<String> 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<ParseError> for MainError {
fn from(value: ParseError) -> MainError {
MainError::from(format!("BTRFS format error: {value}"))
}
}
impl From<std::io::Error> 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("<body>\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!("<p style=\"{}\">{:x} {} {} {}\n<ul>\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!("<li style=\"{}\">{:016x} {:?} {:x}</li>\n",
style,
item.key.key_id,
item.key.key_type,
item.key.key_offset);
result.push_str(&newline);
}
result.push_str("</ul></p>\n");
}
Response::html(result)
}
*/
/*
fn read_node_log(image: &[u8], trans: &AddressTranslation, log: u64) -> Option<Box<BtrfsNode>> {
let phys = trans.to_phys(log)?;
Some(read_node(image, phys as usize))
}
fn read_node(image: &[u8], offset: usize) -> Box<BtrfsNode> {
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
} }
*/ */

45
src/main_error.rs Normal file
View File

@ -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<String> 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<crate::btrfs_structs::ParseError> for MainError {
fn from(value: crate::btrfs_structs::ParseError) -> MainError {
MainError::from(format!("BTRFS format error: {value}"))
}
}
impl From<std::io::Error> for MainError {
fn from(value: std::io::Error) -> MainError {
MainError::from(format!("IO error: {value}"))
}
}
impl From<std::num::ParseIntError> for MainError {
fn from(value: std::num::ParseIntError) -> MainError {
MainError::from(format!("Not an integer: {value}"))
}
}

43
src/nodereader.rs Normal file
View File

@ -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<HashMap<u64, Arc<Node>>>,
}
impl<'a> NodeReader<'a> {
pub fn new(image: &'a [u8]) -> Result<NodeReader<'a>, 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<NodeReader<'a>, 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<Arc<Node>, 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
}
}

161
src/render_tree.rs Normal file
View File

@ -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<String>,
pub key_id: Option<u64>,
pub items: Vec<(&'a Item, &'a [u8])>,
pub first_key: Key,
pub last_key: Key,
}
static HTML_HEADER: &str = r###"<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/style.css">
</head>
<body>
"###;
static HTML_FOOTER: &str = r###"
<script src="/htmx.min.js"></script>
</body>
</html>
"###;
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!("<h1>Tree {} ({})</h1>", table.tree_id, desc));
} else {
result.push_str(&format!("<h1>Tree {}</h1>", table.tree_id));
}
// link to root tree
if table.tree_id != 1 {
result.push_str(&format!("<a href=\"/tree/1\">go back to root tree</a>"));
}
// search field
let key_input_value = table.key_id.map_or(String::new(), |x| format!("{:X}", x));
result.push_str(&format!("<form method=get action=\"/tree/{}\">\n", table.tree_id));
result.push_str(&format!("<input type=text name=\"key\" value=\"{}\">\n", key_input_value));
result.push_str("<input type=submit value=\"Search\">\n");
result.push_str("</form>\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!("<a href=\"/tree/{}\">first</a>\n",
table.tree_id));
result.push_str(&format!("<a href=\"/tree/{}/to/{:016X}-{:02X}-{:016X}\">prev</a>\n",
table.tree_id, first.key_id, u8::from(first.key_type), first.key_offset));
result.push_str(&format!("<a href=\"/tree/{}/from/{:016X}-{:02X}-{:016X}\">next</a>\n",
table.tree_id, last.key_id, u8::from(last.key_type), last.key_offset));
result.push_str(&format!("<a href=\"/tree/{}/to/{:016X}-{:02X}-{:016X}\">last</a>\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("<table><tbody>\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!("<details{}><summary><span class=\"key key_id {key_type_class}\">{}</span><span class=\"key key_type {key_type_class}\">{}</span><span class=\"key key_offset {key_type_class}\">{}</span><span class=\"itemvalue\">{}</span></summary><div class=\"details\">{}</div></details>\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("</tbody></table>\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!("<a href=\"/tree/{}/{:X}\">{:X}</a>", 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!("<a href=\"/tree/{}\">go to tree {}</a>", key.key_id, key.key_id)
},
Value::Dir(dir_item) =>
format!("{:?} @ <a href=\"/tree/{}/{:x}\">{:X}</a>",
&dir_item.name,
tree_id,
dir_item.location.key_id,
dir_item.location.key_id),
Value::DirIndex(dir_item) =>
format!("{:?} @ <a href=\"/tree/{}/{:x}\">{:X}</a>",
&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()
},
}
}

View File

@ -26,6 +26,10 @@ table > tbody > tr.even {
background: #eee; background: #eee;
} }
table > tbody > tr.highlight {
background: #0cc;
}
table > tbody > tr.fold { table > tbody > tr.fold {
display: none; display: none;
} }
@ -50,3 +54,84 @@ table > tbody > tr.view.open td:first-child:before {
color: #333; 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;
}