ic-query 0.54.2

Internet Computer query library for CloudEngine, NNS, SNS, ICRC, system canisters, and public network metadata
Documentation
use super::*;
use crate::test_support::temp_dir;
use std::{
    fs,
    io::{self, Write},
    path::Path,
};

#[cfg(unix)]
use std::os::unix::fs::{PermissionsExt, symlink};

#[test]
fn managed_round_trip_uses_owner_only_modes() {
    let root = temp_dir("ic-query-confined-round-trip");
    let path = root.join("nns/ic/report.json");

    write_managed_text_atomically(&root, &path, "evidence").expect("write managed cache");

    assert_eq!(
        read_bounded_managed_file(&root, &path, 1024, None).expect("read managed cache"),
        Some(b"evidence".to_vec())
    );
    #[cfg(unix)]
    {
        assert_eq!(
            fs::metadata(&root)
                .expect("root metadata")
                .permissions()
                .mode()
                & 0o777,
            0o700
        );
        assert_eq!(
            fs::metadata(&path)
                .expect("file metadata")
                .permissions()
                .mode()
                & 0o777,
            0o600
        );
    }
    let _ = fs::remove_dir_all(root);
}

#[cfg(unix)]
#[test]
fn managed_directory_creation_tolerates_an_observation_race_without_weakening_confinement() {
    let parent_path = temp_dir("ic-query-confined-directory-race-parent");
    fs::create_dir(&parent_path).expect("create race parent");
    let parent = cap_std::fs::Dir::open_ambient_dir(&parent_path, cap_std::ambient_authority())
        .expect("open race parent");
    let root = parent_path.join("cache");
    let name = std::ffi::OsStr::new("cache");

    create_directory_component(&parent, name, &root, &root).expect("first creator succeeds");
    create_directory_component(&parent, name, &root, &root)
        .expect("creator that lost the observation race validates the directory");

    assert!(root.is_dir());

    let file_path = parent_path.join("file");
    fs::write(&file_path, "not a directory").expect("create race file");
    let file_error = create_directory_component(
        &parent,
        std::ffi::OsStr::new("file"),
        &file_path,
        &file_path,
    )
    .expect_err("existing file is rejected");
    assert!(matches!(file_error, CacheFileError::Confinement { .. }));

    let outside = temp_dir("ic-query-confined-directory-race-outside");
    fs::create_dir(&outside).expect("create symlink target");
    let symlink_path = parent_path.join("symlink");
    symlink(&outside, &symlink_path).expect("create race symlink");
    let symlink_error = create_directory_component(
        &parent,
        std::ffi::OsStr::new("symlink"),
        &symlink_path,
        &symlink_path,
    )
    .expect_err("existing symlink is rejected");
    assert!(matches!(symlink_error, CacheFileError::Confinement { .. }));

    let _ = fs::remove_dir_all(parent_path);
    let _ = fs::remove_dir_all(outside);
}

#[test]
fn bounded_managed_read_accepts_exact_limit_and_rejects_larger_file() {
    let root = temp_dir("ic-query-confined-bounded-read");
    let path = root.join("nns/ic/archive/object.json");
    write_managed_text_atomically(&root, &path, "12345678").expect("write bounded fixture");

    assert_eq!(
        read_bounded_managed_file(&root, &path, 8, None).expect("read exact limit"),
        Some(b"12345678".to_vec())
    );
    let error = read_bounded_managed_file(&root, &path, 7, None)
        .expect_err("larger managed file is rejected");
    assert!(matches!(
        error,
        BoundedManagedFileReadError::LimitExceeded {
            actual: 8,
            maximum: 7,
            ..
        }
    ));

    let _ = fs::remove_dir_all(root);
}

#[test]
fn streamed_atomic_write_failure_preserves_existing_file() {
    let root = temp_dir("ic-query-confined-stream-failure");
    let path = root.join("nns/ic/archive/manifest.json");
    write_managed_text_atomically(&root, &path, "complete").expect("initial managed file");

    let error = write_managed_file_atomically(&root, &path, |file| {
        file.write_all(b"partial")?;
        Err(io::Error::other("fixture write failure"))
    })
    .expect_err("streamed replacement failure");

    assert!(matches!(
        error,
        CacheFileError::PublishManagedFile {
            published: false,
            cleanup_error: None,
            ..
        }
    ));
    assert_eq!(
        read_bounded_managed_file(&root, &path, 1024, None).expect("preserved managed file"),
        Some(b"complete".to_vec())
    );
    assert_eq!(
        fs::read_dir(path.parent().expect("manifest parent"))
            .expect("manifest directory")
            .count(),
        1,
        "failed temporary file is removed"
    );
    let _ = fs::remove_dir_all(root);
}

