btrfs_explorer/btrfs_explorer_bin/src/main.rs

274 lines
9.9 KiB
Rust

use std::{
collections::HashMap, env, fs::{File, OpenOptions}, iter,
};
use memmap2::MmapOptions;
use rouille::{Request, Response, router};
use btrfs_explorer::{
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"];
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)? };
*/
let file = OpenOptions::new().read(true).open(filename)?;
let image = unsafe { MmapOptions::new().len(493921239040usize).map(&file)? };
// return Ok(());
/*
let mystery_addr = 0x2f_2251_c000;
let addr_map = AddressMap::new(&image)?;
let mystery_addr_phys = addr_map.to_phys(mystery_addr).unwrap() as usize;
let mystery_node = Node::parse(&image[mystery_addr_phys .. ])?;
println!("{:#x?}", &mystery_node);
*/
rouille::start_server("127.0.0.1:8080", move |request| {
router!(
request,
(GET) ["/"] =>
http_main_boxes(&image, request),
(GET) ["/root"] =>
btrfs_explorer::http_tree::http_root(&image, None, request),
(GET) ["/tree/{tree}", tree: String] =>
btrfs_explorer::http_tree::http_tree(&image, &tree, None, request.get_param("key").as_deref(), request).unwrap(),
(GET) ["/tree/{tree}/{key}", tree: String, key: String] =>
btrfs_explorer::http_tree::http_tree(&image, &tree, None, Some(&key), request).unwrap(),
(GET) ["/tree/{tree}?key={key}", tree: String, key: String] =>
btrfs_explorer::http_tree::http_tree(&image, &tree, None, Some(&key), request).unwrap(),
(GET) ["/tree/{tree}/{method}/{key}", tree: String, method: String, key: String] =>
btrfs_explorer::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(),
)
});
}
static CIRCLE_IMAGE: &str =
"data:image/png;base64,\
iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAP0lEQVQY02NgoBn4//+//P///yf9\
////DRRP+v//vzw2hZP+Y4JJ2BS+waLwDUyeiVinIStchkV+GfmeoRoAAJqLWnEf4UboAAAAAElF\
TkSuQmCC";
static EXPLANATION_TEXT: &str = "\
<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>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 http_main_boxes(image: &[u8], _req: &Request) -> Response {
let mut treecolors: HashMap<u64, &str> = HashMap::new();
let mut result = String::new();
let explanation_tablerowformat = |c: &str, t: &str| format!(
"<tr>\
<td><table><tr><td style=\"height:10px;width:10px;padding:0;background:{};\"></td></tr></table></td>\
<td><table><tr><td style=\"height:10px;width:10px;padding:0;background:{};\"><img src=\"{}\" /></td></tr></table></td>\
<td>{}</td>\
</tr>\n",
c, c, CIRCLE_IMAGE, t);
let explanation_tablerowformat_leafonly = |c,t| format!(
"<tr>\
<td><table><tr><td style=\"height:10px;width:10px;padding:0;background:{};\"></td></tr></table></td>\
<td></td>\
<td>{}</td>\
</tr>\n",
c, t);
let cellformat = |c| format!(
"<td style=\"height:10px;width:10px;padding:0;background:{};\"></td>\n",
c);
let cellformat_higher = |c,_| format!(
"<td style=\"height:10px;width:10px;padding:0;background:{}\"><img src=\"{}\" /></td>\n",
c, CIRCLE_IMAGE);
result.push_str(&"<details>\n<summary>What am I seeing here?</summary>");
result.push_str(EXPLANATION_TEXT);
// tree explanations
result.push_str(&"<table style=\"margin: 0 auto;\">\n");
result.push_str(&explanation_tablerowformat_leafonly("lightgrey", "unused or outdated node"));
treecolors.insert(1, COLORS[treecolors.len() % COLORS.len()]);
result.push_str(&explanation_tablerowformat(treecolors[&1], "root tree"));
treecolors.insert(3, COLORS[treecolors.len() % COLORS.len()]);
result.push_str(&explanation_tablerowformat(treecolors[&3], "chunk tree"));
let roots = Tree::root(image).unwrap();
for item in roots.iter() {
if item.key.key_type == ItemType::Root {
let treedesc: String = match &item.key.key_id {
1 => format!("root tree"),
2 => format!("extent tree"),
3 => format!("chunk tree"),
4 => format!("device tree"),
5 => format!("filesystem tree"),
6 => format!("root directory"),
7 => format!("checksum tree"),
8 => format!("quota tree"),
9 => format!("UUID tree"),
10 => format!("free space tree"),
11 => format!("block group tree"),
0xffff_ffff_ffff_fff7 => format!("data reloc tree"),
x @ 0x100 ..= 0xffff_ffff_ffff_feff => format!("file tree, id = {}", x),
x => format!("other tree, id = {}", x),
};
treecolors.insert(item.key.key_id, COLORS[treecolors.len() % COLORS.len()]);
result.push_str(&explanation_tablerowformat(
treecolors[&item.key.key_id],
&treedesc
));
}
}
result.push_str(&"</table>\n");
result.push_str(&"</details>\n");
let extent_tree = Tree::new(&image, TreeID::Extent).unwrap();
let mut extent_tree_iterator = extent_tree.iter();
// current_blockgroup == None: haven't encountered a blockgroup yet
// metadata_items == None: current blockgroup is not metadata or system
let mut current_blockgroup = None;
let mut metadata_items: Option<Vec<Option<(u64, u64)>>> = None;
let metadata_blockgroups = iter::from_fn(|| {
while let Some(item) = extent_tree_iterator.next() {
// println!("Got key: {:x?}", &item.key);
match &item.value {
BlockGroup(bg) => {
println!("{:x?}", item.key);
let result = (current_blockgroup.take(), metadata_items.take());
let nodes_in_blockgroup = item.key.key_offset as usize / NODE_SIZE;
if bg.flags & 0x01 == 0 {
metadata_items = Some(vec![None; nodes_in_blockgroup]);
} else {
metadata_items = None;
}
current_blockgroup = Some(item);
if let (Some(bg), met) = result {
return Some((bg, met));
}
},
Extent(e) => {
if let Some(bg_item) = &current_blockgroup {
if let Some(met) = &mut metadata_items {
let bg_start = bg_item.key.key_id;
let node_addr = item.key.key_id;
let tree_id = e.block_refs.iter().count() as u64;
let index = (node_addr - bg_start) as usize / NODE_SIZE;
if index < met.len() {
met[index] = Some((tree_id, item.key.key_offset));
} else {
println!("Warning: extent out of block group range: {:x?}", &item.key);
}
}
} else {
println!("Warning: extent without matching block group: {:x?}", &item.key);
}
},
_ => {},//panic!("Unexpected item in extent tree: {:x?}", item.key)
}
}
let result = (current_blockgroup.take(), metadata_items.take());
if let (Some(bg), met) = result {
return Some((bg, met));
} else {
return None;
}
});
let mut last_key = 0;
// colorful table
for (bg, nodes) in metadata_blockgroups {
if bg.key.key_id < last_key {
println!("Error: going backwards!");
break;
} else {
last_key = bg.key.key_id;
}
let bg_value = match &bg.value {
BlockGroup(bgv) => bgv,
_ => panic!("Expected BlockGroup value"),
};
// header
let addr_map: &AddressMap = extent_tree.reader.as_ref().addr_map();
result.push_str(
&format!(
"<h3 style=\"text-align: center;\">{:x} - {:x} ({}, {})</h3><p>Physical: {}</p>\n",
bg.key.key_id,
bg.key.key_id + bg.key.key_offset,
match bg.key.key_offset {
x if x <= (1<<11) => format!("{} B", x),
x if x <= (1<<21) => format!("{} KiB", x as f64 / (1u64<<10) as f64),
x if x <= (1<<31) => format!("{} MiB", x as f64 / (1u64<<20) as f64),
x if x <= (1<<41) => format!("{} GiB", x as f64 / (1u64<<30) as f64),
x if x <= (1<<51) => format!("{} TiB", x as f64 / (1u64<<40) as f64),
x @ _ => format!("{} PiB", x as f64 / (1u64<<50) as f64),
},
match bg_value.flags & 0x07 {
0x01 => "Data",
0x02 => "System",
0x04 => "Metadata",
_ => "???",
},
match addr_map.0.binary_search_by_key(&bg.key.key_id, |x|x.0) {
Ok(i) => format!("{:x?}", &addr_map.0[i].2),
_ => String::from(""),
}
)
);
if let Some(nodes) = nodes {
result.push_str("<table style=\"margin: 0 auto;\">\n<tr>\n");
for (i, &n) in nodes.iter().enumerate() {
if i % 64 == 0 && i != 0 {
result.push_str("</tr>\n<tr>\n");
}
if let Some((tid, level)) = n {
let color: Option<&str> = treecolors.get(&tid).map(|x|*x);
let color = color.unwrap_or_else(|| {
println!("Unknown color for id: {}", &tid);
let color: &str = COLORS[treecolors.len() % COLORS.len()];
treecolors.insert(tid, color);
color
});
if level == 0 {
result.push_str(&cellformat(color));
} else {
result.push_str(&cellformat_higher(color, level));
}
} else {
result.push_str(&cellformat("lightgrey"));
}
}
result.push_str("</tr>\n</table>\n");
}
}
Response::html(result)
}