use super::*;
use crate::{
durable::supported::{FileCommitStep, commit_at_with_hook},
test_support::Fixture,
};
use std::{
ffi::{OsStr, OsString},
io::{self, Write as _},
os::{
fd::AsFd as _,
unix::{
ffi::OsStringExt as _,
fs::{PermissionsExt as _, symlink},
},
},
};
const REPLACE: WriteOptions = WriteOptions {
mode: PublicationMode::Replace,
permissions: 0o600,
};
const CREATE: WriteOptions = WriteOptions {
mode: PublicationMode::CreateNew,
..REPLACE
};
#[test]
fn typed_serialization_preserves_errors_limits_and_previous_output() {
struct InvalidValue;
impl serde::Serialize for InvalidValue {
fn serialize<S: serde::Serializer>(&self, _: S) -> Result<S::Ok, S::Error> {
Err(serde::ser::Error::custom("invalid fixture value"))
}
}
let fixture = Fixture::new();
let path = fixture.root.join("nested/output");
write_typed_with(&path, REPLACE, |file| -> io::Result<_> {
file.write_all(b"old")?;
Ok(42)
})
.unwrap();
let error = write_typed_with(&path, REPLACE, |file| {
serde_json::to_writer(file, &("prefix", InvalidValue))
})
.unwrap_err();
assert!(
std::error::Error::source(&error)
.unwrap()
.is::<serde_json::Error>()
);
assert!(
matches!(error, NamedWriteError::Producer { source, cleanup_error: None } if source.is_data())
);
let error = write_typed_with(&path, REPLACE, |file| {
serde_json::to_writer(
ic_host_artifacts::artifact::BoundedWriter::new(file, 3),
&"too long",
)
})
.unwrap_err();
assert!(
matches!(error, NamedWriteError::Producer { source, cleanup_error: None } if source.is_io())
);
assert_eq!(fs::read(&path).unwrap(), b"old");
assert_eq!(fs::read_dir(path.parent().unwrap()).unwrap().count(), 1);
write_typed_with(&path, REPLACE, |file| {
serde_json::to_writer(file, &vec![1, 2])
})
.unwrap();
assert_eq!(fs::read(&path).unwrap(), b"[1,2]");
assert_eq!(fs::metadata(path).unwrap().permissions().mode() & 0o077, 0);
}
#[test]
fn create_only_refuses_existing_entries_and_a_competing_publication() {
let fixture = Fixture::new();
let path = fixture.root.join("output");
let error = write_typed_with(&path, CREATE, |file| -> io::Result<()> {
file.write_all(b"ours")?;
fs::write(&path, b"winner")
})
.unwrap_err();
assert!(
matches!(error, NamedWriteError::BeforePublication { source, cleanup_error: None } if source.kind() == io::ErrorKind::AlreadyExists)
);
assert_eq!(fs::read(&path).unwrap(), b"winner");
assert_eq!(fs::read_dir(&fixture.root).unwrap().count(), 1);
let fresh = fixture.root.join("fresh");
write_typed_with(&fresh, CREATE, |file| file.write_all(b"new")).unwrap();
assert_eq!(fs::read(fresh).unwrap(), b"new");
let link = fixture.root.join("link");
symlink(&path, &link).unwrap();
assert!(
matches!(write_typed_with(&link, CREATE, |file| file.write_all(b"blocked")),
Err(NamedWriteError::BeforePublication { source, .. }) if source.kind() == io::ErrorKind::AlreadyExists)
);
assert!(
fs::symlink_metadata(&link)
.unwrap()
.file_type()
.is_symlink()
);
let dir = fixture.root.join("directory");
fs::create_dir(&dir).unwrap();
assert!(write_typed_with(&dir, CREATE, |file| file.write_all(b"blocked")).is_err());
assert!(dir.is_dir());
}
#[test]
fn held_directory_survives_parent_rename_and_replacement() {
let fixture = Fixture::new();
let original = fixture.root.join("original parent");
let moved = fixture.root.join("moved parent");
fs::create_dir(&original).unwrap();
let directory = fs::File::open(&original).unwrap();
let result = write_at_with(
directory.as_fd(),
OsStr::new("output"),
REPLACE,
|file| -> io::Result<_> {
fs::rename(&original, &moved)?;
fs::create_dir(&original)?;
fs::write(original.join("output"), b"foreign")?;
file.write_all(b"held directory")?;
Ok(73)
},
)
.unwrap();
assert_eq!(result, 73);
assert_eq!(fs::read(moved.join("output")).unwrap(), b"held directory");
assert_eq!(fs::read(original.join("output")).unwrap(), b"foreign");
let error = write_at_with(
directory.as_fd(),
OsStr::new("output"),
REPLACE,
|file| -> io::Result<()> {
file.write_all(b"partial")?;
Err(io::Error::from_raw_os_error(13))
},
)
.unwrap_err();
assert!(
matches!(error, NamedWriteError::Producer { source, cleanup_error: None } if source.raw_os_error() == Some(13))
);
assert_eq!(fs::read(moved.join("output")).unwrap(), b"held directory");
assert_eq!(fs::read_dir(moved).unwrap().count(), 1);
}
#[test]
fn filename_and_descriptor_admission_precedes_any_producer_or_stage() {
let fixture = Fixture::new();
let directory = fs::File::open(&fixture.root).unwrap();
for name in ["", ".", "..", "a/b", "a/", "/absolute", "a\0b"] {
let error = write_at_with(
directory.as_fd(),
OsStr::new(name),
REPLACE,
|_| -> io::Result<()> { panic!("invalid name dispatched") },
)
.unwrap_err();
assert!(
matches!(error, NamedWriteError::BeforePublication { source, .. } if source.kind() == io::ErrorKind::InvalidInput)
);
}
let path = fixture.root.join("must not create/output");
for permissions in [0o1000, 0o4600, 0x1_0000 | 0o600, u32::MAX] {
let options = WriteOptions {
permissions,
..CREATE
};
let error = write_typed_with(&path, options, |_| -> io::Result<()> {
panic!("invalid mode dispatched")
})
.unwrap_err();
assert!(
matches!(error, NamedWriteError::BeforePublication { source, cleanup_error: None } if source.kind() == io::ErrorKind::InvalidInput)
);
let error = write_at_with(
directory.as_fd(),
OsStr::new("output"),
options,
|_| -> io::Result<()> { panic!("invalid descriptor mode dispatched") },
)
.unwrap_err();
assert!(
matches!(error, NamedWriteError::BeforePublication { source, cleanup_error: None } if source.kind() == io::ErrorKind::InvalidInput)
);
}
assert_eq!(fs::read_dir(&fixture.root).unwrap().count(), 0);
let regular = fs::File::create(fixture.root.join("file")).unwrap();
let error = write_at_with(
regular.as_fd(),
OsStr::new("output"),
REPLACE,
|_| -> io::Result<()> { panic!("non-directory dispatched") },
)
.unwrap_err();
assert!(
matches!(error, NamedWriteError::BeforePublication { source, .. } if source.kind() == io::ErrorKind::NotADirectory)
);
}
#[test]
fn non_utf8_publication_matches_native_filesystem_admission() {
for options in [REPLACE, CREATE] {
let fixture = Fixture::new();
let directory = fs::File::open(&fixture.root).unwrap();
let name = OsString::from_vec(vec![0xff, b'x']);
let destination = fixture.root.join(&name);
let native_stage = fixture.root.join("native-stage");
fs::write(&native_stage, b"native").unwrap();
let native = match options.mode {
PublicationMode::Replace => fs::rename(&native_stage, &destination),
PublicationMode::CreateNew => fs::hard_link(&native_stage, &destination),
};
if native.is_ok() {
assert_eq!(fs::read(&destination).unwrap(), b"native");
fs::remove_file(&destination).unwrap();
}
if native.is_err() || options.mode == PublicationMode::CreateNew {
assert_eq!(fs::read(&native_stage).unwrap(), b"native");
fs::remove_file(&native_stage).unwrap();
}
let mut produced = false;
let result = write_at_with(directory.as_fd(), &name, options, |file| {
produced = true;
file.write_all(b"non-UTF-8")
});
assert!(produced);
match native {
Ok(()) => {
result.unwrap();
assert_eq!(fs::read(&destination).unwrap(), b"non-UTF-8");
let names: Vec<_> = fs::read_dir(&fixture.root)
.unwrap()
.map(|entry| entry.unwrap().file_name())
.collect();
assert_eq!(names, [name]);
}
Err(native_error) => {
assert_eq!(
native_error.raw_os_error(),
Some(rustix::io::Errno::ILSEQ.raw_os_error())
);
assert!(matches!(
result,
Err(NamedWriteError::BeforePublication { source, cleanup_error: None })
if source.raw_os_error() == native_error.raw_os_error()
));
assert_eq!(fs::read_dir(&fixture.root).unwrap().count(), 0);
}
}
}
}
#[test]
fn replace_does_not_follow_a_final_symlink_and_refuses_a_directory() {
let fixture = Fixture::new();
let directory = fs::File::open(&fixture.root).unwrap();
let target = fixture.root.join("target");
fs::write(&target, b"retained").unwrap();
symlink(&target, fixture.root.join("link")).unwrap();
write_at_with(directory.as_fd(), OsStr::new("link"), REPLACE, |file| {
file.write_all(b"replacement")
})
.unwrap();
assert_eq!(fs::read(target).unwrap(), b"retained");
assert!(
!fs::symlink_metadata(fixture.root.join("link"))
.unwrap()
.file_type()
.is_symlink()
);
fs::create_dir(fixture.root.join("dir")).unwrap();
assert!(matches!(
write_at_with(directory.as_fd(), OsStr::new("dir"), REPLACE, |file| file
.write_all(b"no")),
Err(NamedWriteError::BeforePublication {
cleanup_error: None,
..
})
));
assert_eq!(fs::read_dir(&fixture.root).unwrap().count(), 3);
}
#[test]
fn descriptor_publication_preserves_sync_state_and_foreign_stage_cleanup_evidence() {
let fixture = Fixture::new();
let directory = fs::File::open(&fixture.root).unwrap();
let target = fixture.root.join("output");
for step in [
FileCommitStep::TemporaryFileSync,
FileCommitStep::Publication,
FileCommitStep::FinalParentSync,
] {
fs::write(&target, b"old").unwrap();
let error = commit_at_with_hook(
directory.as_fd(),
None,
OsStr::new("output"),
REPLACE,
|_, file| file.write_all(b"new"),
|current, _| {
if current == step {
Err(io::Error::from_raw_os_error(5))
} else {
Ok(())
}
},
)
.unwrap_err();
if step == FileCommitStep::FinalParentSync {
assert!(
matches!(error, NamedWriteError::AfterPublication { source } if source.raw_os_error() == Some(5))
);
assert_eq!(fs::read(&target).unwrap(), b"new");
} else {
assert!(
matches!(error, NamedWriteError::BeforePublication { source, cleanup_error: None } if source.raw_os_error() == Some(5))
);
assert_eq!(fs::read(&target).unwrap(), b"old");
}
assert_eq!(fs::read_dir(&fixture.root).unwrap().count(), 1);
}
let error = commit_at_with_hook(
directory.as_fd(),
None,
OsStr::new("output"),
REPLACE,
|stage, _| -> Result<(), &'static str> {
fs::remove_file(fixture.root.join(stage)).unwrap();
fs::write(fixture.root.join(stage), b"foreign").unwrap();
Err("producer evidence")
},
|_, _| Ok(()),
)
.unwrap_err();
assert!(
matches!(error, NamedWriteError::Producer { source: "producer evidence", cleanup_error: Some(source) } if source.kind() == io::ErrorKind::InvalidData)
);
assert_eq!(fs::read_dir(&fixture.root).unwrap().count(), 2);
}
#[test]
fn link_fallback_cleanup_failure_reports_already_published_output() {
let fixture = Fixture::new();
let directory = fs::File::open(&fixture.root).unwrap();
let stage = OsStr::new("stage");
fs::write(fixture.root.join(stage), b"complete").unwrap();
let error = super::super::supported::finish_create_new_publication::<io::Error>(
&directory,
stage,
OsStr::new("output"),
Err(rustix::io::Errno::OPNOTSUPP),
|| Err(io::Error::from_raw_os_error(13)),
)
.unwrap_err();
assert!(
matches!(error, NamedWriteError::AfterPublication { source } if source.raw_os_error() == Some(13))
);
assert_eq!(fs::read(fixture.root.join("output")).unwrap(), b"complete");
assert_eq!(fs::read(fixture.root.join(stage)).unwrap(), b"complete");
}