#![expect(
clippy::significant_drop_tightening,
reason = "fixture ownership and serialization must last through filesystem assertions"
)]
use super::read_file_no_follow;
use crate::{
artifact::{ArtifactError, read_file, read_opened_file},
test_support::Fixture,
};
use rustix::fs::{Mode, OFlags, open};
use std::{
fs::{self, File},
io::{self, Seek as _, SeekFrom},
os::unix::fs::symlink,
path::Path,
process::Command,
sync::mpsc,
thread,
time::Duration,
};
#[test]
fn exact_limits_and_empty_files_preserve_the_selected_bytes() {
let fixture = Fixture::new();
let path = fixture.root.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!(
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_eq!(fs::read(&path).unwrap(), bytes);
fs::write(&path, []).unwrap();
assert_eq!(read_file_no_follow(&path, 0).unwrap(), [] as [u8; 0]);
fs::write(&path, [1]).unwrap();
assert!(matches!(
read_file_no_follow(&path, 0),
Err(ArtifactError::LimitExceeded { limit: 0 })
));
}
#[test]
fn final_symlinks_are_rejected_without_changing_existing_following_reads() {
let fixture = Fixture::new();
let target = fixture.root.join("target");
let link = fixture.root.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!(
read_opened_file(File::open(&link).unwrap(), 8).unwrap(),
b"retained"
);
assert_eq!(fs::read(&target).unwrap(), b"retained");
let dangling = fixture.root.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()))
);
}
#[test]
fn ancestor_symlinks_remain_an_explicit_consumer_confinement_obligation() {
let fixture = Fixture::new();
let actual = fixture.root.join("actual");
fs::create_dir(&actual).unwrap();
fs::write(actual.join("artifact"), b"data").unwrap();
symlink("actual", fixture.root.join("alias")).unwrap();
assert_eq!(
read_file_no_follow(&fixture.root.join("alias/artifact"), 4).unwrap(),
b"data"
);
}
#[test]
fn fifo_without_a_writer_is_rejected_without_waiting_for_one() {
let fixture = Fixture::new();
let path = fixture.root.join("fifo");
assert!(
Command::new("/usr/bin/mkfifo")
.args(["-m", "600"])
.arg(&path)
.env_clear()
.status()
.unwrap()
.success()
);
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();
});
assert!(matches!(
rx.recv_timeout(Duration::from_secs(5))
.expect("FIFO read 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::new();
assert!(matches!(
read_file_no_follow(&fixture.root, 1024),
Err(ArtifactError::NotRegularFile)
));
assert!(matches!(
read_opened_file(File::open(&fixture.root).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::new();
let path = fixture.root.join("artifact");
let replacement = fixture.root.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::new();
assert!(
matches!(read_file_no_follow(&fixture.root.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)
);
}