ic-host-fs 0.24.0

Local host filesystem reads, durable publication and file locking
Documentation
use super::*;
use crate::test_support::Fixture;
use std::{fs, io::Read, os::unix::fs::symlink};

#[test]
fn root_relative_entry_preserves_file_and_directory_descriptors() {
    let fixture = Fixture::new();
    fs::create_dir(fixture.root.join("tree")).unwrap();
    fs::write(fixture.root.join("tree/data"), b"original").unwrap();
    let root = File::open(&fixture.root).unwrap();
    let OpenedEntry::Directory(directory) =
        open_entry_beneath(root.as_fd(), Path::new("tree")).unwrap()
    else {
        panic!("selected directory");
    };
    let OpenedEntry::RegularFile(mut file) =
        open_entry_beneath(root.as_fd(), Path::new("tree/data")).unwrap()
    else {
        panic!("selected regular file");
    };
    fs::rename(fixture.root.join("tree"), fixture.root.join("moved")).unwrap();
    fs::create_dir(fixture.root.join("tree")).unwrap();
    fs::write(fixture.root.join("tree/data"), b"changed").unwrap();
    assert!(directory.metadata().unwrap().is_dir());
    let mut bytes = Vec::new();
    file.read_to_end(&mut bytes).unwrap();
    assert_eq!(bytes, b"original");
    assert_eq!(
        fs::read(fixture.root.join("tree/data")).unwrap(),
        b"changed"
    );
}

#[test]
fn root_relative_entry_refuses_aliases_and_special_files() {
    let fixture = Fixture::new();
    fs::create_dir(fixture.root.join("tree")).unwrap();
    fs::write(fixture.root.join("tree/data"), b"data").unwrap();
    symlink("tree", fixture.root.join("alias")).unwrap();
    symlink("data", fixture.root.join("tree/link")).unwrap();
    assert!(
        std::process::Command::new("/usr/bin/mkfifo")
            .arg(fixture.root.join("tree/fifo"))
            .status()
            .unwrap()
            .success()
    );
    let root = File::open(&fixture.root).unwrap();
    for path in ["", "/tree", "tree/../tree", "alias", "tree/link"] {
        assert!(matches!(
            open_entry_beneath(root.as_fd(), Path::new(path)),
            Err(OpenEntryError::Io(ref error)) if error.kind() == io::ErrorKind::InvalidInput
        ));
    }
    assert!(matches!(
        open_entry_beneath(root.as_fd(), Path::new("tree/fifo")),
        Err(OpenEntryError::UnsupportedEntry {
            kind: FileType::Fifo
        })
    ));
}

#[test]
fn descriptor_selection_survives_path_replacement_and_bounds_reads() {
    let fixture = Fixture::new();
    let dir = &fixture.root;
    fs::create_dir(dir.join("input")).unwrap();
    fs::write(dir.join("input/body"), b"original").unwrap();
    let root = File::open(dir).unwrap();
    let mut body = open_regular_file_beneath(root.as_fd(), Path::new("input/body"), 8).unwrap();
    fs::rename(dir.join("input"), dir.join("retained")).unwrap();
    fs::create_dir(dir.join("input")).unwrap();
    fs::write(dir.join("input/body"), b"replacement").unwrap();
    let mut bytes = Vec::new();
    body.read_to_end(&mut bytes).unwrap();
    assert_eq!(bytes, b"original");
    assert!(open_regular_file_beneath(root.as_fd(), Path::new("input/body"), 8).is_err());
    drop(body);
    drop(root);
    drop(fixture);
}

#[test]
fn traversal_aliases_special_files_and_missing_inputs_fail_closed() {
    let fixture = Fixture::new();
    let dir = &fixture.root;
    fs::create_dir(dir.join("input")).unwrap();
    fs::write(dir.join("input/body"), b"data").unwrap();
    symlink("input", dir.join("alias")).unwrap();
    symlink("body", dir.join("input/link")).unwrap();
    assert!(
        std::process::Command::new("/usr/bin/mkfifo")
            .arg(dir.join("input/fifo"))
            .status()
            .unwrap()
            .success()
    );
    let root = File::open(dir).unwrap();
    for path in [
        "",
        "/input/body",
        "../body",
        "input/../input/body",
        "alias/body",
        "input/link",
    ] {
        assert!(
            matches!(open_regular_file_beneath(root.as_fd(), Path::new(path), 100),
            Err(ArtifactError::Io(ref e)) if e.kind() == io::ErrorKind::InvalidInput)
        );
    }
    for path in ["input", "input/fifo"] {
        assert!(matches!(
            open_regular_file_beneath(root.as_fd(), Path::new(path), 100),
            Err(ArtifactError::NotRegularFile)
        ));
    }
    assert!(
        matches!(open_regular_file_beneath(root.as_fd(), Path::new("missing/body"), 100),
        Err(ArtifactError::Io(ref e)) if e.kind() == io::ErrorKind::NotFound)
    );
    assert_eq!(fs::read(dir.join("input/body")).unwrap(), b"data");
    drop(root);
    drop(fixture);
}

#[test]
fn root_opening_refuses_aliases_files_and_fifos_without_waiting() {
    let fixture = Fixture::new();
    let dir = &fixture.root;
    fs::create_dir(dir.join("root")).unwrap();
    fs::write(dir.join("file"), b"data").unwrap();
    symlink("root", dir.join("alias")).unwrap();
    assert!(
        std::process::Command::new("/usr/bin/mkfifo")
            .arg(dir.join("fifo"))
            .status()
            .unwrap()
            .success()
    );
    assert!(open_directory_no_follow(&dir.join("root")).is_ok());
    for name in ["alias", "file", "fifo"] {
        assert!(open_directory_no_follow(&dir.join(name)).is_err());
    }
}