const BTNODE_FIXED_KV_SIZE: u16 = 0x4;
const BTNODE_ROOT: u16 = 0x1;
const BTNODE_LEAF: u16 = 0x2;
const OFF_BTN_FLAGS: usize = 32;
const OFF_BTN_LEVEL: usize = 34;
const OFF_BTN_NKEYS: usize = 36;
const OFF_BTN_TABLE_SPACE: usize = 40;
const BTN_DATA_OFF: usize = 56;
const BTREE_NODE_MIN_LEN: usize = BTN_DATA_OFF;
const BTREE_INFO_LEN: usize = 40;
const TOC_FIXED_LEN: usize = 4;
const TOC_VAR_LEN: usize = 8;
const MAX_BTN_NKEYS: u32 = 1 << 20;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum BTreeSubtype {
Omap,
FsTree,
}
impl BTreeSubtype {
pub(crate) const fn fixed_key_len(self) -> usize {
match self {
BTreeSubtype::Omap => 16, BTreeSubtype::FsTree => 0, }
}
pub(crate) const fn fixed_leaf_val_len(self) -> usize {
match self {
BTreeSubtype::Omap => 16, BTreeSubtype::FsTree => 0, }
}
pub(crate) const fn fixed_branch_val_len(self) -> usize {
match self {
BTreeSubtype::Omap | BTreeSubtype::FsTree => 8,
}
}
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct BTreeNodeHeader {
pub flags: u16,
pub level: u16,
pub nkeys: u32,
}
impl BTreeNodeHeader {
#[must_use]
pub fn is_leaf(&self) -> bool {
self.flags & BTNODE_LEAF != 0 || self.level == 0
}
#[must_use]
pub fn is_root(&self) -> bool {
self.flags & BTNODE_ROOT != 0
}
#[must_use]
pub fn is_fixed_kv(&self) -> bool {
self.flags & BTNODE_FIXED_KV_SIZE != 0
}
}
pub struct Entry<'a> {
pub key: &'a [u8],
pub value: &'a [u8],
}
#[must_use]
pub fn parse_node_header(block: &[u8]) -> Option<BTreeNodeHeader> {
if block.len() < BTREE_NODE_MIN_LEN {
return None;
}
Some(BTreeNodeHeader {
flags: crate::bytes::le_u16(block, OFF_BTN_FLAGS),
level: crate::bytes::le_u16(block, OFF_BTN_LEVEL),
nkeys: crate::bytes::le_u32(block, OFF_BTN_NKEYS),
})
}
#[must_use]
pub fn node_entries(block: &[u8], subtype: BTreeSubtype) -> Vec<Entry<'_>> {
let Some(hdr) = parse_node_header(block) else {
return Vec::new(); };
let nkeys = hdr.nkeys.min(MAX_BTN_NKEYS) as usize;
if nkeys == 0 {
return Vec::new();
}
let toc_off = crate::bytes::le_u16(block, OFF_BTN_TABLE_SPACE) as usize;
let toc_len = crate::bytes::le_u16(block, OFF_BTN_TABLE_SPACE + 2) as usize;
let toc_start = BTN_DATA_OFF + toc_off; let key_area = toc_start + toc_len;
let val_base = if hdr.is_root() {
block.len().saturating_sub(BTREE_INFO_LEN)
} else {
block.len()
};
let fixed = hdr.is_fixed_kv();
let entry_len = if fixed { TOC_FIXED_LEN } else { TOC_VAR_LEN };
let key_len = subtype.fixed_key_len();
let val_len = if hdr.is_leaf() {
subtype.fixed_leaf_val_len()
} else {
subtype.fixed_branch_val_len()
};
let mut out = Vec::with_capacity(nkeys);
for i in 0..nkeys {
let e = toc_start + i * entry_len; if e + entry_len > key_area || e + entry_len > block.len() {
break;
}
let koff = crate::bytes::le_u16(block, e) as usize;
let (voff, this_key_len, this_val_len) = if fixed {
(
crate::bytes::le_u16(block, e + 2) as usize,
key_len,
val_len,
)
} else {
(
crate::bytes::le_u16(block, e + 4) as usize,
crate::bytes::le_u16(block, e + 2) as usize,
crate::bytes::le_u16(block, e + 6) as usize,
)
};
let kstart = key_area + koff; let kend = kstart + this_key_len;
let Some(vstart) = val_base.checked_sub(voff) else {
continue; };
let vend = vstart + this_val_len;
let (Some(key), Some(value)) = (block.get(kstart..kend), block.get(vstart..vend)) else {
continue; };
out.push(Entry { key, value });
}
out
}
const MAX_BTREE_DEPTH: usize = 64;
pub fn for_each_leaf_entry<R, F>(
reader: &mut R,
root_paddr: u64,
block_size: usize,
subtype: BTreeSubtype,
visit: &mut F,
) -> crate::Result<()>
where
R: std::io::Read + std::io::Seek,
F: FnMut(&[u8], &[u8]),
{
let mut visited = std::collections::HashSet::new();
descend(
reader,
root_paddr,
block_size,
subtype,
0,
&mut visited,
visit,
)
}
fn read_verified_node<R: std::io::Read + std::io::Seek>(
reader: &mut R,
paddr: u64,
block_size: usize,
) -> crate::Result<Option<Vec<u8>>> {
let mut buf = vec![0u8; block_size];
let Some(offset) = paddr.checked_mul(block_size as u64) else {
return Ok(None); };
reader.seek(std::io::SeekFrom::Start(offset))?;
reader.read_exact(&mut buf)?;
let stored = crate::object::fletcher64_stored(&buf);
let computed = crate::object::fletcher64_checksum(&buf);
if stored != computed {
let block = crate::object::ObjPhys::parse(&buf).map_or(paddr, |h| h.oid);
return Err(crate::ApfsError::ChecksumMismatch {
block,
stored,
computed,
});
}
Ok(Some(buf))
}
pub fn find_leaf<R, C, F>(
reader: &mut R,
root_paddr: u64,
block_size: usize,
subtype: BTreeSubtype,
cmp: C,
visit: &mut F,
) -> crate::Result<()>
where
R: std::io::Read + std::io::Seek,
C: Fn(&[u8]) -> std::cmp::Ordering,
F: FnMut(&[u8], &[u8]),
{
let mut visited = std::collections::HashSet::new();
let mut paddr = root_paddr;
for _ in 0..MAX_BTREE_DEPTH {
if !visited.insert(paddr) {
return Err(crate::ApfsError::CycleGuard {
cap: MAX_BTREE_DEPTH,
});
}
let Some(buf) = read_verified_node(reader, paddr, block_size)? else {
return Ok(()); };
let Some(hdr) = parse_node_header(&buf) else {
return Ok(()); };
if hdr.is_leaf() {
for e in node_entries(&buf, subtype) {
visit(e.key, e.value);
}
return Ok(());
}
let entries = node_entries(&buf, subtype);
let Some(first) = entries.first() else {
return Ok(()); };
let mut child = crate::bytes::le_u64(first.value, 0);
for e in &entries {
if cmp(e.key) == std::cmp::Ordering::Greater {
break;
}
child = crate::bytes::le_u64(e.value, 0);
}
paddr = child;
}
Err(crate::ApfsError::CycleGuard {
cap: MAX_BTREE_DEPTH,
})
}
fn descend<R, F>(
reader: &mut R,
paddr: u64,
block_size: usize,
subtype: BTreeSubtype,
depth: usize,
visited: &mut std::collections::HashSet<u64>,
visit: &mut F,
) -> crate::Result<()>
where
R: std::io::Read + std::io::Seek,
F: FnMut(&[u8], &[u8]),
{
if depth >= MAX_BTREE_DEPTH {
return Err(crate::ApfsError::CycleGuard {
cap: MAX_BTREE_DEPTH,
});
}
if !visited.insert(paddr) {
return Err(crate::ApfsError::CycleGuard {
cap: MAX_BTREE_DEPTH,
});
}
let Some(buf) = read_verified_node(reader, paddr, block_size)? else {
return Ok(()); };
let Some(hdr) = parse_node_header(&buf) else {
return Ok(()); };
if hdr.is_leaf() {
for e in node_entries(&buf, subtype) {
visit(e.key, e.value);
}
return Ok(());
}
let children: Vec<u64> = node_entries(&buf, subtype)
.iter()
.map(|e| crate::bytes::le_u64(e.value, 0))
.collect();
for child in children {
descend(
reader,
child,
block_size,
subtype,
depth + 1,
visited,
visit,
)?;
}
Ok(())
}