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());
}