v2 refactor
This commit is contained in:
parent
899b759b3d
commit
2f2311d13a
59
btrfs/src/v2/mod.rs
Normal file
59
btrfs/src/v2/mod.rs
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
|
pub mod error {
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("read failed")]
|
||||||
|
ReadFailed,
|
||||||
|
#[error("invalid magic signature")]
|
||||||
|
InvalidMagic,
|
||||||
|
#[error("invalid offset")]
|
||||||
|
InvalidOffset,
|
||||||
|
#[error("Expected an internal node")]
|
||||||
|
ExpectedInternalNode,
|
||||||
|
#[error("Expected a leaf node")]
|
||||||
|
ExpectedLeafNode,
|
||||||
|
#[error("Failed to read with logical offset")]
|
||||||
|
BadLogicalAddress,
|
||||||
|
#[error("Default subvolume root could not be found")]
|
||||||
|
NoDefaultSubvolRoot,
|
||||||
|
#[error("Default subvolume root fs tree could not be found")]
|
||||||
|
NoDefaultSubvolFsRoot,
|
||||||
|
#[error("Invalid checksum: expected {expected:#?} but got {actual:#?}")]
|
||||||
|
InvalidChecksum {
|
||||||
|
expected: [u8; 32],
|
||||||
|
actual: [u8; 32],
|
||||||
|
},
|
||||||
|
#[error("{0}")]
|
||||||
|
ScrollError(scroll::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<scroll::Error> for Error {
|
||||||
|
fn from(value: scroll::Error) -> Self {
|
||||||
|
Self::ScrollError(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Result<T> = core::result::Result<T, Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Read {
|
||||||
|
fn read(&self, dst: &mut [u8], address: u64) -> error::Result<()>;
|
||||||
|
fn alignment(&self) -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(any(feature = "std", test), unix))]
|
||||||
|
impl Read for std::fs::File {
|
||||||
|
fn read(&self, dst: &mut [u8], address: u64) -> error::Result<()> {
|
||||||
|
use std::os::unix::prelude::FileExt;
|
||||||
|
self.read_at(dst, address).map_err(|_| Error::ReadFailed)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod tree;
|
||||||
|
pub mod volume;
|
674
btrfs/src/v2/tree.rs
Normal file
674
btrfs/src/v2/tree.rs
Normal file
|
@ -0,0 +1,674 @@
|
||||||
|
use core::mem::size_of;
|
||||||
|
use core::ops::Deref;
|
||||||
|
|
||||||
|
use crate::structs::{Header, Item, Key, KeyPtr, KnownObjectId, ObjectType, TreeItem};
|
||||||
|
use crate::{Error, Result};
|
||||||
|
use alloc::rc::Rc;
|
||||||
|
use alloc::vec::Vec;
|
||||||
|
use derivative::Derivative;
|
||||||
|
use scroll::Pread;
|
||||||
|
use zerocopy::FromBytes;
|
||||||
|
|
||||||
|
use super::volume::Volume;
|
||||||
|
|
||||||
|
/// A leaf node in a btrfs tree, containing different items
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct BTreeLeafNode {
|
||||||
|
pub header: Header,
|
||||||
|
/// actual leaf data
|
||||||
|
pub items: Vec<Item>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An internal node in a btrfs tree, containing `KeyPtr`s to other internal nodes or leaf nodes.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct BTreeInternalNode {
|
||||||
|
pub header: Header,
|
||||||
|
pub children: Vec<KeyPtr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for BTreeInternalNode {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.header == other.header
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for BTreeLeafNode {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.header == other.header
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for BTreeLeafNode {}
|
||||||
|
impl Eq for BTreeInternalNode {}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum BTreeNode {
|
||||||
|
Internal(BTreeInternalNode),
|
||||||
|
Leaf(BTreeLeafNode),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Derivative, Eq)]
|
||||||
|
#[derivative(Debug, PartialEq)]
|
||||||
|
pub struct Node {
|
||||||
|
inner: BTreeNode,
|
||||||
|
#[derivative(Debug = "ignore")]
|
||||||
|
#[derivative(PartialEq = "ignore")]
|
||||||
|
bytes: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
type BoxedNode = Rc<Node>;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct NodeHandle {
|
||||||
|
node: BoxedNode,
|
||||||
|
idx: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Range<R: super::Read> {
|
||||||
|
volume: Rc<Volume<R>>,
|
||||||
|
parents: Vec<NodeHandle>,
|
||||||
|
start: RootOrEdge,
|
||||||
|
end: RootOrEdge,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Derivative)]
|
||||||
|
#[derivative(Debug)]
|
||||||
|
pub struct Tree<R: super::Read> {
|
||||||
|
#[derivative(Debug = "ignore")]
|
||||||
|
volume: Rc<Volume<R>>,
|
||||||
|
root: BoxedNode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: super::Read> Clone for Tree<R> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
volume: self.volume.clone(),
|
||||||
|
root: self.root.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: super::Read> Tree<R> {
|
||||||
|
pub fn from_logical_offset(volume: Rc<Volume<R>>, logical: u64) -> Result<Self> {
|
||||||
|
let bytes = volume
|
||||||
|
.read_range_from_logical(logical)?
|
||||||
|
.ok_or(Error::BadLogicalAddress)?; // TODO: make this a better error
|
||||||
|
let root = Rc::new(Node::from_bytes(bytes)?);
|
||||||
|
|
||||||
|
Ok(Self { volume, root })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_key<K: PartialEq<Key> + PartialOrd<Key>>(
|
||||||
|
&self,
|
||||||
|
key: &K,
|
||||||
|
) -> Result<Option<(Item, TreeItem)>> {
|
||||||
|
let mut node = self.root.clone();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let search = node.find_key(key);
|
||||||
|
match search {
|
||||||
|
SearchResult::Leaf(a) => {
|
||||||
|
return a.parse_item();
|
||||||
|
}
|
||||||
|
SearchResult::Edge(edge) => match &edge.node.inner {
|
||||||
|
BTreeNode::Internal(internal) => {
|
||||||
|
let child_ptr = internal.children.get(edge.idx as usize).expect("adsf");
|
||||||
|
|
||||||
|
let bytes = self.volume.read_keyptr(child_ptr)?;
|
||||||
|
node = Rc::new(Node::from_bytes(bytes)?);
|
||||||
|
// recurse
|
||||||
|
}
|
||||||
|
BTreeNode::Leaf(_) => {
|
||||||
|
// leaf node returning and edge means key is not present
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter(&self) -> Range<R> {
|
||||||
|
Range::new(self.volume.clone(), self.root.clone(), self.root.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BTreeLeafNode {
|
||||||
|
pub fn parse(header: Header, bytes: &[u8]) -> Result<Self> {
|
||||||
|
log::debug!("leaf:");
|
||||||
|
|
||||||
|
let offset = &mut 0;
|
||||||
|
let items = core::iter::from_fn(|| {
|
||||||
|
if *offset as usize + size_of::<Item>() < bytes.len() {
|
||||||
|
let item = Item::read_from(&bytes[*offset..*offset + size_of::<Item>()]);
|
||||||
|
*offset += size_of::<Item>();
|
||||||
|
|
||||||
|
if let Some(item) = item.as_ref() {
|
||||||
|
log::debug!("\t{item:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
item
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.take(header.nritems.get() as usize)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
Ok(Self { header, items })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BTreeInternalNode {
|
||||||
|
pub fn parse(header: Header, bytes: &[u8]) -> Result<Self> {
|
||||||
|
log::debug!("internal lvl: {}", header.level);
|
||||||
|
|
||||||
|
let offset = &mut 0;
|
||||||
|
let size = size_of::<KeyPtr>();
|
||||||
|
let children = core::iter::from_fn(|| {
|
||||||
|
if *offset as usize + size < bytes.len() {
|
||||||
|
let item = KeyPtr::read_from(&bytes[*offset..*offset + size]);
|
||||||
|
*offset += size;
|
||||||
|
|
||||||
|
if let Some(item) = item.as_ref() {
|
||||||
|
log::debug!(
|
||||||
|
"\tchild gen: {} offset: {}",
|
||||||
|
item.generation.get(),
|
||||||
|
item.key.offset.get()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
item
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.take(header.nritems.get() as usize)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(Self { header, children })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BTreeNode {
|
||||||
|
pub fn parse(bytes: &[u8]) -> Result<Self> {
|
||||||
|
let offset = &mut 0;
|
||||||
|
let header = bytes.gread::<Header>(offset)?;
|
||||||
|
|
||||||
|
if header.level == 0 {
|
||||||
|
Ok(Self::Leaf(BTreeLeafNode::parse(header, &bytes[*offset..])?))
|
||||||
|
} else {
|
||||||
|
Ok(Self::Internal(BTreeInternalNode::parse(
|
||||||
|
header,
|
||||||
|
&bytes[*offset..],
|
||||||
|
)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn header(&self) -> &Header {
|
||||||
|
match self {
|
||||||
|
BTreeNode::Internal(node) => &node.header,
|
||||||
|
BTreeNode::Leaf(node) => &node.header,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the btree node is [`Internal`].
|
||||||
|
///
|
||||||
|
/// [`Internal`]: BTreeNode::Internal
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_internal(&self) -> bool {
|
||||||
|
matches!(self, Self::Internal(..))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the btree node is [`Leaf`].
|
||||||
|
///
|
||||||
|
/// [`Leaf`]: BTreeNode::Leaf
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_leaf(&self) -> bool {
|
||||||
|
matches!(self, Self::Leaf(..))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_internal(&self) -> Option<&BTreeInternalNode> {
|
||||||
|
if let Self::Internal(v) = self {
|
||||||
|
Some(v)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_leaf(&self) -> Option<&BTreeLeafNode> {
|
||||||
|
if let Self::Leaf(v) = self {
|
||||||
|
Some(v)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum NodeHandleAdvanceResult {
|
||||||
|
Decend {
|
||||||
|
parent: NodeHandle,
|
||||||
|
child_ptr: KeyPtr,
|
||||||
|
},
|
||||||
|
Next(NodeHandle),
|
||||||
|
Ascend,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum SearchResult {
|
||||||
|
Leaf(NodeHandle),
|
||||||
|
Edge(NodeHandle),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Eq)]
|
||||||
|
enum RootOrEdge {
|
||||||
|
Root(NodeHandle),
|
||||||
|
Edge(NodeHandle),
|
||||||
|
}
|
||||||
|
impl RootOrEdge {
|
||||||
|
pub fn into_handle(&self) -> NodeHandle {
|
||||||
|
match self {
|
||||||
|
RootOrEdge::Root(handle) => handle,
|
||||||
|
RootOrEdge::Edge(handle) => handle,
|
||||||
|
}
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_handle(&self) -> &NodeHandle {
|
||||||
|
match self {
|
||||||
|
RootOrEdge::Root(handle) => handle,
|
||||||
|
RootOrEdge::Edge(handle) => handle,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for RootOrEdge {
|
||||||
|
type Target = NodeHandle;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
match self {
|
||||||
|
RootOrEdge::Root(node) => node,
|
||||||
|
RootOrEdge::Edge(node) => node,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for RootOrEdge {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
Deref::deref(self).eq(Deref::deref(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Node {
|
||||||
|
pub fn from_bytes(bytes: Vec<u8>) -> Result<Self> {
|
||||||
|
let inner = BTreeNode::parse(&bytes)?;
|
||||||
|
|
||||||
|
Ok(Self { inner, bytes })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_nth_item(&self, i: usize) -> Result<Option<(Item, TreeItem)>> {
|
||||||
|
match &self.inner {
|
||||||
|
BTreeNode::Internal(_) => Ok(None),
|
||||||
|
BTreeNode::Leaf(leaf) => {
|
||||||
|
// TODO: better error to indicate that i was out of bounds
|
||||||
|
let item = leaf.items.get(i).ok_or(Error::ReadFailed)?;
|
||||||
|
let start = size_of::<Header>() + item.offset.get() as usize;
|
||||||
|
let size = item.size.get() as usize;
|
||||||
|
let bytes = &self.bytes[start..start + size];
|
||||||
|
|
||||||
|
let value = TreeItem::parse(item, bytes)?;
|
||||||
|
|
||||||
|
Ok(Some((*item, value)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_key<K: PartialEq<Key> + PartialOrd<Key>>(self: &Rc<Self>, key: &K) -> SearchResult {
|
||||||
|
match &self.inner {
|
||||||
|
BTreeNode::Internal(node) => {
|
||||||
|
for (i, child) in node.children.iter().enumerate() {
|
||||||
|
match key.partial_cmp(&child.key) {
|
||||||
|
Some(core::cmp::Ordering::Less) => {
|
||||||
|
return SearchResult::Edge(NodeHandle {
|
||||||
|
node: self.clone(),
|
||||||
|
idx: if i == 0 { 0 } else { i as u32 - 1 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Some(core::cmp::Ordering::Equal) | None => {
|
||||||
|
return SearchResult::Edge(NodeHandle {
|
||||||
|
node: self.clone(),
|
||||||
|
idx: i as u32,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchResult::Edge(NodeHandle {
|
||||||
|
node: self.clone(),
|
||||||
|
idx: node.children.len() as u32 - 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
BTreeNode::Leaf(node) => {
|
||||||
|
for (i, child) in node.items.iter().enumerate() {
|
||||||
|
// if key < &child.key {
|
||||||
|
// return SearchResult::Leaf(NodeHandle {
|
||||||
|
// node: self.clone(),
|
||||||
|
// idx: if i == 0 { 0 } else { i as u32 - 1 },
|
||||||
|
// });
|
||||||
|
// } else
|
||||||
|
if key.eq(&child.key) {
|
||||||
|
return SearchResult::Leaf(NodeHandle {
|
||||||
|
node: self.clone(),
|
||||||
|
idx: i as u32,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log::debug!("key definitely not found!");
|
||||||
|
SearchResult::Edge(NodeHandle {
|
||||||
|
node: self.clone(),
|
||||||
|
idx: node.items.len() as u32 - 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R> Range<R>
|
||||||
|
where
|
||||||
|
R: super::Read,
|
||||||
|
{
|
||||||
|
pub fn new(volume: Rc<Volume<R>>, start: Rc<Node>, end: Rc<Node>) -> Self {
|
||||||
|
Self {
|
||||||
|
volume,
|
||||||
|
parents: Default::default(),
|
||||||
|
start: RootOrEdge::Root(NodeHandle::start(start)),
|
||||||
|
end: RootOrEdge::Root(NodeHandle::end(end)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
return self.start == self.end;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_start(&mut self) -> Result<()> {
|
||||||
|
let start = match &self.start {
|
||||||
|
RootOrEdge::Root(root) => {
|
||||||
|
match root.node.inner {
|
||||||
|
BTreeNode::Internal(_) => {
|
||||||
|
// descend until leaf
|
||||||
|
let mut advance = root.clone().advance_down();
|
||||||
|
loop {
|
||||||
|
match advance {
|
||||||
|
NodeHandleAdvanceResult::Decend { parent, child_ptr } => {
|
||||||
|
let bytes = self.volume.read_keyptr(&child_ptr)?;
|
||||||
|
let child =
|
||||||
|
NodeHandle::start(Rc::new(Node::from_bytes(bytes)?));
|
||||||
|
self.parents.push(parent);
|
||||||
|
|
||||||
|
match &child.node.inner {
|
||||||
|
BTreeNode::Internal(_) => {
|
||||||
|
// continue loop
|
||||||
|
advance = child.advance_down();
|
||||||
|
}
|
||||||
|
BTreeNode::Leaf(_) => {
|
||||||
|
break child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NodeHandleAdvanceResult::Next(handle) => {
|
||||||
|
break handle;
|
||||||
|
}
|
||||||
|
NodeHandleAdvanceResult::Ascend => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BTreeNode::Leaf(_) => root.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RootOrEdge::Edge(edge) => edge.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.start = RootOrEdge::Edge(start);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn advance(&mut self) -> Result<Option<()>> {
|
||||||
|
if !self.is_empty() {
|
||||||
|
let start = self.start.into_handle();
|
||||||
|
let mut advance = start.advance_down();
|
||||||
|
loop {
|
||||||
|
match advance {
|
||||||
|
NodeHandleAdvanceResult::Decend { parent, child_ptr } => {
|
||||||
|
let bytes = self.volume.read_keyptr(&child_ptr)?;
|
||||||
|
let child = NodeHandle::start(Rc::new(Node::from_bytes(bytes)?));
|
||||||
|
self.parents.push(parent);
|
||||||
|
self.start = RootOrEdge::Edge(child);
|
||||||
|
|
||||||
|
match &self.start.node.inner {
|
||||||
|
BTreeNode::Internal(_) => {
|
||||||
|
return self.advance();
|
||||||
|
}
|
||||||
|
BTreeNode::Leaf(_) => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NodeHandleAdvanceResult::Next(next) => {
|
||||||
|
self.start = RootOrEdge::Edge(next);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
NodeHandleAdvanceResult::Ascend => {
|
||||||
|
if let Some(last) = self.parents.pop() {
|
||||||
|
advance = NodeHandle {
|
||||||
|
idx: last.idx + 1,
|
||||||
|
..last
|
||||||
|
}
|
||||||
|
.advance_down();
|
||||||
|
} else {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Some(()))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R> Iterator for Range<R>
|
||||||
|
where
|
||||||
|
R: super::Read,
|
||||||
|
{
|
||||||
|
type Item = (Item, TreeItem);
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
let handle = match &self.start {
|
||||||
|
RootOrEdge::Root(_) => {
|
||||||
|
self.init_start().expect("error");
|
||||||
|
match &self.start.node.inner {
|
||||||
|
BTreeNode::Internal(_) => None,
|
||||||
|
BTreeNode::Leaf(_) => Some(self.start.into_handle()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RootOrEdge::Edge(_) => self
|
||||||
|
.advance()
|
||||||
|
.expect("cant advance range")
|
||||||
|
.map(|_| self.start.into_handle()),
|
||||||
|
};
|
||||||
|
|
||||||
|
handle
|
||||||
|
.map(|handle| handle.parse_item())?
|
||||||
|
.expect("failed to parse item")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NodeHandle {
|
||||||
|
pub fn start(node: BoxedNode) -> Self {
|
||||||
|
Self { node, idx: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_item(&self) -> Result<Option<(Item, TreeItem)>> {
|
||||||
|
self.node.read_nth_item(self.idx as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn advance_sideways(self) -> NodeHandleAdvanceResult {
|
||||||
|
let header = self.node.inner.header();
|
||||||
|
if header.nritems.get() >= self.idx + 1 {
|
||||||
|
NodeHandleAdvanceResult::Ascend
|
||||||
|
} else {
|
||||||
|
match &self.node.inner {
|
||||||
|
BTreeNode::Internal(_) => NodeHandleAdvanceResult::Next(Self {
|
||||||
|
idx: self.idx + 1,
|
||||||
|
..self
|
||||||
|
}),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the next node in ascending sequential order
|
||||||
|
pub fn advance_down(self) -> NodeHandleAdvanceResult {
|
||||||
|
let header = self.node.inner.header();
|
||||||
|
if self.idx + 1 >= header.nritems.get() {
|
||||||
|
NodeHandleAdvanceResult::Ascend
|
||||||
|
} else {
|
||||||
|
match &self.node.inner {
|
||||||
|
BTreeNode::Internal(node) => NodeHandleAdvanceResult::Decend {
|
||||||
|
parent: self.clone(),
|
||||||
|
child_ptr: *node.children.first().expect("no children in node"),
|
||||||
|
},
|
||||||
|
BTreeNode::Leaf(_) => NodeHandleAdvanceResult::Next(Self {
|
||||||
|
idx: self.idx + 1,
|
||||||
|
..self
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn end(node: BoxedNode) -> Self {
|
||||||
|
Self {
|
||||||
|
idx: node.inner.header().nritems.get() - 1,
|
||||||
|
node,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PARTIAL KEY
|
||||||
|
|
||||||
|
/// key lookup that will find the first key that matches the present items in this partial key
|
||||||
|
pub struct PartialKey {
|
||||||
|
pub id: Option<KnownObjectId>,
|
||||||
|
pub ty: Option<ObjectType>,
|
||||||
|
pub offset: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialKey {
|
||||||
|
pub fn new(id: Option<KnownObjectId>, ty: Option<ObjectType>, offset: Option<u64>) -> Self {
|
||||||
|
Self { id, ty, offset }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq<Key> for PartialKey {
|
||||||
|
fn eq(&self, other: &Key) -> bool {
|
||||||
|
self.id.map(|id| id == other.id()).unwrap_or(true)
|
||||||
|
&& self.ty.map(|ty| ty == other.ty()).unwrap_or(true)
|
||||||
|
&& self
|
||||||
|
.offset
|
||||||
|
.map(|offset| offset == other.offset.get())
|
||||||
|
.unwrap_or(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// compares Self to a key, by comparing each item with the element in key if present, and skipping to the next item if missing.
|
||||||
|
impl PartialOrd<Key> for PartialKey {
|
||||||
|
fn partial_cmp(&self, other: &Key) -> Option<core::cmp::Ordering> {
|
||||||
|
let id = self.id.and_then(|id| id.partial_cmp(&other.id()));
|
||||||
|
let ty = self.ty.and_then(|ty| ty.partial_cmp(&other.ty()));
|
||||||
|
let offset = self
|
||||||
|
.offset
|
||||||
|
.and_then(|offset| offset.partial_cmp(&other.offset.get()));
|
||||||
|
|
||||||
|
match id {
|
||||||
|
Some(core::cmp::Ordering::Equal) | None => match ty {
|
||||||
|
Some(core::cmp::Ordering::Equal) | None => offset,
|
||||||
|
ord => ord,
|
||||||
|
},
|
||||||
|
ord => ord,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod partial_key_tests {
|
||||||
|
use test_log::test;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_partial_key_ord() {
|
||||||
|
let key = Key::new(
|
||||||
|
KnownObjectId::ChunkTree,
|
||||||
|
ObjectType::DirItem,
|
||||||
|
0x8dbfc2d2, // crc of "default"
|
||||||
|
);
|
||||||
|
|
||||||
|
let pkey = PartialKey::new(
|
||||||
|
Some(KnownObjectId::ChunkTree),
|
||||||
|
Some(ObjectType::DirItem),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
assert_eq!(pkey.partial_cmp(&key), None);
|
||||||
|
|
||||||
|
let pkey = PartialKey::new(
|
||||||
|
Some(KnownObjectId::ChunkTree),
|
||||||
|
Some(ObjectType::DirItem),
|
||||||
|
Some(0xdeadbeef),
|
||||||
|
);
|
||||||
|
assert_ne!(pkey.partial_cmp(&key), Some(core::cmp::Ordering::Equal));
|
||||||
|
|
||||||
|
let pkey = PartialKey::new(None, Some(ObjectType::DirItem), Some(0xdeadbeef));
|
||||||
|
assert_ne!(pkey.partial_cmp(&key), None);
|
||||||
|
|
||||||
|
let pkey = PartialKey::new(
|
||||||
|
Some(KnownObjectId::ChunkTree),
|
||||||
|
Some(ObjectType::DirItem),
|
||||||
|
Some(0x8dbfc2d2),
|
||||||
|
);
|
||||||
|
assert_eq!(pkey.partial_cmp(&key), Some(core::cmp::Ordering::Equal));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_partial_eq_partial_key() {
|
||||||
|
let key = Key::new(
|
||||||
|
KnownObjectId::ChunkTree,
|
||||||
|
ObjectType::DirItem,
|
||||||
|
0x8dbfc2d2, // crc of "default"
|
||||||
|
);
|
||||||
|
|
||||||
|
let pkey = PartialKey::new(
|
||||||
|
Some(KnownObjectId::ChunkTree),
|
||||||
|
Some(ObjectType::DirItem),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
assert!(pkey.eq(&key));
|
||||||
|
|
||||||
|
let pkey = PartialKey::new(
|
||||||
|
Some(KnownObjectId::ChunkTree),
|
||||||
|
Some(ObjectType::DirItem),
|
||||||
|
Some(0xdeadbeef),
|
||||||
|
);
|
||||||
|
assert!(!pkey.eq(&key));
|
||||||
|
|
||||||
|
let pkey = PartialKey::new(None, Some(ObjectType::DirItem), Some(0xdeadbeef));
|
||||||
|
assert!(!pkey.eq(&key));
|
||||||
|
|
||||||
|
let pkey = PartialKey::new(
|
||||||
|
Some(KnownObjectId::ChunkTree),
|
||||||
|
Some(ObjectType::DirItem),
|
||||||
|
Some(0x8dbfc2d2),
|
||||||
|
);
|
||||||
|
assert!(pkey.eq(&key));
|
||||||
|
}
|
||||||
|
}
|
385
btrfs/src/v2/volume.rs
Normal file
385
btrfs/src/v2/volume.rs
Normal file
|
@ -0,0 +1,385 @@
|
||||||
|
use core::mem::size_of;
|
||||||
|
|
||||||
|
use alloc::collections::btree_map::Entry;
|
||||||
|
use alloc::{collections::BTreeMap, rc::Rc, vec, vec::Vec};
|
||||||
|
use scroll::Pread;
|
||||||
|
|
||||||
|
use crate::structs::{
|
||||||
|
Chunk, Key, KeyPtr, KnownObjectId, ObjectType, RootItem, Stripe, Superblock, TreeItem,
|
||||||
|
};
|
||||||
|
use crate::v2::tree::PartialKey;
|
||||||
|
use crate::{Error, Result};
|
||||||
|
|
||||||
|
use super::tree::Tree;
|
||||||
|
|
||||||
|
/// equal if overlapping, ordered by lower bound
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ChunkTreeKey {
|
||||||
|
range: core::ops::Range<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u64> for ChunkTreeKey {
|
||||||
|
fn from(value: u64) -> Self {
|
||||||
|
Self {
|
||||||
|
range: value..value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChunkTreeKey {
|
||||||
|
pub fn start(&self) -> u64 {
|
||||||
|
self.range.start
|
||||||
|
}
|
||||||
|
pub fn end(&self) -> u64 {
|
||||||
|
self.range.end
|
||||||
|
}
|
||||||
|
pub fn size(&self) -> u64 {
|
||||||
|
self.range.end - self.range.start
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delta(&self, point: u64) -> u64 {
|
||||||
|
point - self.range.start
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sub_range(&self, point: u64) -> core::ops::Range<u64> {
|
||||||
|
self.delta(point)..self.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for ChunkTreeKey {}
|
||||||
|
impl Ord for ChunkTreeKey {
|
||||||
|
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
|
||||||
|
self.partial_cmp(other).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for ChunkTreeKey {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.range.contains(&other.range.start) || other.range.contains(&self.range.start)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for ChunkTreeKey {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
|
||||||
|
self.eq(other)
|
||||||
|
.then_some(core::cmp::Ordering::Equal)
|
||||||
|
.or_else(|| self.range.start.partial_cmp(&other.range.start))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// inner volume struct that contains all the info needed to read/parse trees
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Volume<R: super::Read> {
|
||||||
|
read: R,
|
||||||
|
superblock: Superblock,
|
||||||
|
chunk_cache: BTreeMap<ChunkTreeKey, u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: find better name
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Volume2<R: super::Read> {
|
||||||
|
inner: Rc<Volume<R>>,
|
||||||
|
roots: BTreeMap<KnownObjectId, Tree<R>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: find better name
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Fs<R: super::Read> {
|
||||||
|
volume: Rc<Volume2<R>>,
|
||||||
|
fs_root: Tree<R>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: super::Read> Volume<R> {
|
||||||
|
pub fn new(read: R) -> Result<Rc<Self>> {
|
||||||
|
let mut sb = vec![0u8; size_of::<Superblock>()];
|
||||||
|
read.read(&mut sb, Superblock::SUPERBLOCK_BASE_OFFSET as _)?;
|
||||||
|
|
||||||
|
let superblock = Superblock::parse(&sb)?;
|
||||||
|
let chunk_cache = Self::bootstrap_chunk_tree(&superblock)?;
|
||||||
|
|
||||||
|
let volume = Rc::new(
|
||||||
|
Self {
|
||||||
|
read,
|
||||||
|
superblock,
|
||||||
|
chunk_cache,
|
||||||
|
}
|
||||||
|
.parse_chunk_tree()?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(volume)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_volume2(self: Rc<Self>) -> Result<Rc<Volume2<R>>> {
|
||||||
|
Ok(Rc::new(Volume2 {
|
||||||
|
inner: self.clone(),
|
||||||
|
roots: self.parse_root_tree()?,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_chunk_tree(mut self) -> Result<Self> {
|
||||||
|
let this = Rc::new(self);
|
||||||
|
|
||||||
|
let chunk_tree =
|
||||||
|
Tree::from_logical_offset(this.clone(), this.superblock().chunk_root.get())?;
|
||||||
|
|
||||||
|
let chunks = chunk_tree
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(item, v)| match v {
|
||||||
|
TreeItem::Chunk(chunk) => Some((item, chunk)),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
drop(chunk_tree);
|
||||||
|
|
||||||
|
self = match Rc::try_unwrap(this) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (item, chunk) in chunks {
|
||||||
|
let start = item.key.offset.get() as u64;
|
||||||
|
let end = start + chunk.length.get();
|
||||||
|
|
||||||
|
match self.chunk_cache.entry(ChunkTreeKey { range: start..end }) {
|
||||||
|
Entry::Vacant(entry) => {
|
||||||
|
log::info!("inserting chunk [{start}, {end})");
|
||||||
|
entry.insert(chunk.stripe.offset.get());
|
||||||
|
}
|
||||||
|
Entry::Occupied(entry) => {
|
||||||
|
log::warn!("overlapping stripes!");
|
||||||
|
log::warn!(
|
||||||
|
"\t{:?} and {:?}",
|
||||||
|
entry.key(),
|
||||||
|
ChunkTreeKey { range: start..end }
|
||||||
|
);
|
||||||
|
log::warn!(
|
||||||
|
"\twith offsets: {} and {}",
|
||||||
|
entry.get(),
|
||||||
|
chunk.stripe.offset.get()
|
||||||
|
);
|
||||||
|
|
||||||
|
if *entry.get() != chunk.stripe.offset.get() {
|
||||||
|
log::error!("\tprobably an error?");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_root_tree(self: Rc<Self>) -> Result<BTreeMap<KnownObjectId, Tree<R>>> {
|
||||||
|
let root_tree_root = self.superblock().root.get();
|
||||||
|
let root_tree = Tree::from_logical_offset(self.clone(), root_tree_root)?;
|
||||||
|
|
||||||
|
let roots = root_tree
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(item, v)| match v {
|
||||||
|
TreeItem::Root(root) => Some((item, root)),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.map(|(item, root)| {
|
||||||
|
let id = item.key.id();
|
||||||
|
let a = Tree::from_logical_offset(self.clone(), root.bytenr.get());
|
||||||
|
|
||||||
|
a.map(|root| (id, root))
|
||||||
|
})
|
||||||
|
.collect::<Result<BTreeMap<_, _>>>()?;
|
||||||
|
|
||||||
|
Ok(roots)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size_from_logical(&self, logical: u64) -> Option<u64> {
|
||||||
|
self.chunk_cache
|
||||||
|
.get_key_value(&logical.into())
|
||||||
|
.map(|(key, _)| key.size())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn offset_from_logical(&self, logical: u64) -> Option<u64> {
|
||||||
|
self.chunk_cache
|
||||||
|
.get_key_value(&logical.into())
|
||||||
|
.map(|(key, offset)| offset + key.delta(logical))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn range_from_logical(&self, logical: u64) -> Option<core::ops::Range<u64>> {
|
||||||
|
self.chunk_cache
|
||||||
|
.get_key_value(&logical.into())
|
||||||
|
.map(|(key, offset)| {
|
||||||
|
let delta = key.delta(logical);
|
||||||
|
(offset + delta)..(offset + key.size() - delta)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_range_from_logical(&self, logical: u64) -> Result<Option<Vec<u8>>> {
|
||||||
|
if let Some(range) = self.range_from_logical(logical) {
|
||||||
|
Ok(Some(self.read_range(range)?))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_range(&self, range: core::ops::Range<u64>) -> Result<Vec<u8>> {
|
||||||
|
let mut buf = vec![0; (range.end - range.start) as usize];
|
||||||
|
self.read.read(&mut buf, range.start)?;
|
||||||
|
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_keyptr(&self, keyptr: &KeyPtr) -> Result<Vec<u8>> {
|
||||||
|
self.read_range(
|
||||||
|
self.range_from_logical(keyptr.blockptr.get())
|
||||||
|
.ok_or(Error::ReadFailed)?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bootstrap_chunk_tree(superblock: &Superblock) -> Result<BTreeMap<ChunkTreeKey, u64>> {
|
||||||
|
let array_size = superblock.sys_chunk_array_size.get() as usize;
|
||||||
|
let mut offset: usize = 0;
|
||||||
|
|
||||||
|
let key_size = size_of::<Key>();
|
||||||
|
let mut chunk_tree = BTreeMap::new();
|
||||||
|
|
||||||
|
let bytes = &superblock.sys_chunk_array;
|
||||||
|
|
||||||
|
while offset < array_size {
|
||||||
|
if offset + key_size > array_size {
|
||||||
|
log::error!("short key read");
|
||||||
|
return Err(Error::InvalidOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = bytes.gread::<Key>(&mut offset)?;
|
||||||
|
if key.ty() != ObjectType::ChunkItem {
|
||||||
|
log::error!("key is not of type ChunkItem");
|
||||||
|
return Err(Error::InvalidOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
let chunk = bytes.gread::<Chunk>(&mut offset)?;
|
||||||
|
let num_stripes = chunk.num_stripes.get(); // copy to prevent unaligned access
|
||||||
|
|
||||||
|
if num_stripes == 0 {
|
||||||
|
log::error!("num_stripes cannot be 0");
|
||||||
|
return Err(Error::InvalidOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
if num_stripes != 1 {
|
||||||
|
log::warn!(
|
||||||
|
"warning: {} stripes detected but only processing 1",
|
||||||
|
num_stripes
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let key_offset = key.offset.get();
|
||||||
|
let chunk_length = chunk.length.get();
|
||||||
|
|
||||||
|
match chunk_tree.entry(ChunkTreeKey {
|
||||||
|
range: key_offset..(key_offset + chunk_length),
|
||||||
|
}) {
|
||||||
|
Entry::Vacant(entry) => {
|
||||||
|
entry.insert(chunk.stripe.offset.get());
|
||||||
|
}
|
||||||
|
Entry::Occupied(_) => {
|
||||||
|
log::error!("overlapping stripes!");
|
||||||
|
return Err(Error::InvalidOffset);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
offset += (num_stripes - 1) as usize * size_of::<Stripe>();
|
||||||
|
if offset > array_size {
|
||||||
|
log::error!("short chunk item + stripes read");
|
||||||
|
return Err(Error::InvalidOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(chunk_tree)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn superblock(&self) -> Superblock {
|
||||||
|
self.superblock
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: super::Read> Volume2<R> {
|
||||||
|
pub fn default_subvolume(self: Rc<Self>) -> Result<Fs<R>> {
|
||||||
|
let root_tree =
|
||||||
|
Tree::from_logical_offset(self.inner.clone(), self.inner.superblock().root.get())?;
|
||||||
|
|
||||||
|
// we are looking for the root tree directory (?)
|
||||||
|
// this is a DIR_ITEM entry in the root tree, with the name "default",
|
||||||
|
// and the crc32 of "default" as its offset
|
||||||
|
let key = Key::new(
|
||||||
|
KnownObjectId::Custom(self.inner.superblock().root_dir_objectid.get()),
|
||||||
|
ObjectType::DirItem,
|
||||||
|
0x8dbfc2d2, // crc of "default"
|
||||||
|
);
|
||||||
|
|
||||||
|
let subvol_root = root_tree
|
||||||
|
.find_key(&key)?
|
||||||
|
.ok_or(Error::NoDefaultSubvolRoot)?;
|
||||||
|
// if we found the dir entry of the "default subvol" (mharmstone nomenclature)
|
||||||
|
// we then look for the root fs tree in the root tree with the ID found in the `.location` of the dir_item only (from mharmstone)
|
||||||
|
|
||||||
|
let subvol_id = subvol_root
|
||||||
|
.1
|
||||||
|
.as_dir_item()
|
||||||
|
.expect("dir item")
|
||||||
|
.first()
|
||||||
|
.expect("dir item entry")
|
||||||
|
.dir_item
|
||||||
|
.location
|
||||||
|
.id();
|
||||||
|
|
||||||
|
let root = self
|
||||||
|
.roots
|
||||||
|
.get(&subvol_id)
|
||||||
|
.ok_or(Error::NoDefaultSubvolFsRoot)?
|
||||||
|
.clone();
|
||||||
|
Ok(Fs {
|
||||||
|
volume: self.clone(),
|
||||||
|
fs_root: root.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::fs::File;
|
||||||
|
use test_log::test;
|
||||||
|
|
||||||
|
fn open_btrfs_file() -> File {
|
||||||
|
let file = std::fs::File::open("btrfs.img").expect("btrfs image");
|
||||||
|
file
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_volume() {
|
||||||
|
let file = open_btrfs_file();
|
||||||
|
let vol = Volume::new(file).expect("volume");
|
||||||
|
let v2 = vol.into_volume2().expect("volume2");
|
||||||
|
v2.default_subvolume().expect("default subvol");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn iter_roots() {
|
||||||
|
let file = open_btrfs_file();
|
||||||
|
let vol = Volume::new(file).expect("volume");
|
||||||
|
let v2 = vol.into_volume2().expect("volume2");
|
||||||
|
|
||||||
|
for (id, v) in v2.roots.iter() {
|
||||||
|
log::info!("[{id:?}] {v:#?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn iter_default_subvol() {
|
||||||
|
let file = open_btrfs_file();
|
||||||
|
let vol = Volume::new(file).expect("volume");
|
||||||
|
let v2 = vol.into_volume2().expect("volume2");
|
||||||
|
let fs = v2.default_subvolume().expect("default subvol");
|
||||||
|
|
||||||
|
for (id, entry) in fs.fs_root.iter() {
|
||||||
|
log::info!("[{id:?}] {entry:#?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue