use super::*;
pub(super) const REPLACE_OPTIONS: WriteOptions = WriteOptions {
mode: PublicationMode::Replace,
permissions: 0o666,
};
const CREATE_NEW_OPTIONS: WriteOptions = WriteOptions {
mode: PublicationMode::CreateNew,
permissions: 0o666,
};
const CREATE_PRIVATE_OPTIONS: WriteOptions = WriteOptions {
mode: PublicationMode::CreateNew,
permissions: 0o600,
};
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
use super::supported::{FileCommitStep, commit_with_hook, publish_create_new_after_error};
use std::{
fs,
path::{Path, PathBuf},
};
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
#[test]
fn durable_stream_copy_preserves_outputs_on_read_failure_and_create_only_refusal() {
use std::io::{self, Cursor, Read};
struct PartialInput(bool);
impl Read for PartialInput {
fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
if buffer.is_empty() {
return Ok(0);
}
if self.0 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"injected input failure",
));
}
self.0 = true;
buffer[0] = b'x';
Ok(1)
}
}
let fixture = crate::test_support::fixture();
let output = fixture.path().join("copy/output");
let options = WriteOptions {
mode: PublicationMode::Replace,
permissions: 0o666,
};
let mut source = Cursor::new(b"prefixcomplete");
source.set_position(6);
assert_eq!(copy_from(&mut source, &output, options, None).unwrap(), 8);
assert_eq!(source.position(), 14);
assert_eq!(fs::read(&output).unwrap(), b"complete");
assert!(matches!(
copy_from(&mut PartialInput(false), &output, options, None),
Err(NamedWriteError::Producer {
source, cleanup_error: None,
}) if source.kind() == io::ErrorKind::InvalidData
&& source.to_string() == "injected input failure"
));
assert_eq!(fs::read(&output).unwrap(), b"complete");
let create_only = WriteOptions {
mode: PublicationMode::CreateNew,
..options
};
assert!(matches!(
copy_from(&mut b"replacement".as_slice(), &output, create_only, None),
Err(NamedWriteError::BeforePublication { .. })
));
assert_eq!(fs::read(&output).unwrap(), b"complete");
assert_eq!(
copy_from(&mut b"".as_slice(), &output, options, None).unwrap(),
0
);
assert_eq!(fs::read(&output).unwrap(), [] as [u8; 0]);
assert_eq!(fs::read_dir(output.parent().unwrap()).unwrap().count(), 1);
}
#[cfg(unix)]
#[test]
fn durable_stream_copy_keeps_default_or_exact_caller_permissions() {
use std::os::unix::fs::PermissionsExt;
let fixture = crate::test_support::fixture();
let output = fixture.path().join("copy");
let control = fixture.path().join("ordinary");
fs::File::create(&control).unwrap();
let ordinary_mode = fs::metadata(&control).unwrap().permissions().mode() & 0o777;
let options = WriteOptions {
mode: PublicationMode::Replace,
permissions: 0o666,
};
copy_from(&mut b"bytes".as_slice(), &output, options, None).unwrap();
assert_eq!(
fs::metadata(&output).unwrap().permissions().mode() & 0o777,
ordinary_mode
);
for permissions in [0o600, 0o4750] {
copy_from(
&mut b"bytes".as_slice(),
&output,
options,
Some(fs::Permissions::from_mode(permissions)),
)
.unwrap();
assert_eq!(
fs::metadata(&output).unwrap().permissions().mode() & 0o7777,
permissions
);
}
assert_eq!(fs::read(&output).unwrap(), b"bytes");
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
#[test]
fn private_parent_preparation_preserves_modes_and_refuses_symlinks() {
use std::os::unix::fs::{MetadataExt, symlink};
let fixture = crate::test_support::fixture();
let first = fixture.path().join("private");
let parent = first.join("nested");
prepare_private_parents(&parent).unwrap();
for directory in [&first, &parent] {
assert_eq!(
fs::symlink_metadata(directory).unwrap().mode() & 0o777,
0o700
);
}
prepare_private_parents(&parent).unwrap();
symlink(&first, fixture.path().join("linked")).unwrap();
for path in ["linked", "linked/nested", "linked/other"] {
assert_eq!(
prepare_private_parents(&fixture.path().join(path))
.unwrap_err()
.kind(),
std::io::ErrorKind::NotADirectory
);
}
assert_eq!(fs::read_link(fixture.path().join("linked")).unwrap(), first);
assert!(!first.join("other").exists());
for path in ["private/../outside", "missing/../outside"] {
assert_eq!(
prepare_private_parents(&fixture.path().join(path))
.unwrap_err()
.kind(),
std::io::ErrorKind::InvalidInput
);
}
assert!(!fixture.path().join("missing").exists());
assert!(!fixture.path().join("outside").exists());
let file = fixture.path().join("file");
fs::write(&file, b"preserved").unwrap();
assert_eq!(
prepare_private_parents(&file.join("child"))
.unwrap_err()
.kind(),
std::io::ErrorKind::NotADirectory
);
assert_eq!(fs::read(file).unwrap(), b"preserved");
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
#[test]
fn streamed_replacement_returns_producer_value_only_after_complete_publication() {
use std::io::Write as _;
let fixture = crate::test_support::fixture();
let root = fixture.path().join("streamed-replacement");
let path = root.join("nested/output");
let value = write_with(&path, REPLACE_OPTIONS, |file| {
assert!(!path.exists());
file.write_all(b"first ")?;
file.write_all(b"second")?;
Ok::<_, io::Error>(42)
})
.unwrap();
assert_eq!(value, 42);
assert_eq!(fs::read(&path).unwrap(), b"first second");
assert_eq!(fs::read_dir(path.parent().unwrap()).unwrap().count(), 1);
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
#[test]
fn producer_failure_and_bounded_copy_preserve_previous_destination() {
use std::io::Write as _;
struct BrokenInput;
impl io::Read for BrokenInput {
fn read(&mut self, _: &mut [u8]) -> io::Result<usize> {
Err(io::Error::from_raw_os_error(13))
}
}
let fixture = crate::test_support::fixture();
let root = fixture.path().join("streamed-failure");
let path = root.join("output");
write_bytes(&path, REPLACE_OPTIONS, b"original").unwrap();
let error = write_with(&path, REPLACE_OPTIONS, |file| {
file.write_all(b"partial")?;
Err::<(), _>(io::Error::from(io::ErrorKind::InvalidData))
})
.unwrap_err();
assert!(
matches!(error, NamedWriteError::Producer { source, cleanup_error: None } if source.kind() == io::ErrorKind::InvalidData)
);
assert_eq!(fs::read(&path).unwrap(), b"original");
assert_eq!(fs::read_dir(&root).unwrap().count(), 1);
let copy = |maximum| {
write_with(&path, REPLACE_OPTIONS, |file| {
ic_host_artifacts::artifact::copy_reader(b"new bytes".as_slice(), file, maximum)
.map_err(io::Error::from)
})
};
let error = copy(8).unwrap_err();
let NamedWriteError::Producer {
source: error,
cleanup_error: None,
} = error
else {
panic!("bounded-copy error must retain its producer and successful cleanup");
};
assert!(matches!(
error
.get_ref()
.unwrap()
.downcast_ref::<ic_host_artifacts::artifact::CopyError>(),
Some(ic_host_artifacts::artifact::CopyError::Input(
ic_host_artifacts::artifact::ArtifactError::LimitExceeded { limit: 8 }
))
));
let error = write_with(&path, REPLACE_OPTIONS, |file| {
ic_host_artifacts::artifact::copy_reader(BrokenInput, file, 9).map_err(io::Error::from)
})
.unwrap_err();
assert!(
matches!(error, NamedWriteError::Producer { source, cleanup_error: None } if source.raw_os_error() == Some(13))
);
assert_eq!(fs::read(&path).unwrap(), b"original");
let identity = copy(9).unwrap();
assert_eq!(identity.bytes, 9);
assert_eq!(fs::read(&path).unwrap(), b"new bytes");
assert_eq!(fs::read_dir(&root).unwrap().count(), 1);
let error = write_with(&path, REPLACE_OPTIONS, |_| -> io::Result<()> {
let stage = fs::read_dir(&root)
.unwrap()
.map(|entry| entry.unwrap().path())
.find(|entry| entry != &path)
.unwrap();
fs::remove_file(&stage).unwrap();
fs::write(stage, b"foreign entry").unwrap();
Err(io::Error::from_raw_os_error(13))
})
.unwrap_err();
assert!(
matches!(error, NamedWriteError::Producer { source, cleanup_error: Some(cleanup) }
if source.raw_os_error() == Some(13) && cleanup.kind() == io::ErrorKind::InvalidData)
);
assert_eq!(fs::read(&path).unwrap(), b"new bytes");
assert_eq!(fs::read_dir(&root).unwrap().count(), 2);
}
#[cfg(unix)]
#[test]
fn durable_lock_reports_wait_for_another_process_and_retains_exclusion() {
use std::{
io::{BufRead, Write},
process::{Command, Stdio},
time::{Duration, Instant},
};
const DRIVER: &str = "CANIC_TEST_PROGRESS_LOCK_DRIVER";
const CHILD_ROOT: &str = "CANIC_TEST_PROGRESS_LOCK_ROOT";
let thread = std::thread::current();
let test_name = thread.name().expect("libtest names each test thread");
if std::env::var_os(DRIVER).is_none() {
let output = Command::new(std::env::current_exe().unwrap())
.args(["--exact", test_name])
.env(DRIVER, "1")
.output()
.unwrap();
assert!(
output.status.success(),
"isolated lock test failed:\n{}\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
return;
}
if let Some(root) = std::env::var_os(CHILD_ROOT) {
hold_regular_lock_until_release(&PathBuf::from(root).join("complete-build-reuse.lock"));
return;
}
let fixture = crate::test_support::fixture();
let root = fixture.path().join("progress-lock");
let mut child = Command::new(std::env::current_exe().unwrap())
.args(["--exact", test_name, "--nocapture"])
.env(CHILD_ROOT, &root)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
let mut stdout = io::BufReader::new(child.stdout.take().unwrap());
loop {
let mut line = String::new();
assert_ne!(
stdout.read_line(&mut line).unwrap(),
0,
"child must hold lock"
);
if line.contains("LOCK_HELD") {
break;
}
}
let path = root.join("complete-build-reuse.lock");
let contender = open_regular_lock_file_with_parents(&path).unwrap();
assert!(matches!(
try_lock_exclusive(&contender),
Err(error) if error.kind() == io::ErrorKind::WouldBlock
));
drop(contender);
assert!(child.try_wait().unwrap().is_none());
assert_eq!(fs::read(&path).unwrap(), b"existing lock contents");
let lock = open_regular_lock_file_with_parents(&path).unwrap();
assert_eq!(
lock_exclusive_with_wait(&lock, Duration::from_millis(1), |_| Err(
io::ErrorKind::PermissionDenied.into()
))
.unwrap_err()
.kind(),
io::ErrorKind::PermissionDenied
);
let started = Instant::now();
let mut progress = None;
let waited = lock_exclusive_with_wait(&lock, Duration::from_millis(1), |elapsed| {
if progress.is_none() {
progress = Some(elapsed);
child
.stdin
.as_mut()
.unwrap()
.write_all(b"release\n")
.unwrap();
}
Ok(())
})
.unwrap();
assert!(child.wait().unwrap().success());
assert!(waited >= progress.unwrap());
assert!(started.elapsed() >= progress.unwrap());
let contender = fs::File::open(&path).unwrap();
assert_eq!(
try_lock_exclusive(&contender).unwrap_err().kind(),
io::ErrorKind::WouldBlock
);
drop(lock);
try_lock_exclusive(&contender).unwrap();
drop(contender);
let acquired = open_regular_lock_file_with_parents(&path).unwrap();
try_lock_exclusive(&acquired).unwrap();
assert_eq!(fs::read(&path).unwrap(), b"existing lock contents");
drop(acquired);
}
#[cfg(unix)]
fn hold_regular_lock_until_release(path: &Path) {
use std::io::Write as _;
let lock = open_regular_lock_file_with_parents(path).unwrap();
try_lock_exclusive(&lock).unwrap();
assert!(
rustix::io::fcntl_getfd(&lock)
.unwrap()
.contains(rustix::io::FdFlags::CLOEXEC)
);
fs::write(path, b"existing lock contents").unwrap();
println!("LOCK_HELD");
io::stdout().flush().unwrap();
let mut line = String::new();
io::stdin().read_line(&mut line).unwrap();
}
#[test]
fn durable_write_creates_parents_and_replaces_complete_contents() {
let fixture = crate::test_support::fixture();
let root = fixture.path().join("replace");
let path = root.join("reports/nested/state.json");
write_bytes(&path, REPLACE_OPTIONS, b"old").expect("write initial contents");
write_bytes(&path, REPLACE_OPTIONS, b"new complete contents").expect("replace contents");
assert_eq!(
fs::read(&path).expect("read target"),
b"new complete contents"
);
let private = WriteOptions {
permissions: 0o600,
..REPLACE_OPTIONS
};
write_bytes(&path, private, b"private replacement").unwrap();
assert_eq!(fs::read(&path).unwrap(), b"private replacement");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
assert_eq!(fs::metadata(&path).unwrap().permissions().mode() & 0o077, 0);
}
assert_no_temporary_files(&root);
}
#[cfg(unix)]
#[test]
fn private_lock_creation_is_owner_only_with_permissive_umask() {
use std::{os::unix::fs::PermissionsExt, process::Command};
const DRIVER: &str = "IC_HOST_PRIVATE_LOCK_UMASK_CHILD";
if std::env::var_os(DRIVER).is_none() {
let thread = std::thread::current();
let name = thread.name().unwrap();
let output = Command::new("/bin/sh")
.arg("-c")
.arg("umask 000; exec \"$@\"")
.arg("private-sidecar-fixture")
.arg(std::env::current_exe().unwrap())
.args(["--exact", name, "--nocapture"])
.env(DRIVER, "1")
.output()
.unwrap();
assert!(output.status.success(), "{output:?}");
assert!(String::from_utf8_lossy(&output.stdout).contains("PRIVATE_SIDECAR_CHECKED"));
return;
}
let fixture = crate::test_support::fixture();
let path = fixture.path().join("command.lock");
let file = open_private_regular_lock_file(&path).unwrap();
let metadata = file.metadata().unwrap();
assert!(metadata.is_file());
assert_eq!(metadata.len(), 0);
assert_eq!(metadata.permissions().mode() & 0o777, 0o600);
assert!(
rustix::io::fcntl_getfd(&file)
.unwrap()
.contains(rustix::io::FdFlags::CLOEXEC)
);
assert_eq!(fs::read_dir(fixture.path()).unwrap().count(), 1);
println!("PRIVATE_SIDECAR_CHECKED");
}
#[cfg(unix)]
#[test]
fn private_lock_preserves_existing_identity_and_caller_owned_acquisition() {
use rustix::fs::{FlockOperation::NonBlockingLockExclusive, flock};
use std::os::unix::fs::{MetadataExt, PermissionsExt};
const DRIVER: &str = "IC_HOST_FS_PRIVATE_LOCK_DRIVER";
if std::env::var_os(DRIVER).is_none() {
let thread = std::thread::current();
let test_name = thread.name().expect("libtest names each test thread");
let output = std::process::Command::new(std::env::current_exe().unwrap())
.args(["--exact", test_name, "--test-threads=1"])
.env(DRIVER, "1")
.output()
.unwrap();
assert!(
output.status.success(),
"isolated private lock test failed:\n{}\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
return;
}
let fixture = crate::test_support::fixture();
let path = fixture.path().join("command.lock");
fs::write(&path, b"custody evidence").unwrap();
fs::set_permissions(&path, fs::Permissions::from_mode(0o640)).unwrap();
let before = fs::metadata(&path).unwrap();
let first = open_private_regular_lock_file(&path).unwrap();
let second = open_private_regular_lock_file(&path).unwrap();
flock(&second, NonBlockingLockExclusive).unwrap();
let error = flock(&first, NonBlockingLockExclusive).unwrap_err();
let projected = io::Error::from(errno_to_lock_error(error));
assert_eq!(projected.kind(), io::ErrorKind::WouldBlock);
for after in [
first.metadata().unwrap(),
second.metadata().unwrap(),
fs::metadata(&path).unwrap(),
] {
assert_eq!((before.dev(), before.ino()), (after.dev(), after.ino()));
assert_eq!(before.permissions().mode(), after.permissions().mode());
}
assert_eq!(fs::read(&path).unwrap(), b"custody evidence");
drop(second);
flock(&first, NonBlockingLockExclusive).unwrap();
drop(first);
let reopened = open_private_regular_lock_file(&path).unwrap();
flock(&reopened, NonBlockingLockExclusive).unwrap();
assert_eq!(fs::read(&path).unwrap(), b"custody evidence");
}
#[cfg(unix)]
#[test]
fn private_lock_refuses_missing_parents_and_unsafe_entries_without_changes() {
use std::os::unix::fs::{MetadataExt, symlink};
let fixture = crate::test_support::fixture();
let missing = fixture.path().join("missing/nested/command.lock");
assert!(matches!(open_private_regular_lock_file(&missing),
Err(RegularFileLockError::Io(error)) if error.kind() == io::ErrorKind::NotFound));
assert_eq!(fs::read_dir(fixture.path()).unwrap().count(), 0);
assert!(
matches!(open_private_regular_lock_file(&fixture.path().join("invalid\0path")),
Err(RegularFileLockError::Io(error)) if error.kind() == io::ErrorKind::InvalidInput)
);
let target = fixture.path().join("target");
fs::write(&target, b"original target").unwrap();
let link = fixture.path().join("link");
symlink(&target, &link).unwrap();
let dangling = fixture.path().join("dangling");
symlink(&missing, &dangling).unwrap();
let fifo = fixture.path().join("fifo");
crate::test_support::create_fifo(&fifo, None).unwrap();
for path in [&link, &dangling, fixture.path(), &fifo] {
let before = fs::symlink_metadata(path).unwrap();
assert!(matches!(
open_private_regular_lock_file(path),
Err(RegularFileLockError::NotRegular)
));
let after = fs::symlink_metadata(path).unwrap();
assert_eq!((before.dev(), before.ino()), (after.dev(), after.ino()));
assert_eq!(before.mode(), after.mode());
}
assert!(matches!(
open_regular_lock_descriptor(&fifo, Some(rustix::fs::Mode::RUSR)),
Err(RegularFileLockError::NotRegular)
));
assert!(matches!(
open_regular_lock_descriptor(&link, Some(rustix::fs::Mode::RUSR)),
Err(RegularFileLockError::Io(_))
));
assert_eq!(fs::read(&target).unwrap(), b"original target");
assert!(!fixture.path().join("missing").exists());
assert_eq!(fs::read_dir(fixture.path()).unwrap().count(), 4);
}
#[cfg(unix)]
#[test]
fn existing_sidecar_open_never_creates_and_one_try_reports_contention() {
use std::os::unix::fs::{MetadataExt, symlink};
let fixture = crate::test_support::fixture();
let path = fixture.path().join("original.lock");
assert!(matches!(
open_existing_regular_lock_file(&path),
Err(RegularFileLockError::Io(error)) if error.kind() == io::ErrorKind::NotFound
));
assert!(!path.exists());
fs::write(&path, b"retained evidence").unwrap();
let before = fs::metadata(&path).unwrap();
let first = open_existing_regular_lock_file(&path).unwrap();
let second = open_existing_regular_lock_file(&path).unwrap();
try_lock_exclusive(&first).unwrap();
assert_eq!(
try_lock_exclusive(&second).unwrap_err().kind(),
io::ErrorKind::WouldBlock
);
unlock(&first).unwrap();
try_lock_exclusive(&second).unwrap();
unlock(&second).unwrap();
assert_eq!(fs::read(&path).unwrap(), b"retained evidence");
let after = fs::metadata(&path).unwrap();
assert_eq!((before.dev(), before.ino()), (after.dev(), after.ino()));
let link = fixture.path().join("link.lock");
symlink(&path, &link).unwrap();
assert!(matches!(
open_existing_regular_lock_file(&link),
Err(RegularFileLockError::NotRegular)
));
assert!(matches!(
open_existing_regular_lock_file(fixture.path()),
Err(RegularFileLockError::NotRegular)
));
}
#[cfg(unix)]
#[test]
fn lock_errors_preserve_admission_and_original_io_causes() {
use std::error::Error as _;
let fixture = crate::test_support::fixture();
let target = fixture.path().join("target");
fs::write(&target, b"unchanged").unwrap();
let link = fixture.path().join("link");
std::os::unix::fs::symlink(&target, &link).unwrap();
let fifo = fixture.path().join("fifo");
crate::test_support::create_fifo(&fifo, None).unwrap();
for path in [fixture.path(), &link, &fifo, Path::new("/dev/null")] {
let result = open_regular_lock_file_with_parents(path);
let error = result.as_ref().unwrap_err();
assert_eq!(error.to_string(), "lock target is not a regular file");
assert!(error.source().is_none());
assert!(
matches!(result, Err(RegularFileLockError::NotRegular)),
"{path:?}: {result:?}"
);
}
assert_eq!(fs::read(&target).unwrap(), b"unchanged");
let error =
open_regular_lock_file_with_parents(&fixture.path().join("invalid\0path")).unwrap_err();
let RegularFileLockError::Io(original) = &error else {
panic!("expected original I/O cause");
};
let retained = error.source().unwrap().downcast_ref::<io::Error>().unwrap();
assert_eq!(error.to_string(), "regular file lock I/O failed");
assert!(std::ptr::eq(original, retained));
assert_eq!(retained.kind(), io::ErrorKind::InvalidInput);
let projected = io::Error::from(error);
assert_eq!(projected.kind(), io::ErrorKind::InvalidInput);
let projected = io::Error::from(RegularFileLockError::NotRegular);
assert!(matches!(
projected
.get_ref()
.unwrap()
.downcast_ref::<RegularFileLockError>(),
Some(RegularFileLockError::NotRegular)
));
}
#[cfg(unix)]
#[test]
fn lock_creation_preserves_publication_error_through_io_boundary() {
use std::error::Error as _;
let fixture = crate::test_support::fixture();
let link = fixture.path().join("parent");
std::os::unix::fs::symlink(fixture.path().join("missing"), &link).unwrap();
let error = open_regular_lock_file_with_parents(&link.join("lock")).unwrap_err();
let RegularFileLockError::Publication(original) = &error else {
panic!("expected typed lock-file creation failure: {error:?}");
};
assert_eq!(error.to_string(), "durable lock-file creation failed");
assert!(
matches!(original, NamedWriteError::BeforePublication { source, cleanup_error: None }
if source.kind() == io::ErrorKind::NotADirectory)
);
assert!(std::ptr::eq(
original,
error
.source()
.unwrap()
.downcast_ref::<NamedWriteError<io::Error>>()
.unwrap()
));
let projected = io::Error::from(error);
assert!(
matches!(projected.get_ref().unwrap().downcast_ref::<RegularFileLockError>(),
Some(RegularFileLockError::Publication(NamedWriteError::BeforePublication { source, cleanup_error: None }))
if source.kind() == io::ErrorKind::NotADirectory)
);
assert!(!fixture.path().join("missing").exists());
}
#[cfg(unix)]
#[test]
fn existing_lock_admission_does_not_require_parent_write_permission() {
use std::os::unix::fs::PermissionsExt as _;
let fixture = crate::test_support::fixture();
let path = fixture.path().join("lock");
fs::write(&path, b"existing lock bytes").unwrap();
fs::set_permissions(fixture.path(), fs::Permissions::from_mode(0o555)).unwrap();
let result = open_regular_lock_file_with_parents(&path);
fs::set_permissions(fixture.path(), fs::Permissions::from_mode(0o755)).unwrap();
let file = result.unwrap();
assert!(file.metadata().unwrap().is_file());
assert_eq!(fs::read(&path).unwrap(), b"existing lock bytes");
}
#[cfg(unix)]
#[test]
fn concurrent_missing_lock_openers_converge_on_one_regular_file() {
use std::{os::unix::fs::MetadataExt as _, sync::Barrier};
let fixture = crate::test_support::fixture();
let path = fixture.path().join("nested/lock");
let start = Barrier::new(8);
let files = std::thread::scope(|scope| {
let workers: Vec<_> = (0..8)
.map(|_| {
scope.spawn(|| {
start.wait();
open_regular_lock_file_with_parents(&path).unwrap()
})
})
.collect();
workers
.into_iter()
.map(|worker| worker.join().unwrap())
.collect::<Vec<_>>()
});
let expected = fs::metadata(&path).unwrap();
for file in files {
let metadata = file.metadata().unwrap();
assert_eq!(
(metadata.dev(), metadata.ino()),
(expected.dev(), expected.ino())
);
}
assert_eq!(fs::read_dir(path.parent().unwrap()).unwrap().count(), 1);
assert_eq!(expected.len(), 0);
}
#[cfg(unix)]
#[test]
fn admitted_lock_files_compose_with_shared_locks_and_explicit_clone_unlock() {
use rustix::fs::{FlockOperation as Op, flock};
let fixture = crate::test_support::fixture();
let root = fixture.path().join("admitted-lock");
let path = root.join("nested/lock");
let first = open_regular_lock_file_with_parents(&path).unwrap();
fs::write(&path, b"retained contents").unwrap();
let second = open_regular_lock_file_with_parents(&path).unwrap();
assert!(
rustix::io::fcntl_getfd(&first)
.unwrap()
.contains(rustix::io::FdFlags::CLOEXEC)
);
flock(&second, Op::NonBlockingLockExclusive).unwrap();
flock(&second, Op::Unlock).unwrap();
flock(&first, Op::NonBlockingLockShared).unwrap();
flock(&second, Op::NonBlockingLockShared).unwrap();
let contender = open_regular_lock_file_with_parents(&path).unwrap();
assert_eq!(
flock(&contender, Op::NonBlockingLockExclusive),
Err(rustix::io::Errno::WOULDBLOCK)
);
let duplicate = first.try_clone().unwrap();
flock(&first, Op::Unlock).unwrap();
flock(&second, Op::Unlock).unwrap();
lock_exclusive_with_wait(&contender, std::time::Duration::from_millis(1), |_| {
panic!("explicitly unlocked descriptors must not retain contention")
})
.unwrap();
assert_eq!(fs::read(&path).unwrap(), b"retained contents");
flock(&contender, Op::Unlock).unwrap();
drop((first, second, duplicate, contender));
}
#[test]
fn durable_create_new_with_parents_never_replaces_an_existing_file() {
let fixture = crate::test_support::fixture();
let root = fixture.path().join("create-new-with-parents");
fs::create_dir_all(&root).expect("create temp root");
let path = root.join("nested/report.json");
write_bytes(&path, CREATE_NEW_OPTIONS, b"first complete contents").expect("create output");
let error = write_bytes(&path, CREATE_NEW_OPTIONS, b"replacement")
.expect_err("existing output must reject");
assert!(
matches!(error, NamedWriteError::BeforePublication { source, cleanup_error: None } if source.kind() == io::ErrorKind::AlreadyExists)
);
assert_eq!(
fs::read(&path).expect("read target"),
b"first complete contents"
);
assert_no_temporary_files(&root);
}
#[cfg(unix)]
#[test]
fn durable_create_new_with_parents_rejects_a_symlinked_parent() {
use std::os::unix::fs::symlink;
let fixture = crate::test_support::fixture();
let root = fixture.path().join("create-new-symlink-parent");
let outside = fixture.path().join("create-new-symlink-outside");
fs::create_dir_all(&root).expect("create temp root");
fs::create_dir_all(&outside).expect("create outside root");
symlink(&outside, root.join("linked")).expect("create parent symlink");
let error = write_bytes(
&root.join("linked/report.json"),
CREATE_NEW_OPTIONS,
b"contents",
)
.expect_err("symlinked parent must reject");
assert!(
matches!(error, NamedWriteError::BeforePublication { source, cleanup_error: None } if source.kind() == io::ErrorKind::NotADirectory)
);
assert!(!outside.join("report.json").exists());
}
#[test]
fn durable_write_rejects_a_target_without_a_file_name() {
let error = write_bytes(Path::new("/"), REPLACE_OPTIONS, b"value")
.expect_err("directory target must fail");
assert!(
matches!(error, NamedWriteError::BeforePublication { source, cleanup_error: None } if source.kind() == io::ErrorKind::InvalidInput)
);
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
#[test]
fn prepublication_failures_preserve_old_complete_bytes_and_remove_staging() {
let steps = [
FileCommitStep::TemporaryFileCreate,
FileCommitStep::TemporaryFileWrite,
FileCommitStep::TemporaryFileSync,
FileCommitStep::Publication,
];
for step in steps {
let fixture = crate::test_support::fixture();
let root = fixture.path().join(format!("prepublication-{step:?}"));
fs::create_dir_all(&root).expect("create temp root");
let path = root.join("report.json");
fs::write(&path, b"old complete contents").expect("write old contents");
let mut failed = false;
let error = commit_with_hook(
&path,
b"new complete contents",
REPLACE_OPTIONS,
|current, _| {
if current == step && !failed {
failed = true;
return Err(io::Error::other("injected file commit failure"));
}
Ok(())
},
)
.expect_err("injected step must fail");
assert!(
matches!(error, NamedWriteError::BeforePublication { source, cleanup_error: None } if source.kind() == io::ErrorKind::Other)
);
assert_eq!(
fs::read(&path).expect("read preserved target"),
b"old complete contents"
);
assert_no_temporary_files(&root);
}
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
#[test]
fn postpublication_sync_failure_exposes_only_new_complete_bytes() {
for options in [REPLACE_OPTIONS, CREATE_NEW_OPTIONS, CREATE_PRIVATE_OPTIONS] {
let fixture = crate::test_support::fixture();
let root = fixture.path().join("postpublication");
fs::create_dir_all(&root).expect("create temp root");
let path = root.join("report.json");
if options.mode == PublicationMode::Replace {
fs::write(&path, b"old complete contents").expect("write old contents");
}
let error = commit_with_hook(&path, b"new complete contents", options, |step, _| {
if step == FileCommitStep::FinalParentSync {
return Err(io::Error::other("injected parent sync failure"));
}
Ok(())
})
.expect_err("postpublication sync must fail");
assert!(
matches!(error, NamedWriteError::AfterPublication { source } if source.kind() == io::ErrorKind::Other)
);
assert_eq!(
fs::read(&path).expect("read published target"),
b"new complete contents"
);
assert_no_temporary_files(&root);
}
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
#[test]
fn create_new_publication_race_cannot_replace_the_winner() {
let fixture = crate::test_support::fixture();
let root = fixture.path().join("create-new-race");
fs::create_dir_all(&root).expect("create temp root");
let path = root.join("report.json");
let error = commit_with_hook(
&path,
b"our complete contents",
CREATE_NEW_OPTIONS,
|step, _| {
if step == FileCommitStep::Publication {
fs::write(&path, b"raced complete contents")?;
}
Ok(())
},
)
.expect_err("atomic create-new must reject a publication race");
assert!(
matches!(error, NamedWriteError::BeforePublication { source, cleanup_error: None } if source.kind() == io::ErrorKind::AlreadyExists)
);
assert_eq!(
fs::read(&path).expect("read winning target"),
b"raced complete contents"
);
assert_no_temporary_files(&root);
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
#[test]
fn unsupported_no_replace_rename_uses_atomic_link_publication() {
let unsupported = [
("invalid", rustix::io::Errno::INVAL),
("no-syscall", rustix::io::Errno::NOSYS),
("not-supported", rustix::io::Errno::OPNOTSUPP),
];
for (label, error) in unsupported {
let fixture = crate::test_support::fixture();
let root = fixture.path().join(format!("create-new-link-{label}"));
fs::create_dir_all(&root).expect("create temp root");
let temp_name = std::ffi::OsStr::new(".report.json.canic-tmp-test");
let file_name = std::ffi::OsStr::new("report.json");
let temp_path = root.join(temp_name);
let path = root.join(file_name);
fs::write(&temp_path, b"complete contents").expect("write staged contents");
publish_create_new_after_error(&root, temp_name, file_name, error)
.expect("publish through hard-link fallback");
assert_eq!(fs::read(&path).expect("read target"), b"complete contents");
assert!(!temp_path.exists());
}
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
#[test]
fn link_publication_fallback_cannot_replace_an_existing_file() {
let fixture = crate::test_support::fixture();
let root = fixture.path().join("create-new-link-race");
fs::create_dir_all(&root).expect("create temp root");
let temp_name = std::ffi::OsStr::new(".report.json.canic-tmp-test");
let file_name = std::ffi::OsStr::new("report.json");
let temp_path = root.join(temp_name);
let path = root.join(file_name);
fs::write(&temp_path, b"our complete contents").expect("write staged contents");
fs::write(&path, b"raced complete contents").expect("write winning target");
let error =
publish_create_new_after_error(&root, temp_name, file_name, rustix::io::Errno::INVAL)
.expect_err("link fallback must reject an existing destination");
assert!(
matches!(error, NamedWriteError::BeforePublication { source, cleanup_error: None } if source.kind() == io::ErrorKind::AlreadyExists)
);
assert_eq!(
fs::read(&path).expect("read winning target"),
b"raced complete contents"
);
assert_eq!(
fs::read(&temp_path).expect("read preserved staging"),
b"our complete contents"
);
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
#[test]
fn new_parent_failures_never_create_the_final_file_or_staging() {
let steps = [
FileCommitStep::ParentDirectoryCreate,
FileCommitStep::CreatedDirectorySync,
FileCommitStep::CreatedDirectoryParentSync,
];
for step in steps {
let fixture = crate::test_support::fixture();
let root = fixture.path().join(format!("parent-{step:?}"));
fs::create_dir_all(&root).expect("create temp root");
let path = root.join("reports/nested/report.json");
let mut failed = false;
let error = commit_with_hook(
&path,
b"complete contents",
REPLACE_OPTIONS,
|current, _| {
if current == step && !failed {
failed = true;
return Err(io::Error::other("injected parent persistence failure"));
}
Ok(())
},
)
.expect_err("injected parent step must fail");
assert!(
matches!(error, NamedWriteError::BeforePublication { source, cleanup_error: None } if source.kind() == io::ErrorKind::Other)
);
assert!(!path.exists());
assert_no_temporary_files(&root);
}
}
#[test]
fn failed_replacement_removes_its_staging_file() {
let fixture = crate::test_support::fixture();
let root = fixture.path().join("replace-error");
let path = root.join("occupied");
fs::create_dir_all(&path).expect("create non-file destination");
fs::write(path.join("child"), b"preserved").expect("write destination child");
write_bytes(&path, REPLACE_OPTIONS, b"new contents")
.expect_err("non-empty directory must reject replacement");
assert_eq!(
fs::read(path.join("child")).expect("read destination child"),
b"preserved"
);
assert_no_temporary_files(&root);
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
#[test]
fn raced_parent_creation_completes_both_syncs_before_staging() {
let fixture = crate::test_support::fixture();
let root = fixture.path().join("raced-parent-sync");
fs::create_dir(&root).unwrap();
let parent = root.join("new-parent");
let path = parent.join("output");
let mut observed = Vec::new();
commit_with_hook(&path, b"complete", REPLACE_OPTIONS, |step, selected| {
match step {
FileCommitStep::ParentDirectoryCreate => {
assert_eq!(selected, parent);
fs::create_dir(selected)?;
}
FileCommitStep::CreatedDirectorySync => {
assert_eq!(selected, parent);
observed.push(step);
}
FileCommitStep::CreatedDirectoryParentSync => {
assert_eq!(selected, root);
observed.push(step);
}
FileCommitStep::TemporaryFileCreate => {
assert_eq!(
observed,
[
FileCommitStep::CreatedDirectorySync,
FileCommitStep::CreatedDirectoryParentSync,
]
);
}
_ => {}
}
Ok(())
})
.unwrap();
assert_eq!(fs::read(&path).unwrap(), b"complete");
assert_eq!(fs::read_dir(&parent).unwrap().count(), 1);
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
#[test]
fn raced_parent_creation_sync_failures_stop_before_staging() {
for failure in [
FileCommitStep::CreatedDirectorySync,
FileCommitStep::CreatedDirectoryParentSync,
] {
let fixture = crate::test_support::fixture();
let root = fixture.path().join("raced-parent-failure");
fs::create_dir(&root).unwrap();
let parent = root.join("new-parent");
let error = commit_with_hook(
&parent.join("output"),
b"never publish",
REPLACE_OPTIONS,
|step, selected| {
if step == FileCommitStep::ParentDirectoryCreate {
fs::create_dir(selected)?;
}
if step == failure {
return Err(io::Error::from_raw_os_error(5));
}
Ok(())
},
)
.expect_err("raced parent still requires our own synchronization");
assert!(
matches!(error, NamedWriteError::BeforePublication { source, cleanup_error: None }
if source.raw_os_error() == Some(5))
);
assert_eq!(fs::read_dir(&parent).unwrap().count(), 0);
}
}
fn assert_no_temporary_files(root: &Path) {
if !root.exists() {
return;
}
for entry in fs::read_dir(root).expect("read test directory") {
let entry = entry.expect("read test entry");
let path = entry.path();
if path.is_dir() {
assert_no_temporary_files(&path);
} else {
assert!(
!entry
.file_name()
.to_string_lossy()
.starts_with(".ic-host-tmp-"),
"temporary file remains at {}",
path.display()
);
}
}
}
#[test]
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
fn private_publication_is_owner_only_before_writing_and_never_replaces() {
use std::os::unix::fs::PermissionsExt as _;
let fixture = crate::test_support::fixture();
let root = fixture.path().join("private-publication");
let path = root.join("private/key");
commit_with_hook(&path, &[7; 32], CREATE_PRIVATE_OPTIONS, |step, observed| {
if step == FileCommitStep::TemporaryFileWrite {
assert_eq!(fs::metadata(observed)?.permissions().mode() & 0o777, 0o600);
}
Ok(())
})
.unwrap();
assert_eq!(
crate::read::read_private_bytes::<32>(&path).unwrap(),
Some([7; 32])
);
let error = write_bytes(&path, CREATE_PRIVATE_OPTIONS, &[8; 32]).unwrap_err();
assert!(
matches!(error, NamedWriteError::BeforePublication { source, cleanup_error: None } if source.kind() == io::ErrorKind::AlreadyExists)
);
assert_eq!(
crate::read::read_private_bytes::<32>(&path).unwrap(),
Some([7; 32])
);
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
#[test]
fn replacement_supports_long_destination_names() {
let fixture = crate::test_support::fixture();
let root = fixture.path().join("long-replacement");
fs::create_dir(&root).unwrap();
let path = root.join("a".repeat(255));
fs::write(&path, b"previous").unwrap();
write_bytes(&path, REPLACE_OPTIONS, b"complete").unwrap();
let copied = write_with(&path, REPLACE_OPTIONS, |file| {
std::io::Write::write_all(file, b"streamed")?;
Ok::<_, io::Error>(8)
})
.unwrap();
assert_eq!(copied, 8);
assert_eq!(fs::read(&path).unwrap(), b"streamed");
assert_eq!(fs::read_dir(&root).unwrap().count(), 1);
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
#[test]
fn staging_collisions_retry_without_touching_unowned_files() {
let fixture = crate::test_support::fixture();
let root = fixture.path().join("collision-replacement");
fs::create_dir(&root).unwrap();
let path = root.join("output");
fs::write(&path, b"previous").unwrap();
let mut collision = None;
commit_with_hook(&path, b"complete", REPLACE_OPTIONS, |step, candidate| {
if step == FileCommitStep::TemporaryFileCreate && collision.is_none() {
fs::write(candidate, b"unowned")?;
collision = Some(candidate.to_owned());
}
Ok(())
})
.unwrap();
assert_eq!(fs::read(&path).unwrap(), b"complete");
assert_eq!(fs::read(collision.as_ref().unwrap()).unwrap(), b"unowned");
let mut blocked = Vec::new();
let error = commit_with_hook(
&path,
b"never publish",
REPLACE_OPTIONS,
|step, candidate| {
if step == FileCommitStep::TemporaryFileCreate {
fs::write(candidate, b"unowned")?;
blocked.push(candidate.to_owned());
}
Ok(())
},
)
.unwrap_err();
assert!(
matches!(error, NamedWriteError::BeforePublication { source, cleanup_error: None } if source.kind() == io::ErrorKind::AlreadyExists)
);
assert!(!blocked.is_empty() && blocked.len() <= 64);
assert_eq!(fs::read(&path).unwrap(), b"complete");
for candidate in blocked {
assert_eq!(fs::read(candidate).unwrap(), b"unowned");
}
}
#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
#[test]
fn selected_staging_namespace_destinations_remain_absent_until_publication() {
const CHILD: &str = "IC_HOST_FS_STAGING_DESTINATION_CHILD";
if std::env::var_os(CHILD).is_none() {
let output = std::process::Command::new(std::env::current_exe().unwrap())
.args(["--exact", "durable::tests::selected_staging_namespace_destinations_remain_absent_until_publication", "--test-threads=1"])
.env(CHILD, "1")
.output().unwrap();
assert!(
output.status.success(),
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
return;
}
let fixture = crate::test_support::fixture();
let root = fixture.path().join("staging-destination");
fs::create_dir(&root).unwrap();
for (prefix, sequence) in [(".ic-host-tmp", 0), (".IC-HOST-TMP", 2)] {
let path = root.join(format!("{prefix}-{}-{sequence}", std::process::id()));
write_with(&path, REPLACE_OPTIONS, |file| {
assert!(!path.exists());
std::io::Write::write_all(file, b"complete")
})
.unwrap();
assert_eq!(fs::read(&path).unwrap(), b"complete");
}
}