#[cfg(unix)]
#[test]
fn managed_publication_stays_with_the_admitted_parent_after_path_replacement() {
    let root = temp_dir("ic-query-confined-parent-move");
    let path = root.join("parent/report.json");
    write_managed_text_atomically(&root, &path, "complete").expect("initial managed file");
    let moved = root.join("moved");
    write_managed_file_atomically(&root, &path, |file| {
        fs::rename(root.join("parent"), &moved)?;
        fs::create_dir(root.join("parent"))?;
        fs::write(&path, "replacement evidence")?;
        file.write_all(b"new complete")
    })
    .expect("publish through held parent");

    assert_eq!(
        fs::read_to_string(moved.join("report.json")).unwrap(),
        "new complete"
    );
    assert_eq!(fs::read_to_string(&path).unwrap(), "replacement evidence");
    assert_eq!(fs::read_dir(&moved).unwrap().count(), 1);
    assert_eq!(fs::read_dir(root.join("parent")).unwrap().count(), 1);
    let _ = fs::remove_dir_all(root);
}

#[cfg(unix)]
#[test]
fn failed_managed_publication_cleans_only_the_admitted_parent_after_path_replacement() {
    let root = temp_dir("ic-query-confined-parent-move-failure");
    let path = root.join("parent/report.json");
    write_managed_text_atomically(&root, &path, "complete").expect("initial managed file");
    let moved = root.join("moved");
    let error = write_managed_file_atomically(&root, &path, |file| {
        fs::rename(root.join("parent"), &moved)?;
        fs::create_dir(root.join("parent"))?;
        fs::write(&path, "replacement evidence")?;
        file.write_all(b"partial")?;
        Err(io::Error::from_raw_os_error(5))
    })
    .expect_err("producer failure");

    assert!(matches!(error, CacheFileError::PublishManagedFile {
        path: failed_path,
        published: false,
        source,
        cleanup_error: None,
    } if failed_path == path && source.raw_os_error() == Some(5)));
    assert_eq!(
        fs::read_to_string(moved.join("report.json")).unwrap(),
        "complete"
    );
    assert_eq!(fs::read_to_string(&path).unwrap(), "replacement evidence");
    assert_eq!(fs::read_dir(&moved).unwrap().count(), 1);
    assert_eq!(fs::read_dir(root.join("parent")).unwrap().count(), 1);
    let _ = fs::remove_dir_all(root);
}

#[cfg(unix)]
#[test]
fn publication_errors_retain_cleanup_evidence_and_visibility() {
    let path = Path::new("report.json");
    let error = managed_publication_error(
        path,
        NamedWriteError::Producer {
            source: serde_json::from_str::<String>("invalid").unwrap_err(),
            cleanup_error: Some(io::Error::from_raw_os_error(13)),
        },
    );
    assert!(matches!(error, CacheFileError::PublishManagedFile {
        published: false, source, cleanup_error: Some(cleanup), ..
    } if source.kind() == io::ErrorKind::InvalidData
        && source.get_ref().unwrap().is::<serde_json::Error>()
        && cleanup.raw_os_error() == Some(13)));

    let error = managed_publication_error(
        path,
        NamedWriteError::<io::Error>::BeforePublication {
            source: io::Error::from_raw_os_error(5),
            cleanup_error: Some(io::Error::from_raw_os_error(13)),
        },
    );
    assert!(matches!(error, CacheFileError::PublishManagedFile {
        published: false, source, cleanup_error: Some(cleanup), ..
    } if source.raw_os_error() == Some(5) && cleanup.raw_os_error() == Some(13)));

    let error = managed_publication_error(
        path,
        NamedWriteError::<io::Error>::AfterPublication {
            source: io::Error::from_raw_os_error(5),
        },
    );
    assert!(
        error
            .to_string()
            .contains("reconcile the destination before retrying")
    );
    assert!(matches!(error, CacheFileError::PublishManagedFile {
        published: true, source, cleanup_error: None, ..
    } if source.raw_os_error() == Some(5)));
}

#[test]
fn managed_path_rejects_escape() {
    let root = temp_dir("ic-query-confined-escape");
    let path = root.join("../outside.json");

    let error = write_managed_text_atomically(&root, &path, "evidence")
        .expect_err("parent traversal rejected");

    assert!(matches!(error, CacheFileError::Confinement { .. }));

    let outside = temp_dir("ic-query-confined-absolute-outside").join("cache.json");
    let outside_error = write_managed_text_atomically(&root, &outside, "evidence")
        .expect_err("absolute outside path rejected");
    assert!(matches!(outside_error, CacheFileError::Confinement { .. }));
    assert!(!outside.exists());
    let _ = fs::remove_dir_all(root);
}

