ic-host-fs 0.27.4

Local host filesystem reads, durable publication and file locking
Documentation
use super::*;
use crate::{read::tree::checksum::checksum_directory, test_support::fixture};
use ic_host_artifacts::artifact::ArtifactError;
use std::{fs, os::unix::fs::symlink, path::PathBuf};

#[test]
fn syncs_files_then_directories_and_preserves_exact_component_framing() {
    let fixture = fixture();
    let root = fixture.path();
    fs::create_dir(root.join("a")).unwrap();
    fs::write(root.join("a/leaf"), b"a").unwrap();
    fs::write(root.join("a-file"), b"b").unwrap();
    let mut steps = Vec::new();
    let actual = sync_checksum_directory_at(&File::open(root).unwrap(), root, 1, |step, path| {
        steps.push((step, path.strip_prefix(root).unwrap().to_path_buf()));
        Ok(())
    })
    .unwrap();
    assert_eq!(
        actual.to_string(),
        "8ebe1da155356d34952f1515a80ee0c223ee530c1f87f6d10efa48f875b7c9b0"
    );
    assert_eq!(actual, checksum_directory(root, 1).unwrap());
    assert_eq!(
        steps.last(),
        Some(&(TreeSyncStep::RootDirectory, PathBuf::new()))
    );
    let position = |step, name: &str| {
        steps
            .iter()
            .position(|entry| entry == &(step, name.into()))
            .unwrap()
    };
    assert!(
        position(TreeSyncStep::RegularFile, "a/leaf")
            < position(TreeSyncStep::NestedDirectory, "a")
    );
    assert!(
        position(TreeSyncStep::NestedDirectory, "a") < position(TreeSyncStep::RootDirectory, "")
    );
    assert!(
        position(TreeSyncStep::RegularFile, "a-file") < position(TreeSyncStep::RootDirectory, "")
    );
    assert_eq!(steps.len(), 4);
    assert_eq!(fs::read(root.join("a/leaf")).unwrap(), b"a");
}

#[test]
fn each_sync_hook_can_refuse_without_later_steps_or_byte_replacement() {
    for denied in [
        TreeSyncStep::RegularFile,
        TreeSyncStep::NestedDirectory,
        TreeSyncStep::RootDirectory,
    ] {
        let fixture = fixture();
        fs::create_dir(fixture.path().join("nested")).unwrap();
        let path = fixture.path().join("nested/bytes");
        fs::write(&path, b"original").unwrap();
        let mut steps = Vec::new();
        let result = sync_checksum_directory_at(
            &File::open(fixture.path()).unwrap(),
            fixture.path(),
            8,
            |step, _| {
                steps.push(step);
                if step == denied {
                    Err(io::Error::from_raw_os_error(13))
                } else {
                    Ok(())
                }
            },
        );
        assert!(
            matches!(result, Err(TreeChecksumError::Io(error)) if error.raw_os_error() == Some(13))
        );
        assert_eq!(steps.last(), Some(&denied));
        assert_eq!(fs::read(path).unwrap(), b"original");
    }
}

#[test]
fn bounds_and_unsafe_entries_reject_without_removing_evidence() {
    let fixture = fixture();
    let root = fixture.path();
    let file = root.join("bytes");
    fs::write(&file, b"abc").unwrap();
    let directory = File::open(root).unwrap();
    assert!(matches!(
        sync_checksum_directory_at(&directory, root, 2, |_, _| Ok(())),
        Err(TreeChecksumError::Input(ArtifactError::LimitExceeded {
            limit: 2
        }))
    ));
    fs::rename(&file, root.join("retained")).unwrap();
    symlink(root.join("retained"), &file).unwrap();
    assert!(
        matches!(sync_checksum_directory_at(&directory, root, 3, |_, _| Ok(())),
        Err(TreeChecksumError::UnsupportedEntry { kind: FileType::Symlink, path }) if path == file)
    );
    fs::remove_file(&file).unwrap();
    crate::test_support::create_fifo(&file, None).unwrap();
    assert!(
        matches!(sync_checksum_directory_at(&directory, root, 3, |_, _| Ok(())),
        Err(TreeChecksumError::UnsupportedEntry { kind: FileType::Fifo, path }) if path == file)
    );
    assert_eq!(fs::read(root.join("retained")).unwrap(), b"abc");
}

#[test]
fn empty_and_renamed_held_roots_keep_exact_identity_without_reopening_display_path() {
    let fixture = fixture();
    let original = fixture.path().join("original");
    fs::create_dir(&original).unwrap();
    let directory = File::open(&original).unwrap();
    assert_eq!(
        sync_checksum_directory_at(&directory, &original, 0, |_, _| Ok(())).unwrap(),
        Sha256Digest::compute(b"")
    );
    fs::write(original.join("bytes"), b"abc").unwrap();
    let expected = checksum_directory(&original, 3).unwrap();
    let retained = fixture.path().join("retained");
    fs::rename(&original, &retained).unwrap();
    fs::create_dir(&original).unwrap();
    fs::write(original.join("bytes"), b"replacement").unwrap();
    assert_eq!(
        sync_checksum_directory_at(&directory, &original, 3, |_, _| Ok(())).unwrap(),
        expected
    );
    assert_eq!(fs::read(retained.join("bytes")).unwrap(), b"abc");
    assert_eq!(fs::read(original.join("bytes")).unwrap(), b"replacement");
}

#[test]
fn raw_non_utf8_names_preserve_exact_rejected_path() {
    use std::{ffi::OsStr, os::unix::ffi::OsStrExt};
    let fixture = fixture();
    let path = fixture.path().join(OsStr::from_bytes(b"\xff"));
    if let Err(error) = fs::write(&path, b"bytes") {
        assert!(
            cfg!(target_os = "macos")
                && error.raw_os_error() == Some(rustix::io::Errno::ILSEQ.raw_os_error())
        );
        return;
    }
    assert!(
        matches!(sync_checksum_directory_at(&File::open(fixture.path()).unwrap(), fixture.path(), 5, |_, _| Ok(())),
        Err(TreeChecksumError::NonUtf8Path { path: actual }) if actual == path)
    );
    assert_eq!(fs::read(path).unwrap(), b"bytes");
}