#![allow(clippy::unwrap_used, clippy::expect_used)]
use apfs_core::fsrecord::{decode_jkey, RecordType};
use apfs_core::inode::{ns_to_datetime, Inode};
const FSTREE: &[u8] = include_bytes!("../../tests/data/apfs_fstree.bin");
const BLOCK_SIZE: usize = 4096;
const FSTREE_NODE: usize = 365;
fn u16(b: &[u8], o: usize) -> u16 {
u16::from_le_bytes(b[o..o + 2].try_into().unwrap())
}
fn u32(b: &[u8], o: usize) -> u32 {
u32::from_le_bytes(b[o..o + 4].try_into().unwrap())
}
fn u64(b: &[u8], o: usize) -> u64 {
u64::from_le_bytes(b[o..o + 8].try_into().unwrap())
}
fn inode_value(oid: u64) -> Vec<u8> {
let node = &FSTREE[FSTREE_NODE * BLOCK_SIZE..(FSTREE_NODE + 1) * BLOCK_SIZE];
let nkeys = u32(node, 36) as usize;
let toc_start = 56 + u16(node, 40) as usize;
let key_area = toc_start + u16(node, 42) as usize;
let val_base = node.len() - 40; for i in 0..nkeys {
let e = toc_start + i * 8;
let koff = u16(node, e) as usize;
let klen = u16(node, e + 2) as usize;
let voff = u16(node, e + 4) as usize;
let vlen = u16(node, e + 6) as usize;
let key = &node[key_area + koff..key_area + koff + klen];
let (k_oid, ty) = decode_jkey(u64(key, 0));
if ty == Some(RecordType::Inode) && k_oid == oid {
return node[val_base - voff..val_base - voff + vlen].to_vec();
}
}
panic!("inode {oid} not found in fs-tree node");
}
#[test]
fn parse_beth_txt_inode_metadata() {
let v = inode_value(20);
let inode = Inode::parse(20, &v).expect("parse inode");
assert_eq!(inode.oid, 20);
assert_eq!(inode.parent_id, 18);
assert_eq!(inode.name.as_deref(), Some("Beth.txt"));
assert_eq!(inode.size, Some(38));
assert_eq!(inode.mode, 0o100_644);
assert_eq!(inode.uid, 99);
assert_eq!(inode.gid, 99);
assert_eq!(inode.nlink_or_nchildren, 1);
}
#[test]
fn parse_root_dir_inode_metadata() {
let v = inode_value(2);
let inode = Inode::parse(2, &v).expect("parse root inode");
assert_eq!(inode.mode, 0o040_755);
assert_eq!(inode.uid, 501);
assert_eq!(inode.gid, 20);
assert_eq!(inode.nlink_or_nchildren, 3);
assert_eq!(inode.size, None);
}
#[test]
fn parse_inode_timestamps_match_istat() {
let v = inode_value(20);
let inode = Inode::parse(20, &v).expect("parse inode");
assert_eq!(inode.create_time, 1_782_060_082_608_648_902);
assert_eq!(inode.mod_time, 1_782_060_082_608_686_902);
assert_eq!(inode.change_time, 1_782_060_082_608_686_902);
assert_eq!(inode.access_time, 1_782_060_082_733_745_215);
}
#[test]
fn created_datetime_round_trips() {
let v = inode_value(20);
let inode = Inode::parse(20, &v).expect("parse inode");
let dt = inode.created().expect("create time -> datetime");
assert_eq!(dt.timestamp(), 1_782_060_082);
assert_eq!(dt.timestamp_subsec_nanos(), 608_648_902);
}
#[test]
fn ns_to_datetime_zero_is_epoch() {
let dt = ns_to_datetime(0).expect("epoch");
assert_eq!(dt.timestamp(), 0);
}
#[test]
fn ns_to_datetime_handles_huge_value() {
let dt = ns_to_datetime(u64::MAX).expect("u64::MAX ns is representable");
assert!(dt.timestamp() > 0);
}
#[test]
fn inode_datetime_accessors_round_trip() {
let v = inode_value(20);
let inode = Inode::parse(20, &v).expect("parse inode");
assert_eq!(
inode.modified().unwrap().timestamp_subsec_nanos(),
608_686_902
);
assert_eq!(
inode.changed().unwrap().timestamp_subsec_nanos(),
608_686_902
);
assert_eq!(
inode.accessed().unwrap().timestamp_subsec_nanos(),
733_745_215
);
}
#[test]
fn parse_inode_with_unknown_xfield_is_ignored() {
let mut v = vec![0u8; 92];
v.extend_from_slice(&1u16.to_le_bytes());
v.extend_from_slice(&8u16.to_le_bytes());
v.push(99);
v.push(0);
v.extend_from_slice(&8u16.to_le_bytes());
v.extend_from_slice(&[0u8; 8]);
let inode = Inode::parse(7, &v).expect("parse inode with unknown xfield");
assert_eq!(inode.name, None);
assert_eq!(inode.size, None);
}
#[test]
fn parse_short_inode_value_is_bounded() {
let inode = Inode::parse(99, &[0u8; 8]).expect("parse short value");
assert_eq!(inode.oid, 99);
assert_eq!(inode.parent_id, 0);
assert_eq!(inode.name, None);
}