Compare commits

...

10 Commits

Author SHA1 Message Date
Florian Stecker b41547ddcb check size of items 2024-03-01 13:31:34 -05:00
Florian Stecker bc852d6b6a combine crates to workspace 2024-02-28 00:57:20 -05:00
Florian Stecker e853f8bc46 lots of small changes 2024-02-28 00:43:37 -05:00
Florian Stecker 39def44579 switch to using maud for templating 2024-02-25 14:34:29 -05:00
Florian Stecker 38b1f3d040 refactor and add nice styling 2024-02-24 12:25:14 -05:00
Florian Stecker ad3f782c67 split node reading logic off from tree 2024-02-13 20:56:15 -05:00
Florian Stecker ae91f77d02 Use Arc instead of Rc to prepare for shared tree cache 2024-02-13 17:06:18 -05:00
Florian Stecker 80942c8ed3 display items as table rows 2024-02-09 00:17:10 -05:00
Florian Stecker e2fb0cbb47 refactor reading nodes to prepare caching 2024-02-07 09:31:44 -05:00
Florian Stecker 4397a02c4e new flexible search and iterator implementation 2024-02-07 00:24:14 -05:00
19 changed files with 1459 additions and 550 deletions

View File

@ -1,9 +1,8 @@
[package]
name = "parsebtrfs"
version = "0.1.0"
edition = "2021"
[workspace]
resolver = "2"
[dependencies]
binparse_derive = { path = "../binparse_derive" }
memmap2 = "0.7.1"
rouille = "3.6.2"
members = [
"btrfs_explorer_bin",
"btrfs_parse_derive",
"btrfs_explorer",
]

View File

@ -0,0 +1,9 @@
[package]
name = "btrfs_explorer"
version = "0.1.0"
edition = "2021"
[dependencies]
btrfs_parse_derive = { path = "../btrfs_parse_derive" }
maud = "0.26.0"
rouille = "3.6.2"

View File

@ -2,6 +2,7 @@ use std::rc::Rc;
use crate::btrfs_structs::{ParseBin, Key, ChunkItem, Value, Superblock, ParseError, NODE_SIZE};
use crate::btrfs_lookup::Tree;
use crate::nodereader::NodeReader;
#[derive(Debug, Clone)]
pub struct AddressMap(pub Vec<(u64,u64,Vec<(u64,u64)>)>);
@ -11,10 +12,11 @@ impl AddressMap {
pub fn new(image: &[u8]) -> Result<AddressMap, ParseError> {
let superblock = Superblock::parse(&image[0x10000..])?;
let bootstrap_addr = AddressMap::from_superblock(&superblock)?;
let reader = Rc::new(NodeReader::with_addrmap(image, bootstrap_addr)?);
let chunk_tree = Tree {
image: image,
addr_map: Rc::new(bootstrap_addr),
image,
reader,
root_addr_log: superblock.chunk_root,
};
@ -89,7 +91,7 @@ impl LogToPhys for AddressMap {
}
}
/*
pub fn node_at_log<'a, T: LogToPhys>(image: &'a [u8], addr: &T, log: u64) -> Result<&'a [u8], ParseError> {
if let Some(phys_addr) = addr.to_phys(log) {
Ok(&image[phys_addr as usize .. phys_addr as usize + NODE_SIZE])
@ -97,7 +99,16 @@ pub fn node_at_log<'a, T: LogToPhys>(image: &'a [u8], addr: &T, log: u64) -> Res
err!("Logical address {:x} could not be translated to physical address", log)
}
}
*/
pub trait LogToPhys {
fn to_phys(&self, log: u64) -> Option<u64>;
fn node_at_log<'a>(&self, image: &'a [u8], log: u64) -> Result<&'a [u8], ParseError> {
if let Some(phys_addr) = self.to_phys(log) {
Ok(&image[phys_addr as usize .. phys_addr as usize + NODE_SIZE])
} else {
err!("Logical address {:x} could not be translated to physical address", log)
}
}
}

View File

