#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::io::Cursor;
use apfs_core::snapshot::{list_snapshots, mount_snapshot, resolve_snapshot_xid};
use apfs_core::volume::ApfsVolume;
const CONTENT: &[u8] = include_bytes!("../../tests/data/apfs_content.bin");
const BLOCK_SIZE: usize = 4096;
const APSB_BLOCK: usize = 438;
fn p4_volume() -> ApfsVolume {
let block = &CONTENT[APSB_BLOCK * BLOCK_SIZE..(APSB_BLOCK + 1) * BLOCK_SIZE];
ApfsVolume::parse(block).expect("parse live APSB")
}
#[test]
fn real_unsnapshotted_volume_lists_zero_snapshots() {
let mut r = Cursor::new(CONTENT);
let vol = p4_volume();
let snaps = list_snapshots(&mut r, &vol, BLOCK_SIZE).expect("list snapshots");
assert!(
snaps.is_empty(),
"P4 fixture has no snapshots; got {snaps:?}"
);
}
#[test]
fn real_volume_exposes_snap_meta_tree_oid() {
let vol = p4_volume();
assert_eq!(vol.snap_meta_tree_oid(), 340);
}
#[test]
fn real_unsnapshotted_volume_resolves_no_name() {
let mut r = Cursor::new(CONTENT);
let vol = p4_volume();
let xid = resolve_snapshot_xid(&mut r, &vol, "nonexistent", BLOCK_SIZE)
.expect("resolve snapshot name");
assert_eq!(xid, None, "no snapshot named 'nonexistent' exists");
}
fn sha256_hex(data: &[u8]) -> String {
use sha2::{Digest, Sha256};
use std::fmt::Write;
Sha256::digest(data).iter().fold(String::new(), |mut s, b| {
let _ = write!(s, "{b:02x}");
s
})
}
struct PartitionView<R> {
inner: R,
base: u64,
}
impl<R: std::io::Seek> PartitionView<R> {
fn new(mut inner: R, base: u64) -> std::io::Result<Self> {
inner.seek(std::io::SeekFrom::Start(base))?;
Ok(Self { inner, base })
}
}
impl<R: std::io::Read> std::io::Read for PartitionView<R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.inner.read(buf)
}
}
impl<R: std::io::Seek> std::io::Seek for PartitionView<R> {
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
use std::io::SeekFrom;
let abs = match pos {
SeekFrom::Start(o) => self.inner.seek(SeekFrom::Start(self.base + o))?,
SeekFrom::Current(d) => self.inner.seek(SeekFrom::Current(d))?,
SeekFrom::End(_) => {
return Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"End-relative seek unsupported on a partition view",
))
}
};
Ok(abs.saturating_sub(self.base))
}
}
fn read_block<R: std::io::Read + std::io::Seek>(r: &mut R, block: u64, bs: usize) -> Vec<u8> {
use std::io::SeekFrom;
r.seek(SeekFrom::Start(block * bs as u64))
.expect("seek block");
let mut buf = vec![0u8; bs];
r.read_exact(&mut buf).expect("read block");
buf
}
#[test]
fn populated_fixture_point_in_time_read() {
let Ok(path) = std::env::var("APFS_P5_FIXTURE") else {
eprintln!("APFS_P5_FIXTURE unset; skipping populated point-in-time test");
return;
};
let part_offset: u64 = std::env::var("APFS_P5_PART_OFFSET")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(0);
let file = std::env::var("APFS_P5_FILE").unwrap_or_else(|_| "/changing.txt".to_string());
let f = std::fs::File::open(&path).expect("open APFS_P5_FIXTURE");
let mut r = PartitionView::new(f, part_offset).expect("seek to container partition");
let mut container = apfs_core::ApfsContainer::open(&mut r).expect("open container");
let apsb_addrs = container
.volume_superblock_addrs()
.expect("resolve volume APSB addrs");
drop(container);
let mut best: Option<(ApfsVolume, Vec<apfs_core::snapshot::Snapshot>)> = None;
for &paddr in &apsb_addrs {
let block = read_block(&mut r, paddr, BLOCK_SIZE);
let Ok(vol) = ApfsVolume::parse(&block) else {
continue;
};
let snaps = list_snapshots(&mut r, &vol, BLOCK_SIZE).unwrap_or_default();
if best.as_ref().is_none_or(|(_, b)| snaps.len() > b.len()) {
best = Some((vol, snaps));
}
}
let (live, snaps) = best.expect("fixture must expose at least one volume");
assert!(
snaps.len() >= 2,
"expected >=2 snapshots in the populated fixture, got {}",
snaps.len()
);
for s in &snaps {
let resolved =
resolve_snapshot_xid(&mut r, &live, &s.name, BLOCK_SIZE).expect("resolve name");
assert_eq!(resolved, Some(s.xid), "snap-name {} -> xid", s.name);
}
use apfs_core::dir::open_path;
use apfs_core::extent::read_data;
let snap_v1 = snaps.iter().min_by_key(|s| s.xid).expect("snapshot");
let snap_vol =
mount_snapshot(&mut r, &live, snap_v1, BLOCK_SIZE).expect("mount earliest snapshot");
let v1_inode = open_path(&mut r, &snap_vol, &file, BLOCK_SIZE).expect("open v1 file");
let v1 = read_data(&mut r, &snap_vol, &v1_inode, BLOCK_SIZE).expect("read v1");
let live_inode = open_path(&mut r, &live, &file, BLOCK_SIZE).expect("open live file");
let v2 = read_data(&mut r, &live, &live_inode, BLOCK_SIZE).expect("read v2");
let v1_sha = std::env::var("APFS_P5_V1_SHA256").expect("APFS_P5_V1_SHA256 must be set");
let v2_sha = std::env::var("APFS_P5_V2_SHA256").expect("APFS_P5_V2_SHA256 must be set");
assert_eq!(sha256_hex(&v1), v1_sha, "snapshot file = v1 bytes");
assert_eq!(sha256_hex(&v2), v2_sha, "live file = v2 bytes");
assert_ne!(
v1, v2,
"v1 and v2 must differ (content changed across snapshot)"
);
}