ic-query 0.29.3

Internet Computer query library for NNS, SNS, ICRC, system canisters, and public network metadata
Documentation
use super::*;
use crate::test_support::temp_dir;
use std::{fs, 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_managed_text(&root, &path).expect("read managed cache"),
        Some("evidence".to_string())
    );
    #[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);
}

#[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_managed_text(&root, &root.join("linked-dir/cache.json"))
        .expect_err("symlink parent rejected");
    assert!(matches!(parent_error, 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_managed_text(&root, &root.join("linked.json")).expect_err("symlink file rejected");
    assert!(matches!(file_error, CacheFileError::Confinement { .. }));

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

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

#[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(0o644)).expect("widen file mode");
    let file_error = read_managed_text(&root, &path).expect_err("unsafe file mode rejected");
    assert!(matches!(
        file_error,
        CacheFileError::UnsafeManagedPermissions {
            actual_mode: 0o644,
            ..
        }
    ));

    fs::set_permissions(&path, fs::Permissions::from_mode(0o600)).expect("restore file mode");
    fs::set_permissions(&directory, fs::Permissions::from_mode(0o755))
        .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: 0o755,
            ..
        }
    ));

    fs::set_permissions(&directory, fs::Permissions::from_mode(0o700))
        .expect("restore managed directory mode");
    fs::set_permissions(&root, fs::Permissions::from_mode(0o755)).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: 0o755,
            ..
        }
    ));

    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_managed_file(&root, &path).expect("missing read"), None);
    assert!(!Path::new(&root).exists());
}