#[cfg(unix)]
#[test]
fn managed_path_rejects_symlinked_parent_and_file() {
    let root = temp_dir("ic-query-confined-symlink");
    let outside = temp_dir("ic-query-confined-outside");
    fs::create_dir_all(&outside).expect("create outside");
    fs::set_permissions(&outside, fs::Permissions::from_mode(0o700)).expect("secure outside");
    write_managed_text_atomically(&root, &root.join("seed.json"), "seed").expect("create root");

    symlink(&outside, root.join("linked-dir")).expect("link directory");
    let parent_error =
        read_bounded_managed_file(&root, &root.join("linked-dir/cache.json"), 1024, None)
            .expect_err("symlink parent rejected");
    assert!(matches!(
        parent_error,
        BoundedManagedFileReadError::Operation(CacheFileError::Confinement { .. })
    ));

    fs::write(outside.join("target.json"), "outside").expect("write outside target");
    symlink(outside.join("target.json"), root.join("linked.json")).expect("link file");
    let file_error = read_bounded_managed_file(&root, &root.join("linked.json"), 1024, None)
        .expect_err("symlink file rejected");
    assert!(matches!(
        file_error,
        BoundedManagedFileReadError::Operation(CacheFileError::Confinement { .. })
    ));

    let linked_root = temp_dir("ic-query-confined-root-link");
    symlink(&outside, &linked_root).expect("link cache root");
    let root_error =
        read_bounded_managed_file(&linked_root, &linked_root.join("target.json"), 1024, None)
            .expect_err("symlink root rejected");
    assert!(matches!(
        root_error,
        BoundedManagedFileReadError::Operation(CacheFileError::Confinement { .. })
    ));

    let _ = fs::remove_dir_all(root);
    let _ = fs::remove_file(linked_root);
    let _ = fs::remove_dir_all(outside);
}

#[cfg(unix)]
#[test]
fn readable_cache_tree_supports_load_and_publication_without_permission_repair() {
    let root = temp_dir("ic-query-confined-readable");
    let directory = root.join("nns");
    let path = directory.join("cache.json");
    fs::create_dir_all(&directory).expect("create readable cache tree");
    for directory in [&root, &directory] {
        fs::set_permissions(directory, fs::Permissions::from_mode(0o755))
            .expect("set readable directory mode");
    }
    fs::write(&path, "evidence").expect("write readable cache");
    fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).expect("set readable file mode");

    assert_eq!(
        read_bounded_managed_file(&root, &path, 1024, None).expect("read readable cache"),
        Some(b"evidence".to_vec())
    );
    assert!(managed_file_exists(&root, &path).expect("inspect readable cache"));
    write_managed_text_atomically(&root, &path, "replacement")
        .expect("publish into readable cache tree");
    assert_eq!(fs::read(&path).expect("read replacement"), b"replacement");
    for directory in [&root, &directory] {
        assert_eq!(
            fs::metadata(directory).unwrap().permissions().mode() & 0o777,
            0o755
        );
    }
    assert_eq!(
        fs::metadata(&path).unwrap().permissions().mode() & 0o777,
        0o600
    );
    fs::remove_dir_all(root).expect("remove readable cache tree");
}

#[cfg(unix)]
#[test]
fn managed_path_rejects_unsafe_directory_and_file_modes() {
    let root = temp_dir("ic-query-confined-mode");
    let directory = root.join("nns");
    let path = directory.join("cache.json");
    write_managed_text_atomically(&root, &path, "evidence").expect("write managed cache");

    fs::set_permissions(&path, fs::Permissions::from_mode(0o664)).expect("widen file mode");
    let file_error =
        read_bounded_managed_file(&root, &path, 1024, None).expect_err("unsafe file mode rejected");
    assert!(matches!(
        file_error,
        BoundedManagedFileReadError::Operation(CacheFileError::UnsafeManagedPermissions {
            actual_mode: 0o664,
            ..
        })
    ));

    fs::set_permissions(&path, fs::Permissions::from_mode(0o600)).expect("restore file mode");
    fs::set_permissions(&directory, fs::Permissions::from_mode(0o775))
        .expect("widen managed directory mode");
    let directory_error =
        managed_file_exists(&root, &path).expect_err("unsafe managed directory rejected");
    assert!(matches!(
        directory_error,
        CacheFileError::UnsafeManagedPermissions {
            actual_mode: 0o775,
            ..
        }
    ));

    fs::set_permissions(&directory, fs::Permissions::from_mode(0o700))
        .expect("restore managed directory mode");
    fs::set_permissions(&root, fs::Permissions::from_mode(0o757)).expect("widen root mode");
    let root_error = managed_file_exists(&root, &path).expect_err("unsafe root mode rejected");
    assert!(matches!(
        root_error,
        CacheFileError::UnsafeManagedPermissions {
            actual_mode: 0o757,
            ..
        }
    ));

    fs::set_permissions(&root, fs::Permissions::from_mode(0o700)).expect("restore root mode");
    let _ = fs::remove_dir_all(root);
}

#[test]
fn missing_root_and_file_are_observed_without_creation() {
    let root = temp_dir("ic-query-confined-missing");
    let path = root.join("cache.json");

    assert_eq!(
        read_bounded_managed_file(&root, &path, 1024, None).expect("missing read"),
        None
    );
    assert!(!Path::new(&root).exists());
}