use super::{
hash_file_no_follow, open_no_follow, open_optional_no_follow, read_file_no_follow,
read_optional_file_no_follow,
};
use crate::{
read::{check_metadata, hash_file, read_file, read_opened_file},
test_support::fixture,
};
use ic_host_artifacts::artifact::{ArtifactError, ArtifactIdentity, Sha256Digest, hash_reader};
use rustix::fs::{Mode, OFlags, open};
use std::{
fs::{self, File},
io::{self, Seek as _, SeekFrom},
os::unix::fs::symlink,
path::Path,
sync::mpsc,
thread,
time::Duration,
};
#[test]
fn exact_limits_and_empty_files_preserve_the_selected_bytes() {
let fixture = fixture();
let path = fixture.path().join("artifact");
let bytes = vec![42; 40_001];
fs::write(&path, &bytes).unwrap();
assert_eq!(read_file_no_follow(&path, bytes.len()).unwrap(), bytes);
assert_eq!(
hash_file_no_follow(&path, bytes.len() as u64).unwrap(),
ArtifactIdentity {
bytes: bytes.len() as u64,
sha256: Sha256Digest::compute(&bytes)
}
);
assert_eq!(
read_opened_file(File::open(&path).unwrap(), bytes.len()).unwrap(),
bytes
);
assert!(matches!(
read_file_no_follow(&path, bytes.len() - 1),
Err(ArtifactError::LimitExceeded { limit: 40_000 })
));
assert!(matches!(
hash_file_no_follow(&path, bytes.len() as u64 - 1),
Err(ArtifactError::LimitExceeded { limit: 40_000 })
));
assert_eq!(fs::read(&path).unwrap(), bytes);
fs::write(&path, []).unwrap();
assert_eq!(read_file_no_follow(&path, 0).unwrap(), [] as [u8; 0]);
assert_eq!(
hash_file_no_follow(&path, 0).unwrap(),
ArtifactIdentity {
bytes: 0,
sha256: Sha256Digest::compute(&[])
}
);
fs::write(&path, [1]).unwrap();
assert!(matches!(
read_file_no_follow(&path, 0),
Err(ArtifactError::LimitExceeded { limit: 0 })
));
assert!(matches!(
hash_file_no_follow(&path, 0),
Err(ArtifactError::LimitExceeded { limit: 0 })
));
}
#[test]
fn final_symlinks_are_rejected_without_changing_existing_following_reads() {
let fixture = fixture();
let target = fixture.path().join("target");
let link = fixture.path().join("link");
fs::write(&target, b"retained").unwrap();
symlink("target", &link).unwrap();
assert!(
matches!(read_file_no_follow(&link, 8), Err(ArtifactError::Io(error))
if error.raw_os_error() == Some(rustix::io::Errno::LOOP.raw_os_error()))
);
assert_eq!(read_file(&link, 8).unwrap(), b"retained");
assert_eq!(
hash_file(&link, 8).unwrap().sha256,
Sha256Digest::compute(b"retained")
);
assert!(
matches!(hash_file_no_follow(&link, 8), Err(ArtifactError::Io(error))
if error.raw_os_error() == Some(rustix::io::Errno::LOOP.raw_os_error()))
);
assert_eq!(
read_opened_file(File::open(&link).unwrap(), 8).unwrap(),
b"retained"
);
assert_eq!(fs::read(&target).unwrap(), b"retained");
let dangling = fixture.path().join("dangling");
symlink("missing", &dangling).unwrap();
assert!(
matches!(read_file_no_follow(&dangling, 8), Err(ArtifactError::Io(error))
if error.raw_os_error() == Some(rustix::io::Errno::LOOP.raw_os_error()))
);
assert!(
matches!(hash_file_no_follow(&dangling, 8), Err(ArtifactError::Io(error))
if error.raw_os_error() == Some(rustix::io::Errno::LOOP.raw_os_error()))
);
}
#[test]
fn ancestor_symlinks_remain_an_explicit_consumer_confinement_obligation() {
let fixture = fixture();
let actual = fixture.path().join("actual");
fs::create_dir(&actual).unwrap();
fs::write(actual.join("artifact"), b"data").unwrap();
symlink("actual", fixture.path().join("alias")).unwrap();
assert_eq!(
read_file_no_follow(&fixture.path().join("alias/artifact"), 4).unwrap(),
b"data"
);
assert_eq!(
hash_file_no_follow(&fixture.path().join("alias/artifact"), 4)
.unwrap()
.sha256,
Sha256Digest::compute(b"data")
);
}
#[test]
fn fifo_without_a_writer_is_rejected_without_waiting_for_one() {
let fixture = fixture();
let path = fixture.path().join("fifo");
crate::test_support::create_fifo(&path, Some(0o600)).unwrap();
let (tx, rx) = mpsc::channel();
let path_for_reader = path.clone();
let reader = thread::spawn(move || {
tx.send(read_file_no_follow(&path_for_reader, 1024))
.unwrap();
tx.send(hash_file_no_follow(&path_for_reader, 1024).map(|_| Vec::new()))
.unwrap();
});
for _ in 0..2 {
assert!(matches!(
rx.recv_timeout(Duration::from_secs(5))
.expect("FIFO read/hash must not wait for a writer"),
Err(ArtifactError::NotRegularFile)
));
}
reader.join().unwrap();
let fd = open(
&path,
OFlags::RDONLY | OFlags::NONBLOCK | OFlags::CLOEXEC,
Mode::empty(),
)
.unwrap();
assert!(matches!(
read_opened_file(File::from(fd), 1024),
Err(ArtifactError::NotRegularFile)
));
}
#[test]
fn opened_directories_and_devices_are_rejected_before_reading() {
let fixture = fixture();
for path in [fixture.path(), Path::new("/dev/null")] {
assert!(matches!(
hash_file_no_follow(path, 1024),
Err(ArtifactError::NotRegularFile)
));
}
assert!(matches!(
read_file_no_follow(fixture.path(), 1024),
Err(ArtifactError::NotRegularFile)
));
assert!(matches!(
read_opened_file(File::open(fixture.path()).unwrap(), 1024),
Err(ArtifactError::NotRegularFile)
));
assert!(matches!(
read_file_no_follow(Path::new("/dev/null"), 1024),
Err(ArtifactError::NotRegularFile)
));
}
#[test]
fn descriptor_identity_survives_path_replacement_and_reads_from_its_current_position() {
let fixture = fixture();
let path = fixture.path().join("artifact");
let replacement = fixture.path().join("replacement");
fs::write(&path, b"original").unwrap();
let mut file = File::open(&path).unwrap();
file.seek(SeekFrom::Start(3)).unwrap();
fs::write(&replacement, b"different").unwrap();
fs::rename(&replacement, &path).unwrap();
assert_eq!(read_opened_file(file, 8).unwrap(), b"ginal");
assert_eq!(fs::read(&path).unwrap(), b"different");
let mut file = File::open(&path).unwrap();
file.seek(SeekFrom::End(-1)).unwrap();
assert!(matches!(
read_opened_file(file, 1),
Err(ArtifactError::LimitExceeded { limit: 1 })
));
}
#[test]
fn missing_and_invalid_paths_preserve_structured_open_failures() {
let fixture = fixture();
assert!(
matches!(hash_file_no_follow(&fixture.path().join("missing"), 1024),
Err(ArtifactError::Io(error)) if error.raw_os_error() == Some(rustix::io::Errno::NOENT.raw_os_error()))
);
assert!(
matches!(hash_file_no_follow(Path::new("invalid\0path"), 1024),
Err(ArtifactError::Io(error)) if error.kind() == io::ErrorKind::InvalidInput)
);
assert!(
matches!(read_file_no_follow(&fixture.path().join("missing"), 1024),
Err(ArtifactError::Io(error)) if error.kind() == io::ErrorKind::NotFound)
);
assert!(
matches!(read_file_no_follow(Path::new("invalid\0path"), 1024),
Err(ArtifactError::Io(error)) if error.kind() == io::ErrorKind::InvalidInput)
);
}
#[test]
fn streaming_hash_rejects_growth_after_descriptor_metadata_admission() {
let fixture = fixture();
let path = fixture.path().join("growing");
fs::write(&path, b"1234").unwrap();
let file = open_no_follow(&path).unwrap();
check_metadata(&file.metadata().unwrap(), 4).unwrap();
fs::write(&path, b"12345").unwrap();
assert!(matches!(
hash_reader(file, 4),
Err(ArtifactError::LimitExceeded { limit: 4 })
));
assert_eq!(fs::read(&path).unwrap(), b"12345");
}
#[test]
fn bounded_regular_read_rejects_existing_and_growing_oversize_content() {
let fixture = fixture();
let path = fixture.path().join("object");
fs::write(&path, b"12345").unwrap();
assert!(matches!(
read_optional_file_no_follow(&path, 4),
Err(ArtifactError::LimitExceeded { limit: 4 })
));
fs::write(&path, b"1234").unwrap();
let file = open_optional_no_follow(&path).unwrap().unwrap();
fs::write(&path, b"12345").unwrap();
assert!(matches!(
read_opened_file(file, 4),
Err(ArtifactError::LimitExceeded { limit: 4 })
));
}
#[test]
fn bounded_regular_read_preserves_exact_bytes_and_rejects_links() {
let fixture = fixture();
let path = fixture.path().join("object");
assert!(read_optional_file_no_follow(&path, 4).unwrap().is_none());
fs::write(&path, b"1234").unwrap();
assert_eq!(
read_optional_file_no_follow(&path, 4).unwrap(),
Some(b"1234".to_vec())
);
for (name, target) in [("linked", "object"), ("dangling", "missing")] {
let link = fixture.path().join(name);
symlink(target, &link).unwrap();
assert!(matches!(
read_optional_file_no_follow(&link, 4),
Err(ArtifactError::NotRegularFile)
));
}
assert!(matches!(
read_optional_file_no_follow(fixture.path(), 4),
Err(ArtifactError::NotRegularFile)
));
assert!(
matches!(read_optional_file_no_follow(Path::new("invalid\0path"), 4),
Err(ArtifactError::Io(error)) if error.kind() == io::ErrorKind::InvalidInput)
);
fs::write(&path, []).unwrap();
assert_eq!(
read_optional_file_no_follow(&path, 0).unwrap(),
Some(Vec::new())
);
}