#![deny(unsafe_code)]
use std::ffi::{OsStr, OsString};
use std::fs::File;
use std::io;
use std::path::{Path, PathBuf};
mod cache_io;
mod platform;
#[cfg(windows)]
#[allow(unsafe_code)]
mod windows;
#[cfg(windows)]
mod windows_cas;
pub use cache_io::DEFAULT_CACHE_RELEASE_WINDOW_BYTES;
pub use cache_io::DurableFileCacheWriter;
pub use cache_io::FileCacheReleaseEvidence;
pub use cache_io::FileCacheReleaseOutcome;
pub use cache_io::FileCacheReleaseTracker;
pub use cache_io::FileCacheReleasingReader;
pub use cache_io::cache_release_window_for_streams;
pub use cache_io::release_file_cache;
pub use cache_io::validate_cache_release_operation_windows;
use platform::create_private_directory_platform;
use platform::file_identity_platform;
use platform::file_link_count_platform;
use platform::file_space_usage_platform;
use platform::install_new_file_platform;
pub use platform::is_link_or_reparse;
use platform::link_count;
use platform::path_identity_platform;
use platform::path_link_count_platform;
use platform::rename_no_replace_platform;
use platform::stable_child_names;
use platform::stable_child_names_bounded;
use platform::stable_create_child_directory;
use platform::stable_link_child;
use platform::stable_open_child_directory;
use platform::stable_open_child_file;
use platform::stable_open_directory;
#[cfg(windows)]
use platform::stable_open_directory_for_sync;
use platform::stable_open_or_create_child_file;
use platform::stable_open_publishing_child_file;
use platform::stable_open_replaceable_child_file;
use platform::stable_remove_child_directory_if_identity;
use platform::stable_unlink_child_if_identity;
use platform::visit_regular_files_platform;
use platform::{replace_file_from_platform, replace_file_platform};
#[cfg(windows)]
pub use windows_cas::{WindowsCasWriter, WindowsLegacyCasAdopter, WindowsSealedCasFile};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FileIdentity {
pub volume_serial: u64,
pub file_id: [u8; 16],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FileSpaceUsage {
pub logical_bytes: u64,
pub allocated_bytes: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DirectoryValidationStage {
NamedMetadata,
RetainedMetadata,
NamedIdentity,
RetainedIdentity,
IdentityChanged,
}
#[derive(Debug)]
pub struct DirectoryValidationError {
stage: DirectoryValidationStage,
source: io::Error,
}
impl DirectoryValidationError {
fn new(stage: DirectoryValidationStage, source: io::Error) -> Self {
Self { stage, source }
}
#[must_use]
pub fn stage(&self) -> DirectoryValidationStage {
self.stage
}
#[must_use]
pub fn into_io_error(self) -> io::Error {
self.source
}
}
impl std::fmt::Display for DirectoryValidationError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.source, formatter)
}
}
impl std::error::Error for DirectoryValidationError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.source)
}
}
#[derive(Debug)]
pub struct OpenedDirectoryHandle {
file: File,
}
impl OpenedDirectoryHandle {
#[must_use]
pub fn as_file(&self) -> &File {
&self.file
}
#[must_use]
pub fn into_file(self) -> File {
self.file
}
}
#[derive(Debug)]
pub struct StableDirectory {
path: PathBuf,
file: File,
identity: FileIdentity,
}
#[derive(Debug)]
pub struct UnpublishedArtifactGuard {
directory: StableDirectory,
candidate_names: Vec<OsString>,
identity: Option<FileIdentity>,
file: Option<File>,
published: bool,
parent_synced: bool,
armed: bool,
}
impl UnpublishedArtifactGuard {
pub fn identity(&self) -> io::Result<FileIdentity> {
self.identity
.ok_or_else(|| io::Error::other("unpublished artifact identity is not initialized"))
}
pub fn verify_identity_with(
&mut self,
setup_check: impl FnOnce() -> io::Result<()>,
) -> io::Result<FileIdentity> {
let file = self
.file
.as_ref()
.ok_or_else(|| io::Error::other("unpublished artifact descriptor was transferred"))?;
let observed = file_identity(file)?;
self.identity = Some(observed);
setup_check()?;
Ok(observed)
}
pub fn temporary_name(&self) -> io::Result<&OsStr> {
self.candidate_names
.first()
.map(OsString::as_os_str)
.ok_or_else(|| io::Error::other("unpublished artifact has no temporary name"))
}
pub fn open_sibling(&self, name: &OsStr) -> io::Result<File> {
self.directory.open_child_file(name)
}
pub fn take_file(&mut self) -> io::Result<File> {
if self.identity.is_none() {
let file = self
.file
.as_ref()
.ok_or_else(|| io::Error::other("unpublished artifact file already transferred"))?;
self.identity = Some(file_identity(file)?);
}
self.file
.take()
.ok_or_else(|| io::Error::other("unpublished artifact file already transferred"))
}
pub fn transfer_unpublished_owner(
self,
file: &File,
) -> io::Result<(StableDirectory, OsString, FileIdentity)> {
let directory = self.directory.try_clone()?;
self.transfer_unpublished_owner_in(directory, file)
}
pub fn transfer_unpublished_owner_in(
mut self,
directory: StableDirectory,
file: &File,
) -> io::Result<(StableDirectory, OsString, FileIdentity)> {
if self.published || self.file.is_some() || self.candidate_names.len() != 1 {
return Err(io::Error::other(
"unpublished owner transfer invariants are incomplete",
));
}
let expected = self.identity()?;
self.directory.revalidate_named()?;
directory.revalidate_named()?;
let name = self.candidate_names[0].clone();
let named = directory.open_child_file(&name)?;
if directory.identity() != self.directory.identity()
|| file_identity(file)? != expected
|| file_identity(&named)? != expected
|| file_link_count(file)? != 1
{
return Err(io::Error::other(
"unpublished owner transfer identity changed",
));
}
self.armed = false;
Ok((directory, name, expected))
}
pub fn install_child(&mut self, target: &OsStr) -> io::Result<()> {
validate_child_name(target)?;
let temporary = self
.candidate_names
.first()
.ok_or_else(|| io::Error::other("unpublished artifact has no temporary name"))?
.clone();
if !self.candidate_names.iter().any(|name| name == target) {
self.candidate_names.push(target.to_owned());
}
self.directory
.install_child(&temporary, self.identity()?, target)?;
self.published = true;
self.parent_synced = false;
Ok(())
}
pub fn sync_parent(&mut self) -> io::Result<()> {
self.sync_parent_with(StableDirectory::sync)
}
pub fn sync_parent_with(
&mut self,
acknowledge: impl FnOnce(&StableDirectory) -> io::Result<()>,
) -> io::Result<()> {
acknowledge(&self.directory)?;
self.parent_synced = true;
Ok(())
}
pub fn commit(mut self) -> io::Result<()> {
if self.file.is_some() || !self.published || !self.parent_synced {
return Err(io::Error::other(
"unpublished artifact commit invariants are incomplete",
));
}
self.armed = false;
Ok(())
}
pub fn cleanup(&mut self) -> io::Result<()> {
if !self.armed {
return Ok(());
}
let mut cleanup = Ok(());
let identity = match self.identity {
Some(identity) => Some(identity),
None => match self
.file
.as_ref()
.ok_or_else(|| io::Error::other("unpublished artifact descriptor is unavailable"))
.and_then(file_identity)
{
Ok(identity) => {
self.identity = Some(identity);
Some(identity)
}
Err(error) => {
cleanup = append_sanitized_cleanup(
cleanup,
&error,
"unpublished artifact identity recovery failed",
);
None
}
},
};
self.file.take();
for name in &self.candidate_names {
let Some(identity) = identity else {
break;
};
let removed = self.directory.open_child_file(name).and_then(|file| {
if file_identity(&file)? == identity {
drop(file);
self.directory.unlink_child_if_identity(name, identity)?;
}
Ok(())
});
match removed {
Ok(()) => {}
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
Err(error) => {
cleanup = append_sanitized_cleanup(
cleanup,
&error,
"unpublished artifact unlink failed",
);
}
}
}
cleanup = match self.directory.sync() {
Ok(()) => cleanup,
Err(error) => append_sanitized_cleanup(
cleanup,
&error,
"unpublished artifact directory synchronization failed",
),
};
self.armed = false;
cleanup
}
pub fn cleanup_checked(
&mut self,
finalization_check: impl FnOnce() -> io::Result<()>,
) -> io::Result<()> {
let cleanup = self.cleanup();
match finalization_check() {
Ok(()) => cleanup,
Err(error) => append_sanitized_cleanup(
cleanup,
&error,
"unpublished artifact cleanup finalization failed",
),
}
}
}
impl Drop for UnpublishedArtifactGuard {
fn drop(&mut self) {
let _ = self.cleanup();
}
}
fn append_sanitized_cleanup(
primary: io::Result<()>,
cleanup: &io::Error,
context: &'static str,
) -> io::Result<()> {
let cleanup = io::Error::new(cleanup.kind(), context);
match primary {
Ok(()) => Err(cleanup),
Err(primary) => Err(io::Error::new(
primary.kind(),
format!("{primary}; {cleanup}"),
)),
}
}
impl StableDirectory {
pub fn try_clone(&self) -> io::Result<Self> {
self.revalidate_named()?;
let clone = Self {
path: self.path.clone(),
file: self.file.try_clone()?,
identity: self.identity,
};
clone.revalidate_named()?;
Ok(clone)
}
#[cfg(unix)]
pub fn lock_shared(&self) -> io::Result<()> {
<File as fs4::FileExt>::lock_shared(&self.file)
}
#[cfg(unix)]
pub fn lock_exclusive(&self) -> io::Result<()> {
<File as fs4::FileExt>::lock(&self.file)
}
#[cfg(unix)]
pub fn try_lock_exclusive(&self) -> io::Result<bool> {
match <File as fs4::FileExt>::try_lock(&self.file) {
Ok(()) => Ok(true),
Err(fs4::TryLockError::WouldBlock) => Ok(false),
Err(fs4::TryLockError::Error(error)) => Err(error),
}
}
#[cfg(unix)]
pub fn unlock(&self) -> io::Result<()> {
<File as fs4::FileExt>::unlock(&self.file)
}
pub fn open(path: &Path) -> io::Result<Self> {
let file = stable_open_directory(path)?;
let identity = file_identity(&file)?;
let directory = Self {
path: path.to_path_buf(),
file,
identity,
};
directory.revalidate_named()?;
Ok(directory)
}
pub fn from_retained_handle(
path: PathBuf,
handle: OpenedDirectoryHandle,
identity: FileIdentity,
) -> Result<Self, DirectoryValidationError> {
let directory = Self {
path,
file: handle.file,
identity,
};
directory.revalidate_named_detailed()?;
Ok(directory)
}
#[must_use]
pub fn as_file(&self) -> &File {
&self.file
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
#[must_use]
pub fn into_handle(self) -> OpenedDirectoryHandle {
OpenedDirectoryHandle { file: self.file }
}
pub fn revalidate_named_detailed(&self) -> Result<(), DirectoryValidationError> {
use DirectoryValidationStage as Stage;
let named = std::fs::symlink_metadata(&self.path)
.map_err(|error| DirectoryValidationError::new(Stage::NamedMetadata, error))?;
let retained = self
.file
.metadata()
.map_err(|error| DirectoryValidationError::new(Stage::RetainedMetadata, error))?;
if !named.is_dir() || is_link_or_reparse(&named) || !retained.is_dir() {
return Err(DirectoryValidationError::new(
Stage::IdentityChanged,
io::Error::other("stable directory path is linked or special"),
));
}
#[cfg(unix)]
let named_identity = platform::metadata_identity(&named);
#[cfg(not(unix))]
let named_identity = path_identity(&self.path)
.map_err(|error| DirectoryValidationError::new(Stage::NamedIdentity, error))?;
if named_identity != self.identity {
return Err(DirectoryValidationError::new(
Stage::IdentityChanged,
io::Error::other("stable directory identity changed"),
));
}
#[cfg(unix)]
let retained_identity = platform::metadata_identity(&retained);
#[cfg(not(unix))]
let retained_identity = file_identity(&self.file)
.map_err(|error| DirectoryValidationError::new(Stage::RetainedIdentity, error))?;
if retained_identity != self.identity {
return Err(DirectoryValidationError::new(
Stage::IdentityChanged,
io::Error::other("stable directory identity changed"),
));
}
Ok(())
}
pub fn revalidate_named(&self) -> io::Result<()> {
self.revalidate_named_detailed()
.map_err(DirectoryValidationError::into_io_error)
}
pub fn open_child_directory(&self, name: &OsStr) -> io::Result<Self> {
validate_child_name(name)?;
self.revalidate_named()?;
let path = self.path.join(name);
let file = stable_open_child_directory(&self.file, &path, name)?;
let child = Self {
identity: file_identity(&file)?,
path,
file,
};
child.revalidate_named()?;
self.revalidate_named()?;
Ok(child)
}
pub fn create_child_directory(&self, name: &OsStr) -> io::Result<Self> {
validate_child_name(name)?;
self.revalidate_named()?;
stable_create_child_directory(&self.file, &self.path.join(name), name)?;
self.open_child_directory(name)
}
pub fn open_child_file(&self, name: &OsStr) -> io::Result<File> {
validate_child_name(name)?;
self.revalidate_named()?;
let path = self.path.join(name);
let file = stable_open_child_file(&self.file, &path, name, false)?;
validate_stable_child_file(&file, &path)?;
self.revalidate_named()?;
Ok(file)
}
pub fn revalidate_child_file(&self, name: &OsStr, file: &File) -> io::Result<()> {
validate_child_name(name)?;
self.revalidate_named()?;
validate_stable_child_file(file, &self.path.join(name))?;
self.revalidate_named()
}
pub fn open_publishing_child_file(
&self,
name: &OsStr,
expected: FileIdentity,
) -> io::Result<File> {
validate_child_name(name)?;
self.revalidate_named()?;
let path = self.path.join(name);
let file = stable_open_publishing_child_file(&self.file, &path, name)?;
validate_stable_child_file(&file, &path)?;
if file_identity(&file)? != expected || file_link_count(&file)? != 1 {
return Err(io::Error::other(
"publication source identity or link count changed",
));
}
self.revalidate_named()?;
Ok(file)
}
pub fn visit_regular_files(
&self,
remaining: &mut usize,
visit: &mut impl FnMut(&File) -> io::Result<()>,
) -> io::Result<()> {
self.revalidate_named()?;
visit_regular_files_platform(self, remaining, visit)?;
self.revalidate_named()
}
pub fn create_child_file(&self, name: &OsStr) -> io::Result<File> {
validate_child_name(name)?;
self.revalidate_named()?;
let path = self.path.join(name);
let file = stable_open_child_file(&self.file, &path, name, true)?;
validate_stable_child_file(&file, &path)?;
self.revalidate_named()?;
Ok(file)
}
#[cfg(windows)]
pub fn create_cas_child_file(&self, name: &OsStr) -> io::Result<WindowsCasWriter> {
validate_child_name(name)?;
self.revalidate_named()?;
let path = self.path.join(name);
let file = windows::create_cas_writer(&path)?;
validate_stable_child_file(&file, &path)?;
self.revalidate_named()?;
Ok(WindowsCasWriter {
identity: file_identity(&file)?,
file,
})
}
#[cfg(windows)]
pub fn seal_cas_child_file(
&self,
name: &OsStr,
writer: WindowsCasWriter,
) -> io::Result<WindowsSealedCasFile> {
validate_child_name(name)?;
self.revalidate_named()?;
let path = self.path.join(name);
let expected = writer.identity;
if file_identity(&writer.file)? != expected || path_identity(&path)? != expected {
return Err(io::Error::other(
"CAS writer identity changed before sealing",
));
}
windows::seal_cas_writer(&writer.file)?;
if file_identity(&writer.file)? != expected || path_identity(&path)? != expected {
return Err(io::Error::other(
"CAS writer identity changed while sealing",
));
}
let bridge = windows::open_cas_bridge(&path)?;
if file_identity(&bridge)? != expected {
return Err(io::Error::other(
"CAS bridge identity changed while sealing",
));
}
drop(writer.file);
let reader = windows::open_sealed_cas_reader(&path)?;
validate_stable_child_file(&reader, &path)?;
if file_identity(&reader)? != expected || path_identity(&path)? != expected {
return Err(io::Error::other(
"CAS identity changed while reopening sealed reader",
));
}
drop(bridge);
self.revalidate_named()?;
Ok(WindowsSealedCasFile(reader))
}
#[cfg(windows)]
pub fn open_cas_child_file(&self, name: &OsStr) -> io::Result<WindowsSealedCasFile> {
validate_child_name(name)?;
self.revalidate_named()?;
let path = self.path.join(name);
let reader = windows::open_sealed_cas_reader(&path)?;
validate_stable_child_file(&reader, &path)?;
if !reader.metadata()?.permissions().readonly()
|| !windows::has_canonical_cas_dacl(&reader)?
{
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"CAS child is not canonically sealed",
));
}
self.revalidate_named()?;
Ok(WindowsSealedCasFile(reader))
}
#[cfg(windows)]
pub fn open_legacy_cas_child_for_adoption(
&self,
name: &OsStr,
) -> io::Result<WindowsLegacyCasAdopter> {
validate_child_name(name)?;
self.revalidate_named()?;
let path = self.path.join(name);
let file = windows::open_legacy_cas_adopter(&path)?;
validate_stable_child_file(&file, &path)?;
let identity = file_identity(&file)?;
if path_identity(&path)? != identity {
return Err(io::Error::other("legacy CAS identity changed during open"));
}
self.revalidate_named()?;
Ok(WindowsLegacyCasAdopter { file, identity })
}
#[cfg(windows)]
pub fn adopt_legacy_cas_child(
&self,
name: &OsStr,
adopter: WindowsLegacyCasAdopter,
) -> io::Result<WindowsSealedCasFile> {
validate_child_name(name)?;
self.revalidate_named()?;
let path = self.path.join(name);
windows::set_canonical_cas_dacl(&adopter.file)?;
let bridge = windows::open_cas_bridge(&path)?;
if file_identity(&bridge)? != adopter.identity {
return Err(io::Error::other("legacy CAS bridge identity changed"));
}
drop(adopter.file);
let reader = windows::open_sealed_cas_reader(&path)?;
if file_identity(&reader)? != adopter.identity || path_identity(&path)? != adopter.identity
{
return Err(io::Error::other(
"legacy CAS identity changed during adoption",
));
}
drop(bridge);
self.revalidate_named()?;
Ok(WindowsSealedCasFile(reader))
}
pub fn create_replaceable_child_file(&self, name: &OsStr) -> io::Result<File> {
validate_child_name(name)?;
self.revalidate_named()?;
let path = self.path.join(name);
let file = stable_open_replaceable_child_file(&self.file, &path, name)?;
validate_stable_child_file(&file, &path)?;
self.revalidate_named()?;
Ok(file)
}
pub fn create_unpublished_replaceable_child(
&self,
name: &OsStr,
) -> io::Result<UnpublishedArtifactGuard> {
let directory = self.try_clone()?;
let file = self.create_replaceable_child_file(name)?;
Ok(UnpublishedArtifactGuard {
directory,
candidate_names: vec![name.to_owned()],
identity: None,
file: Some(file),
published: false,
parent_synced: false,
armed: true,
})
}
pub fn open_or_create_child_file(&self, name: &OsStr) -> io::Result<File> {
validate_child_name(name)?;
self.revalidate_named()?;
let path = self.path.join(name);
let file = stable_open_or_create_child_file(&self.file, &path, name)?;
validate_stable_child_file(&file, &path)?;
self.revalidate_named()?;
Ok(file)
}
pub fn child_names(&self) -> io::Result<Vec<std::ffi::OsString>> {
self.revalidate_named()?;
stable_child_names(&self.file, &self.path)
}
pub fn child_names_bounded(&self, limit: usize) -> io::Result<Vec<std::ffi::OsString>> {
self.revalidate_named()?;
stable_child_names_bounded(&self.file, &self.path, limit)
}
pub fn link_child_into(
&self,
source_name: &OsStr,
source: &File,
expected_source: FileIdentity,
destination: &Self,
destination_name: &OsStr,
) -> io::Result<(File, FileIdentity)> {
validate_child_name(source_name)?;
validate_child_name(destination_name)?;
self.revalidate_named()?;
destination.revalidate_named()?;
validate_stable_child_file(source, &self.path.join(source_name))?;
if file_identity(source)? != expected_source {
return Err(io::Error::other("hard-link source identity changed"));
}
stable_link_child(
&self.file,
&self.path,
source_name,
&destination.file,
&destination.path,
destination_name,
)?;
self.revalidate_named()?;
destination.revalidate_named()?;
let installed = destination.open_child_file(destination_name)?;
let installed_identity = file_identity(&installed)?;
if installed_identity != expected_source {
return Err(io::Error::other("hard-link destination identity mismatch"));
}
Ok((installed, installed_identity))
}
pub fn unlink_child_if_identity(&self, name: &OsStr, expected: FileIdentity) -> io::Result<()> {
validate_child_name(name)?;
self.revalidate_named()?;
stable_unlink_child_if_identity(&self.file, &self.path, name, expected)?;
self.revalidate_named()
}
pub fn remove_child_directory_if_identity(
&self,
name: &OsStr,
expected: FileIdentity,
) -> io::Result<()> {
validate_child_name(name)?;
self.revalidate_named()?;
stable_remove_child_directory_if_identity(&self.file, &self.path, name, expected)?;
self.revalidate_named()
}
pub fn replace_child(
&self,
temporary: &OsStr,
expected_temporary: FileIdentity,
target: &OsStr,
) -> io::Result<()> {
self.replace_child_typed(temporary, expected_temporary, target)
.map_err(|error| io::Error::other(error.to_string()))
}
pub fn replace_child_typed(
&self,
temporary: &OsStr,
expected_temporary: FileIdentity,
target: &OsStr,
) -> Result<(), ReplaceFileError> {
let prepare = || -> io::Result<bool> {
validate_child_name(temporary)?;
validate_child_name(target)?;
self.revalidate_named()?;
let temporary_file = self.open_child_file(temporary)?;
if file_identity(&temporary_file)? != expected_temporary
|| file_link_count(&temporary_file)? != 1
{
return Err(io::Error::other(
"atomic temporary child identity or link count changed",
));
}
match self.open_child_file(target) {
Ok(_) => Ok(true),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(false),
Err(error) => Err(error),
}
};
if prepare().map_err(ReplaceFileError::NotReplaced)? {
replace_file_platform(
&self.file,
temporary,
target,
Some(expected_temporary),
None,
)?;
} else {
install_new_file_platform(&self.file, temporary, target, Some(expected_temporary))
.map_err(ReplaceFileError::StateUnknown)?;
}
self.validate_replaced_child(target, expected_temporary)
.map_err(ReplaceFileError::StateUnknown)
}
fn validate_replaced_child(&self, target: &OsStr, expected: FileIdentity) -> io::Result<()> {
self.revalidate_named()?;
let installed = self.open_child_file(target)?;
if file_identity(&installed)? != expected || file_link_count(&installed)? != 1 {
return Err(io::Error::other(
"replacement target identity or link count changed",
));
}
Ok(())
}
pub fn replace_authenticated_child(
&self,
temporary: &OsStr,
expected_temporary: FileIdentity,
target: &OsStr,
expected_target: FileIdentity,
) -> io::Result<()> {
self.replace_authenticated_child_typed(
temporary,
expected_temporary,
target,
expected_target,
)
.map_err(|error| io::Error::other(error.to_string()))
}
pub fn replace_authenticated_child_typed(
&self,
temporary: &OsStr,
expected_temporary: FileIdentity,
target: &OsStr,
expected_target: FileIdentity,
) -> Result<(), ReplaceFileError> {
validate_child_name(temporary).map_err(ReplaceFileError::NotReplaced)?;
validate_child_name(target).map_err(ReplaceFileError::NotReplaced)?;
self.revalidate_named()
.map_err(ReplaceFileError::NotReplaced)?;
replace_file_platform(
&self.file,
temporary,
target,
Some(expected_temporary),
Some(expected_target),
)?;
self.validate_replaced_child(target, expected_temporary)
.map_err(ReplaceFileError::StateUnknown)
}
#[doc(hidden)]
pub fn replace_authenticated_child_from(
&self,
source_directory: &Self,
temporary: &OsStr,
expected_temporary: FileIdentity,
target: &OsStr,
expected_target: FileIdentity,
) -> Result<(), ReplaceFileError> {
validate_child_name(temporary).map_err(ReplaceFileError::NotReplaced)?;
validate_child_name(target).map_err(ReplaceFileError::NotReplaced)?;
source_directory
.revalidate_named()
.map_err(ReplaceFileError::NotReplaced)?;
self.revalidate_named()
.map_err(ReplaceFileError::NotReplaced)?;
replace_file_from_platform(
&source_directory.file,
&self.file,
temporary,
target,
Some(expected_temporary),
Some(expected_target),
)?;
source_directory
.revalidate_named()
.map_err(ReplaceFileError::StateUnknown)?;
self.validate_replaced_child(target, expected_temporary)
.map_err(ReplaceFileError::StateUnknown)
}
pub fn install_child(
&self,
temporary: &OsStr,
expected_temporary: FileIdentity,
target: &OsStr,
) -> io::Result<()> {
validate_child_name(temporary)?;
validate_child_name(target)?;
self.revalidate_named()?;
let temporary_file = self.open_child_file(temporary)?;
if file_identity(&temporary_file)? != expected_temporary
|| file_link_count(&temporary_file)? != 1
{
return Err(io::Error::other(
"atomic temporary child identity or link count changed",
));
}
drop(temporary_file);
install_new_file_platform(&self.file, temporary, target, Some(expected_temporary))?;
self.revalidate_named()?;
self.open_child_file(target).map(|_| ())
}
pub fn install_child_typed(
&self,
temporary: &OsStr,
expected_temporary: FileIdentity,
target: &OsStr,
) -> Result<(), ReplaceFileError> {
let prepare = || -> io::Result<()> {
validate_child_name(temporary)?;
validate_child_name(target)?;
self.revalidate_named()?;
let file = self.open_child_file(temporary)?;
if file_identity(&file)? != expected_temporary || file_link_count(&file)? != 1 {
return Err(io::Error::other(
"atomic temporary child identity or link count changed",
));
}
Ok(())
};
prepare().map_err(ReplaceFileError::NotReplaced)?;
install_new_file_platform(&self.file, temporary, target, Some(expected_temporary))
.map_err(|error| {
if error.kind() == io::ErrorKind::AlreadyExists {
ReplaceFileError::NotReplaced(error)
} else {
ReplaceFileError::StateUnknown(error)
}
})?;
self.validate_replaced_child(target, expected_temporary)
.map_err(ReplaceFileError::StateUnknown)
}
pub fn sync(&self) -> io::Result<()> {
self.revalidate_named()?;
#[cfg(windows)]
{
let directory = stable_open_directory_for_sync(&self.path)?;
if file_identity(&directory)? != self.identity {
return Err(io::Error::other(
"stable directory identity changed before sync",
));
}
directory.observed_sync_all()?;
self.revalidate_named()
}
#[cfg(not(windows))]
{
self.file.observed_sync_all()?;
self.revalidate_named()
}
}
#[must_use]
pub fn identity(&self) -> FileIdentity {
self.identity
}
}
pub fn open_directory_handle(path: &Path) -> io::Result<OpenedDirectoryHandle> {
stable_open_directory(path).map(|file| OpenedDirectoryHandle { file })
}
fn validate_child_name(name: &OsStr) -> io::Result<()> {
let path = Path::new(name);
if name.is_empty()
|| path.is_absolute()
|| path.components().count() != 1
|| matches!(name.to_str(), Some("." | ".."))
{
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid child name",
));
}
Ok(())
}
fn validate_stable_child_file(file: &File, path: &Path) -> io::Result<()> {
let metadata = file.metadata()?;
let named = std::fs::symlink_metadata(path)?;
if !metadata.is_file()
|| !named.is_file()
|| is_link_or_reparse(&named)
|| file_identity(file)? != path_identity(path)?
{
return Err(io::Error::other(
"stable child is linked, special, or substituted",
));
}
Ok(())
}
#[cfg(windows)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WindowsVolumeInformation {
pub filesystem_name: String,
pub read_only: bool,
pub fixed: bool,
}
pub fn create_private_directory(path: &Path) -> io::Result<()> {
create_private_directory_platform(path)
}
pub fn file_identity(file: &File) -> io::Result<FileIdentity> {
file_identity_platform(file)
}
pub fn file_space_usage(file: &File) -> io::Result<FileSpaceUsage> {
file_space_usage_platform(file)
}
pub fn path_identity(path: &Path) -> io::Result<FileIdentity> {
path_identity_platform(path)
}
pub fn file_link_count(file: &File) -> io::Result<u64> {
file_link_count_platform(file)
}
pub fn path_link_count(path: &Path) -> io::Result<u64> {
path_link_count_platform(path)
}
#[cfg(windows)]
pub fn windows_volume_information(path: &Path) -> io::Result<WindowsVolumeInformation> {
windows::volume_information(path)
}
#[derive(Debug)]
pub enum ReplaceFileError {
NotReplaced(io::Error),
StateUnknown(io::Error),
}
impl std::fmt::Display for ReplaceFileError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotReplaced(error) => write!(formatter, "file was not replaced: {error}"),
Self::StateUnknown(error) => {
write!(
formatter,
"replacement state requires reconciliation: {error}"
)
}
}
}
}
impl std::error::Error for ReplaceFileError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(match self {
Self::NotReplaced(error) | Self::StateUnknown(error) => error,
})
}
}
#[doc(hidden)]
#[must_use]
pub fn classify_failed_replacement(
error: io::Error,
source_before: FileIdentity,
target_before: FileIdentity,
source_after: Option<FileIdentity>,
target_after: Option<FileIdentity>,
) -> ReplaceFileError {
if source_after == Some(source_before) && target_after == Some(target_before) {
ReplaceFileError::NotReplaced(error)
} else {
ReplaceFileError::StateUnknown(error)
}
}
pub fn replace_file(
directory: &File,
source_name: &OsStr,
target_name: &OsStr,
) -> Result<(), ReplaceFileError> {
verify_single_component(source_name).map_err(ReplaceFileError::NotReplaced)?;
verify_single_component(target_name).map_err(ReplaceFileError::NotReplaced)?;
replace_file_platform(directory, source_name, target_name, None, None)
}
pub fn install_new_file(
directory: &File,
source_name: &OsStr,
target_name: &OsStr,
) -> io::Result<()> {
verify_single_component(source_name)?;
verify_single_component(target_name)?;
install_new_file_platform(directory, source_name, target_name, None)
}
pub fn rename_no_replace(source: &Path, destination: &Path) -> io::Result<()> {
rename_no_replace_platform(source, destination)
}
fn verify_single_component(name: &OsStr) -> io::Result<()> {
let mut components = Path::new(name).components();
if !matches!(components.next(), Some(std::path::Component::Normal(_)))
|| components.next().is_some()
{
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"filesystem operation requires one plain name",
));
}
Ok(())
}
fn verify_regular_metadata(metadata: &std::fs::Metadata) -> io::Result<()> {
if is_link_or_reparse(metadata) || !metadata.is_file() || link_count(metadata) != 1 {
return Err(io::Error::other(
"replacement path is not a regular non-link file",
));
}
Ok(())
}
fn verify_space_usage_metadata(metadata: &std::fs::Metadata) -> io::Result<()> {
if is_link_or_reparse(metadata) || !metadata.is_file() {
return Err(io::Error::other(
"space usage handle is not a regular non-reparse file",
));
}
Ok(())
}
#[cfg(test)]
mod tests;
pub mod observation;
pub use observation::ObservedSync;