#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::io::Cursor;
use apfs_core::dir::{list_dir, lookup_child, open_path};
use apfs_core::volume::ApfsVolume;
const FSTREE: &[u8] = include_bytes!("../../tests/data/apfs_fstree.bin");
const BLOCK_SIZE: usize = 4096;
const APSB_BLOCK: usize = 371;
const ROOT_DIR_INO_NUM: u64 = 2;
fn volume() -> ApfsVolume {
let block = &FSTREE[APSB_BLOCK * BLOCK_SIZE..(APSB_BLOCK + 1) * BLOCK_SIZE];
ApfsVolume::parse(block).expect("parse APSB")
}
#[test]
fn list_root_directory() {
let mut r = Cursor::new(FSTREE);
let vol = volume();
let mut entries = list_dir(&mut r, &vol, ROOT_DIR_INO_NUM, BLOCK_SIZE).expect("list root");
entries.sort_by(|a, b| a.name.cmp(&b.name));
let named: Vec<(&str, u64)> = entries
.iter()
.map(|e| (e.name.as_str(), e.file_id))
.collect();
assert!(named.contains(&("top.txt", 22)), "got {named:?}");
assert!(named.contains(&("Dir1", 18)), "got {named:?}");
assert!(named.contains(&(".fseventsd", 16)), "got {named:?}");
}
#[test]
fn list_dir1_children() {
let mut r = Cursor::new(FSTREE);
let vol = volume();
let entries = list_dir(&mut r, &vol, 18, BLOCK_SIZE).expect("list Dir1");
let named: Vec<(&str, u64)> = entries
.iter()
.map(|e| (e.name.as_str(), e.file_id))
.collect();
assert!(named.contains(&("Beth.txt", 20)), "got {named:?}");
assert!(named.contains(&("Sub", 19)), "got {named:?}");
assert_eq!(entries.len(), 2);
}
#[test]
fn lookup_child_resolves_single_component() {
let mut r = Cursor::new(FSTREE);
let vol = volume();
assert_eq!(
lookup_child(&mut r, &vol, ROOT_DIR_INO_NUM, "Dir1", BLOCK_SIZE).unwrap(),
Some(18)
);
assert_eq!(
lookup_child(&mut r, &vol, 18, "Beth.txt", BLOCK_SIZE).unwrap(),
Some(20)
);
}
#[test]
fn lookup_child_missing_is_none() {
let mut r = Cursor::new(FSTREE);
let vol = volume();
assert_eq!(
lookup_child(&mut r, &vol, ROOT_DIR_INO_NUM, "nope", BLOCK_SIZE).unwrap(),
None
);
}
#[test]
fn open_path_resolves_top_level_file() {
let mut r = Cursor::new(FSTREE);
let vol = volume();
let inode = open_path(&mut r, &vol, "/top.txt", BLOCK_SIZE).expect("open /top.txt");
assert_eq!(inode.oid, 22);
assert_eq!(inode.size, Some(15));
assert_eq!(inode.name.as_deref(), Some("top.txt"));
assert_eq!(inode.parent_id, ROOT_DIR_INO_NUM);
}
#[test]
fn open_path_resolves_nested_file() {
let mut r = Cursor::new(FSTREE);
let vol = volume();
let inode = open_path(&mut r, &vol, "/Dir1/Beth.txt", BLOCK_SIZE).expect("open Beth");
assert_eq!(inode.oid, 20);
assert_eq!(inode.size, Some(38));
assert_eq!(inode.parent_id, 18);
}
#[test]
fn open_path_resolves_deeply_nested_file() {
let mut r = Cursor::new(FSTREE);
let vol = volume();
let inode = open_path(&mut r, &vol, "/Dir1/Sub/secret.bin", BLOCK_SIZE).expect("open secret");
assert_eq!(inode.oid, 21);
assert_eq!(inode.size, Some(26));
assert_eq!(inode.parent_id, 19);
}
#[test]
fn open_path_root_returns_root_inode() {
let mut r = Cursor::new(FSTREE);
let vol = volume();
let inode = open_path(&mut r, &vol, "/", BLOCK_SIZE).expect("open root");
assert_eq!(inode.oid, ROOT_DIR_INO_NUM);
}
#[test]
fn open_path_missing_component_errors() {
let mut r = Cursor::new(FSTREE);
let vol = volume();
assert!(open_path(&mut r, &vol, "/Dir1/missing.txt", BLOCK_SIZE).is_err());
assert!(open_path(&mut r, &vol, "/nope/Beth.txt", BLOCK_SIZE).is_err());
}
#[test]
fn open_path_handles_trailing_and_double_slashes() {
let mut r = Cursor::new(FSTREE);
let vol = volume();
let a = open_path(&mut r, &vol, "/Dir1/", BLOCK_SIZE).expect("trailing slash");
assert_eq!(a.oid, 18);
let b = open_path(&mut r, &vol, "/Dir1//Sub", BLOCK_SIZE).expect("double slash");
assert_eq!(b.oid, 19);
}
#[test]
fn fs_tree_node_checksum_mismatch_errors() {
let mut img = FSTREE.to_vec();
img[365 * BLOCK_SIZE + 200] ^= 0xff;
let mut r = Cursor::new(img);
let vol = volume();
match list_dir(&mut r, &vol, ROOT_DIR_INO_NUM, BLOCK_SIZE) {
Err(apfs_core::ApfsError::ChecksumMismatch { .. }) => {}
other => panic!("expected ChecksumMismatch, got {other:?}"),
}
}
mod synthetic {
use super::{Cursor, BLOCK_SIZE};
use apfs_core::object::fletcher64_checksum;
const OMAP_TYPE: u16 = 0xb;
fn put_u16(b: &mut [u8], o: usize, v: u16) {
b[o..o + 2].copy_from_slice(&v.to_le_bytes());
}
fn put_u32(b: &mut [u8], o: usize, v: u32) {
b[o..o + 4].copy_from_slice(&v.to_le_bytes());
}
fn put_u64(b: &mut [u8], o: usize, v: u64) {
b[o..o + 8].copy_from_slice(&v.to_le_bytes());
}
fn seal(block: &mut [u8], obj_type: u16) {
put_u32(block, 24, u32::from(obj_type));
let c = fletcher64_checksum(block);
block[0..8].copy_from_slice(&c.to_le_bytes());
}
fn write_omap_tree(block: &mut [u8], maps: &[(u64, u64)]) {
put_u16(block, 32, 0x7);
put_u16(block, 34, 0);
put_u32(block, 36, maps.len() as u32);
put_u16(block, 40, 0);
put_u16(block, 42, (maps.len() * 4) as u16);
let toc = 56;
let key_area = toc + maps.len() * 4;
let val_base = BLOCK_SIZE - 40; for (i, &(oid, paddr)) in maps.iter().enumerate() {
let koff = (i * 16) as u16;
let voff = ((i + 1) * 16) as u16;
put_u16(block, toc + i * 4, koff);
put_u16(block, toc + i * 4 + 2, voff);
put_u64(block, key_area + i * 16, oid);
put_u64(block, key_area + i * 16 + 8, 1);
let vs = val_base - voff as usize;
put_u32(block, vs, 0);
put_u32(block, vs + 4, BLOCK_SIZE as u32);
put_u64(block, vs + 8, paddr);
}
seal(block, OMAP_TYPE);
}
fn write_omap_header(block: &mut [u8], tree_paddr: u64) {
put_u64(block, 48, tree_paddr); seal(block, OMAP_TYPE);
}
fn write_fs_node(block: &mut [u8], level: u16, entries: &[(Vec<u8>, Vec<u8>)], root: bool) {
let mut flags = if level == 0 { 0x2 } else { 0x0 }; if root {
flags |= 0x1;
}
put_u16(block, 32, flags);
put_u16(block, 34, level);
put_u32(block, 36, entries.len() as u32);
let toc_len = (entries.len() * 8) as u16;
put_u16(block, 40, 0);
put_u16(block, 42, toc_len);
let toc = 56;
let key_area = toc + entries.len() * 8;
let val_base = if root { BLOCK_SIZE - 40 } else { BLOCK_SIZE };
let mut koff = 0usize;
let mut voff_acc = 0usize;
for (i, (k, v)) in entries.iter().enumerate() {
put_u16(block, toc + i * 8, koff as u16);
put_u16(block, toc + i * 8 + 2, k.len() as u16);
voff_acc += v.len();
put_u16(block, toc + i * 8 + 4, voff_acc as u16);
put_u16(block, toc + i * 8 + 6, v.len() as u16);
block[key_area + koff..key_area + koff + k.len()].copy_from_slice(k);
let vs = val_base - voff_acc;
block[vs..vs + v.len()].copy_from_slice(v);
koff += k.len();
}
seal(block, 0xe);
}
fn write_apsb(block: &mut [u8], omap_oid: u64, root_tree_oid: u64, xid: u64) {
put_u64(block, 16, xid); put_u32(block, 32, 0x4253_5041); put_u64(block, 128, omap_oid);
put_u64(block, 136, root_tree_oid);
block[704..707].copy_from_slice(b"SYN");
seal(block, 0xd); }
fn build_index_image() -> Vec<u8> {
let mut img = vec![0u8; BLOCK_SIZE * 6];
write_apsb(&mut img[0..BLOCK_SIZE], 1, 100, 1);
write_omap_header(&mut img[BLOCK_SIZE..2 * BLOCK_SIZE], 2);
write_omap_tree(
&mut img[2 * BLOCK_SIZE..3 * BLOCK_SIZE],
&[(100, 3), (101, 4)],
);
let idx_key = (9u64 << 60).to_le_bytes().to_vec(); let idx_val = 101u64.to_le_bytes().to_vec();
write_fs_node(
&mut img[3 * BLOCK_SIZE..4 * BLOCK_SIZE],
1,
&[(idx_key, idx_val)],
true,
);
let mut k = Vec::new();
k.extend_from_slice(&((9u64 << 60) | 2).to_le_bytes()); k.extend_from_slice(&5u32.to_le_bytes()); k.extend_from_slice(b"leaf\0");
let mut v = Vec::new();
v.extend_from_slice(&55u64.to_le_bytes()); v.extend_from_slice(&0u64.to_le_bytes()); v.extend_from_slice(&0u16.to_le_bytes()); write_fs_node(
&mut img[4 * BLOCK_SIZE..5 * BLOCK_SIZE],
0,
&[(k, v)],
false,
);
img
}
#[test]
fn descends_index_node_to_leaf() {
use apfs_core::dir::{list_dir, lookup_child, ROOT_DIR_INO_NUM};
use apfs_core::volume::ApfsVolume;
let img = build_index_image();
let vol = ApfsVolume::parse(&img[0..BLOCK_SIZE]).expect("parse synthetic APSB");
let mut r = Cursor::new(img);
let entries = list_dir(&mut r, &vol, ROOT_DIR_INO_NUM, BLOCK_SIZE).expect("list");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "leaf");
assert_eq!(entries[0].file_id, 55);
assert_eq!(
lookup_child(&mut r, &vol, ROOT_DIR_INO_NUM, "leaf", BLOCK_SIZE).unwrap(),
Some(55)
);
}
#[test]
fn cycle_guard_fires_on_self_referential_node() {
use apfs_core::dir::{list_dir, ROOT_DIR_INO_NUM};
use apfs_core::volume::ApfsVolume;
let mut img = vec![0u8; BLOCK_SIZE * 4];
write_apsb(&mut img[0..BLOCK_SIZE], 1, 100, 1);
write_omap_header(&mut img[BLOCK_SIZE..2 * BLOCK_SIZE], 2);
write_omap_tree(&mut img[2 * BLOCK_SIZE..3 * BLOCK_SIZE], &[(100, 3)]);
let key = (9u64 << 60).to_le_bytes().to_vec();
let val = 100u64.to_le_bytes().to_vec();
write_fs_node(
&mut img[3 * BLOCK_SIZE..4 * BLOCK_SIZE],
1,
&[(key, val)],
true,
);
let vol = ApfsVolume::parse(&img[0..BLOCK_SIZE]).expect("parse APSB");
let mut r = Cursor::new(img);
match list_dir(&mut r, &vol, ROOT_DIR_INO_NUM, BLOCK_SIZE) {
Err(apfs_core::ApfsError::CycleGuard { .. }) => {}
other => panic!("expected CycleGuard, got {other:?}"),
}
}
}