@ -0,0 +1,369 @@
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, LAST_KEY};
use crate::nodereader::NodeReader;
/// Represents a B-Tree inside a filesystem image. Can be used to look up keys,
/// and handles the tree traversal and the virtual address translation.
pub struct Tree<'a> {
pub image: &'a [u8],
pub reader: Rc<NodeReader<'a>>,
pub root_addr_log: u64,
}
impl<'a> Tree<'a> {
pub fn new<T: Into<u64>>(image: &'a [u8], tree_id: T) -> Result<Tree<'a>, ParseError> {
let superblock = Superblock::parse(&image[0x10000..])?;
let reader = Rc::new(NodeReader::new(image)?);
let root_tree = Tree {
image,
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 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 })
}
pub fn root(image: &'a [u8]) -> Result<Tree<'a>, ParseError> {
let reader = Rc::new(NodeReader::new(image)?);
let superblock = Superblock::parse(&image[0x10000..])?;
Ok(Tree { image, reader, root_addr_log: superblock.root })
}
pub fn chunk(image: &'a [u8]) -> Result<Tree<'a>, ParseError> {
let reader = Rc::new(NodeReader::new(image)?);
let superblock = Superblock::parse(&image[0x10000..])?;
Ok(Tree { image, reader, root_addr_log: superblock.chunk_root })
}
}
/***** looking up keys *****/
impl Leaf {
pub fn find_key(&self, key: Key) -> Option<Item> {
self.items
.iter()
.find(|x|x.key == key)
.map(|x|x.clone())
}
pub fn find_key_or_previous(&self, key: Key) -> Option<usize> {
self.items
.iter()
.take_while(|x|x.key <= key)
.enumerate()
.last()
.map(|x|x.0)
}
}
impl InteriorNode {
/// Return the index of the last child which has key at most `key`. This is the
/// 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<usize> {
// 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),
}
}
}
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<Item, ParseError> {
let node = self.reader.get_node(addr)?;
match node.deref() {
Node::Interior(interior_node) => {
let next_node_index = interior_node.find_key_or_previous(key).unwrap();
let next_node_log = interior_node.children[next_node_index].ptr;
self.find_key_in_node(next_node_log, key)
},
Node::Leaf(leaf) => {
leaf.find_key(key).ok_or(
error!(
"Item with key ({},{:?},{}) was not found in the leaf at logical address 0x{:x}",
key.key_id, key.key_type, key.key_offset, addr)
)
}
}
}
pub fn find_key(&self, key: Key) -> Result<Item, ParseError> {
self.find_key_in_node(self.root_addr_log, key)
}
}
/***** iterator *****/
pub struct RangeIter<'a, 'b> {
tree: &'b Tree<'a>,
start: Bound<Key>,
end: Bound<Key>,
forward_skip_fn: Box<dyn Fn(Key) -> Key>,
backward_skip_fn: Box<dyn Fn(Key) -> Key>,
}
impl<'a> Tree<'a> {
/// Given a tree, a range of indices, and two "skip functions", produces a double
/// 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
/// `backward_skip_fn(k)` are skipped.
///
/// If `forward_skip_fn` and `backward_skip_fn` are the identity, nothing is skipped
pub fn range_with_skip<'b, R, F1, F2>(&'b self, range: R, forward_skip_fn: F1, backward_skip_fn: F2) -> RangeIter<'a, 'b>
where
R: RangeBounds<Key>,
F1: Fn(Key) -> Key + 'static,
F2: Fn(Key) -> Key + 'static {
RangeIter {
tree: self,
start: range.start_bound().cloned(),
end: range.end_bound().cloned(),
forward_skip_fn: Box::new(forward_skip_fn),
backward_skip_fn: Box::new(backward_skip_fn),
}
}
pub fn range<'b, R: RangeBounds<Key>>(&'b self, range: R) -> RangeIter<'a, 'b> {
RangeIter {
tree: self,
start: range.start_bound().cloned(),
end: range.end_bound().cloned(),
forward_skip_fn: Box::new(identity),
backward_skip_fn: Box::new(identity),
}
}
pub fn iter<'b>(&'b self) -> RangeIter<'a, 'b> {
RangeIter {
tree: self,
start: Bound::Unbounded,
end: Bound::Unbounded,
forward_skip_fn: Box::new(identity),
backward_skip_fn: Box::new(identity),
}
}
}
/// Get the first item under the node at logical address `addr`.
/// This function panics if there are no items
fn get_first_item(tree: &Tree, addr: u64) -> Result<Item, ParseError> {
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()),
}
}
/// Get the last item under the node at logical address `addr`.
/// This function panics if there are no items
fn get_last_item(tree: &Tree, addr: u64) -> Result<Item, ParseError> {
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()),
}
}
#[derive(Debug,PartialEq,Eq,Clone,Copy)]
enum FindKeyMode {LT, GT, GE, LE}
/// Try to find the item with key `key` if it exists in the tree, and return
/// the "closest" match. The exact meaning of "closest" is given by the `mode` argument:
/// If `mode` is `LT`/`GT`/`GE`/`LE`, return the item with the greatest / least / greatest / least
/// key less than / greater than / greater or equal to / less or equal to `key`.
fn find_closest_key(tree: &Tree, key: Key, mode: FindKeyMode) -> Result<Option<Item>, ParseError> {
// in some cases, this task can't be accomplished by a single traversal
// but we might have to go back up the tree; prev/next allows to quickly go back to the right node
let mut current: u64 = tree.root_addr_log;
let mut prev: Option<u64> = None;
let mut next: Option<u64> = None;
loop {
let node = tree.reader.get_node(current)?;
match node.deref() {
Node::Interior(intnode) => {
match intnode.find_key_or_previous(key) {
Some(idx) => {
if let Some(kp) = (idx > 0).then(|| intnode.children.get(idx-1)).flatten() {
prev = Some(kp.ptr);
}
if let Some(kp) = intnode.children.get(idx+1) {
next = Some(kp.ptr);
}
current = intnode.children[idx].ptr;
},
None => {
// this can only happen if every key in the current node is `> key`
// which really should only happen if we're in the root node, as otherwise
// we wouldn't have descended into this branch; so assume every key in the
// tree is above `> key`.
if mode == FindKeyMode::LT || mode == FindKeyMode::LE {
return Ok(None);
} else {
// return the first item in tree; we are an interior node so we really should have
// at least one child
let addr = intnode.children[0].ptr;
return Ok(Some(get_first_item(tree, addr)?));
}
}
}
},
Node::Leaf(leafnode) => {
match leafnode.find_key_or_previous(key) {
Some(idx) => {
// the standard case, we found a key `k` with the guarantee that `k <= key`
let Item {key: k, value: v} = leafnode.items[idx].clone();
if mode == FindKeyMode::LE || mode == FindKeyMode::LT && k < key || mode == FindKeyMode::GE && k == key {
return Ok(Some(Item {key: k, value: v}))
} else if mode == FindKeyMode::LT && k == key {
// prev
if idx > 0 {
return Ok(Some(leafnode.items[idx-1].clone()));
} else {
// use prev
if let Some(addr) = prev {
return Ok(Some(get_last_item(tree, addr)?));
} else {
return Ok(None);
}
}
} else {
// next
if let Some(item) = leafnode.items.get(idx+1) {
return Ok(Some(item.clone()));
} else {
// use next
if let Some(addr) = next {
return Ok(Some(get_first_item(tree, addr)?));
} else {
return Ok(None);
}
}
}
},
None => {
// same as above, but this can only happen if the root node is a leaf
if mode == FindKeyMode::LT || mode == FindKeyMode::LE {
return Ok(None);
} else {
// return the first item in tree if it exists
return Ok(leafnode.items.get(0).map(|x|x.clone()));
}
},
}
},
}
}
}
fn range_valid<T: Ord>(start: Bound<T>, end: Bound<T>) -> bool {
match (start, end) {
(Bound::Included(x), Bound::Included(y)) => x <= y,
(Bound::Excluded(x), Bound::Included(y)) => x < y,
(Bound::Included(x), Bound::Excluded(y)) => x < y,
(Bound::Excluded(x), Bound::Excluded(y)) => x < y, // could technically be empty if "y = x+1", but we can't check
(_, _) => true, // one of them is unbounded
}
}
impl<'a, 'b> Iterator for RangeIter<'a, 'b> {
type Item = Item;
fn next(&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.start {
&Bound::Included(x) => (x, FindKeyMode::GE),
&Bound::Excluded(x) => (x, FindKeyMode::GT),
&Bound::Unbounded => (ZERO_KEY, FindKeyMode::GE),
};
// FIX: proper error handling
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.start = Bound::Excluded((self.forward_skip_fn)(item.key));
}
let end_filter = |item: &Item| {
match &self.end {
&Bound::Included(x) => item.key <= x,
&Bound::Excluded(x) => item.key < x,
&Bound::Unbounded => true,
}
};
result
.filter(end_filter)
.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

@ -1,5 +1,5 @@
use binparse_derive::AllVariants;
use binparse_derive::ParseBin;
use btrfs_parse_derive::AllVariants;
use btrfs_parse_derive::ParseBin;
use std::fmt;
use std::error;
use std::ffi::CString;
@ -12,11 +12,11 @@ pub const NODE_SIZE: usize = 0x4000;
#[derive(Debug,Clone,Copy,AllVariants,PartialEq,Eq,PartialOrd,Ord)]
#[repr(u8)]
pub enum ItemType {
Invalid = 0x00, // invalid
Invalid = 0x00, // invalid, but seems to exist?
Inode = 0x01, // implemented
Ref = 0x0c, // implemented
ExtRef = 0x0d,
XAttr = 0x18,
XAttr = 0x18, // TODO
VerityDesc = 0x24,
VerityMerkle = 0x25,
Orphan = 0x30,
@ -25,12 +25,12 @@ pub enum ItemType {
Dir = 0x54, // implemented (better with len feature; allow multiple?)
DirIndex = 0x60, // implemented
ExtentData = 0x6c, // implemented
ExtentCsum = 0x80,
ExtentCsum = 0x80, // TODO
Root = 0x84, // implemented
RootBackRef = 0x90,
RootRef = 0x9c,
Extent = 0xa8, // implemented (with only one version of extra data)
Metadata = 0xa9, // implemented (with only one version of extra data)
RootBackRef = 0x90, // implemented
RootRef = 0x9c, // implemented
Extent = 0xa8, // implemented (with only one version of extra data!!)
Metadata = 0xa9, // implemented (with only one version of extra data!!)
TreeBlockRef = 0xb0,
ExtentDataRef = 0xb2,
ExtentRefV0 = 0xb4,
@ -53,6 +53,7 @@ pub enum ItemType {
UUIDSubvol = 0xfb, // implemented
UUIDReceivedSubvol = 0xfc,
String = 0xfd,
InvalidMax = 0xff, // invalid
}
#[allow(unused)]
@ -73,6 +74,9 @@ 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)]
pub enum Value {
@ -90,6 +94,7 @@ pub enum Value {
DevExtent(DevExtentItem),
ExtentData(ExtentDataItem),
Ref(RefItem),
RootRef(RootRefItem),
Unknown(Vec<u8>),
}
@ -304,68 +309,70 @@ pub struct RootItem {
pub otime: Time,
pub stime: Time,
pub rtime: Time,
data: Vec<u8>,
}
#[allow(unused)]
#[derive(Debug,Clone,ParseBin)]
pub struct DirItem {
location: Key,
transid: u64,
data_len: u16,
name_len: u16,
dir_type: u8,
pub location: Key,
pub transid: u64,
pub data_len: u16,
pub name_len: u16,
pub dir_type: u8,
// #[len = "name_len"]
name: CString,
#[len = "name_len"]
pub name: CString,
}
#[allow(unused)]
#[derive(Debug,Clone,ParseBin)]
pub struct FreeSpaceInfoItem {
extent_count: u32,
flags: u32,
pub extent_count: u32,
pub flags: u32,
}
#[allow(unused)]
#[derive(Debug,Clone,ParseBin)]
pub struct UUIDSubvolItem {
subvol_id: u64,
pub subvol_id: u64,
}
#[allow(unused)]
#[derive(Debug,Clone,ParseBin)]
pub struct DevItem {
devid: u64,
total_bytes: u64,
bytes_used: u64,
io_align: u32,
io_width: u32,
sector_size: u32,
dev_type: u64,
generation: u64,
start_offset: u64,
dev_group: u32,
seek_speed: u8,
bandwidth: u8,
uuid: UUID,
fsid: UUID,
pub devid: u64,
pub total_bytes: u64,
pub bytes_used: u64,
pub io_align: u32,
pub io_width: u32,
pub sector_size: u32,
pub dev_type: u64,
pub generation: u64,
pub start_offset: u64,
pub dev_group: u32,
pub seek_speed: u8,
pub bandwidth: u8,
pub uuid: UUID,
pub fsid: UUID,
}
#[allow(unused)]
#[derive(Debug,Clone,ParseBin)]
pub struct DevExtentItem {
chunk_tree: u64,
chunk_objectid: u64,
chunk_offset: u64,
length: u64,
chunk_tree_uuid: UUID,
pub chunk_tree: u64,
pub chunk_objectid: u64,
pub chunk_offset: u64,
pub length: u64,
pub chunk_tree_uuid: UUID,
}
#[allow(unused)]
#[derive(Debug,Clone)]
pub struct ExtentDataItem {
header: ExtentDataHeader,
data: ExtentDataBody,
pub header: ExtentDataHeader,
pub data: ExtentDataBody,
}
#[allow(unused)]
@ -378,31 +385,42 @@ pub enum ExtentDataBody {
#[allow(unused)]
#[derive(Debug,Clone,ParseBin)]
pub struct ExternalExtent {
disk_bytenr: u64,
disk_num_bytes: u64,
offset: u64,
num_bytes: u64,
pub disk_bytenr: u64,
pub disk_num_bytes: u64,
pub offset: u64,
pub num_bytes: u64,
}
#[allow(unused)]
#[derive(Debug,Clone,ParseBin)]
pub struct ExtentDataHeader {
generation: u64,
ram_bytes: u64,
compression: u8,
encryption: u8,
other_encoding: u16,
extent_type: u8,
pub generation: u64,
pub ram_bytes: u64,
pub compression: u8,
pub encryption: u8,
pub other_encoding: u16,
pub extent_type: u8,
}
#[allow(unused)]
#[derive(Debug,Clone,ParseBin)]
pub struct RefItem {
index: u64,
name_len: u16,
pub index: u64,
pub name_len: u16,
// #[len = "name_len"]
name: Vec<u8>,
#[len = "name_len"]
pub name: CString,
}
#[allow(unused)]
#[derive(Debug,Clone,ParseBin)]
pub struct RootRefItem {
pub directory: u64,
pub index: u64,
pub name_len: u16,
#[len = "name_len"]
pub name: CString,
}
#[allow(unused)]
@ -526,7 +544,7 @@ impl ParseBin for CString {
fn parse_len(bytes: &[u8]) -> Result<(Self, usize), ParseError> {
let mut chars = Vec::from(bytes);
chars.push(0);
Ok((CString::from_vec_with_nul(chars).unwrap(), bytes.len()))
Ok((CString::from_vec_with_nul(chars).unwrap_or(CString::new("<invalid string>").unwrap()), bytes.len()))
}
}
@ -543,7 +561,10 @@ impl From<u8> 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
},
}
}
}
@ -554,8 +575,17 @@ impl ParseBin for ItemType {
}
}
fn parse_check_size<T: ParseBin>(bytes: &[u8]) -> Result<T, ParseError> {
let (result, real_len) = T::parse_len(bytes)?;
if real_len != bytes.len() {
eprintln!("{} parsing incomplete! Parsed {} of {} bytes", std::any::type_name::<T>(), real_len, bytes.len());
}
Ok(result)
}
impl ParseBin for Node {
fn parse_len(bytes: &[u8]) -> Result<(Node, usize), ParseError> {
if bytes.len() < 0x65 {
return err!("Not enough data to parse node header");
}
@ -586,40 +616,45 @@ impl ParseBin for Node {
let value = match key.key_type {
ItemType::BlockGroup =>
Value::BlockGroup(BlockGroupItem::parse(data_slice)?),
Value::BlockGroup(parse_check_size(data_slice)?),
ItemType::Metadata => {
let item = ExtentItem::parse(data_slice)?;
let item: ExtentItem = parse_check_size(data_slice)?;
if item.flags != 2 || item.refs > 1 {
println!("Metadata item with refs = {}, flags = {}, data = {:x?}", item.refs, item.flags, &data_slice[0x18..]);
}
Value::Extent(item)
},
ItemType::Extent =>
Value::Extent(ExtentItem::parse(data_slice)?),
Value::Extent(parse_check_size(data_slice)?),
ItemType::Inode =>
Value::Inode(InodeItem::parse(data_slice)?),
Value::Inode(parse_check_size(data_slice)?),
ItemType::Root =>
Value::Root(RootItem::parse(data_slice)?),
Value::Root(parse_check_size(data_slice)?),
ItemType::Dir =>
Value::Dir(DirItem::parse(data_slice)?),
Value::Dir(parse_check_size(data_slice)?),
ItemType::DirIndex =>
Value::DirIndex(DirItem::parse(data_slice)?),
Value::DirIndex(parse_check_size(data_slice)?),
ItemType::Chunk =>
Value::Chunk(ChunkItem::parse(data_slice)?),
Value::Chunk(parse_check_size(data_slice)?),
ItemType::FreeSpaceInfo =>
Value::FreeSpaceInfo(FreeSpaceInfoItem::parse(data_slice)?),
Value::FreeSpaceInfo(parse_check_size(data_slice)?),
ItemType::FreeSpaceExtent =>
Value::FreeSpaceExtent,
ItemType::UUIDSubvol =>
Value::UUIDSubvol(UUIDSubvolItem::parse(data_slice)?),
Value::UUIDSubvol(parse_check_size(data_slice)?),
ItemType::Dev =>
Value::Dev(DevItem::parse(data_slice)?),
Value::Dev(parse_check_size(data_slice)?),
ItemType::DevExtent =>
Value::DevExtent(DevExtentItem::parse(data_slice)?),
Value::DevExtent(parse_check_size(data_slice)?),
ItemType::ExtentData =>
Value::ExtentData(ExtentDataItem::parse(data_slice)?),
ItemType::Ref =>
Value::Ref(RefItem::parse(data_slice)?),
Value::ExtentData(parse_check_size(data_slice)?),
ItemType::Ref => {
Value::Ref(parse_check_size(data_slice)?)
}
ItemType::RootRef =>
Value::RootRef(parse_check_size(data_slice)?),
ItemType::RootBackRef =>
Value::RootRef(parse_check_size(data_slice)?),
_ =>
Value::Unknown(Vec::from(data_slice)),
};
@ -719,6 +754,7 @@ impl fmt::Debug for Checksum {
}
}
#[macro_export]
macro_rules! key {
($arg1:expr) => {
btrfs_structs::Key { key_id: $arg1, key_type: btrfs_structs::ItemType::Invalid, key_offset: 0 }
@ -731,4 +767,4 @@ macro_rules! key {
};
}
pub(crate) use key;
//pub(crate) use key;

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

@ -3,6 +3,11 @@ pub mod util;
pub mod btrfs_structs;
pub mod btrfs_lookup;
pub mod addrmap;
pub mod nodereader;
pub mod http_tree;
pub mod render_common;
pub mod render_tree;
pub mod main_error;
#[cfg(test)]
mod test;

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}"))
}
}

