hadris-apfs 2.5.0

An experimental, read-only APFS container and filesystem reader
Documentation
#![cfg(all(feature = "sync", feature = "alloc"))]

mod common;

use common::*;
use hadris_apfs::sync::Container;
use hadris_io::Cursor;
use hadris_storage::sync::SeekBlockDevice;
use hadris_storage::{BlockCount, BlockGeometry, BlockSize};

fn open(image: &[u8]) -> Container<SeekBlockDevice<Cursor<'_>>> {
    let geometry = BlockGeometry::new(
        BlockSize::new(BLOCK as u32).unwrap(),
        BlockCount(IMAGE_BLOCKS as u64),
    );
    Container::open(SeekBlockDevice::new(Cursor::new(image), geometry)).unwrap()
}

fn check_reads_every_file(image: &[u8]) {
    let mut container = open(image);
    let superblock = container.superblock().clone();
    let volumes = container.volume_superblocks(&superblock).unwrap();
    assert_eq!(volumes.len(), 1);
    let volume = &volumes[0];

    let entries = container.root_directory_owned_entries(volume).unwrap();
    let mut names: Vec<_> = entries.iter().map(|entry| entry.name.clone()).collect();
    names.sort();
    let expected: Vec<_> = (0..FILE_COUNT).map(file_name).collect();
    assert_eq!(names, expected);

    for index in 0..FILE_COUNT {
        let entry = container
            .resolve_path(volume, &file_name(index))
            .unwrap()
            .expect("file present");
        let bytes = container
            .read_file(volume, entry.file_id, usize::MAX)
            .unwrap();
        assert_eq!(bytes, file_contents(index));
    }
}

#[test]
fn multi_level_filesystem_tree_resolves_virtual_children() {
    check_reads_every_file(&build_image());
}

#[test]
fn sealed_volume_tree_with_headerless_hashed_nodes_is_readable() {
    check_reads_every_file(&build_image_variant(Variant::Sealed));
}

#[test]
fn encrypted_tree_reports_unsupported() {
    let image = build_image_variant(Variant::Encrypted);
    let mut container = open(&image);
    let superblock = container.superblock().clone();
    let volumes = container.volume_superblocks(&superblock).unwrap();
    let error = container
        .root_directory_owned_entries(&volumes[0])
        .unwrap_err();
    assert_eq!(
        error,
        hadris_apfs::ApfsError::InvalidValue("encrypted B-tree nodes are not supported")
    );
}

#[test]
fn physical_walk_does_not_follow_virtual_links() {
    let image = build_image();
    let mut container = open(&image);
    assert!(container.btree_leaf_entries(FS_ROOT_BLOCK).is_err());
}

#[test]
fn a_child_named_twice_is_rejected() {
    let image = build_image_variant(Variant::Revisit);
    let mut container = open(&image);
    let superblock = container.superblock().clone();
    let volumes = container.volume_superblocks(&superblock).unwrap();
    assert_eq!(
        container
            .root_directory_owned_entries(&volumes[0])
            .unwrap_err(),
        hadris_apfs::ApfsError::InvalidValue("B-tree node revisited")
    );
}

#[test]
fn holes_read_as_zeros() {
    let image = build_image();
    let mut container = open(&image);
    let expected = holey_contents(1);
    let size = expected.len() as u64;
    let extents = holey_extents(1);

    let mut buf = vec![0xff_u8; expected.len() + 100];
    let n = container
        .read_extents_at(&extents, size, 0, &mut buf)
        .unwrap();
    assert_eq!(&buf[..n], expected);

    let mut buf = [0xff_u8; 10];
    let n = container
        .read_extents_at(&extents, size, 2 * BLOCK as u64 - 4, &mut buf)
        .unwrap();
    assert_eq!(&buf[..n], &expected[2 * BLOCK - 4..2 * BLOCK + 6]);
    assert_eq!(
        container
            .read_extents_at(&extents, size, size, &mut buf)
            .unwrap(),
        0
    );
}

#[test]
fn lookups_follow_the_volume_case_sensitivity() {
    let mut image = build_image();
    let upper = file_name(3).to_uppercase();
    {
        let mut container = open(&image);
        let superblock = container.superblock().clone();
        let volume = container.volume_superblocks(&superblock).unwrap().remove(0);
        assert!(container.resolve_path(&volume, &upper).unwrap().is_none());
    }
    set_volume_incompatible_features(&mut image, 1);
    let mut container = open(&image);
    let superblock = container.superblock().clone();
    let volume = container.volume_superblocks(&superblock).unwrap().remove(0);
    let entry = container.resolve_path(&volume, &upper).unwrap().unwrap();
    assert_eq!(
        container
            .read_file(&volume, entry.file_id, usize::MAX)
            .unwrap(),
        file_contents(3)
    );
}

#[test]
fn an_impossible_file_size_is_an_error() {
    let image = build_image_variant(Variant::HugeFile);
    let mut container = open(&image);
    let superblock = container.superblock().clone();
    let volume = container.volume_superblocks(&superblock).unwrap().remove(0);
    let entry = container
        .resolve_path(&volume, &file_name(0))
        .unwrap()
        .unwrap();
    assert_eq!(
        container
            .read_file(&volume, entry.file_id, usize::MAX)
            .unwrap_err(),
        hadris_apfs::ApfsError::InvalidValue("file is too large to read into memory")
    );
    let head = container.read_file(&volume, entry.file_id, 8).unwrap();
    assert_eq!(head, file_contents(0)[..8]);
}