Compare commits
3 Commits
3240b6a9f2
...
61d7680af8
Author | SHA1 | Date | |
---|---|---|---|
|
61d7680af8 | ||
|
83bdade363 | ||
|
4d90426eee |
@ -2,7 +2,7 @@ use std::convert::identity;
|
||||
use std::rc::Rc;
|
||||
use std::ops::{Deref, RangeBounds, Bound};
|
||||
|
||||
use crate::btrfs_structs::{DirItem, InteriorNode, Item, ItemType, Key, Leaf, Node, ParseBin, ParseError, Superblock, Value, LAST_KEY, ZERO_KEY};
|
||||
use crate::btrfs_structs::{InteriorNode, Item, ItemType, Key, Leaf, Node, ParseBin, ParseError, Superblock, Value, LAST_KEY, ZERO_KEY};
|
||||
use crate::nodereader::NodeReader;
|
||||
|
||||
/// Represents a B-Tree inside a filesystem image. Can be used to look up keys,
|
||||
@ -135,9 +135,9 @@ impl<'a> Tree<'a> {
|
||||
/// ended iterator which iterates through the keys contained in the range, in ascending
|
||||
/// or descending order.
|
||||
///
|
||||
/// The skip functions are ignored for now, but are intended as an optimization:
|
||||
/// after a key `k` was returned by the iterator (or the reverse iterator), all keys
|
||||
/// strictly lower than `forward_skip_fn(k)` are skipped (resp. all keys strictly above
|
||||
/// The skip functions make it possible to efficiently iterate only through certain types of items.
|
||||
/// After a key `k` was returned by the iterator (or the reverse iterator), all keys
|
||||
/// lower or equal `forward_skip_fn(k)` are skipped (resp. all keys higher or equal
|
||||
/// `backward_skip_fn(k)` are skipped.
|
||||
///
|
||||
/// If `forward_skip_fn` and `backward_skip_fn` are the identity, nothing is skipped
|
||||
|
324
btrfs_explorer/src/http_chunk.rs
Normal file
324
btrfs_explorer/src/http_chunk.rs
Normal file
@ -0,0 +1,324 @@
|
||||
use maud::{Markup, html};
|
||||
use rouille::{Request, Response};
|
||||
use crate::{
|
||||
btrfs_lookup::Tree, btrfs_structs::{self, BlockRef, ItemType, TreeID, Value}, key, main_error::MainError, render_common::{render_page, size_name}
|
||||
};
|
||||
|
||||
struct ChunkLineDisplay {
|
||||
logical_address: Option<u64>,
|
||||
link: bool,
|
||||
physical_address: u64,
|
||||
size: u64,
|
||||
description: String,
|
||||
}
|
||||
|
||||
struct ChunkResult {
|
||||
pub offset: u64,
|
||||
pub refs: Vec<Vec<u64>>,
|
||||
pub color_special: bool,
|
||||
}
|
||||
|
||||
pub fn http_allchunks(image: &[u8], _req: &Request) -> Result<Response, MainError> {
|
||||
let tree = Tree::chunk(image)?;
|
||||
|
||||
let mut chunks: Vec<ChunkLineDisplay> = Vec::new();
|
||||
|
||||
for item in tree.iter() {
|
||||
let Value::Chunk(chunk_item) = &item.value else { continue; };
|
||||
|
||||
for stripe in &chunk_item.stripes {
|
||||
if stripe.devid != 1 {
|
||||
println!("multiple devices not supported!");
|
||||
continue;
|
||||
}
|
||||
|
||||
let desc = match chunk_item.chunktype & 0x7 {
|
||||
1 => format!("data chunk"),
|
||||
2 => format!("system chunk"),
|
||||
4 => format!("metadata chunk"),
|
||||
_ => format!("(unknown chunk type)"),
|
||||
};
|
||||
|
||||
chunks.push(ChunkLineDisplay {
|
||||
logical_address: Some(item.key.key_offset),
|
||||
link: chunk_item.chunktype & 0x7 != 1,
|
||||
physical_address: stripe.offset,
|
||||
size: chunk_item.size,
|
||||
description: desc,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
chunks.sort_by_key(|x|x.physical_address);
|
||||
|
||||
let mut chunks_filled: Vec<ChunkLineDisplay> = Vec::with_capacity(chunks.len());
|
||||
|
||||
let mut total_size: u64 = 0;
|
||||
for x in chunks {
|
||||
if total_size > x.physical_address {
|
||||
// not so good
|
||||
} else if total_size < x.physical_address {
|
||||
chunks_filled.push(ChunkLineDisplay {
|
||||
logical_address: None,
|
||||
link: false,
|
||||
physical_address: total_size,
|
||||
size: x.physical_address - total_size,
|
||||
description: format!("unassigned"),
|
||||
});
|
||||
}
|
||||
total_size = x.physical_address + x.size;
|
||||
|
||||
chunks_filled.push(x);
|
||||
}
|
||||
|
||||
Ok(Response::html(render_allchunks(chunks_filled)))
|
||||
}
|
||||
|
||||
pub fn http_chunk(image: &[u8], offset: &str, _req: &Request) -> Result<Response, MainError> {
|
||||
let logical_offset: u64 = u64::from_str_radix(offset, 16)?;
|
||||
|
||||
let tree = Tree::new(image, TreeID::Extent)?;
|
||||
|
||||
let start = key!(logical_offset, ItemType::BlockGroup, 0);
|
||||
let end = key!(logical_offset, ItemType::BlockGroup, u64::MAX);
|
||||
if let Some(bg) = tree.range(start..=end).next() {
|
||||
// let extent_list: Vec<(u64, u64, Vec<u64>)> = Vec::new();
|
||||
let blockgroup_size = bg.key.key_offset;
|
||||
|
||||
// we'll just assume for now this is metadata, for every node we store the referencing trees
|
||||
let nr_nodes = (blockgroup_size >> 14) as usize;
|
||||
let mut node_list: Vec<Vec<u64>> = Vec::with_capacity(nr_nodes);
|
||||
|
||||
let start = key!(logical_offset, ItemType::Invalid, 0);
|
||||
let end = key!(logical_offset + blockgroup_size, ItemType::Invalid, 0);
|
||||
|
||||
for item in tree.range(start..end) {
|
||||
let Value::Extent(extent_item) = item.value else { continue };
|
||||
|
||||
if item.key.key_type == ItemType::Metadata {
|
||||
let index = ((item.key.key_id - logical_offset) >> 14) as usize;
|
||||
|
||||
let process_ref = |rf: &BlockRef| {
|
||||
match rf {
|
||||
&BlockRef::Tree { id } => Some(id),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
let refs: Vec<u64> = extent_item.block_refs.iter().filter_map(process_ref).collect();
|
||||
|
||||
while node_list.len() < index {
|
||||
node_list.push(Vec::new());
|
||||
}
|
||||
node_list.push(refs);
|
||||
}
|
||||
}
|
||||
|
||||
while node_list.len() < nr_nodes {
|
||||
node_list.push(Vec::new());
|
||||
}
|
||||
|
||||
let chunk_data = ChunkResult {
|
||||
offset: logical_offset,
|
||||
refs: node_list,
|
||||
color_special: true,
|
||||
};
|
||||
return Ok(Response::html(render_chunk(chunk_data)))
|
||||
}
|
||||
|
||||
err!("No block group found at logical address {:x}", logical_offset)
|
||||
}
|
||||
|
||||
// const COLORS: &[&str] = &["lightgray", "#e6194b", "#3cb44b", "#ffe119", "#4363d8", "#f58231", "#911eb4", "#46f0f0", "#f032e6", "#bcf60c", "#fabebe", "#008080", "#e6beff", "#9a6324", "#fffac8", "#800000", "#aaffc3", "#808000", "#ffd8b1", "#000075", "#808080", "#000000"];
|
||||
|
||||
fn render_chunk(data: ChunkResult) -> Markup {
|
||||
let header = format!("Metadata nodes in chunk at address {:x}", data.offset);
|
||||
|
||||
let boxes: Vec<Vec<&str>> = data.refs
|
||||
.chunks(64)
|
||||
.map(|row|row.iter()
|
||||
.map(|noderefs| {
|
||||
if noderefs.len() == 0 {
|
||||
"lightgrey"
|
||||
} else if noderefs[0] == 1 { // root
|
||||
if data.color_special {"darkgreen"} else {"black"}
|
||||
} else if noderefs[0] == 2 { // extent
|
||||
if data.color_special {"orange"} else {"black"}
|
||||
} else if noderefs[0] == 3 { // extent
|
||||
if data.color_special {"brown"} else {"black"}
|
||||
} else if noderefs[0] == 4 { // device
|
||||
if data.color_special {"cyan"} else {"black"}
|
||||
} else if noderefs[0] == 7 { // checksum
|
||||
if data.color_special {"magenta"} else {"black"}
|
||||
} else if noderefs[0] == 9 { // uuid
|
||||
if data.color_special {"yellow"} else {"black"}
|
||||
} else if noderefs[0] == 10 { // free space
|
||||
if data.color_special {"#00ff00"} else {"black"}
|
||||
} else if noderefs[0] < 0x100 && noderefs[0] != 5 || noderefs[0] > u64::MAX - 0x100 {
|
||||
if data.color_special {"red"} else {"black"}
|
||||
} else if noderefs.len() == 1 {
|
||||
if noderefs[0] == 5 {
|
||||
if data.color_special {"black"} else {"darkgreen"}
|
||||
} else {
|
||||
if data.color_special {"black"} else {"blue"}
|
||||
}
|
||||
} else {
|
||||
if data.color_special {"black"} else {
|
||||
"conic-gradient(blue 0deg 45deg, darkgreen 45deg 225deg, blue 225deg 360deg)"
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
.collect();
|
||||
|
||||
let content = html! {
|
||||
h1 {
|
||||
(header)
|
||||
}
|
||||
|
||||
details open {
|
||||
summary { "Explanation" }
|
||||
(explanation_chunk())
|
||||
}
|
||||
|
||||
br {}
|
||||
|
||||
table.blocks {
|
||||
@for row in boxes {
|
||||
tr {
|
||||
@for cell in row {
|
||||
td style={"background:" (cell) ";"} {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render_page(&header, content)
|
||||
}
|
||||
|
||||
fn render_allchunks(data: Vec<ChunkLineDisplay>) -> Markup {
|
||||
let content = html! {
|
||||
h1 {
|
||||
"Physical disk layout"
|
||||
}
|
||||
|
||||
details open {
|
||||
summary { "Explanation" }
|
||||
(explanation_allchunks())
|
||||
}
|
||||
|
||||
br {}
|
||||
|
||||
@for bg in data {
|
||||
div.item {
|
||||
span.key.key_offset {
|
||||
(format!("{:x}", bg.physical_address))
|
||||
}
|
||||
@if let Some(addr) = bg.logical_address {
|
||||
span.key.key_offset {
|
||||
(format!("{:x}", addr))
|
||||
}
|
||||
}
|
||||
span.itemvalue {
|
||||
@if bg.link {
|
||||
a href=(format!("/chunk/{:x}", bg.logical_address.unwrap())) { (bg.description) } ", " (size_name(bg.size))
|
||||
|
||||
} @else {
|
||||
(bg.description) ", " (size_name(bg.size))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render_page("Physical disk layout", content)
|
||||
}
|
||||
|
||||
fn explanation_allchunks() -> Markup {
|
||||
html! {
|
||||
p {
|
||||
"This shows the on-disk format of a BTRFS file system on the most zoomed-out level. BTRFS includes a device mapper functionality where a single logical file system can be spread over multiple physical disks, and a logical block can have multiple copies of it stored on disk. Here we assume the file system has only one physical disk and there are no RAID features enabled."
|
||||
}
|
||||
p {
|
||||
"This page shows the layout of the phyiscal disk. It is organized in \"chunks\", typically a few megabytes to a gigabyte in size, with possibly some unassigned space in between. There are three types of chunks:"
|
||||
}
|
||||
|
||||
ul {
|
||||
li {
|
||||
b { "Data: " }
|
||||
"A data chunk contains actual contents of files. The contents of one file do not have to be stored in the data chunk in a contiguous way, and can be spread out over multiple data chunks. The data chunk itself also contains no information about which files the data belongs to, all of that is stored in the metadata. Very small files (e.g. under 1 KiB) do not have their contents in here, but they are entirely stored in the metadata section."
|
||||
}
|
||||
|
||||
li {
|
||||
b { "Metadata: " }
|
||||
"This contains all information about file names, directories, checksums, used and free space, devices etc. The data here is organized in the eponymous \"B-Trees\", which is essentially a type of key-value store. Click on a metadata chunk to find out what is stored inside it."
|
||||
}
|
||||
|
||||
li {
|
||||
b { "System: " }
|
||||
"The system chunks are just additional metadata chunks, except that they are reserved for special kinds of metadata. Most importantly, the system chunks contain the mapping from logical to physical addresses, which is needed to find the other metadata chunks. The physical locations of the system chunks are stored in the superblock, thereby avoiding a chicken-and-egg problem while mounting the drive."
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
"The first column in the following table shows the physical address of a chunk and the second column shows its logical address. You can click on the metadata or system chunks to find out how they are laid out."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn explanation_chunk() -> Markup {
|
||||
html! {
|
||||
p { "The metadata of a BTRFS file system is organized in the form of multiple " b { "B-trees" } ", which consist of " b { "nodes" } ". Each node is 16 KiB in size." }
|
||||
|
||||
p { "This page shows the contents of a single metadata chunk. Every little box is one node of 16 KiB. Together they add up to the total size of the chunk. We show 64 nodes per row, so each row is 1 MiB." }
|
||||
|
||||
p { "The colors indicate which B-tree the node belongs to. Most of them belong to the filesystem trees. There is one filesystem tree for every subvolume, but we draw them all in the same color here. The other trees are:" }
|
||||
|
||||
table.legend {
|
||||
tr {
|
||||
td { table.blocks { tr { td style="background: darkgreen;" {} } } }
|
||||
td { "root tree" }
|
||||
}
|
||||
|
||||
tr {
|
||||
td { table.blocks { tr { td style="background: orange;" {} } } }
|
||||
td { "extent tree" }
|
||||
}
|
||||
|
||||
tr {
|
||||
td { table.blocks { tr { td style="background: brown;" {} } } }
|
||||
td { "chunk tree" }
|
||||
}
|
||||
|
||||
tr {
|
||||
td { table.blocks { tr { td style="background: cyan;" {} } } }
|
||||
td { "device tree" }
|
||||
}
|
||||
|
||||
tr {
|
||||
td { table.blocks { tr { td style="background: magenta;" {} } } }
|
||||
td { "checksum tree" }
|
||||
}
|
||||
|
||||
tr {
|
||||
td { table.blocks { tr { td style="background: yellow;" {} } } }
|
||||
td { "uuid tree" }
|
||||
}
|
||||
|
||||
tr {
|
||||
td { table.blocks { tr { td style="background: #00ff00;" {} } } }
|
||||
td { "free space tree" }
|
||||
}
|
||||
|
||||
tr {
|
||||
td { table.blocks { tr { td style="background: red;" {} } } }
|
||||
td { "other trees" }
|
||||
}
|
||||
|
||||
tr {
|
||||
td { table.blocks { tr { td style="background: black;" {} } } }
|
||||
td { "filesystem trees" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ pub mod http_tree;
|
||||
pub mod render_common;
|
||||
pub mod render_tree;
|
||||
pub mod main_error;
|
||||
pub mod http_chunk;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
@ -1,5 +1,6 @@
|
||||
use maud::Render;
|
||||
use maud::{html, DOCTYPE, Markup, Render};
|
||||
use std::fmt::{Debug, UpperHex};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
pub struct DebugRender<T>(pub T);
|
||||
|
||||
@ -36,3 +37,28 @@ pub fn size_name(x: u64) -> String {
|
||||
format!("{} EiB", x / (1<<60))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_page(title: &str, content: Markup) -> Markup {
|
||||
html! {
|
||||
(DOCTYPE)
|
||||
head {
|
||||
link rel="stylesheet" href={(http_path()) "/style.css"};
|
||||
title {
|
||||
(title)
|
||||
}
|
||||
}
|
||||
body {
|
||||
(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static HTTP_PATH: OnceLock<String> = OnceLock::new();
|
||||
|
||||
pub fn http_path() -> &'static str {
|
||||
HTTP_PATH.get().expect("HTTP_PATH should have been initialized before usage.")
|
||||
}
|
||||
|
||||
pub fn http_path_set(path: String) {
|
||||
HTTP_PATH.set(path).expect("HTTP_PATH can only be set once.");
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::btrfs_structs::{Item, Key, ItemType, Value, ExtentDataBody};
|
||||
use crate::render_common::{Hex, size_name};
|
||||
use crate::render_common::{Hex, size_name, http_path};
|
||||
use maud::{Markup, html, DOCTYPE, PreEscaped};
|
||||
use std::ffi::CStr;
|
||||
|
||||
@ -13,11 +13,8 @@ pub struct TableResult<'a> {
|
||||
pub last_key: Key,
|
||||
}
|
||||
|
||||
const HTTP_PATH: &str = "/btrfs";
|
||||
|
||||
pub fn render_table(table: TableResult) -> Markup {
|
||||
|
||||
let header: String = if let Some(desc) = table.tree_desc {
|
||||
let header = if let Some(desc) = table.tree_desc {
|
||||
format!("Tree {} ({})", table.tree_id, desc)
|
||||
} else {
|
||||
format!("Tree {}", table.tree_id)
|
||||
@ -25,19 +22,22 @@ pub fn render_table(table: TableResult) -> Markup {
|
||||
|
||||
let key_input_value = table.key_id.map_or(String::new(), |x| format!("{:X}", x));
|
||||
|
||||
let first_key_url = format!("{HTTP_PATH}/tree/{}",
|
||||
table.tree_id);
|
||||
let prev_key_url = format!("{HTTP_PATH}/tree/{}/to/{:016X}-{:02X}-{:016X}",
|
||||
let first_key_url = format!("{}/tree/{}",
|
||||
http_path(), table.tree_id);
|
||||
let prev_key_url = format!("{}/tree/{}/to/{:016X}-{:02X}-{:016X}",
|
||||
http_path(),
|
||||
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!("{HTTP_PATH}/tree/{}/from/{:016X}-{:02X}-{:016X}",
|
||||
let next_key_url = format!("{}/tree/{}/from/{:016X}-{:02X}-{:016X}",
|
||||
http_path(),
|
||||
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!("{HTTP_PATH}/tree/{}/to/{:016X}-{:02X}-{:016X}",
|
||||
let last_key_url = format!("{}/tree/{}/to/{:016X}-{:02X}-{:016X}",
|
||||
http_path(),
|
||||
table.tree_id,
|
||||
u64::wrapping_sub(0,1),
|
||||
u8::wrapping_sub(0,1),
|
||||
@ -102,20 +102,25 @@ pub fn render_table(table: TableResult) -> Markup {
|
||||
html! {
|
||||
(DOCTYPE)
|
||||
head {
|
||||
link rel="stylesheet" href={(HTTP_PATH) "/style.css"};
|
||||
link rel="stylesheet" href={(http_path()) "/style.css"};
|
||||
}
|
||||
body {
|
||||
h1 {
|
||||
(header)
|
||||
}
|
||||
|
||||
details {
|
||||
summary { "Explanation" }
|
||||
(explanation_tree())
|
||||
}
|
||||
|
||||
@if table.tree_id != 1 {
|
||||
a href={(HTTP_PATH) "/tree/1"} {
|
||||
a href={(http_path()) "/tree/1"} {
|
||||
"go back to root tree"
|
||||
}
|
||||
}
|
||||
|
||||
form method="get" action={(HTTP_PATH) "/tree/" (table.tree_id)} {
|
||||
form method="get" action={(http_path()) "/tree/" (table.tree_id)} {
|
||||
input type="text" name="key" value=(key_input_value);
|
||||
input type="submit" value="Search";
|
||||
}
|
||||
@ -131,6 +136,26 @@ pub fn render_table(table: TableResult) -> Markup {
|
||||
}
|
||||
}
|
||||
|
||||
fn explanation_tree() -> Markup {
|
||||
html! {
|
||||
p {
|
||||
"This page shows the content of a tree. It is essentially a list of items, each of which consist of a key and a value."
|
||||
}
|
||||
|
||||
p {
|
||||
"The key is shown in the boxes on the left. It is a triple of a 64-bit id, an 8-bit type, and a 64-bit offset. What each of them means depends on the tree we're in. You can search for a key id by using the search field below."
|
||||
}
|
||||
|
||||
p {
|
||||
"The value is summarized to the right of the key. To see the value in more detail, unfold the key by clicking on it."
|
||||
}
|
||||
|
||||
p {
|
||||
"Finally, to the very right, we have the logical address of the metadata node which the item is stored in."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn key_type_class(key: Key) -> &'static str {
|
||||
match key.key_type {
|
||||
ItemType::Inode => "inode",
|
||||
@ -149,7 +174,7 @@ 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 {
|
||||
format!("<a href=\"{HTTP_PATH}/tree/{}/{:X}\">{:X}</a>", tree_id, key.key_offset, key.key_offset)
|
||||
format!("<a href=\"{}/tree/{}/{:X}\">{:X}</a>", http_path(), tree_id, key.key_offset, key.key_offset)
|
||||
} else {
|
||||
format!("{:X}", key.key_offset)
|
||||
};
|
||||
@ -159,7 +184,7 @@ fn row_id_desc(key: Key, tree_id: u64) -> (Markup, Markup, Markup) {
|
||||
fn item_value_string(tree_id: u64, item: &Item) -> Markup {
|
||||
match &item.value {
|
||||
Value::Root(_) => {
|
||||
html! { a href={(HTTP_PATH) "/tree/" (item.key.key_id)} { "go to tree " (item.key.key_id) } }
|
||||
html! { a href={(http_path()) "/tree/" (item.key.key_id)} { "go to tree " (item.key.key_id) } }
|
||||
},
|
||||
Value::Dir(dir_item) | Value::DirIndex(dir_item) => {
|
||||
let name = format!("{:?}", &dir_item.name);
|
||||
@ -168,12 +193,12 @@ fn item_value_string(tree_id: u64, item: &Item) -> Markup {
|
||||
(name)
|
||||
" @ "
|
||||
@if dir_item.location.key_type == ItemType::Root {
|
||||
a href=(format!("{HTTP_PATH}/tree/{id}")) {
|
||||
a href=(format!("{}/tree/{id}", http_path())) {
|
||||
"subvolume "
|
||||
(Hex(id))
|
||||
}
|
||||
} @else {
|
||||
a href=(format!("{HTTP_PATH}/tree/{tree_id}/{id:x}")) {
|
||||
a href=(format!("{}/tree/{tree_id}/{id:x}", http_path())) {
|
||||
(Hex(id))
|
||||
}
|
||||
}
|
||||
|
@ -1,46 +1,40 @@
|
||||
use std::{
|
||||
collections::HashMap, env, fs::{File, OpenOptions}, iter,
|
||||
env, fs::OpenOptions, ops::Deref, include_str,
|
||||
};
|
||||
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,
|
||||
};
|
||||
use rouille::{Response, router};
|
||||
use btrfs_explorer::main_error::MainError;
|
||||
use btrfs_explorer::render_common::http_path_set;
|
||||
|
||||
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 CSS_FILE: &'static str = include_str!("style.css");
|
||||
|
||||
fn main() -> Result<(), MainError> {
|
||||
let filename = env::args().skip(1).next().ok_or("Argument required")?;
|
||||
let args: Vec<String> = env::args().collect();
|
||||
|
||||
/*
|
||||
let file = OpenOptions::new().read(true).open(filename)?;
|
||||
let image = unsafe { Mmap::map(&file)? };
|
||||
*/
|
||||
if args.len() < 2 {
|
||||
return Err("Argument required".into());
|
||||
}
|
||||
|
||||
let filename: &str = args[1].as_ref();
|
||||
let sockaddr: &str = args.get(2)
|
||||
.map_or("localhost:8080", <String as Deref>::deref);
|
||||
let http_path: String = args.get(3)
|
||||
.map_or(String::new(), <String as ToOwned>::to_owned);
|
||||
http_path_set(http_path);
|
||||
|
||||
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)? };
|
||||
let image = unsafe { MmapOptions::new().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| {
|
||||
rouille::start_server(sockaddr, move |request| {
|
||||
router!(
|
||||
request,
|
||||
(GET) ["/"] =>
|
||||
http_main_boxes(&image, request),
|
||||
btrfs_explorer::http_chunk::http_allchunks(&image, request).unwrap(),
|
||||
(GET) ["/root"] =>
|
||||
btrfs_explorer::http_tree::http_root(&image, None, request),
|
||||
(GET) ["/chunk/{offset}", offset: String] =>
|
||||
btrfs_explorer::http_chunk::http_chunk(&image, &offset, request).unwrap(),
|
||||
(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] =>
|
||||
@ -50,224 +44,9 @@ fn main() -> Result<(), MainError> {
|
||||
(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()),
|
||||
(GET) ["/style.css"] => Response::from_data("text/css", CSS_FILE),// 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) = ¤t_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)
|
||||
}
|
||||
|
@ -2,43 +2,6 @@ body {
|
||||
padding: 0.2em 2em;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table td {
|
||||
padding: 0.1em 0.2em;
|
||||
}
|
||||
|
||||
table th {
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
table > tbody > tr.view {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
table > tbody > tr.even {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
table > tbody > tr.highlight {
|
||||
background: #0cc;
|
||||
}
|
||||
|
||||
table > tbody > tr.fold {
|
||||
display: none;
|
||||
}
|
||||
|
||||
table > tbody > tr.fold > td {
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
table > tbody > tr.fold.open {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
div.nav {
|
||||
padding: 5px;
|
||||
background-color: #dde;
|
||||
@ -52,7 +15,7 @@ a.nav {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
details.item {
|
||||
.item {
|
||||
padding: 3px;
|
||||
background-color: #dde;
|
||||
border-radius: 4px;
|
||||
@ -76,7 +39,7 @@ details .details {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
details .itemvalue {
|
||||
.itemvalue {
|
||||
color: black;
|
||||
padding: 3px;
|
||||
margin: 1px 2px;
|
||||
@ -84,7 +47,7 @@ details .itemvalue {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
details .key {
|
||||
.key {
|
||||
color: white;
|
||||
background-color: #999;
|
||||
border-radius: 4px;
|
||||
@ -95,7 +58,7 @@ details .key {
|
||||
font-size: 12pt;
|
||||
}
|
||||
|
||||
details .key a {
|
||||
.key a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
@ -166,3 +129,19 @@ span.nodeaddr {
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
table.blocks {
|
||||
margin: 0 auto;
|
||||
border-collapse: separate;
|
||||
border-spacing: 2px;
|
||||
}
|
||||
|
||||
table.blocks td {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
table.legend {
|
||||
margin: 0 auto;
|
||||
}
|
Loading…
Reference in New Issue
Block a user