#[cfg(feature = "fs")]
mod temporary;
#[cfg(feature = "fs")]
pub use temporary::{TemporaryDirectory, MAX_TEMP_PREFIX_BYTES};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RawDescriptor(usize);
impl RawDescriptor {
pub(crate) fn from_value(value: usize) -> Self {
Self(value)
}
pub(crate) fn value(self) -> usize {
self.0
}
}
pub use crate::fs_write_all_to_descriptor as write_all_to_descriptor;
#[cfg(feature = "wasm-sketch-worker")]
pub(crate) struct OwnedScratchDirectory {
directory: Option<crate::ScratchDirectoryAnchor>,
path: std::path::PathBuf,
}
#[cfg(feature = "wasm-sketch-worker")]
impl OwnedScratchDirectory {
pub(crate) fn create_in(parent: &std::path::Path) -> std::io::Result<Self> {
let temporary = tempfile::Builder::new()
.prefix(".kernal-worker-output-")
.tempdir_in(parent)?;
let directory = crate::ScratchDirectoryAnchor::open(temporary.path())?;
let path = temporary.keep();
Ok(Self {
directory: Some(directory),
path,
})
}
pub(crate) fn path(&self) -> &std::path::Path {
&self.path
}
pub(crate) fn close(mut self) -> std::io::Result<()> {
self.directory
.take()
.expect("scratch owner is live")
.remove()
}
}
#[cfg(feature = "wasm-sketch-worker")]
impl Drop for OwnedScratchDirectory {
fn drop(&mut self) {
if let Some(directory) = self.directory.take() {
let _ = directory.remove();
}
}
}
#[cfg(feature = "fs")]
mod async_io;
#[cfg(feature = "fs")]
pub use async_io::AsyncFileIo;
#[cfg(feature = "fs")]
pub fn user_home_dir() -> Option<std::path::PathBuf> {
dirs::home_dir()
}
#[cfg(feature = "fs")]
pub use crate::{
fs_create_dir_all_private as create_dir_all_private,
fs_create_private_file as create_private_file, fs_decode_path_bytes as decode_path_bytes,
fs_encode_path_bytes as encode_path_bytes, fs_ensure_dir_private as ensure_dir_private,
fs_file_identity as file_identity, fs_is_lock_conflict as is_lock_conflict,
fs_open_lock_file as open_lock_file, fs_open_shared_append as open_shared_append,
fs_path_identity as path_identity, fs_replace_file as replace_file,
fs_sync_directory as sync_directory, fs_user_config_dir as user_config_dir,
fs_user_data_dir as user_data_dir, fs_user_run_data_root as user_run_data_root,
fs_user_runtime_dir as user_runtime_dir, fs_user_state_dir as user_state_dir,
FsFileIdentity as FileIdentity,
};
#[cfg(feature = "fs")]
pub const MAX_PRIVATE_REGULAR_FILE_BYTES: usize = 64 * 1024 * 1024;
#[cfg(feature = "fs")]
pub const MAX_CONTEXT_REGULAR_FILE_BYTES: usize = 64 * 1024 * 1024;
#[cfg(feature = "fs")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ContextPathKind {
RegularFile,
Directory,
Symlink,
Other,
}
#[cfg(feature = "fs")]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ContextPathMetadata {
pub kind: ContextPathKind,
pub len: Option<u64>,
pub modified: Option<SystemTime>,
pub identity: Option<FileIdentity>,
}
#[cfg(feature = "fs")]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ContextFileObservation {
pub bytes: Vec<u8>,
pub metadata: ContextPathMetadata,
}
#[cfg(feature = "fs")]
pub(crate) fn context_path_kind(metadata: &std::fs::Metadata) -> ContextPathKind {
#[cfg(windows)]
{
use std::os::windows::fs::MetadataExt as _;
if metadata.file_attributes() & 0x400 != 0 {
return ContextPathKind::Symlink;
}
}
let file_type = metadata.file_type();
if file_type.is_symlink() {
ContextPathKind::Symlink
} else if file_type.is_file() {
ContextPathKind::RegularFile
} else if file_type.is_dir() {
ContextPathKind::Directory
} else {
ContextPathKind::Other
}
}
#[cfg(feature = "fs")]
pub(crate) fn context_regular_file_metadata(
metadata: &std::fs::Metadata,
identity: FileIdentity,
) -> io::Result<ContextPathMetadata> {
if !metadata.is_file() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"context input is not a regular file",
));
}
Ok(ContextPathMetadata {
kind: ContextPathKind::RegularFile,
len: Some(metadata.len()),
modified: Some(metadata.modified()?),
identity: Some(identity),
})
}
#[cfg(feature = "fs")]
pub fn context_path_metadata_no_follow(path: &Path) -> io::Result<ContextPathMetadata> {
let metadata = std::fs::symlink_metadata(path)?;
let kind = context_path_kind(&metadata);
Ok(ContextPathMetadata {
kind,
len: (kind == ContextPathKind::RegularFile).then_some(metadata.len()),
modified: metadata.modified().ok(),
identity: None,
})
}
#[cfg(feature = "fs")]
pub fn read_context_link(path: &Path) -> io::Result<PathBuf> {
std::fs::read_link(path)
}
#[cfg(feature = "fs")]
pub fn canonical_context_path(path: &Path) -> io::Result<PathBuf> {
std::fs::canonicalize(path)
}
#[cfg(feature = "fs")]
pub fn read_context_regular_file_bounded(
path: &Path,
max_bytes: usize,
) -> io::Result<ContextFileObservation> {
if max_bytes > MAX_CONTEXT_REGULAR_FILE_BYTES {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"context regular-file limit {max_bytes} exceeds the {} byte facade cap",
MAX_CONTEXT_REGULAR_FILE_BYTES
),
));
}
crate::fs_read_context_regular_file_bounded(path, max_bytes)
}
#[cfg(feature = "fs")]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DirectoryCursorEntry {
path: PathBuf,
file_name: OsString,
kind: ContextPathKind,
}
#[cfg(feature = "fs")]
impl DirectoryCursorEntry {
pub fn path(&self) -> &Path {
&self.path
}
pub fn file_name(&self) -> &OsStr {
&self.file_name
}
pub fn kind(&self) -> ContextPathKind {
self.kind
}
}
#[cfg(feature = "fs")]
pub struct DirectoryCursor {
native: std::fs::ReadDir,
}
#[cfg(feature = "fs")]
impl DirectoryCursor {
pub fn open(directory: impl AsRef<Path>) -> io::Result<Self> {
Ok(Self {
native: std::fs::read_dir(directory)?,
})
}
pub fn next_entry(&mut self) -> io::Result<Option<DirectoryCursorEntry>> {
let Some(entry) = self.native.next() else {
return Ok(None);
};
let entry = entry?;
let path = entry.path();
let file_name = entry.file_name();
let kind = context_path_kind(&std::fs::symlink_metadata(&path)?);
Ok(Some(DirectoryCursorEntry {
path,
file_name,
kind,
}))
}
}
#[cfg(feature = "fs")]
pub fn read_private_regular_file_bounded(path: &Path, max_bytes: usize) -> io::Result<Vec<u8>> {
if max_bytes > MAX_PRIVATE_REGULAR_FILE_BYTES {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"private regular-file limit {max_bytes} exceeds the {} byte facade cap",
MAX_PRIVATE_REGULAR_FILE_BYTES
),
));
}
crate::fs_read_private_regular_file_bounded(path, max_bytes)
}
#[cfg(feature = "fs")]
use std::ffi::{OsStr, OsString};
#[cfg(feature = "fs")]
use std::fs::File;
#[cfg(feature = "fs")]
use std::io;
#[cfg(feature = "fs")]
use std::path::{Path, PathBuf};
#[cfg(feature = "fs")]
use std::sync::Arc;
#[cfg(feature = "fs")]
use std::time::SystemTime;
#[cfg(feature = "fs")]
#[derive(Debug)]
pub struct FileLock<'file> {
file: &'file File,
}
#[cfg(feature = "fs")]
impl<'file> FileLock<'file> {
fn new(file: &'file File) -> Self {
Self { file }
}
}
#[cfg(feature = "fs")]
impl Drop for FileLock<'_> {
fn drop(&mut self) {
let _ = crate::fs_unlock(self.file);
}
}
#[cfg(feature = "fs")]
pub fn lock_exclusive(file: &File) -> io::Result<FileLock<'_>> {
crate::fs_lock_exclusive(file)?;
Ok(FileLock::new(file))
}
#[cfg(feature = "fs")]
pub fn lock_shared(file: &File) -> io::Result<FileLock<'_>> {
crate::fs_lock_shared(file)?;
Ok(FileLock::new(file))
}
#[cfg(feature = "fs")]
pub fn try_lock_exclusive(file: &File) -> io::Result<FileLock<'_>> {
crate::fs_try_lock_exclusive(file)?;
Ok(FileLock::new(file))
}
#[cfg(feature = "fs")]
pub fn try_lock_shared(file: &File) -> io::Result<FileLock<'_>> {
crate::fs_try_lock_shared(file)?;
Ok(FileLock::new(file))
}
#[cfg(feature = "fs")]
#[derive(Debug)]
pub struct OwnedFileLock {
file: Option<File>,
}
#[cfg(feature = "fs")]
impl OwnedFileLock {
pub fn file(&self) -> &File {
self.file
.as_ref()
.expect("the handle is taken only by unlock, which consumes self")
}
pub fn unlock(mut self) -> Result<File, (File, io::Error)> {
let file = self
.file
.take()
.expect("the handle is taken only here, and this consumes self");
match crate::fs_unlock(&file) {
Ok(()) => Ok(file),
Err(error) => Err((file, error)),
}
}
}
#[cfg(feature = "fs")]
impl Drop for OwnedFileLock {
fn drop(&mut self) {
if let Some(file) = self.file.as_ref() {
let _ = crate::fs_unlock(file);
}
}
}
#[cfg(feature = "fs")]
pub fn lock_exclusive_owned(file: File) -> io::Result<OwnedFileLock> {
crate::fs_lock_exclusive(&file)?;
Ok(OwnedFileLock { file: Some(file) })
}
#[cfg(feature = "fs")]
pub fn lock_shared_owned(file: File) -> io::Result<OwnedFileLock> {
crate::fs_lock_shared(&file)?;
Ok(OwnedFileLock { file: Some(file) })
}
#[cfg(feature = "fs")]
pub fn try_lock_exclusive_owned(file: File) -> io::Result<OwnedFileLock> {
crate::fs_try_lock_exclusive(&file)?;
Ok(OwnedFileLock { file: Some(file) })
}
#[cfg(feature = "fs")]
pub fn try_lock_shared_owned(file: File) -> io::Result<OwnedFileLock> {
crate::fs_try_lock_shared(&file)?;
Ok(OwnedFileLock { file: Some(file) })
}
#[cfg(feature = "fs")]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FileTime {
seconds_since_unix_epoch: i64,
nanoseconds: u32,
}
#[cfg(feature = "fs")]
impl FileTime {
pub fn from_unix_time(seconds_since_unix_epoch: i64, nanoseconds: u32) -> Self {
Self {
seconds_since_unix_epoch,
nanoseconds,
}
}
pub const fn unix_seconds(self) -> i64 {
self.seconds_since_unix_epoch
}
pub const fn nanoseconds(self) -> u32 {
self.nanoseconds
}
pub fn from_system_time(time: SystemTime) -> Self {
match time.duration_since(SystemTime::UNIX_EPOCH) {
Ok(duration) => Self {
seconds_since_unix_epoch: duration.as_secs() as i64,
nanoseconds: duration.subsec_nanos(),
},
Err(before_epoch) => {
let duration = before_epoch.duration();
let subsec = duration.subsec_nanos();
let (seconds, nanoseconds) = if subsec == 0 {
(-(duration.as_secs() as i64), 0)
} else {
(-(duration.as_secs() as i64) - 1, 1_000_000_000 - subsec)
};
Self {
seconds_since_unix_epoch: seconds,
nanoseconds,
}
}
}
}
pub fn now() -> Self {
Self::from_system_time(SystemTime::now())
}
pub fn from_last_modification_time(metadata: &std::fs::Metadata) -> Self {
last_modification_time(metadata)
}
}
#[cfg(all(feature = "fs", unix))]
fn last_modification_time(metadata: &std::fs::Metadata) -> FileTime {
use std::os::unix::fs::MetadataExt as _;
FileTime {
seconds_since_unix_epoch: metadata.mtime(),
nanoseconds: metadata.mtime_nsec().clamp(0, 999_999_999) as u32,
}
}
#[cfg(all(feature = "fs", windows))]
fn last_modification_time(metadata: &std::fs::Metadata) -> FileTime {
use std::os::windows::fs::MetadataExt as _;
windows_file_time_to_unix(metadata.last_write_time())
}
#[cfg(all(feature = "fs", windows))]
fn windows_file_time_to_unix(ticks: u64) -> FileTime {
const WINDOWS_TO_UNIX_EPOCH_TICKS: i64 = 116_444_736_000_000_000;
let ticks = ticks as i64 - WINDOWS_TO_UNIX_EPOCH_TICKS;
let seconds_since_unix_epoch = ticks.div_euclid(10_000_000);
let remainder_ticks = ticks.rem_euclid(10_000_000);
FileTime {
seconds_since_unix_epoch,
nanoseconds: (remainder_ticks * 100) as u32,
}
}
#[cfg(feature = "fs")]
pub fn set_file_mtime(path: &Path, time: FileTime) -> io::Result<()> {
crate::fs_set_file_mtime(path, time.seconds_since_unix_epoch, time.nanoseconds)
}
#[cfg(feature = "fs")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CopyOutcome {
Reflinked,
Copied {
bytes: u64,
},
}
#[cfg(feature = "fs")]
pub fn copy_file(source: &Path, destination: &Path) -> io::Result<CopyOutcome> {
match reflink_copy::reflink_or_copy(source, destination)? {
None => Ok(CopyOutcome::Reflinked),
Some(bytes) => Ok(CopyOutcome::Copied { bytes }),
}
}
#[cfg(feature = "fs")]
pub fn reflink_file(source: &Path, destination: &Path) -> io::Result<()> {
reflink_copy::reflink(source, destination)
}
#[cfg(feature = "fs")]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DirectoryEntry {
path: PathBuf,
depth: usize,
is_directory: bool,
is_file: bool,
is_symbolic_link: bool,
}
#[cfg(feature = "fs")]
impl DirectoryEntry {
pub fn path(&self) -> &Path {
&self.path
}
pub fn depth(&self) -> usize {
self.depth
}
pub fn is_directory(&self) -> bool {
self.is_directory
}
pub fn is_file(&self) -> bool {
self.is_file
}
pub fn is_symbolic_link(&self) -> bool {
self.is_symbolic_link
}
pub fn metadata(&self) -> io::Result<std::fs::Metadata> {
std::fs::metadata(&self.path)
}
}
#[cfg(feature = "fs")]
fn directory_entry_from_jwalk(entry: jwalk::DirEntry<((), ())>) -> DirectoryEntry {
let file_type = entry.file_type();
DirectoryEntry {
path: entry.path(),
depth: entry.depth(),
is_directory: file_type.is_dir(),
is_file: file_type.is_file(),
is_symbolic_link: file_type.is_symlink(),
}
}
#[cfg(feature = "fs")]
type PruneDirectories = Arc<dyn Fn(&Path) -> bool + Send + Sync>;
#[cfg(feature = "fs")]
pub struct DirectoryWalk {
root: PathBuf,
follow_symbolic_links: bool,
include_hidden_entries: bool,
sorted: bool,
prune_directories: Option<PruneDirectories>,
}
#[cfg(feature = "fs")]
impl DirectoryWalk {
pub fn new(root: PathBuf) -> Self {
Self {
root,
follow_symbolic_links: false,
include_hidden_entries: true,
sorted: false,
prune_directories: None,
}
}
pub fn follow_symbolic_links(mut self, follow: bool) -> Self {
self.follow_symbolic_links = follow;
self
}
pub fn include_hidden_entries(mut self, include: bool) -> Self {
self.include_hidden_entries = include;
self
}
pub fn sorted(mut self, sorted: bool) -> Self {
self.sorted = sorted;
self
}
pub fn prune_directories<F>(mut self, keep: F) -> Self
where
F: Fn(&Path) -> bool + Send + Sync + 'static,
{
let keep: PruneDirectories = Arc::new(keep);
self.prune_directories = Some(keep);
self
}
pub fn walk(self) -> impl Iterator<Item = io::Result<DirectoryEntry>> {
let prune_directories = self.prune_directories;
let walker = jwalk::WalkDir::new(&self.root)
.follow_links(self.follow_symbolic_links)
.skip_hidden(!self.include_hidden_entries)
.sort(self.sorted)
.process_read_dir(move |_depth, _parent, _state, children| {
let Some(keep) = &prune_directories else {
return;
};
children.retain(|entry| match entry {
Ok(entry) if entry.file_type().is_dir() => keep(&entry.path()),
_ => true,
});
});
walker.into_iter().map(|entry| {
entry
.map(directory_entry_from_jwalk)
.map_err(io::Error::from)
})
}
}
#[cfg(feature = "fs")]
#[derive(Clone, Debug)]
pub struct PatternSet(globset::GlobSet);
#[cfg(feature = "fs")]
impl PatternSet {
pub fn is_match(&self, path: impl AsRef<Path>) -> bool {
self.0.is_match(path.as_ref())
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn len(&self) -> usize {
self.0.len()
}
}
#[cfg(feature = "fs")]
#[derive(Debug, Default)]
pub struct PatternSetBuilder {
patterns: Vec<String>,
}
#[cfg(feature = "fs")]
impl PatternSetBuilder {
pub fn new() -> Self {
Self {
patterns: Vec::new(),
}
}
pub fn add_pattern(mut self, pattern: &str) -> Self {
self.patterns.push(pattern.to_owned());
self
}
pub fn build(self) -> io::Result<PatternSet> {
let mut builder = globset::GlobSetBuilder::new();
for pattern in &self.patterns {
let glob = globset::Glob::new(pattern)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
builder.add(glob);
}
let set = builder
.build()
.map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
Ok(PatternSet(set))
}
}
#[cfg(all(test, feature = "fs"))]
mod tests {
use super::*;
const PRODUCT: &str = "rp-fs-facade-test";
#[test]
fn a_freshly_created_private_directory_needs_no_tightening() {
let root = tempfile::tempdir().expect("temp root");
let nested = root.path().join("outer").join("inner");
create_dir_all_private(&nested).expect("create private directory");
assert!(nested.is_dir(), "the directory and its parents exist");
assert!(
!ensure_dir_private(&nested).expect("inspect a fresh private directory"),
"a directory this facade just created is already private"
);
}
#[test]
fn creating_a_private_directory_twice_succeeds() {
let root = tempfile::tempdir().expect("temp root");
let path = root.path().join("twice");
create_dir_all_private(&path).expect("first create");
create_dir_all_private(&path).expect("second create");
assert!(path.is_dir());
}
#[test]
fn ensuring_a_missing_directory_is_private_reports_not_found() {
let root = tempfile::tempdir().expect("temp root");
let error = ensure_dir_private(&root.path().join("absent")).expect_err("missing directory");
assert_eq!(error.kind(), io::ErrorKind::NotFound);
}
#[test]
fn shared_append_preserves_earlier_bytes_and_admits_a_second_writer() {
use std::io::Write as _;
let root = tempfile::tempdir().expect("temp root");
let path = root.path().join("log");
let mut first = open_shared_append(&path).expect("first append handle");
first.write_all(b"one\n").expect("first write");
let mut second = open_shared_append(&path).expect("second append handle");
second.write_all(b"two\n").expect("second write");
drop((first, second));
assert_eq!(
std::fs::read_to_string(&path).expect("read back"),
"one\ntwo\n"
);
}
#[test]
fn every_role_is_an_absolute_product_scoped_directory() {
for directory in [
user_runtime_dir(PRODUCT),
user_state_dir(PRODUCT),
user_run_data_root(PRODUCT),
] {
assert!(
directory.is_absolute(),
"{} must be absolute",
directory.display()
);
assert!(
directory.to_string_lossy().contains(PRODUCT),
"{} must be scoped to the product",
directory.display()
);
}
}
#[test]
fn distinct_products_do_not_collide() {
let other = "rp-fs-facade-other";
assert_ne!(user_runtime_dir(PRODUCT), user_runtime_dir(other));
assert_ne!(user_state_dir(PRODUCT), user_state_dir(other));
assert_ne!(user_run_data_root(PRODUCT), user_run_data_root(other));
}
#[test]
fn a_file_has_one_identity_through_both_a_handle_and_its_path() {
let dir = std::env::temp_dir().join(format!("rp-fs-identity-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("create dir");
let path = dir.join("subject");
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(&path)
.expect("create subject");
let by_handle = file_identity(&file).expect("identity by handle");
let by_path = path_identity(&path).expect("identity by path");
assert_eq!(by_handle, by_path);
drop(file);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn distinct_files_have_distinct_identities() {
let dir = std::env::temp_dir().join(format!("rp-fs-identity2-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("create dir");
let (first, second) = (dir.join("first"), dir.join("second"));
std::fs::write(&first, b"a").expect("write first");
std::fs::write(&second, b"b").expect("write second");
let a = path_identity(&first).expect("identity a");
let b = path_identity(&second).expect("identity b");
if a.is_some() {
assert_ne!(a, b);
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn an_exclusive_lock_excludes_a_second_holder_until_released() {
let dir = std::env::temp_dir().join(format!("rp-fs-lock-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("create dir");
let path = dir.join("guard.lock");
let first = open_lock_file(&path).expect("open first");
let second = open_lock_file(&path).expect("open second");
let first_lock = try_lock_exclusive(&first).expect("first acquires");
let conflict = try_lock_exclusive(&second).expect_err("second must be refused");
assert!(
is_lock_conflict(&conflict),
"refusal must classify as a conflict, got {conflict:?}"
);
drop(first_lock);
let second_lock = try_lock_exclusive(&second).expect("second acquires after release");
drop(second_lock);
drop((first, second));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_lock_does_not_obstruct_a_process_that_never_locked() {
use std::io::{Read as _, Write as _};
let dir = std::env::temp_dir().join(format!("rp-fs-advisory-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("create dir");
let path = dir.join("holder.lock");
std::fs::write(&path, b"00000").expect("seed contents");
let held = open_lock_file(&path).expect("open holder");
let exclusive = lock_exclusive(&held).expect("holder takes it exclusively");
let mut contents = Vec::new();
std::fs::File::open(&path)
.expect("a non-participant can open it")
.read_to_end(&mut contents)
.expect("a non-participant can read it");
assert_eq!(contents, b"00000");
drop(exclusive);
let shared = lock_shared(&held).expect("holder takes it shared");
std::fs::OpenOptions::new()
.write(true)
.truncate(false)
.open(&path)
.expect("a non-participant can open it for writing")
.write_all(b"11111")
.expect("a non-participant can write it");
drop(shared);
assert_eq!(std::fs::read(&path).expect("read back"), b"11111");
drop(held);
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(windows)]
#[test]
fn one_handle_cannot_upgrade_its_own_shared_lock() {
let dir = std::env::temp_dir().join(format!("rp-fs-upgrade-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("create dir");
let path = dir.join("upgrade.lock");
let file = open_lock_file(&path).expect("open");
let shared = lock_shared(&file).expect("take it shared");
let conflict = try_lock_exclusive(&file).expect_err("an upgrade must be refused");
assert!(
is_lock_conflict(&conflict),
"refusal must classify as a conflict, got {conflict:?}"
);
drop(shared);
drop(file);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn an_unrelated_error_is_not_a_lock_conflict() {
let missing = std::env::temp_dir().join("rp-fs-lock-no-such-file");
let _ = std::fs::remove_file(&missing);
let error = std::fs::File::open(&missing).expect_err("must not exist");
assert!(!is_lock_conflict(&error));
}
#[test]
fn a_path_survives_encoding_and_decoding_unchanged() {
for original in [
std::path::PathBuf::from("relative/leaf.log"),
std::env::temp_dir()
.join("rp path with spaces")
.join("t.log"),
std::env::current_exe().expect("current image"),
] {
let decoded =
decode_path_bytes(&encode_path_bytes(&original)).expect("decode what we encoded");
assert_eq!(decoded, original);
}
}
#[test]
fn an_empty_path_round_trips_as_empty() {
let empty = std::path::PathBuf::new();
assert!(encode_path_bytes(&empty).is_empty());
assert_eq!(
decode_path_bytes(&encode_path_bytes(&empty)).expect("decode empty"),
empty
);
}
#[test]
fn a_file_is_replaced_whether_or_not_the_target_exists() {
let dir = std::env::temp_dir().join(format!("rp-fs-replace-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("create dir");
let target = dir.join("manifest");
let first = dir.join("first.tmp");
std::fs::write(&first, b"first").expect("write first");
replace_file(&first, &target).expect("replace absent target");
assert_eq!(std::fs::read(&target).expect("read"), b"first");
let second = dir.join("second.tmp");
std::fs::write(&second, b"second").expect("write second");
replace_file(&second, &target).expect("replace existing target");
assert_eq!(std::fs::read(&target).expect("read"), b"second");
assert!(!first.exists());
assert!(!second.exists());
sync_directory(&dir).expect("sync the directory that records it");
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(windows)]
#[test]
fn a_first_write_and_an_overwrite_move_the_same_way() {
let dir = std::env::temp_dir().join(format!("rp-fs-replace-win-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("create dir");
let target = dir.join("manifest");
for contents in [&b"first"[..], &b"second"[..]] {
let tmp = dir.join("staged.tmp");
std::fs::write(&tmp, contents).expect("stage");
let staged = path_identity(&tmp).expect("identity of the staged file");
replace_file(&tmp, &target).expect("replace");
assert_eq!(std::fs::read(&target).expect("read"), contents);
assert!(!tmp.exists(), "the staged path is consumed by the move");
if staged.is_some() {
assert_eq!(
path_identity(&target).expect("identity of the target"),
staged,
"the target must be the staged file itself, moved"
);
}
}
sync_directory(&dir).expect("sync the directory that records it");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn syncing_a_directory_that_does_not_exist_is_an_error() {
let missing = std::env::temp_dir()
.join(format!("rp-fs-no-such-dir-{}", std::process::id()))
.join("nested");
let _ = std::fs::remove_dir_all(&missing);
sync_directory(&missing).expect_err("a missing directory cannot be synced");
}
#[test]
fn shared_data_is_its_own_role() {
let data = user_data_dir(PRODUCT);
assert!(data.is_absolute());
assert!(data.to_string_lossy().contains(PRODUCT));
}
#[test]
fn a_private_file_is_created_once_and_refuses_to_reopen() {
let dir = std::env::temp_dir().join(format!("rp-fs-private-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("create dir");
let path = dir.join("artifact.json");
let _ = std::fs::remove_file(&path);
{
let mut file = create_private_file(&path).expect("create private file");
use std::io::Write as _;
file.write_all(b"payload").expect("write");
}
assert_eq!(std::fs::read(&path).expect("read back"), b"payload");
let second = create_private_file(&path).expect_err("must not open over an existing file");
assert_eq!(second.kind(), std::io::ErrorKind::AlreadyExists);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn roles_are_stable_across_calls() {
assert_eq!(user_runtime_dir(PRODUCT), user_runtime_dir(PRODUCT));
assert_eq!(user_state_dir(PRODUCT), user_state_dir(PRODUCT));
assert_eq!(user_run_data_root(PRODUCT), user_run_data_root(PRODUCT));
}
#[test]
fn shared_locks_coexist_but_exclude_an_exclusive_request() {
let dir = std::env::temp_dir().join(format!("rp-fs-lock-shared-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("create dir");
let path = dir.join("guard.lock");
let first = open_lock_file(&path).expect("open first");
let second = open_lock_file(&path).expect("open second");
let third = open_lock_file(&path).expect("open third");
let first_shared = try_lock_shared(&first).expect("first shared holder admitted");
let second_shared = try_lock_shared(&second).expect("second shared holder admitted");
let conflict =
try_lock_exclusive(&third).expect_err("exclusive must be refused while shared holds");
assert!(is_lock_conflict(&conflict));
drop(first_shared);
let conflict = try_lock_exclusive(&third)
.expect_err("exclusive must still be refused with one shared holder left");
assert!(is_lock_conflict(&conflict));
drop(second_shared);
let exclusive =
try_lock_exclusive(&third).expect("exclusive admitted once shared holders release");
drop(exclusive);
drop((first, second, third));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn blocking_lock_acquires_once_the_holder_releases() {
let dir = std::env::temp_dir().join(format!("rp-fs-lock-blocking-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("create dir");
let path = dir.join("guard.lock");
let first = open_lock_file(&path).expect("open first");
let second = open_lock_file(&path).expect("open second");
let held = try_lock_exclusive(&first).expect("first acquires");
drop(held);
let acquired = lock_exclusive(&second).expect("blocking lock acquires");
drop(acquired);
drop((first, second));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_unix_time_survives_the_round_trip_through_a_file() {
let dir = std::env::temp_dir().join(format!("rp-fs-mtime-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("create dir");
let path = dir.join("stamped");
std::fs::write(&path, b"payload").expect("write");
let stamped = FileTime::from_unix_time(1_700_000_000, 500_000_000);
set_file_mtime(&path, stamped).expect("set mtime");
let metadata = std::fs::metadata(&path).expect("read metadata back");
let read_back = FileTime::from_last_modification_time(&metadata);
assert_eq!(read_back, stamped);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_unix_time_survives_the_round_trip_through_a_directory() {
let dir = std::env::temp_dir().join(format!("rp-fs-dir-mtime-{}", std::process::id()));
let stamped_dir = dir.join("nested");
std::fs::create_dir_all(&stamped_dir).expect("create dir");
let stamped = FileTime::from_unix_time(1_600_000_000, 250_000_000);
set_file_mtime(&stamped_dir, stamped).expect("set directory mtime");
let metadata = std::fs::metadata(&stamped_dir).expect("read metadata back");
assert_eq!(FileTime::from_last_modification_time(&metadata), stamped);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn from_system_time_and_from_metadata_agree() {
let dir = std::env::temp_dir().join(format!("rp-fs-mtime-agree-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("create dir");
let path = dir.join("stamped");
std::fs::write(&path, b"payload").expect("write");
let metadata = std::fs::metadata(&path).expect("read metadata");
let modified = metadata.modified().expect("host supports mtime");
assert_eq!(
FileTime::from_last_modification_time(&metadata),
FileTime::from_system_time(modified)
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_time_before_the_unix_epoch_round_trips() {
let before_epoch = SystemTime::UNIX_EPOCH - std::time::Duration::new(3600, 250_000_000);
let converted = FileTime::from_system_time(before_epoch);
assert_eq!(converted, FileTime::from_unix_time(-3601, 750_000_000));
}
#[test]
fn now_reports_a_recent_time() {
let now = FileTime::now();
assert!(now > FileTime::from_unix_time(1_577_836_800, 0));
}
#[test]
fn copy_file_reproduces_the_source_and_reports_its_strategy() {
let dir = std::env::temp_dir().join(format!("rp-fs-copy-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("create dir");
let source = dir.join("source");
let destination = dir.join("destination");
let content = b"reflink or copy, the bytes must match";
std::fs::write(&source, content).expect("write source");
let outcome = copy_file(&source, &destination).expect("copy succeeds");
assert_eq!(
std::fs::read(&destination).expect("read destination"),
content
);
match outcome {
CopyOutcome::Reflinked => {}
CopyOutcome::Copied { bytes } => assert_eq!(bytes, content.len() as u64),
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn reflink_file_either_reflinks_or_reports_that_it_could_not() {
let dir = std::env::temp_dir().join(format!("rp-fs-reflink-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("create dir");
let source = dir.join("source");
let destination = dir.join("destination");
let content = b"a reflink is not a copy";
std::fs::write(&source, content).expect("write source");
match reflink_file(&source, &destination) {
Ok(()) => assert_eq!(
std::fs::read(&destination).expect("read destination"),
content,
"a successful reflink must expose the source's bytes"
),
Err(_) => assert!(
!destination.exists(),
"a failed reflink must not leave a destination behind"
),
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn an_owned_lock_excludes_a_second_holder_until_released() {
let dir = std::env::temp_dir().join(format!("rp-fs-owned-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("create dir");
let path = dir.join("lockfile");
std::fs::write(&path, b"").expect("create lock file");
let open = || std::fs::File::open(&path).expect("open lock file");
{
let held = lock_exclusive_owned(open()).expect("first holder takes the lock");
let conflict =
try_lock_exclusive(&open()).expect_err("a second holder must be refused");
assert!(is_lock_conflict(&conflict));
drop(held);
}
drop(try_lock_exclusive(&open()).expect("the lock is free once the owner drops"));
let held = lock_exclusive_owned(open()).expect("retake the lock");
assert!(is_lock_conflict(
&try_lock_exclusive(&open()).expect_err("still exclusive")
));
let returned = held.unlock().expect("unlock returns the handle");
assert!(
returned.metadata().is_ok(),
"the handle must still be usable after unlocking"
);
drop(try_lock_exclusive(&open()).expect("the lock is free after unlock"));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn an_empty_pattern_set_is_empty_and_matches_nothing() {
let empty = PatternSetBuilder::new().build().expect("empty set builds");
assert!(empty.is_empty());
assert_eq!(empty.len(), 0);
assert!(!empty.is_match("anything"));
assert!(!empty.is_match(Path::new("anything")));
let populated = PatternSetBuilder::new()
.add_pattern("*.rs")
.add_pattern("src/**")
.build()
.expect("set builds");
assert!(!populated.is_empty());
assert_eq!(populated.len(), 2);
assert!(populated.is_match("main.rs"));
assert!(populated.is_match(String::from("main.rs")));
assert!(populated.is_match(Path::new("main.rs")));
assert!(!populated.is_match("main.txt"));
}
#[test]
fn a_file_time_round_trips_through_its_accessors() {
for (seconds, nanoseconds) in [
(0_i64, 0_u32),
(1_700_000_000, 123_456_789),
(-1, 999_999_999),
(-86_400, 1),
] {
let time = FileTime::from_unix_time(seconds, nanoseconds);
assert_eq!(time.unix_seconds(), seconds);
assert_eq!(time.nanoseconds(), nanoseconds);
assert_eq!(
FileTime::from_unix_time(time.unix_seconds(), time.nanoseconds()),
time,
"reconstructing from the accessors must yield the same value"
);
}
}
#[test]
fn a_written_mtime_reads_back_through_the_accessors() {
let dir = std::env::temp_dir().join(format!("rp-fs-mtime-acc-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("create dir");
let path = dir.join("stamped");
std::fs::write(&path, b"x").expect("write file");
let written = FileTime::from_unix_time(1_600_000_000, 500_000_000);
set_file_mtime(&path, written).expect("set mtime");
let metadata = std::fs::metadata(&path).expect("stat");
let read_back = FileTime::from_last_modification_time(&metadata);
assert_eq!(read_back.unix_seconds(), written.unix_seconds());
assert_eq!(read_back.nanoseconds(), written.nanoseconds());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_walk_finds_files_and_honours_pruning() {
let dir = std::env::temp_dir().join(format!("rp-fs-walk-{}", std::process::id()));
let pruned = dir.join("pruned");
let kept = dir.join("kept");
std::fs::create_dir_all(&pruned).expect("create pruned dir");
std::fs::create_dir_all(&kept).expect("create kept dir");
std::fs::write(pruned.join("secret"), b"never seen").expect("write pruned file");
std::fs::write(kept.join("visible"), b"seen").expect("write kept file");
std::fs::write(dir.join("root_file"), b"seen too").expect("write root file");
let entries: Vec<DirectoryEntry> = DirectoryWalk::new(dir.clone())
.prune_directories(|path| {
path.file_name().and_then(|name| name.to_str()) != Some("pruned")
})
.walk()
.collect::<io::Result<Vec<_>>>()
.expect("walk succeeds");
let file_paths: Vec<&Path> = entries
.iter()
.filter(|entry| entry.is_file())
.map(DirectoryEntry::path)
.collect();
assert!(file_paths.contains(&kept.join("visible").as_path()));
assert!(file_paths.contains(&dir.join("root_file").as_path()));
assert!(
!file_paths.contains(&pruned.join("secret").as_path()),
"a pruned directory's contents must never be yielded"
);
assert!(
entries.iter().all(|entry| entry.path() != pruned.as_path()),
"a pruned directory itself must not be yielded either"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_pattern_set_matches_any_included_pattern() {
let set = PatternSetBuilder::new()
.add_pattern("*.rs")
.add_pattern("Cargo.toml")
.build()
.expect("valid patterns compile");
assert!(set.is_match(Path::new("src/lib.rs")));
assert!(set.is_match(Path::new("Cargo.toml")));
assert!(!set.is_match(Path::new("README.md")));
}
#[test]
fn an_invalid_pattern_is_rejected_at_build() {
let error = PatternSetBuilder::new()
.add_pattern("[unterminated")
.build()
.expect_err("malformed glob syntax must be rejected");
assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
}
}