View File

@ -0,0 +1,45 @@
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))
}
println!("Reading node at {:X}", addr);
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
}
}

View File

@ -0,0 +1,38 @@
use maud::Render;
use std::fmt::{Debug, UpperHex};
pub struct DebugRender<T>(pub T);
impl<T: Debug> Render for DebugRender<T> {
fn render_to(&self, w: &mut String) {
format_args!("{0:#?}", self.0).render_to(w);
}
}
pub struct Hex<T>(pub T);
impl<T: UpperHex> Render for Hex<T> {
fn render_to(&self, w: &mut String) {
format_args!("{0:X}", self.0).render_to(w);
}
}
pub fn size_name(x: u64) -> String {
if x == 0 {
format!("0 B")
} else if x % (1<<10) != 0 {
format!("{} B", x)
} else if x % (1<<20) != 0 {
format!("{} KiB", x / (1<<10))
} else if x % (1<<30) != 0 {
format!("{} MiB", x / (1<<20))
} else if x % (1<<40) != 0 {
format!("{} GiB", x / (1<<30))
} else if x % (1<<50) != 0 {
format!("{} TiB", x / (1<<40))
} else if x % (1<<60) != 0 {
format!("{} PiB", x / (1<<50))
} else {
format!("{} EiB", x / (1<<60))
}
}

View File

@ -0,0 +1,290 @@
use crate::btrfs_structs::{Item, Key, ItemType, Value, ExtentDataBody};
use crate::render_common::{Hex, size_name};
use maud::{Markup, html, DOCTYPE, PreEscaped};
#[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,
}
pub fn render_table(table: TableResult) -> Markup {
let header: String = if let Some(desc) = table.tree_desc {
format!("Tree {} ({})", table.tree_id, desc)
} else {
format!("Tree {}", table.tree_id)
};
let key_input_value = table.key_id.map_or(String::new(), |x| format!("{:X}", x));
let first_key_url = format!("/tree/{}",
table.tree_id);
let prev_key_url = format!("/tree/{}/to/{:016X}-{:02X}-{:016X}",
table.tree_id,
table.first_key.key_id,
u8::from(table.first_key.key_type),
table.first_key.key_offset);
let next_key_url = format!("/tree/{}/from/{:016X}-{:02X}-{:016X}",
table.tree_id,
table.last_key.key_id,
u8::from(table.last_key.key_type),
table.first_key.key_offset);
let last_key_url = format!("/tree/{}/to/{:016X}-{:02X}-{:016X}",
table.tree_id,
u64::wrapping_sub(0,1),
u8::wrapping_sub(0,1),
u64::wrapping_sub(0,1));
let mut rows: Vec<Markup> = Vec::new();
for &(it, _it_data) in table.items.iter() {
let highlighted = if table.key_id.filter(|x|*x == it.key.key_id).is_some() { "highlight" } else { "" };
let value_string = item_value_string(table.tree_id, it);
let details_string = item_details_string(table.tree_id, it);
let raw_string = format!("{:#?}", &it.value);
let id_desc = row_id_desc(it.key, table.tree_id);
rows.push(html! {
details.item.(highlighted) {
summary {
span.key.key_id.(key_type_class(it.key)) {
(id_desc.0)
}
span.key.key_type.(key_type_class(it.key)) {
(id_desc.1)
}
span.key.key_offset.(key_type_class(it.key)) {
(id_desc.2)
}
span.itemvalue.(key_type_class(it.key)) {
(&value_string)
}
}
div.details {
(&details_string)
details {
summary {
"show full value"
}
pre {
(&raw_string)
}
}
}
}
});
}
// the complete page
html! {
(DOCTYPE)
head {
link rel="stylesheet" href="/style.css";
}
body {
h1 {
(header)
}
@if table.tree_id != 1 {
a href="/tree/1" {
"go back to root tree"
}
}
form method="get" action={"/tree/" (table.tree_id)} {
input type="text" name="key" value=(key_input_value);
input type="submit" value="Search";
}
a.nav href=(first_key_url) { div.nav { "first" } }
a.nav href=(prev_key_url) { div.nav { "prev" } }
@for row in &rows { (row) }
a.nav href=(next_key_url) { div.nav { "next" } }
a.nav href=(last_key_url) { div.nav { "last" } }
}
}
}
fn key_type_class(key: Key) -> &'static str {
match key.key_type {
ItemType::Inode => "inode",
ItemType::Ref => "ref",
ItemType::RootRef => "ref",
ItemType::RootBackRef => "ref",
ItemType::ExtentData => "extent",
ItemType::Dir => "dir",
ItemType::DirIndex => "dir",
ItemType::Root => "root",
_ => "",
}
}
fn row_id_desc(key: Key, tree_id: u64) -> (Markup, Markup, Markup) {
let x = format!("{:X}", key.key_id);
let y = format!("{:?} ({:02X})", key.key_type, u8::from(key.key_type));
let z = if key.key_type == ItemType::RootRef || key.key_type == ItemType::Ref {
format!("<a href=\"/tree/{}/{:X}\">{:X}</a>", tree_id, key.key_offset, key.key_offset)
} else {
format!("{:X}", key.key_offset)
};
(PreEscaped(x),PreEscaped(y),PreEscaped(z))
}
fn item_value_string(tree_id: u64, item: &Item) -> Markup {
match &item.value {
Value::Root(_) => {
html! { a href={"/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);
let id = dir_item.location.key_id;
html! {
(name)
" @ "
a href=(format!("/tree/{tree_id}/{id:x}")) {
(Hex(id))
}
}
},
Value::Inode(inode_item) => {
let file_type = match inode_item.mode / (1<<12) {
4 => "directory",
2 => "character device",
6 => "block device",
8 => "regular file",
1 => "FIFO",
10 => "symbolic link",
12 => "socket",
_ => "unknown file type",
};
format_escape!("{}, mode {}{}{}{}", file_type,
(inode_item.mode / (1<<9)) % 8,
(inode_item.mode / (1<<6)) % 8,
(inode_item.mode / (1<<3)) % 8,
(inode_item.mode / (1<<0)) % 8)
},
Value::ExtentData(extent_data_item) =>
match &extent_data_item.data {
ExtentDataBody::Inline(data) =>
PreEscaped(format!("inline, length {}", size_name(data.len() as u64))),
ExtentDataBody::External(ext_extent) =>
PreEscaped(format!("external, length {}", size_name(ext_extent.num_bytes))),
},
Value::Ref(ref_item) =>
html! { (format!("{:?}", &ref_item.name)) },
Value::RootRef(ref_item) =>
html! { (format!("{:?}", &ref_item.name)) },
Value::Extent(extent_item) =>
PreEscaped(format!("flags: {}, block_refs: {:?}", extent_item.flags, extent_item.block_refs)),
Value::BlockGroup(blockgroup_item) =>
PreEscaped(format!("{} used", size_name(blockgroup_item.used))),
Value::DevExtent(dev_extent_item) =>
PreEscaped(format!("chunk_tree: {}, chunk_offset: {:x}, length: {}", dev_extent_item.chunk_tree, dev_extent_item.chunk_offset, size_name(dev_extent_item.length))),
Value::UUIDSubvol(uuid_subvol_item) =>
PreEscaped(format!("subvolume id: {}", uuid_subvol_item.subvol_id)),
Value::FreeSpaceInfo(free_space_info) =>
PreEscaped(format!("extent_count: {}, flags: {}", free_space_info.extent_count, free_space_info.flags)),
Value::Dev(dev_item) =>
PreEscaped(format!("total_bytes: {}", size_name(dev_item.total_bytes))),
Value::Chunk(chunk_item) =>
PreEscaped(format!("size: {}", size_name(chunk_item.size))),
_ => {
// println!("{:?} {:?}", item.key, item.valu);
PreEscaped(String::new())
},
}
}
fn item_details_string(_tree_id: u64, item: &Item) -> Markup {
match &item.value {
Value::Inode(inode_item) => {
html! { table { tbody {
tr { td { "size" } td { (inode_item.size) } }
tr { td { "mode" } td { (inode_item.mode) } }
tr { td { "uid" } td { (inode_item.uid) } }
tr { td { "gid" } td { (inode_item.gid) } }
tr { td { "nlink" } td { (inode_item.nlink) } }
tr { td { "atime" } td { (inode_item.atime.sec) } }
tr { td { "ctime" } td { (inode_item.ctime.sec) } }
tr { td { "mtime" } td { (inode_item.mtime.sec) } }
tr { td { "otime" } td { (inode_item.otime.sec) } }
}}}
},
Value::ExtentData(extent_item) => {
match &extent_item.data {
ExtentDataBody::Inline(_data) => {
html! {} // we really want data as string / hex
},
ExtentDataBody::External(ext_extent) => {
html! {
p {
@if ext_extent.disk_bytenr == 0 {
(size_name(ext_extent.num_bytes)) " of zeros."
} @ else {
(format!("{} on disk, starting at offset {:X} within the extent at address {:X}; {} in the file starting from offset {:X}.", size_name(ext_extent.disk_num_bytes), ext_extent.offset, ext_extent.disk_bytenr, size_name(ext_extent.num_bytes), item.key.key_offset))
}
}
table { tbody {
tr { td { "compression" } td { (extent_item.header.compression) } }
tr { td { "encryption" } td { (extent_item.header.encryption) } }
tr { td { "other_encoding" } td { (extent_item.header.other_encoding) } }
}}
}
},
}
},
Value::Ref(ref_item) => {
html! { table { tbody {
tr { td { "name" } td { (format!("{:?}", ref_item.name)) } }
tr { td { "index" } td { (ref_item.index) } }
}}}
},
Value::Dir(dir_item) | Value::DirIndex(dir_item) => {
html! { table { tbody {
tr { td { "name" } td { (format!("{:?}", dir_item.name)) } }
}}}
},
Value::Root(root_item) => {
html! { table { tbody {
tr { td { "root dir id" } td { (format!("{:X}", root_item.root_dirid)) } }
tr { td { "logical address" } td { (format!("{:X}", root_item.bytenr)) } }
tr { td { "bytes used" } td { (size_name(root_item.bytes_used)) } }
tr { td { "last snapshot" } td { (root_item.last_snapshot) } }
tr { td { "flags" } td { (root_item.flags) } }
tr { td { "refs" } td { (root_item.refs) } }
tr { td { "level" } td { (root_item.level) } }
tr { td { "UUID" } td { (format!("{:?}", root_item.uuid)) } }
tr { td { "parent UUID" } td { (format!("{:?}", root_item.parent_uuid)) } }
tr { td { "received UUID" } td { (format!("{:?}", root_item.received_uuid)) } }
tr { td { "ctransid" } td { (root_item.ctransid) } }
tr { td { "otransid" } td { (root_item.otransid) } }
tr { td { "stransid" } td { (root_item.stransid) } }
tr { td { "rtransid" } td { (root_item.rtransid) } }
tr { td { "ctime" } td { (root_item.ctime.sec) } }
tr { td { "otime" } td { (root_item.otime.sec) } }
tr { td { "stime" } td { (root_item.stime.sec) } }
tr { td { "rtime" } td { (root_item.rtime.sec) } }
}}}
},
Value::RootRef(root_ref_item) => {
html! { table { tbody {
tr { td { "name" } td { (format!("{:?}", root_ref_item.name)) } }
tr { td { "directory" } td { (root_ref_item.directory) } }
tr { td { "index" } td { (root_ref_item.index) } }
}}}
},
_ => {
html! {}
},
}
}

View File

@ -5,3 +5,9 @@ macro_rules! error {
macro_rules! err {
($($i:expr),*) => { Err(error!($($i),*)) };
}
macro_rules! format_escape {
($($arg:tt)*) => {
html! { (format!($($arg)*)) }
};
}

View File

@ -0,0 +1,12 @@
[package]
name = "btrfs_explorer_bin"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
btrfs_explorer = { path = "../btrfs_explorer" }
memmap2 = "0.7.1"
maud = "0.26.0"
rouille = "3.6.2"

View File

@ -1,14 +1,13 @@
use std::{
iter,
env,
fs::OpenOptions,
collections::HashMap,
collections::HashMap, env, fs::{File, OpenOptions}, iter,
};
use memmap2::Mmap;
use memmap2::MmapOptions;
use rouille::{Request, Response, router};
use parsebtrfs::{
btrfs_structs::{TreeID, Value::Extent, Value::BlockGroup, ParseError, NODE_SIZE, ItemType},
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"];
@ -16,12 +15,13 @@ 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(());
@ -37,8 +37,21 @@ fn main() -> Result<(), MainError> {
rouille::start_server("127.0.0.1:8080", move |request| {
router!(
request,
(GET) ["/"] => http_main_boxes(&image, 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(),
)
});
@ -46,17 +59,9 @@ fn main() -> Result<(), MainError> {
static CIRCLE_IMAGE: &str =
"data:image/png;base64,\
iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9\
kT1Iw0AcxV9bpUUqInYo4pChOtnFLxxrFYpQIdQKrTqYXPoFTRqSFBdHwbXg4Mdi1cHFWVcHV0EQ\
/ABxdXFSdJES/5cUWsR4cNyPd/ced+8Af7PKVLMnAaiaZWRSSSGXXxWCrwhhEAFEMS0xU58TxTQ8\
x9c9fHy9i/Ms73N/jn6lYDLAJxAnmG5YxBvEM5uWznmfOMLKkkJ8Tjxu0AWJH7kuu/zGueSwn2dG\
jGxmnjhCLJS6WO5iVjZU4inimKJqlO/Puaxw3uKsVuusfU/+wnBBW1nmOs0RpLCIJYgQIKOOCqqw\
EKdVI8VEhvaTHv5hxy+SSyZXBYwcC6hBheT4wf/gd7dmcXLCTQongd4X2/4YBYK7QKth29/Htt06\
AQLPwJXW8deawOwn6Y2OFjsCBraBi+uOJu8BlztA9EmXDMmRAjT9xSLwfkbflAeGboG+Nbe39j5O\
H4AsdZW+AQ4OgbESZa97vDvU3du/Z9r9/QChS3K5hXof0gAAAAZiS0dEAP8A/wD/oL2nkwAAAAlw\
SFlzAAAuIwAALiMBeKU/dgAAAAd0SU1FB+cIEQMcKM7EsV8AAAA/SURBVBjTY2CgGfj//7/8////\
J/3///8NFE/6//+/PDaFk/5jgknYFL7BovANTJ6JWKchK1yGRX4Z+Z6hGgAAmotacR/hRugAAAAA\
SUVORK5CYII=";
iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAP0lEQVQY02NgoBn4//+//P///yf9\
////DRRP+v//vzw2hZP+Y4JJ2BS+waLwDUyeiVinIStchkV+GfmeoRoAAJqLWnEf4UboAAAAAElF\
TkSuQmCC";
static EXPLANATION_TEXT: &str = "\
<h3>Chunks</h3>
@ -207,6 +212,7 @@ fn http_main_boxes(image: &[u8], _req: &Request) -> Response {
};
// 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",
@ -226,8 +232,8 @@ fn http_main_boxes(image: &[u8], _req: &Request) -> Response {
0x04 => "Metadata",
_ => "???",
},
match extent_tree.addr_map.as_ref().0.binary_search_by_key(&bg.key.key_id, |x|x.0) {
Ok(i) => format!("{:x?}", &extent_tree.addr_map.as_ref().0[i].2),
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(""),
}
)
@ -265,149 +271,3 @@ fn http_main_boxes(image: &[u8], _req: &Request) -> Response {
Response::html(result)
}
// ----- 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
}
*/

View File

@ -0,0 +1,14 @@
[package]
name = "btrfs_parse_derive"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
proc-macro2 = "1.0.66"
quote = "1.0.32"
syn = "2.0.27"
[lib]
proc-macro = true

View File

@ -0,0 +1,178 @@
use quote::{quote, format_ident};
use proc_macro2::Span;
use proc_macro::TokenStream;
use syn::{DeriveInput, Data::Enum, parse_macro_input};
#[proc_macro_derive(AllVariants)]
pub fn derive_all_variants(input: TokenStream) -> TokenStream {
let syn_item: DeriveInput = parse_macro_input!(input);
let variants = match syn_item.data {
Enum(enum_item) => {
enum_item.variants.into_iter().map(|v|v.ident)
},
_ => panic!("AllVariants only works on enums!"),
};
let enum_name = syn_item.ident;
let expanded = quote! {
impl #enum_name {
fn all_variants() -> &'static[#enum_name] {
&[ #(#enum_name::#variants),* ]
}
}
};
expanded.into()
}
#[proc_macro_derive(ParseBin, attributes(skip_bytes, len))]
pub fn derive_parse_bin(input: TokenStream) -> TokenStream {
let syn_item: DeriveInput = parse_macro_input!(input);
let name = syn_item.ident;
match syn_item.data {
syn::Data::Struct(struct_item) => {
match struct_item.fields {
syn::Fields::Named(fields_named) => {
derive_parse_bin_struct(&name, &fields_named.named)
},
syn::Fields::Unnamed(fields_unnamed) => {
if fields_unnamed.unnamed.len() != 1 {
panic!("ParseBin does not support tuple structs!");
}
let inner_type = fields_unnamed.unnamed.into_iter().next().unwrap().ty;
derive_parse_bin_alias(name, inner_type)
},
_ => panic!("ParseBin on unit structs makes no sense!"),
}
},
_ => panic!("ParseBin only works on structs so far!"),
}
}
fn derive_parse_bin_alias(name: syn::Ident, ty: syn::Type) -> TokenStream {
quote! {
impl ParseBin for #name {
fn parse_len(bytes: &[u8]) -> Result<(Self, usize), ParseError> {
let (result, size) = <#ty>::parse_len(bytes)?;
Ok((#name(result), size))
}
}
}.into()
}
fn derive_parse_bin_struct<'a, T>(name: &syn::Ident, fields: T) -> TokenStream
where T: IntoIterator<Item = &'a syn::Field>
{
let mut parsing_statements = Vec::new();
let mut combining_expressions = Vec::new();
for field in fields {
let field_name = field.ident.as_ref().unwrap();
let field_type = &field.ty;
let mut skip: Option<usize> = None;
let mut veclen: Option<String> = None;
// look for attributes
for at in &field.attrs {
if let syn::Meta::NameValue(nv) = &at.meta {
if nv.path.segments.len() == 1 {
let attr_name = nv.path.segments[0].ident.to_string();
if attr_name == "skip_bytes" {
if let syn::Expr::Lit(expr) = &nv.value {
if let syn::Lit::Int(nbytes) = &expr.lit {
// println!("reserved = {}", nbytes);
skip = nbytes.base10_parse::<usize>().ok()
}
}
} else if attr_name == "len" {
if let syn::Expr::Lit(expr) = &nv.value {
if let syn::Lit::Str(litstr) = &expr.lit {
// println!("len = {}", litstr.value());
veclen = Some(litstr.value());
}
}
}
}
}
}
if let Some(offset) = skip {
parsing_statements.push(quote!{
__parse_bin_derive_size += #offset;
});
}
if let Some(varname) = veclen {
let field_name_item = format_ident!("{}_item", field_name);
enum FieldType<'a> {
Vec(&'a syn::Type),
CString,
}
let syn::Type::Path(tp) = &field_type else { panic!() };
let single = tp.path.segments.iter().next().unwrap();
let field_type = if &single.ident.to_string() == "Vec" {
let syn::PathArguments::AngleBracketed(args) = &single.arguments else { panic!() };
let firstarg = args.args.iter().next().unwrap();
let syn::GenericArgument::Type(ty) = firstarg else { panic!() };
FieldType::Vec(ty)
} else if &single.ident.to_string() == "CString" {
FieldType::CString
} else {
panic!("The len attribute is only allowed on Vec<_> or CString")
};
let varname_ident = syn::Ident::new(&varname, Span::call_site());
match field_type {
FieldType::Vec(field_type_item) => {
parsing_statements.push(quote!{
let mut #field_name = Vec::new();
for i in 0 .. #varname_ident.0 as usize {
let #field_name_item = <#field_type_item>::parse_len(&bytes[__parse_bin_derive_size..])?;
__parse_bin_derive_size += #field_name_item.1;
#field_name.push(#field_name_item.0);
}
});
combining_expressions.push(quote!(#field_name: #field_name));
},
FieldType::CString => {
parsing_statements.push(quote!{
let #field_name = CString::parse_len(&bytes[__parse_bin_derive_size .. __parse_bin_derive_size + #varname_ident.0 as usize])?;
__parse_bin_derive_size += #varname_ident.0 as usize;
});
combining_expressions.push(quote!(#field_name: #field_name.0));
},
}
} else {
parsing_statements.push(quote!{
let #field_name = <#field_type>::parse_len(&bytes[__parse_bin_derive_size..])?;
__parse_bin_derive_size += #field_name.1;
});
combining_expressions.push(quote!(#field_name: #field_name.0));
}
}
quote! {
impl ParseBin for #name {
fn parse_len(bytes: &[u8]) -> Result<(Self, usize), ParseError> {
let mut __parse_bin_derive_size: usize = 0;
#(#parsing_statements)*
let result = #name {
#(#combining_expressions),*
};
Ok((result, __parse_bin_derive_size))
}
}
}.into()
}

View File

@ -1,297 +0,0 @@
use std::rc::Rc;
use std::ops::{Deref, RangeBounds};
use crate::btrfs_structs::{Leaf, Key, Item, InteriorNode, Node, ParseError, ParseBin, Value, Superblock, ItemType};
use crate::addrmap::{node_at_log, LogToPhys, AddressMap};
/// represents a B-Tree inside a filesystem image. Can be used to look up keys,
/// and handles the tree traversal and the virtual address translation.
pub struct Tree<'a> {
pub image: &'a [u8],
pub addr_map: Rc<AddressMap>,
pub root_addr_log: u64,
}
impl<'a> Tree<'a> {
pub fn new<T: Into<u64>>(image: &'a [u8], tree_id: T) -> Result<Tree<'a>, ParseError> {
let addr_map = Rc::new(AddressMap::new(image)?);
let superblock = Superblock::parse(&image[0x10000..])?;
let root_tree = Tree {
image,
addr_map: Rc::clone(&addr_map),
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())
};
Ok(Tree { image, addr_map, root_addr_log })
}
pub fn root(image: &'a [u8]) -> Result<Tree<'a>, ParseError> {
let addr_map = Rc::new(AddressMap::new(image)?);
let superblock = Superblock::parse(&image[0x10000..])?;
Ok(Tree { image, addr_map, root_addr_log: superblock.root })
}
}
/***** looking up keys *****/
impl Leaf {
pub fn find_key(&self, key: Key) -> Option<Item> {
self.items
.iter()
.find(|x|x.key == key)
.map(|x|x.clone())
}
pub fn find_key_or_previous(&self, key: Key) -> Option<Item> {
self.items
.iter()
.take_while(|x|x.key <= key)
.last()
.map(|x|x.clone())
}
}
impl InteriorNode {
pub fn find_key_or_previous(&self, key: Key) -> Option<u64> {
self.children
.iter()
.take_while(|x|x.key <= key)
.last()
.map(|x|x.ptr)
}
}
fn find_key_in_node<T: LogToPhys>(image: &[u8], addr: &T, root_addr_log: u64, key: Key) -> Result<Item, ParseError> {
let node = Node::parse(node_at_log(image, addr, root_addr_log)?)?;
match node {
Node::Interior(interior_node) => {
let next_node_log = interior_node.find_key_or_previous(key).unwrap();
find_key_in_node(image, addr, next_node_log, key)
},
Node::Leaf(leaf) => {
leaf.find_key(key).ok_or(
error!(
"Item with key ({},{:?},{}) was not found in the leaf at logical address 0x{:x}",
key.key_id, key.key_type, key.key_offset, root_addr_log)
)
}
}
}
impl Tree<'_> {
pub fn find_key(&self, key: Key) -> Result<Item, ParseError> {
find_key_in_node(self.image, self.addr_map.deref(), self.root_addr_log, key)
}
}
/***** iterator *****/
pub struct RangeIter<'a, R: RangeBounds<Key>, F: Fn(Key) -> Key = fn(Key) -> Key> {
tree: &'a Tree<'a>,
// path to the last returned item
nodes: Vec<InteriorNode>,
leaf: Option<Box<Leaf>>,
indices: Vec<usize>,
bounds: R,
skip_fn: F,
}
impl Tree<'_> {
pub fn iter<'a>(&'a self) -> RangeIter<'a> {
self.range(None, None)
}
pub fn range<'a>(&'a self, lower: Option<Key>, upper: Option<Key>) -> RangeIter<'a> {
RangeIter {
tree: self,
nodes: Vec::new(),
leaf: None,
indices: Vec::new(), // in nodes and leaf
lower_limit: lower,
upper_limit: upper,
skip_fn: |x|x
}
}
pub fn range_id<'a>(&'a self, id: u64) -> RangeIter<'a> {
if id == u64::MAX {
self.range(
Some(Key::new(id, ItemType::Invalid, 0)),
None
)
} else {
self.range(
Some(Key::new(id, ItemType::Invalid, 0)),
Some(Key::new(id+1, ItemType::Invalid, 0))
)
}
}
/// given a tree, a range of indices, and two "skip functions", produces a double
/// 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
/// `backward_skip_fn` are skipped.
pub fn range_with_skip<'a, R, F>(&'a self, range: R, forward_skip_fn: F, backward_skip_fn: F) -> RangeIter<'a, F>
where
R: RangeBounds<Key>,
F: Fn(Key) -> Key {
RangeIter {
tree: self,
nodes: Vec::new(),
leaf: None,
indices: Vec::new(),
}
}
}
impl<F: Fn(Key) -> Key> RangeIter<'_, F> {
fn move_down_and_get_first_item(&mut self, mut node_addr: u64) -> Option<Item> {
loop {
let node = Node::parse(node_at_log(self.tree.image, self.tree.addr_map.deref(), node_addr).ok()?).ok()?;
match node {
Node::Interior(int_node) => {
node_addr = int_node.children.first()?.ptr;
self.nodes.push(int_node);
self.indices.push(0);
},
Node::Leaf(leaf_node) => {
let result = leaf_node.items.first()?.clone();
self.leaf = Some(Box::new(leaf_node));
self.indices.push(0);
return Some(result);
},
}
}
}
fn move_down_and_get_item_or_previous(&mut self, mut node_addr: u64, key: Key) -> Option<Item> {
loop {
let node = Node::parse(node_at_log(self.tree.image, self.tree.addr_map.deref(), node_addr).ok()?).ok()?;
match node {
Node::Interior(int_node) => {
let (i, new_node_ptr) = int_node
.children
.iter()
.enumerate()
.take_while(|(_,bp)|bp.key <= key)
.last()?;
node_addr = new_node_ptr.ptr;
self.nodes.push(int_node);
self.indices.push(i);
},
Node::Leaf(leaf_node) => {
let (i, result) = leaf_node
.items
.iter()
.enumerate()
.take_while(|(_,item)|item.key <= key)
.last()?;
let result_cloned = result.clone();
self.leaf = Some(Box::new(leaf_node));
self.indices.push(i);
return Some(result_cloned);
},
}
}
}
}
impl<F: Fn(Key) -> Key> Iterator for RangeIter<'_, F> {
type Item = Item;
// for now we just silently stop when we encounter an error, maybe that isn't the best solution
fn next(&mut self) -> Option<Item> {
if self.leaf.is_none() && self.nodes.len() == 0 {
// first item
// finding the first item is a bit tricky
// if there is a lower limit, the B+ tree only allows us to either find the item
// or the previous one if there is no exact match; in the latter case, go one further
let result = if let Some(lim) = self.lower_limit {
let first_res = self.move_down_and_get_item_or_previous(self.tree.root_addr_log, lim);
if let Some(item) = first_res {
if item.key == lim {
// found exactly the limit, that's the easy case
Some(item)
} else {
// found a previous item; so we want the next one
self.next()
}
} else {
// did not find an item, so everything must come after lower limit
// just get the first
self.move_down_and_get_first_item(self.tree.root_addr_log)
}
} else {
// there is no lower limit, so also just get the first
self.move_down_and_get_first_item(self.tree.root_addr_log)
};
result.filter(|item|self.upper_limit.is_none() || item.key < self.upper_limit.unwrap())
} else if self.leaf.is_none() {
// already through the iterator
return None;
} else {
let height = self.indices.len(); // must be at least 1
let leaf = self.leaf.as_ref().unwrap();
self.indices[height-1] += 1;
if let Some(item) = leaf.items.get(self.indices[height-1]) {
// there's a next item in the same leaf
if self.upper_limit.is_none() || item.key < self.upper_limit.unwrap() {
return Some(item.clone());
} else {
return None;
}
} else if height == 1 {
// the tree has height 1 and we're through the (only) leaf, there's nothing left
return None;
} else {
// try to advance in one of the higher nodes
self.leaf = None;
self.indices.pop();
let mut level = height - 2;
// go up until we can move forward in a node
let node_addr = loop {
let node = &self.nodes[level];
self.indices[level] += 1;
if let Some(blockptr) = node.children.get(self.indices[level]) {
break blockptr.ptr;
} else {
if level == 0 {
return None;
}
self.indices.pop();
self.nodes.pop();
level -= 1;
}
};
// first first item under this node
return self.move_down_and_get_first_item(node_addr)
.filter(|item|self.upper_limit.is_none() || item.key < self.upper_limit.unwrap())
}
}
}
}

153
style.css Normal file
View File

@ -0,0 +1,153 @@
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;
border-radius: 4px;
margin: 5px 0;
overflow: hidden;
text-align: center;
}
a.nav {
text-decoration: none;
}
details.item {
padding: 3px;
background-color: #dde;
border-radius: 4px;
margin: 3px 0;
overflow: hidden;
}
a {
color: black;
}
details.highlight {
background-color: #bbc;
}
details .details {
color: black;
// background-color: #222;
padding: 10px;
margin-top: 5px;
border-radius: 4px;
}
details .itemvalue {
color: black;
padding: 3px;
margin: 1px 2px;
width: auto;
display: inline-block;
}
details .key {
color: white;
background-color: #999;
border-radius: 4px;
padding: 3px;
margin: 1px 2px;
display: inline-block;
font-family: monospace;
font-size: 12pt;
}
details .key a {
color: white;
}
span.key_id {
min-width: 160px;
text-align: right;
}
span.key_type {
min-width: 160px;
}
span.key_offset {
min-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;
}
.details table {
border-collapse: collapse;
margin-bottom: 10px;
}
.details td {
border: 1px solid black;
}
.details td:first-child {
border: 1px solid black;
width: 160px;
}
.details p {
padding: 0;
margin: 5px 0;
}