#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::io::Cursor;
use apfs_core::dir::open_path;
use apfs_core::extent::read_data;
use apfs_core::volume::ApfsVolume;
use sha2::{Digest, Sha256};
const CONTENT: &[u8] = include_bytes!("../../tests/data/apfs_content.bin");
const BLOCK_SIZE: usize = 4096;
const APSB_BLOCK: usize = 438;
fn volume() -> ApfsVolume {
let block = &CONTENT[APSB_BLOCK * BLOCK_SIZE..(APSB_BLOCK + 1) * BLOCK_SIZE];
ApfsVolume::parse(block).expect("parse live APSB")
}
fn sha256_hex(data: &[u8]) -> String {
use std::fmt::Write;
Sha256::digest(data).iter().fold(String::new(), |mut s, b| {
let _ = write!(s, "{b:02x}");
s
})
}
#[test]
fn reads_plain_file_byte_identical() {
let mut r = Cursor::new(CONTENT);
let vol = volume();
let inode = open_path(&mut r, &vol, "/plain.txt", BLOCK_SIZE).expect("open plain.txt");
let bytes = read_data(&mut r, &vol, &inode, BLOCK_SIZE).expect("read plain.txt");
assert_eq!(bytes.len(), 35);
assert_eq!(
&bytes, b"APFS P4 plain file. Hello extents.\n",
"plain content must match"
);
assert_eq!(
sha256_hex(&bytes),
"289af0a0b21ede77675f1173c65cc2388d3b26570dbaaa7f2506444e05abf86b",
"plain.txt SHA-256 must match macOS cp"
);
}
#[test]
fn reads_sparse_file_with_hole_byte_identical() {
let mut r = Cursor::new(CONTENT);
let vol = volume();
let inode = open_path(&mut r, &vol, "/sparse.bin", BLOCK_SIZE).expect("open sparse.bin");
let bytes = read_data(&mut r, &vol, &inode, BLOCK_SIZE).expect("read sparse.bin");
assert_eq!(bytes.len(), 69632, "logical size honoured");
assert!(
bytes[..65536].iter().all(|&b| b == 0),
"sparse hole must be zero-filled"
);
assert!(
bytes[65536..].iter().any(|&b| b != 0),
"sparse tail carries data"
);
assert_eq!(
sha256_hex(&bytes),
"fe0fc4fa9e8465dd74086a9e37d21d1a69c0f4d08cf6b9abbb288886d4cc822a",
"sparse.bin SHA-256 must match macOS cp"
);
}
#[test]
fn reads_nested_file_byte_identical() {
let mut r = Cursor::new(CONTENT);
let vol = volume();
let inode = open_path(&mut r, &vol, "/Dir1/Beth.txt", BLOCK_SIZE).expect("open Beth.txt");
let bytes = read_data(&mut r, &vol, &inode, BLOCK_SIZE).expect("read Beth.txt");
assert_eq!(bytes.len(), 33);
assert_eq!(
sha256_hex(&bytes),
"ee7c26829909d9c7ea1bcb444908fff35c1254f3c51894bd49b8b175becfeb96",
"Beth.txt SHA-256 must match macOS cp"
);
}
#[test]
fn list_extents_reports_hole_and_tail() {
use apfs_core::extent::list_extents;
let mut r = Cursor::new(CONTENT);
let vol = volume();
let inode = open_path(&mut r, &vol, "/sparse.bin", BLOCK_SIZE).expect("open sparse.bin");
let extents = list_extents(&mut r, &vol, inode.private_id, BLOCK_SIZE).expect("list extents");
assert_eq!(extents.len(), 2, "got {extents:?}");
let hole = extents.iter().find(|e| e.logical_offset == 0).unwrap();
assert_eq!(hole.phys_block_num, 0, "first extent is a hole");
assert_eq!(hole.len, 65536);
let tail = extents.iter().find(|e| e.logical_offset == 65536).unwrap();
assert_ne!(tail.phys_block_num, 0, "tail is backed by a physical block");
assert_eq!(tail.len, 4096);
}
mod synthetic {
use super::{Cursor, BLOCK_SIZE};
use apfs_core::extent::{read_data, read_stream};
use apfs_core::object::fletcher64_checksum;
use apfs_core::volume::ApfsVolume;
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 jkey(ty: u64, oid: u64) -> u64 {
(ty << 60) | oid
}
fn write_omap_tree(block: &mut [u8], maps: &[(u64, u64)]) {
put_u16(block, 32, 0x7); put_u32(block, 36, maps.len() as u32);
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 voff = ((i + 1) * 16) as u16;
put_u16(block, toc + i * 4, (i * 16) as u16);
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 + 4, BLOCK_SIZE as u32);
put_u64(block, vs + 8, paddr);
}
seal(block, 0xb);
}
fn write_omap_header(block: &mut [u8], tree_paddr: u64) {
put_u64(block, 48, tree_paddr);
seal(block, 0xb);
}
fn write_fs_leaf(block: &mut [u8], entries: &[(Vec<u8>, Vec<u8>)]) {
put_u16(block, 32, 0x3); put_u32(block, 36, entries.len() as u32);
put_u16(block, 42, (entries.len() * 8) as u16);
let toc = 56;
let key_area = toc + entries.len() * 8;
let val_base = BLOCK_SIZE - 40;
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) {
put_u64(block, 16, 1); 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(records: &[(Vec<u8>, Vec<u8>)]) -> Vec<u8> {
let mut img = vec![0u8; BLOCK_SIZE * 5];
write_apsb(&mut img[0..BLOCK_SIZE], 1, 100);
write_omap_header(&mut img[BLOCK_SIZE..2 * BLOCK_SIZE], 2);
write_omap_tree(&mut img[2 * BLOCK_SIZE..3 * BLOCK_SIZE], &[(100, 3)]);
write_fs_leaf(&mut img[3 * BLOCK_SIZE..4 * BLOCK_SIZE], records);
img
}
fn inode_value_with_size(size: u64) -> Vec<u8> {
let mut v = vec![0u8; 92]; v.extend_from_slice(&1u16.to_le_bytes());
v.extend_from_slice(&8u16.to_le_bytes());
v.extend_from_slice(&[8u8, 0]); v.extend_from_slice(&8u16.to_le_bytes()); v.extend_from_slice(&size.to_le_bytes()); v
}
fn file_extent(oid: u64, logical: u64, len: u64, phys: u64) -> (Vec<u8>, Vec<u8>) {
let mut key = jkey(8, oid).to_le_bytes().to_vec();
key.extend_from_slice(&logical.to_le_bytes());
let mut val = (len & 0x00ff_ffff_ffff_ffff).to_le_bytes().to_vec();
val.extend_from_slice(&phys.to_le_bytes());
val.extend_from_slice(&0u64.to_le_bytes()); (key, val)
}
#[test]
fn read_stream_rejects_oversize_dstream() {
let img = build(&[]);
let vol = ApfsVolume::parse(&img[0..BLOCK_SIZE]).expect("apsb");
let mut r = Cursor::new(img);
let huge = (1u64 << 34) + 1;
match read_stream(&mut r, &vol, 50, huge, BLOCK_SIZE) {
Err(apfs_core::ApfsError::FieldOutOfRange { field: "size", .. }) => {}
other => panic!("expected FieldOutOfRange, got {other:?}"),
}
}
#[test]
fn read_stream_rejects_overflowing_phys_block() {
let inode_oid = 50u64;
let ext = file_extent(inode_oid, 0, BLOCK_SIZE as u64, u64::MAX);
let img = build(&[ext]);
let vol = ApfsVolume::parse(&img[0..BLOCK_SIZE]).expect("apsb");
let mut r = Cursor::new(img);
match read_stream(&mut r, &vol, inode_oid, BLOCK_SIZE as u64, BLOCK_SIZE) {
Err(apfs_core::ApfsError::FieldOutOfRange {
field: "phys_block_num",
..
}) => {}
other => panic!("expected phys overflow, got {other:?}"),
}
}
#[test]
fn read_data_decodes_inline_compressed_end_to_end() {
let inode_oid = 50u64;
let content = b"inline decmpfs content via read_data";
let inode_rec = (
jkey(3, inode_oid).to_le_bytes().to_vec(),
inode_value_with_size(0),
);
let mut xattr_key = jkey(4, inode_oid).to_le_bytes().to_vec();
let name = b"com.apple.decmpfs\0";
xattr_key.extend_from_slice(&(name.len() as u16).to_le_bytes());
xattr_key.extend_from_slice(name);
let mut hdr = 0x636d_7066u32.to_le_bytes().to_vec(); hdr.extend_from_slice(&1u32.to_le_bytes()); hdr.extend_from_slice(&(content.len() as u64).to_le_bytes());
hdr.extend_from_slice(content);
let mut xattr_val = 0x0002u16.to_le_bytes().to_vec(); xattr_val.extend_from_slice(&(hdr.len() as u16).to_le_bytes());
xattr_val.extend_from_slice(&hdr);
let img = build(&[inode_rec, (xattr_key, xattr_val)]);
let vol = ApfsVolume::parse(&img[0..BLOCK_SIZE]).expect("apsb");
let mut r = Cursor::new(img);
let inode =
apfs_core::dir::load_inode(&mut r, &vol, inode_oid, BLOCK_SIZE).expect("load inode");
let out = read_data(&mut r, &vol, &inode, BLOCK_SIZE).expect("read inline compressed");
assert_eq!(out, content);
}
}