fully implement ExtentItem, hex data, node addresses, and some other details
This commit is contained in:
parent
b41547ddcb
commit
053a8ff77f
@ -2,7 +2,7 @@ use std::convert::identity;
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::ops::{Deref, RangeBounds, Bound};
|
use std::ops::{Deref, RangeBounds, Bound};
|
||||||
|
|
||||||
use crate::btrfs_structs::{Leaf, Key, Item, InteriorNode, Node, ParseError, ParseBin, Value, Superblock, ItemType, ZERO_KEY, LAST_KEY};
|
use crate::btrfs_structs::{DirItem, InteriorNode, Item, ItemType, Key, Leaf, Node, ParseBin, ParseError, Superblock, Value, LAST_KEY, ZERO_KEY};
|
||||||
use crate::nodereader::NodeReader;
|
use crate::nodereader::NodeReader;
|
||||||
|
|
||||||
/// Represents a B-Tree inside a filesystem image. Can be used to look up keys,
|
/// Represents a B-Tree inside a filesystem image. Can be used to look up keys,
|
||||||
@ -32,7 +32,7 @@ impl<'a> Tree<'a> {
|
|||||||
.filter(|x| x.key.key_id == tree_id && x.key.key_type == ItemType::Root);
|
.filter(|x| x.key.key_id == tree_id && x.key.key_type == ItemType::Root);
|
||||||
|
|
||||||
let root_addr_log = match tree_root_item {
|
let root_addr_log = match tree_root_item {
|
||||||
Some(Item { key: _, value: Value::Root(root)}) => root.bytenr,
|
Some(Item { key: _, range: _, value: Value::Root(root)}) => root.bytenr,
|
||||||
_ => return Err("root item not found or invalid".into())
|
_ => return Err("root item not found or invalid".into())
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -119,7 +119,7 @@ impl Tree<'_> {
|
|||||||
|
|
||||||
/***** iterator *****/
|
/***** iterator *****/
|
||||||
|
|
||||||
pub struct RangeIter<'a, 'b> {
|
pub struct RangeIterWithAddr<'a, 'b> {
|
||||||
tree: &'b Tree<'a>,
|
tree: &'b Tree<'a>,
|
||||||
|
|
||||||
start: Bound<Key>,
|
start: Bound<Key>,
|
||||||
@ -128,6 +128,8 @@ pub struct RangeIter<'a, 'b> {
|
|||||||
backward_skip_fn: Box<dyn Fn(Key) -> Key>,
|
backward_skip_fn: Box<dyn Fn(Key) -> Key>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct RangeIter<'a, 'b> (RangeIterWithAddr<'a, 'b>);
|
||||||
|
|
||||||
impl<'a> Tree<'a> {
|
impl<'a> Tree<'a> {
|
||||||
/// Given a tree, a range of indices, and two "skip functions", produces a double
|
/// 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
|
/// ended iterator which iterates through the keys contained in the range, in ascending
|
||||||
@ -144,33 +146,36 @@ impl<'a> Tree<'a> {
|
|||||||
R: RangeBounds<Key>,
|
R: RangeBounds<Key>,
|
||||||
F1: Fn(Key) -> Key + 'static,
|
F1: Fn(Key) -> Key + 'static,
|
||||||
F2: Fn(Key) -> Key + 'static {
|
F2: Fn(Key) -> Key + 'static {
|
||||||
RangeIter {
|
RangeIter(RangeIterWithAddr {
|
||||||
tree: self,
|
tree: self,
|
||||||
start: range.start_bound().cloned(),
|
start: range.start_bound().cloned(),
|
||||||
end: range.end_bound().cloned(),
|
end: range.end_bound().cloned(),
|
||||||
forward_skip_fn: Box::new(forward_skip_fn),
|
forward_skip_fn: Box::new(forward_skip_fn),
|
||||||
backward_skip_fn: Box::new(backward_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> {
|
pub fn iter<'b>(&'b self) -> RangeIter<'a, 'b> {
|
||||||
RangeIter {
|
RangeIter(RangeIterWithAddr {
|
||||||
tree: self,
|
tree: self,
|
||||||
start: Bound::Unbounded,
|
start: Bound::Unbounded,
|
||||||
end: Bound::Unbounded,
|
end: Bound::Unbounded,
|
||||||
forward_skip_fn: Box::new(identity),
|
forward_skip_fn: Box::new(identity),
|
||||||
backward_skip_fn: Box::new(identity),
|
backward_skip_fn: Box::new(identity),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn range<'b, R: RangeBounds<Key>>(&'b self, range: R) -> RangeIter<'a, 'b> {
|
||||||
|
RangeIter(self.range_with_node_addr(range))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn range_with_node_addr<'b, R: RangeBounds<Key>>(&'b self, range: R) -> RangeIterWithAddr<'a, 'b> {
|
||||||
|
RangeIterWithAddr {
|
||||||
|
tree: self,
|
||||||
|
start: range.start_bound().cloned(),
|
||||||
|
end: range.end_bound().cloned(),
|
||||||
|
forward_skip_fn: Box::new(identity),
|
||||||
|
backward_skip_fn: Box::new(identity),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -178,19 +183,19 @@ impl<'a> Tree<'a> {
|
|||||||
|
|
||||||
/// Get the first item under the node at logical address `addr`.
|
/// Get the first item under the node at logical address `addr`.
|
||||||
/// This function panics if there are no items
|
/// This function panics if there are no items
|
||||||
fn get_first_item(tree: &Tree, addr: u64) -> Result<Item, ParseError> {
|
fn get_first_item(tree: &Tree, addr: u64) -> Result<(Item, u64), ParseError> {
|
||||||
match tree.reader.get_node(addr)?.deref() {
|
match tree.reader.get_node(addr)?.deref() {
|
||||||
Node::Interior(intnode) => get_first_item(tree, intnode.children[0].ptr),
|
Node::Interior(intnode) => get_first_item(tree, intnode.children[0].ptr),
|
||||||
Node::Leaf(leafnode) => Ok(leafnode.items[0].clone()),
|
Node::Leaf(leafnode) => Ok((leafnode.items[0].clone(), addr)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the last item under the node at logical address `addr`.
|
/// Get the last item under the node at logical address `addr`.
|
||||||
/// This function panics if there are no items
|
/// This function panics if there are no items
|
||||||
fn get_last_item(tree: &Tree, addr: u64) -> Result<Item, ParseError> {
|
fn get_last_item(tree: &Tree, addr: u64) -> Result<(Item, u64), ParseError> {
|
||||||
match tree.reader.get_node(addr)?.deref() {
|
match tree.reader.get_node(addr)?.deref() {
|
||||||
Node::Interior(intnode) => get_last_item(tree, intnode.children.last().unwrap().ptr),
|
Node::Interior(intnode) => get_last_item(tree, intnode.children.last().unwrap().ptr),
|
||||||
Node::Leaf(leafnode) => Ok(leafnode.items.last().unwrap().clone()),
|
Node::Leaf(leafnode) => Ok((leafnode.items.last().unwrap().clone(), addr)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,7 +206,7 @@ enum FindKeyMode {LT, GT, GE, LE}
|
|||||||
/// the "closest" match. The exact meaning of "closest" is given by the `mode` argument:
|
/// 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
|
/// 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`.
|
/// 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> {
|
fn find_closest_key(tree: &Tree, key: Key, mode: FindKeyMode) -> Result<Option<(Item, u64)>, ParseError> {
|
||||||
|
|
||||||
// in some cases, this task can't be accomplished by a single traversal
|
// 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
|
// but we might have to go back up the tree; prev/next allows to quickly go back to the right node
|
||||||
@ -244,14 +249,14 @@ fn find_closest_key(tree: &Tree, key: Key, mode: FindKeyMode) -> Result<Option<I
|
|||||||
match leafnode.find_key_or_previous(key) {
|
match leafnode.find_key_or_previous(key) {
|
||||||
Some(idx) => {
|
Some(idx) => {
|
||||||
// the standard case, we found a key `k` with the guarantee that `k <= key`
|
// the standard case, we found a key `k` with the guarantee that `k <= key`
|
||||||
let Item {key: k, value: v} = leafnode.items[idx].clone();
|
let it = leafnode.items[idx].clone();
|
||||||
|
|
||||||
if mode == FindKeyMode::LE || mode == FindKeyMode::LT && k < key || mode == FindKeyMode::GE && k == key {
|
if mode == FindKeyMode::LE || mode == FindKeyMode::LT && it.key < key || mode == FindKeyMode::GE && it.key == key {
|
||||||
return Ok(Some(Item {key: k, value: v}))
|
return Ok(Some((it, current)))
|
||||||
} else if mode == FindKeyMode::LT && k == key {
|
} else if mode == FindKeyMode::LT && it.key == key {
|
||||||
// prev
|
// prev
|
||||||
if idx > 0 {
|
if idx > 0 {
|
||||||
return Ok(Some(leafnode.items[idx-1].clone()));
|
return Ok(Some((leafnode.items[idx-1].clone(), current)));
|
||||||
} else {
|
} else {
|
||||||
// use prev
|
// use prev
|
||||||
if let Some(addr) = prev {
|
if let Some(addr) = prev {
|
||||||
@ -263,7 +268,7 @@ fn find_closest_key(tree: &Tree, key: Key, mode: FindKeyMode) -> Result<Option<I
|
|||||||
} else {
|
} else {
|
||||||
// next
|
// next
|
||||||
if let Some(item) = leafnode.items.get(idx+1) {
|
if let Some(item) = leafnode.items.get(idx+1) {
|
||||||
return Ok(Some(item.clone()));
|
return Ok(Some((item.clone(), current)));
|
||||||
} else {
|
} else {
|
||||||
// use next
|
// use next
|
||||||
if let Some(addr) = next {
|
if let Some(addr) = next {
|
||||||
@ -280,7 +285,7 @@ fn find_closest_key(tree: &Tree, key: Key, mode: FindKeyMode) -> Result<Option<I
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
} else {
|
} else {
|
||||||
// return the first item in tree if it exists
|
// return the first item in tree if it exists
|
||||||
return Ok(leafnode.items.get(0).map(|x|x.clone()));
|
return Ok(leafnode.items.get(0).map(|x|(x.clone(), current)));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -299,10 +304,10 @@ fn range_valid<T: Ord>(start: Bound<T>, end: Bound<T>) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b> Iterator for RangeIter<'a, 'b> {
|
impl<'a, 'b> Iterator for RangeIterWithAddr<'a, 'b> {
|
||||||
type Item = Item;
|
type Item = (Item, u64);
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
if !range_valid(self.start.as_ref(), self.end.as_ref()) {
|
if !range_valid(self.start.as_ref(), self.end.as_ref()) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@ -317,11 +322,11 @@ impl<'a, 'b> Iterator for RangeIter<'a, 'b> {
|
|||||||
let result = find_closest_key(self.tree, start_key, mode)
|
let result = find_closest_key(self.tree, start_key, mode)
|
||||||
.expect("file system should be consistent (or this is a bug)");
|
.expect("file system should be consistent (or this is a bug)");
|
||||||
|
|
||||||
if let Some(item) = &result {
|
if let Some((item, _)) = &result {
|
||||||
self.start = Bound::Excluded((self.forward_skip_fn)(item.key));
|
self.start = Bound::Excluded((self.forward_skip_fn)(item.key));
|
||||||
}
|
}
|
||||||
|
|
||||||
let end_filter = |item: &Item| {
|
let end_filter = |(item, _): &(Item, u64)| {
|
||||||
match &self.end {
|
match &self.end {
|
||||||
&Bound::Included(x) => item.key <= x,
|
&Bound::Included(x) => item.key <= x,
|
||||||
&Bound::Excluded(x) => item.key < x,
|
&Bound::Excluded(x) => item.key < x,
|
||||||
@ -335,8 +340,8 @@ impl<'a, 'b> Iterator for RangeIter<'a, 'b> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b> DoubleEndedIterator for RangeIter<'a, 'b> {
|
impl<'a, 'b> DoubleEndedIterator for RangeIterWithAddr<'a, 'b> {
|
||||||
fn next_back(&mut self) -> Option<Item> {
|
fn next_back(&mut self) -> Option<Self::Item> {
|
||||||
if !range_valid(self.start.as_ref(), self.end.as_ref()) {
|
if !range_valid(self.start.as_ref(), self.end.as_ref()) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@ -350,11 +355,11 @@ impl<'a, 'b> DoubleEndedIterator for RangeIter<'a, 'b> {
|
|||||||
let result = find_closest_key(self.tree, start_key, mode)
|
let result = find_closest_key(self.tree, start_key, mode)
|
||||||
.expect("file system should be consistent (or this is a bug)");
|
.expect("file system should be consistent (or this is a bug)");
|
||||||
|
|
||||||
if let Some(item) = &result {
|
if let Some((item,_)) = &result {
|
||||||
self.end = Bound::Excluded((self.backward_skip_fn)(item.key));
|
self.end = Bound::Excluded((self.backward_skip_fn)(item.key));
|
||||||
}
|
}
|
||||||
|
|
||||||
let start_filter = |item: &Item| {
|
let start_filter = |(item, _): &(Item, u64)| {
|
||||||
match &self.start {
|
match &self.start {
|
||||||
&Bound::Included(x) => item.key >= x,
|
&Bound::Included(x) => item.key >= x,
|
||||||
&Bound::Excluded(x) => item.key > x,
|
&Bound::Excluded(x) => item.key > x,
|
||||||
@ -367,3 +372,17 @@ impl<'a, 'b> DoubleEndedIterator for RangeIter<'a, 'b> {
|
|||||||
.map(|item|item.clone())
|
.map(|item|item.clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> Iterator for RangeIter<'a, 'b> {
|
||||||
|
type Item = Item;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
self.0.next().map(|x|x.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> DoubleEndedIterator for RangeIter<'a, 'b> {
|
||||||
|
fn next_back(&mut self) -> Option<Self::Item> {
|
||||||
|
self.0.next_back().map(|x|x.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
use btrfs_parse_derive::AllVariants;
|
use btrfs_parse_derive::AllVariants;
|
||||||
use btrfs_parse_derive::ParseBin;
|
use btrfs_parse_derive::ParseBin;
|
||||||
|
use std::any::TypeId;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::error;
|
use std::error;
|
||||||
use std::ffi::CString;
|
use std::ffi::CString;
|
||||||
|
|
||||||
|
|
||||||
/***** BTRFS structures *****/
|
/***** BTRFS structures *****/
|
||||||
|
|
||||||
pub const NODE_SIZE: usize = 0x4000;
|
pub const NODE_SIZE: usize = 0x4000;
|
||||||
@ -29,13 +31,13 @@ pub enum ItemType {
|
|||||||
Root = 0x84, // implemented
|
Root = 0x84, // implemented
|
||||||
RootBackRef = 0x90, // implemented
|
RootBackRef = 0x90, // implemented
|
||||||
RootRef = 0x9c, // implemented
|
RootRef = 0x9c, // implemented
|
||||||
Extent = 0xa8, // implemented (with only one version of extra data!!)
|
Extent = 0xa8, // implemented
|
||||||
Metadata = 0xa9, // implemented (with only one version of extra data!!)
|
Metadata = 0xa9, // implemented
|
||||||
TreeBlockRef = 0xb0,
|
TreeBlockRef = 0xb0, // implemented (inside ExtentItem)
|
||||||
ExtentDataRef = 0xb2,
|
ExtentDataRef = 0xb2, // implemented (inside ExtentItem)
|
||||||
ExtentRefV0 = 0xb4,
|
ExtentRefV0 = 0xb4,
|
||||||
SharedBlockRef = 0xb6,
|
SharedBlockRef = 0xb6, // implemented (inside ExtentItem)
|
||||||
SharedDataRef = 0xb8,
|
SharedDataRef = 0xb8, // implemented (inside ExtentItem)
|
||||||
BlockGroup = 0xc0, // implemented
|
BlockGroup = 0xc0, // implemented
|
||||||
FreeSpaceInfo = 0xc6, // implemented
|
FreeSpaceInfo = 0xc6, // implemented
|
||||||
FreeSpaceExtent = 0xc7, // implemented
|
FreeSpaceExtent = 0xc7, // implemented
|
||||||
@ -93,8 +95,8 @@ pub enum Value {
|
|||||||
Dev(DevItem),
|
Dev(DevItem),
|
||||||
DevExtent(DevExtentItem),
|
DevExtent(DevExtentItem),
|
||||||
ExtentData(ExtentDataItem),
|
ExtentData(ExtentDataItem),
|
||||||
Ref(RefItem),
|
Ref(Vec<RefItem>),
|
||||||
RootRef(RootRefItem),
|
RootRef(Vec<RootRefItem>),
|
||||||
Unknown(Vec<u8>),
|
Unknown(Vec<u8>),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,6 +105,7 @@ pub enum Value {
|
|||||||
#[derive(Debug,Clone)]
|
#[derive(Debug,Clone)]
|
||||||
pub struct Item {
|
pub struct Item {
|
||||||
pub key: Key,
|
pub key: Key,
|
||||||
|
pub range: (u32, u32), // start and end offset within node
|
||||||
pub value: Value,
|
pub value: Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,7 +217,7 @@ pub struct BlockGroupItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
#[derive(Debug,Clone)]
|
#[derive(Debug,Clone,ParseBin)]
|
||||||
pub struct ExtentItem {
|
pub struct ExtentItem {
|
||||||
pub refs: u64,
|
pub refs: u64,
|
||||||
pub generation: u64,
|
pub generation: u64,
|
||||||
@ -222,9 +225,16 @@ pub struct ExtentItem {
|
|||||||
// pub data: Vec<u8>,
|
// pub data: Vec<u8>,
|
||||||
|
|
||||||
// this is only correct if flags == 2, fix later!
|
// this is only correct if flags == 2, fix later!
|
||||||
pub block_refs: Vec<(ItemType, u64)>,
|
pub block_refs: Vec<BlockRef>,
|
||||||
// pub tree_block_key_type: ItemType,
|
}
|
||||||
// pub tree_block_key_id: u64,
|
|
||||||
|
#[allow(unused)]
|
||||||
|
#[derive(Debug,Clone)]
|
||||||
|
pub enum BlockRef {
|
||||||
|
Tree { id: u64, },
|
||||||
|
ExtentData { root: u64, id: u64, offset: u64, count: u32, },
|
||||||
|
SharedData { offset: u64, count: u32, },
|
||||||
|
SharedBlockRef { offset: u64 },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
@ -534,12 +544,31 @@ impl<const N: usize> ParseBin for [u8; N] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// we use Vec<u8> for "unknown extra data", so just eat up everything
|
// we use Vec<u8> for "unknown extra data", so just eat up everything
|
||||||
|
|
||||||
impl ParseBin for Vec<u8> {
|
impl ParseBin for Vec<u8> {
|
||||||
fn parse_len(bytes: &[u8]) -> Result<(Self, usize), ParseError> {
|
fn parse_len(bytes: &[u8]) -> Result<(Self, usize), ParseError> {
|
||||||
Ok((Vec::from(bytes), bytes.len()))
|
Ok((Vec::from(bytes), bytes.len()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trait ParseBinVecFallback: ParseBin { }
|
||||||
|
impl ParseBinVecFallback for BlockRef { }
|
||||||
|
|
||||||
|
impl<T: ParseBinVecFallback> ParseBin for Vec<T> {
|
||||||
|
fn parse_len(bytes: &[u8]) -> Result<(Self, usize), ParseError> {
|
||||||
|
let mut result: Vec<T> = Vec::new();
|
||||||
|
let mut offset: usize = 0;
|
||||||
|
|
||||||
|
while offset < bytes.len() {
|
||||||
|
let (item, len) = <T as ParseBin>::parse_len(&bytes[offset..])?;
|
||||||
|
result.push(item);
|
||||||
|
offset += len;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((result, bytes.len()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ParseBin for CString {
|
impl ParseBin for CString {
|
||||||
fn parse_len(bytes: &[u8]) -> Result<(Self, usize), ParseError> {
|
fn parse_len(bytes: &[u8]) -> Result<(Self, usize), ParseError> {
|
||||||
let mut chars = Vec::from(bytes);
|
let mut chars = Vec::from(bytes);
|
||||||
@ -617,14 +646,7 @@ impl ParseBin for Node {
|
|||||||
let value = match key.key_type {
|
let value = match key.key_type {
|
||||||
ItemType::BlockGroup =>
|
ItemType::BlockGroup =>
|
||||||
Value::BlockGroup(parse_check_size(data_slice)?),
|
Value::BlockGroup(parse_check_size(data_slice)?),
|
||||||
ItemType::Metadata => {
|
ItemType::Extent | ItemType::Metadata =>
|
||||||
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(parse_check_size(data_slice)?),
|
Value::Extent(parse_check_size(data_slice)?),
|
||||||
ItemType::Inode =>
|
ItemType::Inode =>
|
||||||
Value::Inode(parse_check_size(data_slice)?),
|
Value::Inode(parse_check_size(data_slice)?),
|
||||||
@ -649,17 +671,32 @@ impl ParseBin for Node {
|
|||||||
ItemType::ExtentData =>
|
ItemType::ExtentData =>
|
||||||
Value::ExtentData(parse_check_size(data_slice)?),
|
Value::ExtentData(parse_check_size(data_slice)?),
|
||||||
ItemType::Ref => {
|
ItemType::Ref => {
|
||||||
Value::Ref(parse_check_size(data_slice)?)
|
let mut result: Vec<RefItem> = vec![];
|
||||||
|
let mut item_offset = 0;
|
||||||
|
|
||||||
|
while item_offset < data_slice.len() {
|
||||||
|
let (item, len) = RefItem::parse_len(&data_slice[item_offset..])?;
|
||||||
|
result.push(item);
|
||||||
|
item_offset += len;
|
||||||
}
|
}
|
||||||
ItemType::RootRef =>
|
Value::Ref(result)
|
||||||
Value::RootRef(parse_check_size(data_slice)?),
|
}
|
||||||
ItemType::RootBackRef =>
|
ItemType::RootRef | ItemType::RootBackRef => {
|
||||||
Value::RootRef(parse_check_size(data_slice)?),
|
let mut result: Vec<RootRefItem> = vec![];
|
||||||
|
let mut item_offset = 0;
|
||||||
|
|
||||||
|
while item_offset < data_slice.len() {
|
||||||
|
let (item, len) = RootRefItem::parse_len(&data_slice[item_offset..])?;
|
||||||
|
result.push(item);
|
||||||
|
item_offset += len;
|
||||||
|
}
|
||||||
|
Value::RootRef(result)
|
||||||
|
},
|
||||||
_ =>
|
_ =>
|
||||||
Value::Unknown(Vec::from(data_slice)),
|
Value::Unknown(Vec::from(data_slice)),
|
||||||
};
|
};
|
||||||
|
|
||||||
items.push(Item { key, value });
|
items.push(Item { key, range: (0x65 + offset, 0x65 + offset + size), value });
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((Node::Leaf(Leaf { header, items }), NODE_SIZE))
|
Ok((Node::Leaf(Leaf { header, items }), NODE_SIZE))
|
||||||
@ -691,18 +728,19 @@ impl ParseBin for ExtentDataItem {
|
|||||||
let (header, header_size) = ExtentDataHeader::parse_len(bytes)?;
|
let (header, header_size) = ExtentDataHeader::parse_len(bytes)?;
|
||||||
if header.extent_type == 1 { // external extent
|
if header.extent_type == 1 { // external extent
|
||||||
let (body, body_size) = ExternalExtent::parse_len(&bytes[header_size..])?;
|
let (body, body_size) = ExternalExtent::parse_len(&bytes[header_size..])?;
|
||||||
return Ok((ExtentDataItem { header: header, data: ExtentDataBody::External(body)},
|
Ok((ExtentDataItem { header, data: ExtentDataBody::External(body)},
|
||||||
header_size + body_size))
|
header_size + body_size))
|
||||||
} else { // inline extent
|
} else { // inline extent
|
||||||
let data_slice = &bytes[header_size..];
|
let data_slice = &bytes[header_size..];
|
||||||
return Ok((ExtentDataItem {
|
Ok((ExtentDataItem {
|
||||||
header: header,
|
header,
|
||||||
data: ExtentDataBody::Inline(Vec::from(data_slice))
|
data: ExtentDataBody::Inline(Vec::from(data_slice))
|
||||||
}, header_size + data_slice.len()))
|
}, header_size + data_slice.len()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
impl ParseBin for ExtentItem {
|
impl ParseBin for ExtentItem {
|
||||||
fn parse_len(bytes: &[u8]) -> Result<(Self, usize), ParseError> {
|
fn parse_len(bytes: &[u8]) -> Result<(Self, usize), ParseError> {
|
||||||
let refs = u64::parse(bytes)?;
|
let refs = u64::parse(bytes)?;
|
||||||
@ -722,6 +760,35 @@ impl ParseBin for ExtentItem {
|
|||||||
Ok((ExtentItem { refs, generation, flags, block_refs }, 0x18 + refs as usize * 0x09))
|
Ok((ExtentItem { refs, generation, flags, block_refs }, 0x18 + refs as usize * 0x09))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
impl ParseBin for BlockRef {
|
||||||
|
fn parse_len(bytes: &[u8]) -> Result<(Self, usize), ParseError> {
|
||||||
|
match ItemType::parse(bytes)? {
|
||||||
|
ItemType::ExtentDataRef => {
|
||||||
|
let root = u64::parse(&bytes[0x01..])?;
|
||||||
|
let id = u64::parse(&bytes[0x09..])?;
|
||||||
|
let offset = u64::parse(&bytes[0x11..])?;
|
||||||
|
let count = u32::parse(&bytes[0x19..])?;
|
||||||
|
Ok((BlockRef::ExtentData { root, id, offset, count }, 0x1d))
|
||||||
|
},
|
||||||
|
ItemType::SharedDataRef => {
|
||||||
|
let offset = u64::parse(&bytes[0x01..])?;
|
||||||
|
let count = u32::parse(&bytes[0x09..])?;
|
||||||
|
Ok((BlockRef::SharedData { offset, count }, 0x0d))
|
||||||
|
},
|
||||||
|
ItemType::TreeBlockRef => {
|
||||||
|
let id = u64::parse(&bytes[0x01..])?;
|
||||||
|
Ok((BlockRef::Tree { id }, 0x09))
|
||||||
|
},
|
||||||
|
ItemType::SharedBlockRef => {
|
||||||
|
let offset = u64::parse(&bytes[0x01..])?;
|
||||||
|
Ok((BlockRef::SharedBlockRef { offset }, 0x09))
|
||||||
|
}
|
||||||
|
x => err!("unknown block ref type: {:?}", x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/***** prettier debug output for UUIDs and checksums *****/
|
/***** prettier debug output for UUIDs and checksums *****/
|
||||||
|
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
use std::str::FromStr;
|
use std::{
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
use rouille::{Request, Response};
|
use rouille::{Request, Response};
|
||||||
use crate::{
|
use crate::{
|
||||||
btrfs_structs::{ItemType, Item, Key, ZERO_KEY, LAST_KEY},
|
btrfs_structs::{ItemType, Item, Key, ZERO_KEY, LAST_KEY},
|
||||||
@ -18,41 +20,45 @@ enum TreeDisplayMode {
|
|||||||
|
|
||||||
|
|
||||||
fn http_tree_internal(tree: &Tree, tree_id: u64, mode: TreeDisplayMode) -> Response {
|
fn http_tree_internal(tree: &Tree, tree_id: u64, mode: TreeDisplayMode) -> Response {
|
||||||
let mut items: Vec<Item>;
|
let mut items: Vec<(Item, u64)>;
|
||||||
let mut highlighted_key_id: Option<u64> = None;
|
let mut highlighted_key_id: Option<u64> = None;
|
||||||
|
|
||||||
match mode {
|
match mode {
|
||||||
TreeDisplayMode::Highlight(key_id, before, after) => {
|
TreeDisplayMode::Highlight(key_id, before, after) => {
|
||||||
let key = Key {key_id, key_type: ItemType::Invalid, key_offset: 0 };
|
let key = Key {key_id, key_type: ItemType::Invalid, key_offset: 0 };
|
||||||
items = tree.range(..key).rev().take(before).collect();
|
items = tree.range_with_node_addr(..key).rev().take(before).collect();
|
||||||
items.reverse();
|
items.reverse();
|
||||||
items.extend(tree.range(key..).take(after));
|
items.extend(tree.range_with_node_addr(key..).take(after));
|
||||||
highlighted_key_id = Some(key_id);
|
highlighted_key_id = Some(key_id);
|
||||||
},
|
},
|
||||||
TreeDisplayMode::From(key, num_lines) => {
|
TreeDisplayMode::From(key, num_lines) => {
|
||||||
items = tree.range(key..).take(num_lines).collect();
|
items = tree.range_with_node_addr(key..).take(num_lines).collect();
|
||||||
if items.len() < num_lines {
|
if items.len() < num_lines {
|
||||||
items.reverse();
|
items.reverse();
|
||||||
items.extend(tree.range(..key).rev().take(num_lines - items.len()));
|
items.extend(tree.range_with_node_addr(..key).rev().take(num_lines - items.len()));
|
||||||
items.reverse();
|
items.reverse();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
TreeDisplayMode::To(key, num_lines) => {
|
TreeDisplayMode::To(key, num_lines) => {
|
||||||
items = tree.range(..key).rev().take(num_lines).collect();
|
items = tree.range_with_node_addr(..key).rev().take(num_lines).collect();
|
||||||
items.reverse();
|
items.reverse();
|
||||||
if items.len() < num_lines {
|
if items.len() < num_lines {
|
||||||
items.extend(tree.range(key..).take(num_lines - items.len()));
|
items.extend(tree.range_with_node_addr(key..).take(num_lines - items.len()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let data_slice = |item: &Item, node_addr: u64| -> &[u8] {
|
||||||
|
tree.reader.get_raw_data(node_addr, item.range.0, item.range.1).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
let table_result = TableResult {
|
let table_result = TableResult {
|
||||||
tree_id,
|
tree_id,
|
||||||
tree_desc: root_key_desc(tree_id).map(|x|x.to_string()),
|
tree_desc: root_key_desc(tree_id).map(|x|x.to_string()),
|
||||||
key_id: highlighted_key_id,
|
key_id: highlighted_key_id,
|
||||||
items: items.iter().map(|it|(it,&[] as &[u8])).collect(),
|
items: items.iter().map(|(it, addr)|(it, *addr, data_slice(it, *addr))).collect(),
|
||||||
first_key: items.first().map(|it|it.key).unwrap_or(LAST_KEY),
|
first_key: items.first().map(|x|x.0.key).unwrap_or(LAST_KEY),
|
||||||
last_key: items.last().map(|it|it.key).unwrap_or(ZERO_KEY),
|
last_key: items.last().map(|x|x.0.key).unwrap_or(ZERO_KEY),
|
||||||
};
|
};
|
||||||
|
|
||||||
Response::html(render_table(table_result))
|
Response::html(render_table(table_result))
|
||||||
|
@ -2,6 +2,7 @@ use std::{
|
|||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
|
time::Instant,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::btrfs_structs::{Node, ParseError, ParseBin};
|
use crate::btrfs_structs::{Node, ParseError, ParseBin};
|
||||||
@ -29,16 +30,25 @@ impl<'a> NodeReader<'a> {
|
|||||||
return Ok(Arc::clone(node))
|
return Ok(Arc::clone(node))
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("Reading node at {:X}", addr);
|
let start_time = Instant::now();
|
||||||
|
|
||||||
let node_data = self.addr_map.node_at_log(self.image, addr)?;
|
let node_data = self.addr_map.node_at_log(self.image, addr)?;
|
||||||
let node = Arc::new(Node::parse(node_data)?);
|
let node = Arc::new(Node::parse(node_data)?);
|
||||||
|
|
||||||
self.cache.borrow_mut().insert(addr, Arc::clone(&node));
|
self.cache.borrow_mut().insert(addr, Arc::clone(&node));
|
||||||
|
|
||||||
|
let t = Instant::now().duration_since(start_time);
|
||||||
|
|
||||||
|
println!("Read node {:X} in {:?}", addr, t);
|
||||||
|
|
||||||
Ok(node)
|
Ok(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_raw_data(&self, addr: u64, start: u32, end: u32) -> Result<&'a [u8], ParseError> {
|
||||||
|
let node_data = self.addr_map.node_at_log(self.image, addr)?;
|
||||||
|
Ok(&node_data[start as usize .. end as usize])
|
||||||
|
}
|
||||||
|
|
||||||
pub fn addr_map(&self) -> &AddressMap {
|
pub fn addr_map(&self) -> &AddressMap {
|
||||||
&self.addr_map
|
&self.addr_map
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
use crate::btrfs_structs::{Item, Key, ItemType, Value, ExtentDataBody};
|
use crate::btrfs_structs::{Item, Key, ItemType, Value, ExtentDataBody};
|
||||||
use crate::render_common::{Hex, size_name};
|
use crate::render_common::{Hex, size_name};
|
||||||
use maud::{Markup, html, DOCTYPE, PreEscaped};
|
use maud::{Markup, html, DOCTYPE, PreEscaped};
|
||||||
|
use std::ffi::CStr;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct TableResult<'a> {
|
pub struct TableResult<'a> {
|
||||||
pub tree_id: u64,
|
pub tree_id: u64,
|
||||||
pub tree_desc: Option<String>,
|
pub tree_desc: Option<String>,
|
||||||
pub key_id: Option<u64>,
|
pub key_id: Option<u64>,
|
||||||
pub items: Vec<(&'a Item, &'a [u8])>,
|
pub items: Vec<(&'a Item, u64, &'a [u8])>, // item, node addr, data
|
||||||
pub first_key: Key,
|
pub first_key: Key,
|
||||||
pub last_key: Key,
|
pub last_key: Key,
|
||||||
}
|
}
|
||||||
@ -42,12 +43,13 @@ pub fn render_table(table: TableResult) -> Markup {
|
|||||||
|
|
||||||
let mut rows: Vec<Markup> = Vec::new();
|
let mut rows: Vec<Markup> = Vec::new();
|
||||||
|
|
||||||
for &(it, _it_data) in table.items.iter() {
|
for &(it, node_addr, it_data) in table.items.iter() {
|
||||||
let highlighted = if table.key_id.filter(|x|*x == it.key.key_id).is_some() { "highlight" } else { "" };
|
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 value_string = item_value_string(table.tree_id, it);
|
||||||
let details_string = item_details_string(table.tree_id, it);
|
let details_string = item_details_string(table.tree_id, it);
|
||||||
let raw_string = format!("{:#?}", &it.value);
|
let raw_string = format!("{:#?}", &it.value);
|
||||||
let id_desc = row_id_desc(it.key, table.tree_id);
|
let id_desc = row_id_desc(it.key, table.tree_id);
|
||||||
|
let hex_data: String = it_data.iter().map(|x|format!("{:02X} ", x)).collect();
|
||||||
|
|
||||||
rows.push(html! {
|
rows.push(html! {
|
||||||
details.item.(highlighted) {
|
details.item.(highlighted) {
|
||||||
@ -64,6 +66,9 @@ pub fn render_table(table: TableResult) -> Markup {
|
|||||||
span.itemvalue.(key_type_class(it.key)) {
|
span.itemvalue.(key_type_class(it.key)) {
|
||||||
(&value_string)
|
(&value_string)
|
||||||
}
|
}
|
||||||
|
span.nodeaddr {
|
||||||
|
(Hex(node_addr))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
div.details {
|
div.details {
|
||||||
@ -77,6 +82,15 @@ pub fn render_table(table: TableResult) -> Markup {
|
|||||||
(&raw_string)
|
(&raw_string)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
details {
|
||||||
|
summary {
|
||||||
|
"show hex data"
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
(&hex_data)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -151,10 +165,17 @@ fn item_value_string(tree_id: u64, item: &Item) -> Markup {
|
|||||||
html! {
|
html! {
|
||||||
(name)
|
(name)
|
||||||
" @ "
|
" @ "
|
||||||
|
@if dir_item.location.key_type == ItemType::Root {
|
||||||
|
a href=(format!("/tree/{id}")) {
|
||||||
|
"subvolume "
|
||||||
|
(Hex(id))
|
||||||
|
}
|
||||||
|
} @else {
|
||||||
a href=(format!("/tree/{tree_id}/{id:x}")) {
|
a href=(format!("/tree/{tree_id}/{id:x}")) {
|
||||||
(Hex(id))
|
(Hex(id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Value::Inode(inode_item) => {
|
Value::Inode(inode_item) => {
|
||||||
let file_type = match inode_item.mode / (1<<12) {
|
let file_type = match inode_item.mode / (1<<12) {
|
||||||
@ -180,12 +201,17 @@ fn item_value_string(tree_id: u64, item: &Item) -> Markup {
|
|||||||
ExtentDataBody::External(ext_extent) =>
|
ExtentDataBody::External(ext_extent) =>
|
||||||
PreEscaped(format!("external, length {}", size_name(ext_extent.num_bytes))),
|
PreEscaped(format!("external, length {}", size_name(ext_extent.num_bytes))),
|
||||||
},
|
},
|
||||||
Value::Ref(ref_item) =>
|
Value::Ref(ref_item) => {
|
||||||
html! { (format!("{:?}", &ref_item.name)) },
|
let names: Vec<&CStr> = ref_item.iter().map(|x|x.name.as_ref()).collect();
|
||||||
Value::RootRef(ref_item) =>
|
html! { (format!("{:?}", &names)) }
|
||||||
html! { (format!("{:?}", &ref_item.name)) },
|
|
||||||
|
},
|
||||||
|
Value::RootRef(ref_item) => {
|
||||||
|
let names: Vec<&CStr> = ref_item.iter().map(|x|x.name.as_ref()).collect();
|
||||||
|
html! { (format!("{:?}", &names)) }
|
||||||
|
},
|
||||||
Value::Extent(extent_item) =>
|
Value::Extent(extent_item) =>
|
||||||
PreEscaped(format!("flags: {}, block_refs: {:?}", extent_item.flags, extent_item.block_refs)),
|
PreEscaped(format!("flags: {}, block_refs: {:X?}", extent_item.flags, extent_item.block_refs)),
|
||||||
Value::BlockGroup(blockgroup_item) =>
|
Value::BlockGroup(blockgroup_item) =>
|
||||||
PreEscaped(format!("{} used", size_name(blockgroup_item.used))),
|
PreEscaped(format!("{} used", size_name(blockgroup_item.used))),
|
||||||
Value::DevExtent(dev_extent_item) =>
|
Value::DevExtent(dev_extent_item) =>
|
||||||
@ -245,13 +271,14 @@ fn item_details_string(_tree_id: u64, item: &Item) -> Markup {
|
|||||||
},
|
},
|
||||||
Value::Ref(ref_item) => {
|
Value::Ref(ref_item) => {
|
||||||
html! { table { tbody {
|
html! { table { tbody {
|
||||||
tr { td { "name" } td { (format!("{:?}", ref_item.name)) } }
|
tr { td { "name" } td { (format!("{:?}", ref_item[0].name)) } }
|
||||||
tr { td { "index" } td { (ref_item.index) } }
|
tr { td { "index" } td { (ref_item[0].index) } }
|
||||||
}}}
|
}}}
|
||||||
},
|
},
|
||||||
Value::Dir(dir_item) | Value::DirIndex(dir_item) => {
|
Value::Dir(dir_item) | Value::DirIndex(dir_item) => {
|
||||||
html! { table { tbody {
|
html! { table { tbody {
|
||||||
tr { td { "name" } td { (format!("{:?}", dir_item.name)) } }
|
tr { td { "name" } td { (format!("{:?}", dir_item.name)) } }
|
||||||
|
tr { td { "target key" } td { (format!("{:X} {:?} {:X}", dir_item.location.key_id, dir_item.location.key_type, dir_item.location.key_offset)) } }
|
||||||
}}}
|
}}}
|
||||||
},
|
},
|
||||||
Value::Root(root_item) => {
|
Value::Root(root_item) => {
|
||||||
@ -278,9 +305,9 @@ fn item_details_string(_tree_id: u64, item: &Item) -> Markup {
|
|||||||
},
|
},
|
||||||
Value::RootRef(root_ref_item) => {
|
Value::RootRef(root_ref_item) => {
|
||||||
html! { table { tbody {
|
html! { table { tbody {
|
||||||
tr { td { "name" } td { (format!("{:?}", root_ref_item.name)) } }
|
tr { td { "name" } td { (format!("{:?}", root_ref_item[0].name)) } }
|
||||||
tr { td { "directory" } td { (root_ref_item.directory) } }
|
tr { td { "directory" } td { (root_ref_item[0].directory) } }
|
||||||
tr { td { "index" } td { (root_ref_item.index) } }
|
tr { td { "index" } td { (root_ref_item[0].index) } }
|
||||||
}}}
|
}}}
|
||||||
},
|
},
|
||||||
_ => {
|
_ => {
|
||||||
|
@ -171,7 +171,7 @@ fn http_main_boxes(image: &[u8], _req: &Request) -> Response {
|
|||||||
if let Some(met) = &mut metadata_items {
|
if let Some(met) = &mut metadata_items {
|
||||||
let bg_start = bg_item.key.key_id;
|
let bg_start = bg_item.key.key_id;
|
||||||
let node_addr = item.key.key_id;
|
let node_addr = item.key.key_id;
|
||||||
let tree_id = e.block_refs.iter().filter(|&(t,_)|t == &ItemType::TreeBlockRef).count() as u64;
|
let tree_id = e.block_refs.iter().count() as u64;
|
||||||
let index = (node_addr - bg_start) as usize / NODE_SIZE;
|
let index = (node_addr - bg_start) as usize / NODE_SIZE;
|
||||||
if index < met.len() {
|
if index < met.len() {
|
||||||
met[index] = Some((tree_id, item.key.key_offset));
|
met[index] = Some((tree_id, item.key.key_offset));
|
||||||
|
15
style.css
15
style.css
@ -133,6 +133,17 @@ span.key_type.root {
|
|||||||
background-color: #111;
|
background-color: #111;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span.nodeaddr {
|
||||||
|
color: white;
|
||||||
|
background-color: #999;
|
||||||
|
text-align: right;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 3px;
|
||||||
|
float: right;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12pt;
|
||||||
|
}
|
||||||
|
|
||||||
.details table {
|
.details table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
@ -151,3 +162,7 @@ span.key_type.root {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user