use crate::bytes::{le_u16, le_u32, le_u64, u8_at};
use crate::error::BtrfsError;
use crate::node::{read_node, ChunkMap, Node};
use crate::superblock::Superblock;
pub const FS_TREE_OBJECTID: u64 = 5;
pub const FS_TREE_ROOT_DIR_OBJECTID: u64 = 256;
pub const INODE_ITEM_KEY: u8 = 1;
pub const INODE_REF_KEY: u8 = 12;
pub const DIR_ITEM_KEY: u8 = 84;
pub const DIR_INDEX_KEY: u8 = 96;
pub const ROOT_ITEM_KEY: u8 = 132;
pub const INODE_ITEM_SIZE: usize = 160;
pub const TIMESTAMP_SIZE: usize = 12;
pub(crate) const INODE_SIZE_OFFSET: usize = 16;
mod inode_off {
pub const GENERATION: usize = 0;
pub const TRANSID: usize = 8;
pub const SIZE: usize = 16;
pub const NBYTES: usize = 24;
pub const NLINK: usize = 40;
pub const UID: usize = 44;
pub const GID: usize = 48;
pub const MODE: usize = 52;
pub const FLAGS: usize = 64;
pub const ATIME: usize = 112;
pub const CTIME: usize = 124;
pub const MTIME: usize = 136;
pub const OTIME: usize = 148;
}
mod root_off {
pub const GENERATION: usize = 160;
pub const ROOT_DIRID: usize = 168;
pub const BYTENR: usize = 176;
pub const LEVEL: usize = 238;
}
mod dir_off {
pub const NAME_LEN: usize = 27;
pub const TYPE: usize = 29;
pub const NAME: usize = 30;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Timestamp {
pub sec: u64,
pub nsec: u32,
}
impl Timestamp {
#[must_use]
fn parse(data: &[u8], off: usize) -> Self {
Timestamp {
sec: le_u64(data, off),
nsec: le_u32(data, off + 8),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Inode {
pub objectid: u64,
pub generation: u64,
pub transid: u64,
pub size: u64,
pub nbytes: u64,
pub nlink: u32,
pub uid: u32,
pub gid: u32,
pub mode: u32,
pub flags: u64,
pub atime: Timestamp,
pub ctime: Timestamp,
pub mtime: Timestamp,
pub otime: Timestamp,
}
const S_IFMT: u32 = 0o170_000;
const S_IFDIR: u32 = 0o40_000;
const S_IFREG: u32 = 0o100_000;
impl Inode {
#[must_use]
fn parse(objectid: u64, data: &[u8]) -> Self {
Inode {
objectid,
generation: le_u64(data, inode_off::GENERATION),
transid: le_u64(data, inode_off::TRANSID),
size: le_u64(data, inode_off::SIZE),
nbytes: le_u64(data, inode_off::NBYTES),
nlink: le_u32(data, inode_off::NLINK),
uid: le_u32(data, inode_off::UID),
gid: le_u32(data, inode_off::GID),
mode: le_u32(data, inode_off::MODE),
flags: le_u64(data, inode_off::FLAGS),
atime: Timestamp::parse(data, inode_off::ATIME),
ctime: Timestamp::parse(data, inode_off::CTIME),
mtime: Timestamp::parse(data, inode_off::MTIME),
otime: Timestamp::parse(data, inode_off::OTIME),
}
}
#[must_use]
pub fn is_dir(&self) -> bool {
self.mode & S_IFMT == S_IFDIR
}
#[must_use]
pub fn is_file(&self) -> bool {
self.mode & S_IFMT == S_IFREG
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum DirItemType {
File,
Dir,
Symlink,
Other(u8),
}
impl DirItemType {
#[must_use]
fn from_byte(b: u8) -> Self {
match b {
1 => DirItemType::File,
2 => DirItemType::Dir,
7 => DirItemType::Symlink,
other => DirItemType::Other(other),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct DirEntry {
pub name: String,
pub child: u64,
pub item_type: DirItemType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct FsTreeRoot {
pub bytenr: u64,
pub level: u8,
pub root_dirid: u64,
pub generation: u64,
}
pub fn fs_tree_root(
image: &[u8],
sb: &Superblock,
chunk_map: &ChunkMap,
) -> Result<FsTreeRoot, BtrfsError> {
let node = read_node(image, sb, chunk_map, sb.root)?;
for (key, data) in node.leaf_items() {
if key.objectid == FS_TREE_OBJECTID && key.key_type == ROOT_ITEM_KEY {
return Ok(FsTreeRoot {
bytenr: le_u64(data, root_off::BYTENR),
level: u8_at(data, root_off::LEVEL),
root_dirid: le_u64(data, root_off::ROOT_DIRID),
generation: le_u64(data, root_off::GENERATION),
});
}
}
Err(BtrfsError::Truncated {
structure: "FS_TREE ROOT_ITEM (objectid 5) in root tree",
need: FS_TREE_OBJECTID as usize,
have: node.header.nritems as usize,
})
}
#[must_use]
pub fn read_inode(leaf: &Node, objectid: u64) -> Option<Inode> {
leaf.leaf_items()
.find(|(key, _)| key.objectid == objectid && key.key_type == INODE_ITEM_KEY)
.map(|(_, data)| Inode::parse(objectid, data))
}
#[must_use]
pub fn list_dir(leaf: &Node, dir_objectid: u64) -> Vec<DirEntry> {
let mut out: Vec<DirEntry> = Vec::new();
for (key, data) in leaf.leaf_items() {
if key.objectid != dir_objectid
|| (key.key_type != DIR_ITEM_KEY && key.key_type != DIR_INDEX_KEY)
{
continue;
}
let Some(entry) = parse_dir_item(data) else {
continue;
};
if out
.iter()
.any(|e| e.name == entry.name && e.child == entry.child)
{
continue;
}
out.push(entry);
}
out
}
#[must_use]
fn parse_dir_item(data: &[u8]) -> Option<DirEntry> {
if data.len() < dir_off::NAME {
return None;
}
let child = le_u64(data, 0); let type_byte = u8_at(data, dir_off::TYPE);
let name_len = le_u16(data, dir_off::NAME_LEN) as usize;
let name_start = dir_off::NAME;
let avail = data.len().saturating_sub(name_start);
let take = name_len.min(avail);
let name_bytes = data.get(name_start..name_start + take).unwrap_or(&[]);
Some(DirEntry {
name: String::from_utf8_lossy(name_bytes).into_owned(),
child,
item_type: DirItemType::from_byte(type_byte),
})
}
#[must_use]
pub fn read_by_path(leaf: &Node, path: &str) -> Option<(u64, Inode)> {
let mut current = FS_TREE_ROOT_DIR_OBJECTID;
let mut inode = read_inode(leaf, current)?;
for component in path.split('/').filter(|c| !c.is_empty()) {
if !inode.is_dir() {
return None;
}
let entry = list_dir(leaf, current)
.into_iter()
.find(|e| e.name == component)?;
current = entry.child;
inode = read_inode(leaf, current)?;
}
Some((current, inode))
}