use std::io::{Read, Seek};
use crate::btree::{self, BTreeSubtype};
use crate::fsrecord::{decode_jkey, RecordType};
use crate::inode::Inode;
use crate::object::{fletcher64_checksum, fletcher64_stored, ObjPhys};
use crate::omap::ObjectMap;
use crate::volume::ApfsVolume;
pub const ROOT_DIR_INO_NUM: u64 = 2;
const J_DREC_LEN_MASK: u32 = 0x0000_03ff;
const OFF_DREC_FILE_ID: usize = 0;
const OFF_DREC_DATE_ADDED: usize = 8;
const OFF_DREC_FLAGS: usize = 16;
const MAX_FSTREE_DEPTH: usize = 64;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct DirEntry {
pub name: String,
pub file_id: u64,
pub date_added: u64,
pub flags: u16,
}
fn decode_drec_name(key: &[u8]) -> Option<String> {
let hashed_len = (crate::bytes::le_u32(key, 8) & J_DREC_LEN_MASK) as usize;
if hashed_len > 0 {
if let Some(name) = key.get(12..12 + hashed_len) {
return Some(decode_cstr(name));
}
}
let unhashed_len = crate::bytes::le_u16(key, 8) as usize;
if unhashed_len > 0 {
if let Some(name) = key.get(10..10 + unhashed_len) {
return Some(decode_cstr(name));
}
}
None
}
fn parse_dir_entry(key: &[u8], value: &[u8]) -> Option<DirEntry> {
let name = decode_drec_name(key)?;
Some(DirEntry {
name,
file_id: crate::bytes::le_u64(value, OFF_DREC_FILE_ID),
date_added: crate::bytes::le_u64(value, OFF_DREC_DATE_ADDED),
flags: crate::bytes::le_u16(value, OFF_DREC_FLAGS),
})
}
pub(crate) fn for_each_fs_record_for_oid<R, F>(
reader: &mut R,
volume: &ApfsVolume,
block_size: usize,
target_oid: u64,
visit: &mut F,
) -> crate::Result<()>
where
R: Read + Seek,
F: FnMut(&[u8], &[u8]),
{
walk_fs_tree(reader, volume, block_size, Some(target_oid), visit)
}
fn walk_fs_tree<R, F>(
reader: &mut R,
volume: &ApfsVolume,
block_size: usize,
target_oid: Option<u64>,
visit: &mut F,
) -> crate::Result<()>
where
R: Read + Seek,
F: FnMut(&[u8], &[u8]),
{
let mut buf = vec![0u8; block_size];
let omap_off = volume.omap_oid().saturating_mul(block_size as u64);
reader.seek(std::io::SeekFrom::Start(omap_off))?;
reader.read_exact(&mut buf)?;
let omap = ObjectMap::parse(&buf)?;
let xid = volume.xid();
let mut visited = std::collections::HashSet::new();
descend_virtual(
reader,
&omap,
volume.root_tree_oid(),
xid,
block_size,
0,
target_oid,
&mut visited,
visit,
)
}
fn child_may_contain_oid(sep_oid: u64, next_sep_oid: Option<u64>, target: u64) -> bool {
sep_oid <= target && next_sep_oid.is_none_or(|next| next >= target)
}
#[allow(clippy::too_many_arguments)]
fn descend_virtual<R, F>(
reader: &mut R,
omap: &ObjectMap,
node_oid: u64,
xid: u64,
block_size: usize,
depth: usize,
target_oid: Option<u64>,
visited: &mut std::collections::HashSet<u64>,
visit: &mut F,
) -> crate::Result<()>
where
R: Read + Seek,
F: FnMut(&[u8], &[u8]),
{
let cycle = || crate::ApfsError::CycleGuard {
cap: MAX_FSTREE_DEPTH,
};
if depth >= MAX_FSTREE_DEPTH {
return Err(cycle()); }
if !visited.insert(node_oid) {
return Err(cycle());
}
let entry = omap.resolve(reader, node_oid, xid, block_size)?;
let mut buf = vec![0u8; block_size];
let offset = entry.paddr.saturating_mul(block_size as u64);
reader.seek(std::io::SeekFrom::Start(offset))?;
reader.read_exact(&mut buf)?;
let stored = fletcher64_stored(&buf);
let computed = fletcher64_checksum(&buf);
if stored != computed {
let block = ObjPhys::parse(&buf).map_or(entry.paddr, |h| h.oid);
return Err(crate::ApfsError::ChecksumMismatch {
block,
stored,
computed,
});
}
let Some(hdr) = btree::parse_node_header(&buf) else {
return Ok(()); };
if hdr.is_leaf() {
for e in btree::node_entries(&buf, BTreeSubtype::FsTree) {
visit(e.key, e.value);
}
return Ok(());
}
let entries = btree::node_entries(&buf, BTreeSubtype::FsTree);
for i in 0..entries.len() {
if let Some(target) = target_oid {
let (sep_oid, _) = decode_jkey(crate::bytes::le_u64(entries[i].key, 0));
let next_sep_oid = entries
.get(i + 1)
.map(|e| decode_jkey(crate::bytes::le_u64(e.key, 0)).0);
if !child_may_contain_oid(sep_oid, next_sep_oid, target) {
continue;
}
}
let child = crate::bytes::le_u64(entries[i].value, 0);
descend_virtual(
reader,
omap,
child,
xid,
block_size,
depth + 1,
target_oid,
visited,
visit,
)?;
}
Ok(())
}
pub fn list_dir<R: Read + Seek>(
reader: &mut R,
volume: &ApfsVolume,
parent_oid: u64,
block_size: usize,
) -> crate::Result<Vec<DirEntry>> {
let mut out = Vec::new();
for_each_fs_record_for_oid(reader, volume, block_size, parent_oid, &mut |key, value| {
let (oid, ty) = decode_jkey(crate::bytes::le_u64(key, 0));
if ty != Some(RecordType::DirRec) || oid != parent_oid {
return;
}
let Some(entry) = parse_dir_entry(key, value) else {
return; };
out.push(entry);
})?;
Ok(out)
}
pub fn lookup_child<R: Read + Seek>(
reader: &mut R,
volume: &ApfsVolume,
parent_oid: u64,
name: &str,
block_size: usize,
) -> crate::Result<Option<u64>> {
let mut found = None;
for_each_fs_record_for_oid(reader, volume, block_size, parent_oid, &mut |key, value| {
if found.is_some() {
return;
}
let (oid, ty) = decode_jkey(crate::bytes::le_u64(key, 0));
if ty != Some(RecordType::DirRec) || oid != parent_oid {
return;
}
let Some(entry) = parse_dir_entry(key, value) else {
return; };
if entry.name == name {
found = Some(entry.file_id);
}
})?;
Ok(found)
}
pub fn load_inode<R: Read + Seek>(
reader: &mut R,
volume: &ApfsVolume,
oid: u64,
block_size: usize,
) -> crate::Result<Inode> {
let mut value: Option<Vec<u8>> = None;
for_each_fs_record_for_oid(reader, volume, block_size, oid, &mut |key, val| {
if value.is_some() {
return;
}
let (k_oid, ty) = decode_jkey(crate::bytes::le_u64(key, 0));
if ty == Some(RecordType::Inode) && k_oid == oid {
value = Some(val.to_vec());
}
})?;
let value = value.ok_or(crate::ApfsError::OmapUnresolved {
oid,
xid: volume.xid(),
})?;
Inode::parse(oid, &value)
}
pub fn open_path<R: Read + Seek>(
reader: &mut R,
volume: &ApfsVolume,
path: &str,
block_size: usize,
) -> crate::Result<Inode> {
let mut current = ROOT_DIR_INO_NUM;
for component in path.split('/').filter(|c| !c.is_empty()) {
match lookup_child(reader, volume, current, component, block_size)? {
Some(child) => current = child,
None => {
return Err(crate::ApfsError::OmapUnresolved {
oid: current,
xid: volume.xid(),
});
}
}
}
load_inode(reader, volume, current, block_size)
}
fn decode_cstr(data: &[u8]) -> String {
let end = data.iter().position(|&b| b == 0).unwrap_or(data.len());
String::from_utf8_lossy(&data[..end]).into_owned()
}
#[cfg(test)]
mod tests {
use super::*;
fn jkey(ty: u64, oid: u64) -> [u8; 8] {
((ty << 60) | oid).to_le_bytes()
}
#[test]
fn decode_hashed_drec_name() {
let mut key = Vec::new();
key.extend_from_slice(&jkey(9, 2));
key.extend_from_slice(&5u32.to_le_bytes()); key.extend_from_slice(b"abcd\0");
assert_eq!(decode_drec_name(&key).as_deref(), Some("abcd"));
}
#[test]
fn child_pruning_selects_only_covering_subtrees() {
assert!(
!child_may_contain_oid(0, Some(10), 15),
"child [0,10) excludes 15"
);
assert!(
child_may_contain_oid(10, Some(20), 15),
"child [10,20) covers 15"
);
assert!(
!child_may_contain_oid(20, None, 15),
"child [20,inf) excludes 15"
);
assert!(
child_may_contain_oid(5, Some(10), 10),
"next_sep == target must descend (record may trail in this child)"
);
assert!(child_may_contain_oid(10, Some(20), 10));
assert!(child_may_contain_oid(5, None, 999));
assert!(!child_may_contain_oid(0, Some(5), 10));
}
const FSTREE: &[u8] = include_bytes!("../../tests/data/apfs_fstree.bin");
const FSTREE_BLOCK_SIZE: usize = 4096;
const FSTREE_APSB_BLOCK: usize = 371;
fn fstree_volume() -> ApfsVolume {
let b = &FSTREE
[FSTREE_APSB_BLOCK * FSTREE_BLOCK_SIZE..(FSTREE_APSB_BLOCK + 1) * FSTREE_BLOCK_SIZE];
ApfsVolume::parse(b).expect("parse APSB")
}
fn keys_for_oid_full(oid: u64) -> Vec<Vec<u8>> {
use std::io::Cursor;
let mut r = Cursor::new(FSTREE);
let vol = fstree_volume();
let mut out = Vec::new();
walk_fs_tree(&mut r, &vol, FSTREE_BLOCK_SIZE, None, &mut |k, _| {
if decode_jkey(crate::bytes::le_u64(k, 0)).0 == oid {
out.push(k.to_vec());
}
})
.expect("full walk");
out
}
fn keys_for_oid_keyed(oid: u64) -> Vec<Vec<u8>> {
use std::io::Cursor;
let mut r = Cursor::new(FSTREE);
let vol = fstree_volume();
let mut out = Vec::new();
for_each_fs_record_for_oid(&mut r, &vol, FSTREE_BLOCK_SIZE, oid, &mut |k, _| {
if decode_jkey(crate::bytes::le_u64(k, 0)).0 == oid {
out.push(k.to_vec());
}
})
.expect("keyed walk");
out
}
#[test]
fn keyed_walk_matches_full_walk_on_real_fs_tree() {
for oid in [2u64, 18, 22] {
assert_eq!(
keys_for_oid_keyed(oid),
keys_for_oid_full(oid),
"keyed vs full record set for oid {oid}"
);
}
assert!(keys_for_oid_keyed(999_999).is_empty());
assert_eq!(keys_for_oid_keyed(999_999), keys_for_oid_full(999_999));
}
#[test]
fn decode_unhashed_drec_name() {
let mut key = Vec::new();
key.extend_from_slice(&jkey(9, 2));
key.extend_from_slice(&3u16.to_le_bytes()); key.extend_from_slice(b"Xy\0");
let name = decode_drec_name(&key);
assert_eq!(name.as_deref(), Some("Xy"));
}
#[test]
fn decode_drec_name_rejects_overlong_length() {
let mut key = Vec::new();
key.extend_from_slice(&jkey(9, 2));
key.extend_from_slice(&0u32.to_le_bytes()); assert_eq!(decode_drec_name(&key), None);
}
#[test]
fn decode_drec_name_unhashed_length_past_key_is_none() {
let mut key = Vec::new();
key.extend_from_slice(&jkey(9, 2));
key.extend_from_slice(&200u16.to_le_bytes());
key.extend_from_slice(b"z"); assert_eq!(decode_drec_name(&key), None);
}
#[test]
fn parse_dir_entry_decodes_value() {
let mut key = Vec::new();
key.extend_from_slice(&jkey(9, 2));
key.extend_from_slice(&5u32.to_le_bytes());
key.extend_from_slice(b"abcd\0");
let mut value = Vec::new();
value.extend_from_slice(&42u64.to_le_bytes()); value.extend_from_slice(&1234u64.to_le_bytes()); value.extend_from_slice(&7u16.to_le_bytes()); let e = parse_dir_entry(&key, &value).expect("parse drec");
assert_eq!(e.name, "abcd");
assert_eq!(e.file_id, 42);
assert_eq!(e.date_added, 1234);
assert_eq!(e.flags, 7);
}
#[test]
fn parse_dir_entry_rejects_unnamed_key() {
let mut key = Vec::new();
key.extend_from_slice(&jkey(9, 2));
key.extend_from_slice(&0u32.to_le_bytes());
assert!(parse_dir_entry(&key, &[0u8; 18]).is_none());
}
}