ic-host-fs 0.26.2

Local host filesystem reads, durable publication and file locking
Documentation
use super::*;
use crate::test_support::Fixture;
use std::{
    error::Error as _,
    os::unix::fs::{PermissionsExt as _, symlink},
};

#[test]
fn complete_private_files_are_absolute_unique_and_caller_retained() {
    let fixture = Fixture::new();
    let alias = fixture.root.join("alias");
    let parent = fixture.root.join("parent");
    fs::create_dir(&parent).unwrap();
    symlink(&parent, &alias).unwrap();
    let payload = vec![0xa5; 128 * 1024];
    let path = create_private_bytes(&alias, "arguments", &payload, 4).unwrap();
    let empty = create_private_bytes(&alias, "arguments", b"", 4).unwrap();
    assert!(path.is_absolute());
    assert_eq!(path.parent().unwrap(), parent.canonicalize().unwrap());
    assert_ne!(path, empty);
    assert_eq!(fs::read(&path).unwrap(), payload);
    assert_eq!(fs::read(&empty).unwrap(), b"");
    let mode = fs::metadata(&path).unwrap().permissions().mode();
    assert_eq!(mode & 0o177, 0);
    assert!(path.exists());
    fs::remove_file(path).unwrap();
    fs::remove_file(empty).unwrap();
}

#[test]
fn collision_allowance_preserves_files_links_and_directories() {
    let fixture = Fixture::new();
    let candidate = |sequence| {
        fixture
            .root
            .join(format!("held-{}-{sequence}", std::process::id()))
    };
    let target = fixture.root.join("target");
    fs::write(&target, b"target").unwrap();
    fs::write(candidate(0), b"existing").unwrap();
    symlink(&target, candidate(1)).unwrap();
    fs::create_dir(candidate(2)).unwrap();
    let mut sequence = 0;
    let error = create_with_sequence(&fixture.root, "held", b"new", 3, || {
        let next = sequence;
        sequence += 1;
        next
    })
    .unwrap_err();
    assert!(
        matches!(error, PrivateBytesError::BeforeCreation(ref source) if source.kind() == io::ErrorKind::AlreadyExists)
    );
    assert_eq!(sequence, 3);
    assert_eq!(fs::read(candidate(0)).unwrap(), b"existing");
    assert_eq!(fs::read_link(candidate(1)).unwrap(), target);
    assert!(candidate(2).is_dir());
    assert!(!candidate(3).exists());
    assert_eq!(fs::read(&target).unwrap(), b"target");
    let path = create_with_sequence(&fixture.root, "held", b"new", 4, {
        let mut n = 0;
        move || {
            let next = n;
            n += 1;
            next
        }
    })
    .unwrap();
    assert_eq!(path, candidate(3));
    assert_eq!(fs::read(path).unwrap(), b"new");
}

#[test]
fn invalid_requests_and_open_failures_create_nothing() {
    let fixture = Fixture::new();
    for prefix in ["", ".", "..", "a/b", "a/", "a\n", "a\r", "a\0"] {
        assert!(
            matches!(create_private_bytes(&fixture.root, prefix, b"secret", 1), Err(PrivateBytesError::BeforeCreation(source)) if source.kind() == io::ErrorKind::InvalidInput)
        );
    }
    assert!(
        matches!(create_private_bytes(&fixture.root, "ok", b"", 0), Err(PrivateBytesError::BeforeCreation(source)) if source.kind() == io::ErrorKind::InvalidInput)
    );
    assert_eq!(fs::read_dir(&fixture.root).unwrap().count(), 0);
    let missing = create_private_bytes(&fixture.root.join("missing"), "ok", b"", 3).unwrap_err();
    assert_eq!(missing.to_string(), "private scratch creation failed");
    assert_eq!(
        missing
            .source()
            .unwrap()
            .downcast_ref::<io::Error>()
            .unwrap()
            .kind(),
        io::ErrorKind::NotFound
    );
    let file = fixture.root.join("file");
    fs::write(&file, b"preserved").unwrap();
    assert!(matches!(
        create_private_bytes(&file, "ok", b"", 3),
        Err(PrivateBytesError::BeforeCreation(_))
    ));
    assert_eq!(fs::read(file).unwrap(), b"preserved");
    assert_eq!(fs::read_dir(&fixture.root).unwrap().count(), 1);
}

#[test]
fn partial_write_failure_removes_only_the_created_candidate() {
    let fixture = Fixture::new();
    let path = fixture.root.join("candidate");
    let retained = fixture.root.join("retained");
    fs::write(&retained, b"retained").unwrap();
    let file = OpenOptions::new()
        .write(true)
        .create_new(true)
        .mode(0o600)
        .open(&path)
        .unwrap();
    let error = finish_candidate(
        file,
        path.clone(),
        |file| {
            file.write_all(b"partial")?;
            Err(io::Error::new(
                io::ErrorKind::BrokenPipe,
                "original write failure",
            ))
        },
        |path| fs::remove_file(path),
    )
    .unwrap_err();
    assert!(!path.exists());
    assert_eq!(error.to_string(), "private scratch write failed");
    assert_eq!(fs::read(retained).unwrap(), b"retained");
    assert!(
        matches!(&error, PrivateBytesError::Write { path: observed, source, cleanup: None } if observed == &path && source.kind() == io::ErrorKind::BrokenPipe)
    );
    assert_eq!(
        error.source().unwrap().to_string(),
        "original write failure"
    );
}

#[test]
fn failed_cleanup_retains_the_partial_file_and_both_causes() {
    let fixture = Fixture::new();
    let path = fixture.root.join("candidate");
    let file = OpenOptions::new()
        .write(true)
        .create_new(true)
        .mode(0o600)
        .open(&path)
        .unwrap();
    let error = finish_candidate(
        file,
        path.clone(),
        |file| {
            file.write_all(b"private partial bytes")?;
            Err(io::Error::new(
                io::ErrorKind::BrokenPipe,
                "original write failure",
            ))
        },
        |_| {
            Err(io::Error::new(
                io::ErrorKind::PermissionDenied,
                "original cleanup failure",
            ))
        },
    )
    .unwrap_err();
    assert_eq!(fs::read(&path).unwrap(), b"private partial bytes");
    assert!(
        matches!(&error, PrivateBytesError::Write { path: observed, source, cleanup: Some(cleanup) } if observed == &path && source.kind() == io::ErrorKind::BrokenPipe && cleanup.kind() == io::ErrorKind::PermissionDenied && cleanup.to_string() == "original cleanup failure")
    );
    assert_eq!(
        error.source().unwrap().to_string(),
        "original write failure"
    );
    assert_eq!(
        error.to_string(),
        "private scratch write and cleanup failed"
    );
}