use std::path::{Path, PathBuf};
use crate::traits::{DirEntry, EntryKind, Filesystem, FsError, FsMetadata, WritableFilesystem};
#[derive(Debug, Default, Clone, Copy)]
pub struct StdFilesystem;
impl StdFilesystem {
#[must_use]
pub const fn new() -> Self {
Self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct StdCanonicalPath(PathBuf);
impl StdCanonicalPath {
#[must_use]
pub fn as_path(&self) -> &Path {
&self.0
}
#[must_use]
pub fn into_path_buf(self) -> PathBuf {
self.0
}
}
impl AsRef<Path> for StdCanonicalPath {
fn as_ref(&self) -> &Path {
&self.0
}
}
impl std::fmt::Display for StdCanonicalPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0.display(), f)
}
}
fn require_absolute(path: &Path) -> Result<(), FsError> {
if path.is_absolute() {
Ok(())
} else {
Err(FsError::NotAbsolute {
path: path.to_path_buf(),
})
}
}
fn map_io(path: &Path, e: std::io::Error) -> FsError {
if is_symlink_loop(&e) {
return FsError::SymlinkLoop {
path: path.to_path_buf(),
};
}
FsError::from_io(path.to_path_buf(), e)
}
#[cfg(unix)]
fn is_symlink_loop(e: &std::io::Error) -> bool {
e.raw_os_error() == Some(libc::ELOOP)
}
#[cfg(unix)]
fn is_is_a_directory(e: &std::io::Error) -> bool {
e.raw_os_error() == Some(libc::EISDIR)
}
#[cfg(unix)]
fn is_not_a_directory(e: &std::io::Error) -> bool {
e.raw_os_error() == Some(libc::ENOTDIR)
}
#[cfg(windows)]
fn is_symlink_loop(e: &std::io::Error) -> bool {
const ERROR_CANT_RESOLVE_FILENAME: i32 = 1921;
e.raw_os_error() == Some(ERROR_CANT_RESOLVE_FILENAME)
}
#[cfg(windows)]
fn is_is_a_directory(_e: &std::io::Error) -> bool {
false
}
#[cfg(windows)]
fn is_not_a_directory(e: &std::io::Error) -> bool {
const ERROR_DIRECTORY: i32 = 267;
e.raw_os_error() == Some(ERROR_DIRECTORY)
}
#[cfg(not(any(unix, windows)))]
fn is_symlink_loop(_e: &std::io::Error) -> bool {
false
}
#[cfg(not(any(unix, windows)))]
fn is_is_a_directory(_e: &std::io::Error) -> bool {
false
}
#[cfg(not(any(unix, windows)))]
fn is_not_a_directory(_e: &std::io::Error) -> bool {
false
}
fn metadata_from_std(path: &Path, m: &std::fs::Metadata) -> Result<FsMetadata, FsError> {
Ok(FsMetadata {
kind: kind_from_file_type(path, m.file_type())?,
size: m.len(),
})
}
#[cfg(unix)]
fn kind_from_file_type(path: &Path, ft: std::fs::FileType) -> Result<EntryKind, FsError> {
use std::os::unix::fs::FileTypeExt;
if ft.is_dir() {
Ok(EntryKind::Dir)
} else if ft.is_file() {
Ok(EntryKind::File)
} else if ft.is_symlink() {
Ok(EntryKind::Symlink)
} else if ft.is_block_device() {
Ok(EntryKind::BlockDevice)
} else if ft.is_char_device() {
Ok(EntryKind::CharDevice)
} else if ft.is_fifo() {
Ok(EntryKind::Fifo)
} else if ft.is_socket() {
Ok(EntryKind::Socket)
} else {
Err(FsError::UnknownEntryKind {
path: path.to_path_buf(),
})
}
}
#[cfg(not(unix))]
fn kind_from_file_type(path: &Path, ft: std::fs::FileType) -> Result<EntryKind, FsError> {
if ft.is_dir() {
Ok(EntryKind::Dir)
} else if ft.is_file() {
Ok(EntryKind::File)
} else if ft.is_symlink() {
Ok(EntryKind::Symlink)
} else {
Err(FsError::UnknownEntryKind {
path: path.to_path_buf(),
})
}
}
impl Filesystem for StdFilesystem {
type CanonicalPath = StdCanonicalPath;
fn metadata(&self, path: &Path) -> Result<FsMetadata, FsError> {
require_absolute(path)?;
let m = std::fs::metadata(path).map_err(|e| map_io(path, e))?;
metadata_from_std(path, &m)
}
fn symlink_metadata(&self, path: &Path) -> Result<FsMetadata, FsError> {
require_absolute(path)?;
let m = std::fs::symlink_metadata(path).map_err(|e| map_io(path, e))?;
metadata_from_std(path, &m)
}
fn read_dir(&self, path: &Path) -> Result<Vec<DirEntry>, FsError> {
require_absolute(path)?;
let iter = std::fs::read_dir(path).map_err(|e| {
if is_not_a_directory(&e) {
FsError::NotADirectory {
path: path.to_path_buf(),
}
} else {
map_io(path, e)
}
})?;
let mut out = Vec::new();
for entry in iter {
let entry = entry.map_err(|e| map_io(path, e))?;
let entry_path = entry.path();
let ft = entry.file_type().map_err(|e| map_io(&entry_path, e))?;
let kind = kind_from_file_type(&entry_path, ft)?;
let m = entry.metadata().map_err(|e| map_io(&entry_path, e))?;
out.push(DirEntry {
path: entry_path,
metadata: FsMetadata {
kind,
size: m.len(),
},
});
}
Ok(out)
}
fn read(&self, path: &Path) -> Result<Vec<u8>, FsError> {
let m = self.metadata(path)?;
if m.kind != EntryKind::File {
return Err(FsError::NotAFile {
path: path.to_path_buf(),
});
}
std::fs::read(path).map_err(|e| {
if is_is_a_directory(&e) {
FsError::NotAFile {
path: path.to_path_buf(),
}
} else {
map_io(path, e)
}
})
}
fn permissions(&self, path: &Path) -> Result<u32, FsError> {
let m = self.metadata(path)?;
if m.kind != EntryKind::File {
return Err(FsError::NotAFile {
path: path.to_path_buf(),
});
}
let m_std = std::fs::metadata(path).map_err(|e| map_io(path, e))?;
Ok(permissions_impl(&m_std))
}
fn canonicalize(&self, path: &Path) -> Result<Self::CanonicalPath, FsError> {
require_absolute(path)?;
std::fs::canonicalize(path)
.map(StdCanonicalPath)
.map_err(|e| map_io(path, e))
}
fn read_link(&self, path: &Path) -> Result<PathBuf, FsError> {
require_absolute(path)?;
match std::fs::read_link(path) {
Ok(p) => Ok(p),
Err(e) if e.kind() == std::io::ErrorKind::InvalidInput => Err(FsError::NotASymlink {
path: path.to_path_buf(),
}),
Err(e) => Err(map_io(path, e)),
}
}
}
impl WritableFilesystem for StdFilesystem {
fn create_dir_all(&self, path: &Path) -> Result<(), FsError> {
require_absolute(path)?;
std::fs::create_dir_all(path).map_err(|e| {
if is_not_a_directory(&e) {
FsError::NotADirectory {
path: path.to_path_buf(),
}
} else {
map_io(path, e)
}
})
}
fn write_file(&self, path: &Path, contents: &[u8]) -> Result<(), FsError> {
require_absolute(path)?;
std::fs::write(path, contents).map_err(|e| {
if is_not_a_directory(&e) {
FsError::NotADirectory {
path: path.to_path_buf(),
}
} else {
map_io(path, e)
}
})
}
fn rename(&self, from: &Path, to: &Path) -> Result<(), FsError> {
require_absolute(from)?;
require_absolute(to)?;
std::fs::rename(from, to).map_err(|e| map_io(from, e))
}
fn remove_dir_all(&self, path: &Path) -> Result<(), FsError> {
require_absolute(path)?;
std::fs::remove_dir_all(path).map_err(|e| map_io(path, e))
}
fn set_permissions(&self, path: &Path, mode: u32) -> Result<(), FsError> {
require_absolute(path)?;
set_permissions_impl(path, mode)
}
fn fsync_file(&self, path: &Path) -> Result<(), FsError> {
require_absolute(path)?;
let f = std::fs::File::open(path).map_err(|e| map_io(path, e))?;
f.sync_all().map_err(|e| map_io(path, e))
}
fn fsync_dir(&self, path: &Path) -> Result<(), FsError> {
require_absolute(path)?;
fsync_dir_impl(path)
}
}
#[cfg(unix)]
fn set_permissions_impl(path: &Path, mode: u32) -> Result<(), FsError> {
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(mode);
std::fs::set_permissions(path, perms).map_err(|e| map_io(path, e))
}
#[cfg(not(unix))]
fn set_permissions_impl(path: &Path, mode: u32) -> Result<(), FsError> {
let m = std::fs::metadata(path).map_err(|e| map_io(path, e))?;
let mut perms = m.permissions();
let owner_writable = (mode & 0o200) != 0;
perms.set_readonly(!owner_writable);
std::fs::set_permissions(path, perms).map_err(|e| map_io(path, e))
}
#[cfg(unix)]
fn permissions_impl(m: &std::fs::Metadata) -> u32 {
use std::os::unix::fs::PermissionsExt;
m.permissions().mode() & 0o7777
}
#[cfg(not(unix))]
fn permissions_impl(m: &std::fs::Metadata) -> u32 {
if m.permissions().readonly() {
0o444
} else {
0o644
}
}
#[cfg(unix)]
fn fsync_dir_impl(path: &Path) -> Result<(), FsError> {
let f = std::fs::File::open(path).map_err(|e| map_io(path, e))?;
f.sync_all().map_err(|e| map_io(path, e))
}
#[cfg(not(unix))]
fn fsync_dir_impl(path: &Path) -> Result<(), FsError> {
let m = std::fs::metadata(path).map_err(|e| map_io(path, e))?;
if !m.is_dir() {
return Err(FsError::NotADirectory {
path: path.to_path_buf(),
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::fs;
use std::os::unix::fs::symlink;
use tempfile::TempDir;
use crate::std_impl::StdFilesystem;
use crate::traits::{EntryKind, Filesystem, FsError};
fn td() -> TempDir {
tempfile::tempdir().expect("create tempdir")
}
#[test]
fn rejects_relative_paths() {
let fs_ = StdFilesystem::new();
let err = fs_.metadata(std::path::Path::new("relative")).unwrap_err();
assert!(matches!(err, FsError::NotAbsolute { .. }));
}
#[test]
fn metadata_follows_symlinks() {
let dir = td();
let target = dir.path().join("target.txt");
fs::write(&target, "hi").unwrap();
let link = dir.path().join("link.txt");
symlink(&target, &link).unwrap();
let fs_ = StdFilesystem::new();
let m = fs_.metadata(&link).unwrap();
assert_eq!(m.kind, EntryKind::File);
}
#[test]
fn symlink_metadata_does_not_follow() {
let dir = td();
let target = dir.path().join("target.txt");
fs::write(&target, "hi").unwrap();
let link = dir.path().join("link.txt");
symlink(&target, &link).unwrap();
let fs_ = StdFilesystem::new();
let m = fs_.symlink_metadata(&link).unwrap();
assert_eq!(m.kind, EntryKind::Symlink);
}
#[test]
fn read_follows_symlinks() {
let dir = td();
let target = dir.path().join("data.txt");
fs::write(&target, b"hello").unwrap();
let link = dir.path().join("link.txt");
symlink(&target, &link).unwrap();
let fs_ = StdFilesystem::new();
let contents = fs_.read(&link).unwrap();
assert_eq!(contents, b"hello");
}
#[test]
fn read_dir_classifies_entries() {
let dir = td();
fs::write(dir.path().join("a.txt"), "").unwrap();
fs::create_dir(dir.path().join("b")).unwrap();
symlink(dir.path().join("a.txt"), dir.path().join("c")).unwrap();
let fs_ = StdFilesystem::new();
let mut entries = fs_.read_dir(dir.path()).unwrap();
entries.sort_by(|x, y| x.path.cmp(&y.path));
assert_eq!(entries.len(), 3);
let by_name: std::collections::BTreeMap<_, _> = entries
.iter()
.map(|e| (e.path.file_name().unwrap().to_owned(), e.metadata.kind))
.collect();
assert_eq!(by_name[std::ffi::OsStr::new("a.txt")], EntryKind::File);
assert_eq!(by_name[std::ffi::OsStr::new("b")], EntryKind::Dir);
assert_eq!(by_name[std::ffi::OsStr::new("c")], EntryKind::Symlink);
}
#[test]
fn canonicalize_detects_symlink_loop() {
let dir = td();
let a = dir.path().join("a");
let b = dir.path().join("b");
symlink(&b, &a).unwrap();
symlink(&a, &b).unwrap();
let fs_ = StdFilesystem::new();
let err = fs_.canonicalize(&a).unwrap_err();
assert!(
matches!(err, FsError::SymlinkLoop { .. }),
"expected SymlinkLoop, got {err:?}"
);
}
#[test]
fn canonicalize_resolves_symlinks() {
let dir = td();
let target = dir.path().join("real");
fs::create_dir(&target).unwrap();
let link = dir.path().join("link");
symlink(&target, &link).unwrap();
let fs_ = StdFilesystem::new();
let canon = fs_.canonicalize(&link).unwrap();
assert_eq!(canon, fs_.canonicalize(&target).unwrap());
}
#[test]
fn read_link_returns_target() {
let dir = td();
let target = dir.path().join("data");
fs::write(&target, "").unwrap();
let link = dir.path().join("link");
symlink(&target, &link).unwrap();
let fs_ = StdFilesystem::new();
let read = fs_.read_link(&link).unwrap();
assert_eq!(read, target);
}
#[test]
fn read_link_on_regular_file_errors() {
let dir = td();
let f = dir.path().join("file");
fs::write(&f, "").unwrap();
let fs_ = StdFilesystem::new();
let err = fs_.read_link(&f).unwrap_err();
assert!(matches!(err, FsError::NotASymlink { .. }));
}
#[test]
fn classifies_fifo() {
let dir = td();
let fifo = dir.path().join("pipe");
let status = std::process::Command::new("mkfifo")
.arg(&fifo)
.status()
.expect("spawn mkfifo");
assert!(status.success(), "mkfifo exited with {status}");
let fs_ = StdFilesystem::new();
let m = fs_.symlink_metadata(&fifo).unwrap();
assert_eq!(m.kind, EntryKind::Fifo);
}
#[test]
fn metadata_not_found() {
let dir = td();
let fs_ = StdFilesystem::new();
let err = fs_.metadata(&dir.path().join("ghost")).unwrap_err();
assert!(matches!(err, FsError::NotFound { .. }));
}
#[test]
fn each_method_rejects_relative_paths() {
let fs_ = StdFilesystem::new();
let p = std::path::Path::new("relative");
assert!(matches!(
fs_.metadata(p).unwrap_err(),
FsError::NotAbsolute { .. }
));
assert!(matches!(
fs_.symlink_metadata(p).unwrap_err(),
FsError::NotAbsolute { .. }
));
assert!(matches!(
fs_.read(p).unwrap_err(),
FsError::NotAbsolute { .. }
));
assert!(matches!(
fs_.read_dir(p).unwrap_err(),
FsError::NotAbsolute { .. }
));
assert!(matches!(
fs_.canonicalize(p).unwrap_err(),
FsError::NotAbsolute { .. }
));
assert!(matches!(
fs_.read_link(p).unwrap_err(),
FsError::NotAbsolute { .. }
));
}
#[test]
fn each_method_propagates_not_found() {
let dir = td();
let fs_ = StdFilesystem::new();
let p = dir.path().join("ghost");
assert!(matches!(
fs_.metadata(&p).unwrap_err(),
FsError::NotFound { .. }
));
assert!(matches!(
fs_.symlink_metadata(&p).unwrap_err(),
FsError::NotFound { .. }
));
assert!(matches!(
fs_.read(&p).unwrap_err(),
FsError::NotFound { .. }
));
assert!(matches!(
fs_.read_dir(&p).unwrap_err(),
FsError::NotFound { .. }
));
assert!(matches!(
fs_.canonicalize(&p).unwrap_err(),
FsError::NotFound { .. }
));
assert!(matches!(
fs_.read_link(&p).unwrap_err(),
FsError::NotFound { .. }
));
}
#[test]
#[cfg(unix)]
fn read_on_directory_errors_not_a_file() {
let dir = td();
let fs_ = StdFilesystem::new();
let err = fs_.read(dir.path()).unwrap_err();
assert!(
matches!(err, FsError::NotAFile { .. }),
"expected NotAFile, got {err:?}"
);
}
#[test]
fn read_dir_on_file_errors_not_a_directory() {
let dir = td();
let f = dir.path().join("file");
fs::write(&f, "").unwrap();
let fs_ = StdFilesystem::new();
let err = fs_.read_dir(&f).unwrap_err();
assert!(
matches!(err, FsError::NotADirectory { .. }),
"expected NotADirectory, got {err:?}"
);
}
#[test]
#[cfg(unix)]
fn permissions_round_trips_with_set_permissions() {
use std::os::unix::fs::PermissionsExt;
let dir = td();
let f = dir.path().join("file");
fs::write(&f, b"").unwrap();
let fs_ = StdFilesystem::new();
for mode in [0o600u32, 0o644, 0o755, 0o700] {
fs::set_permissions(&f, std::fs::Permissions::from_mode(mode)).unwrap();
let got = fs_.permissions(&f).unwrap();
assert_eq!(got, mode, "round-trip {mode:o}");
}
}
#[test]
#[cfg(unix)]
fn permissions_follows_symlinks() {
use std::os::unix::fs::PermissionsExt;
let dir = td();
let target = dir.path().join("target");
fs::write(&target, b"").unwrap();
fs::set_permissions(&target, std::fs::Permissions::from_mode(0o600)).unwrap();
let link = dir.path().join("link");
symlink(&target, &link).unwrap();
let fs_ = StdFilesystem::new();
let mode = fs_.permissions(&link).unwrap();
assert_eq!(mode, 0o600);
}
#[test]
fn permissions_on_directory_errors_not_a_file() {
let dir = td();
let fs_ = StdFilesystem::new();
let err = fs_.permissions(dir.path()).unwrap_err();
assert!(
matches!(err, FsError::NotAFile { .. }),
"expected NotAFile, got {err:?}",
);
}
#[test]
fn permissions_on_missing_errors_not_found() {
let dir = td();
let fs_ = StdFilesystem::new();
let err = fs_.permissions(&dir.path().join("ghost")).unwrap_err();
assert!(matches!(err, FsError::NotFound { .. }));
}
#[test]
fn permissions_rejects_relative_path() {
let fs_ = StdFilesystem::new();
let err = fs_
.permissions(std::path::Path::new("relative"))
.unwrap_err();
assert!(matches!(err, FsError::NotAbsolute { .. }));
}
#[test]
fn read_link_on_directory_errors_not_a_symlink() {
let dir = td();
let fs_ = StdFilesystem::new();
let err = fs_.read_link(dir.path()).unwrap_err();
assert!(matches!(err, FsError::NotASymlink { .. }));
}
#[test]
fn read_link_returns_relative_target_verbatim() {
let dir = td();
let target_rel = std::path::PathBuf::from("../foo/bar");
let link = dir.path().join("link");
symlink(&target_rel, &link).unwrap();
let fs_ = StdFilesystem::new();
let got = fs_.read_link(&link).unwrap();
assert_eq!(got, target_rel);
}
#[test]
fn broken_symlink_metadata_errors_not_found() {
let dir = td();
let link = dir.path().join("link");
symlink(dir.path().join("missing"), &link).unwrap();
let fs_ = StdFilesystem::new();
let err = fs_.metadata(&link).unwrap_err();
assert!(matches!(err, FsError::NotFound { .. }));
}
#[test]
fn broken_symlink_symlink_metadata_returns_symlink() {
let dir = td();
let link = dir.path().join("link");
symlink(dir.path().join("missing"), &link).unwrap();
let fs_ = StdFilesystem::new();
let m = fs_.symlink_metadata(&link).unwrap();
assert_eq!(m.kind, EntryKind::Symlink);
}
#[test]
fn broken_symlink_read_link_returns_target() {
let dir = td();
let target = dir.path().join("missing");
let link = dir.path().join("link");
symlink(&target, &link).unwrap();
let fs_ = StdFilesystem::new();
let got = fs_.read_link(&link).unwrap();
assert_eq!(got, target);
}
#[test]
fn canonicalize_self_loop() {
let dir = td();
let a = dir.path().join("self_loop");
symlink(&a, &a).unwrap();
let fs_ = StdFilesystem::new();
let err = fs_.canonicalize(&a).unwrap_err();
assert!(matches!(err, FsError::SymlinkLoop { .. }));
}
#[test]
fn canonicalize_resolves_dotdot_in_symlink_target() {
let dir = td();
let real = dir.path().join("real");
fs::create_dir(&real).unwrap();
let target = real.join("file");
fs::write(&target, "x").unwrap();
let link = dir.path().join("link");
symlink("real/../real/file", &link).unwrap();
let fs_ = StdFilesystem::new();
let canon = fs_.canonicalize(&link).unwrap();
assert_eq!(canon.as_path(), fs::canonicalize(&target).unwrap());
}
#[test]
fn canonicalize_on_regular_file_returns_resolved_path() {
let dir = td();
let f = dir.path().join("file");
fs::write(&f, "").unwrap();
let fs_ = StdFilesystem::new();
let canon = fs_.canonicalize(&f).unwrap();
assert_eq!(canon.as_path(), fs::canonicalize(&f).unwrap());
}
#[cfg(unix)]
fn make_fifo(dir: &TempDir, name: &str) -> std::path::PathBuf {
let p = dir.path().join(name);
let status = std::process::Command::new("mkfifo")
.arg(&p)
.status()
.expect("spawn mkfifo");
assert!(status.success(), "mkfifo exited with {status}");
p
}
#[test]
#[cfg(unix)]
fn classifies_socket() {
let dir = td();
let sock_path = dir.path().join("sock");
let _listener =
std::os::unix::net::UnixListener::bind(&sock_path).expect("bind UnixListener");
let fs_ = StdFilesystem::new();
let m = fs_.symlink_metadata(&sock_path).unwrap();
assert_eq!(m.kind, EntryKind::Socket);
}
#[test]
#[cfg(unix)]
fn classifies_char_device_via_dev_null() {
let dev_null = std::path::Path::new("/dev/null");
if !dev_null.exists() {
return;
}
let fs_ = StdFilesystem::new();
let m = fs_.symlink_metadata(dev_null).unwrap();
assert_eq!(m.kind, EntryKind::CharDevice);
}
#[test]
#[cfg(unix)]
fn read_on_fifo_errors_not_a_file() {
let dir = td();
let fifo = make_fifo(&dir, "pipe");
let fs_ = StdFilesystem::new();
let err = fs_.read(&fifo).unwrap_err();
assert!(
matches!(err, FsError::NotAFile { .. }),
"expected NotAFile, got {err:?}"
);
}
#[test]
#[cfg(unix)]
fn read_on_socket_errors_not_a_file() {
let dir = td();
let sock_path = dir.path().join("sock");
let _listener =
std::os::unix::net::UnixListener::bind(&sock_path).expect("bind UnixListener");
let fs_ = StdFilesystem::new();
let err = fs_.read(&sock_path).unwrap_err();
assert!(
matches!(err, FsError::NotAFile { .. }),
"expected NotAFile, got {err:?}"
);
}
#[test]
#[cfg(unix)]
fn read_on_char_device_errors_not_a_file() {
let dev_null = std::path::Path::new("/dev/null");
if !dev_null.exists() {
return;
}
let fs_ = StdFilesystem::new();
let err = fs_.read(dev_null).unwrap_err();
assert!(
matches!(err, FsError::NotAFile { .. }),
"expected NotAFile, got {err:?}"
);
}
#[test]
#[cfg(unix)]
fn read_dir_on_fifo_errors_not_a_directory() {
let dir = td();
let fifo = make_fifo(&dir, "pipe");
let fs_ = StdFilesystem::new();
let err = fs_.read_dir(&fifo).unwrap_err();
assert!(
matches!(err, FsError::NotADirectory { .. }),
"expected NotADirectory, got {err:?}"
);
}
#[test]
#[cfg(unix)]
fn read_link_on_fifo_errors_not_a_symlink() {
let dir = td();
let fifo = make_fifo(&dir, "pipe");
let fs_ = StdFilesystem::new();
let err = fs_.read_link(&fifo).unwrap_err();
assert!(
matches!(err, FsError::NotASymlink { .. }),
"expected NotASymlink, got {err:?}"
);
}
#[test]
#[cfg(unix)]
fn canonicalize_on_socket_returns_path() {
let dir = td();
let sock_path = dir.path().join("sock");
let _listener =
std::os::unix::net::UnixListener::bind(&sock_path).expect("bind UnixListener");
let fs_ = StdFilesystem::new();
let canon = fs_.canonicalize(&sock_path).unwrap();
assert_eq!(canon.as_path(), fs::canonicalize(&sock_path).unwrap());
}
#[test]
fn read_on_broken_symlink_errors_not_found() {
let dir = td();
let link = dir.path().join("link");
symlink(dir.path().join("missing"), &link).unwrap();
let fs_ = StdFilesystem::new();
let err = fs_.read(&link).unwrap_err();
assert!(matches!(err, FsError::NotFound { .. }));
}
#[test]
fn canonicalize_on_broken_symlink_errors_not_found() {
let dir = td();
let link = dir.path().join("link");
symlink(dir.path().join("missing"), &link).unwrap();
let fs_ = StdFilesystem::new();
let err = fs_.canonicalize(&link).unwrap_err();
assert!(matches!(err, FsError::NotFound { .. }));
}
#[test]
fn read_on_symlink_to_directory_errors_not_a_file() {
let dir = td();
let real = dir.path().join("real");
fs::create_dir(&real).unwrap();
let link = dir.path().join("link_to_d");
symlink(&real, &link).unwrap();
let fs_ = StdFilesystem::new();
let err = fs_.read(&link).unwrap_err();
assert!(matches!(err, FsError::NotAFile { .. }));
}
#[test]
#[cfg(unix)]
fn read_dir_on_socket_errors_not_a_directory() {
let dir = td();
let sock_path = dir.path().join("sock");
let _listener =
std::os::unix::net::UnixListener::bind(&sock_path).expect("bind UnixListener");
let fs_ = StdFilesystem::new();
let err = fs_.read_dir(&sock_path).unwrap_err();
assert!(matches!(err, FsError::NotADirectory { .. }));
}
#[test]
#[cfg(unix)]
fn read_dir_on_char_device_errors_not_a_directory() {
let dev_null = std::path::Path::new("/dev/null");
if !dev_null.exists() {
return;
}
let fs_ = StdFilesystem::new();
let err = fs_.read_dir(dev_null).unwrap_err();
assert!(matches!(err, FsError::NotADirectory { .. }));
}
#[test]
#[cfg(unix)]
fn read_link_on_socket_errors_not_a_symlink() {
let dir = td();
let sock_path = dir.path().join("sock");
let _listener =
std::os::unix::net::UnixListener::bind(&sock_path).expect("bind UnixListener");
let fs_ = StdFilesystem::new();
let err = fs_.read_link(&sock_path).unwrap_err();
assert!(matches!(err, FsError::NotASymlink { .. }));
}
#[test]
#[cfg(unix)]
fn read_link_on_char_device_errors_not_a_symlink() {
let dev_null = std::path::Path::new("/dev/null");
if !dev_null.exists() {
return;
}
let fs_ = StdFilesystem::new();
let err = fs_.read_link(dev_null).unwrap_err();
assert!(matches!(err, FsError::NotASymlink { .. }));
}
#[test]
#[cfg(unix)]
fn canonicalize_on_fifo_returns_path() {
let dir = td();
let fifo = make_fifo(&dir, "pipe");
let fs_ = StdFilesystem::new();
let canon = fs_.canonicalize(&fifo).unwrap();
assert_eq!(canon.as_path(), fs::canonicalize(&fifo).unwrap());
}
#[test]
#[cfg(unix)]
fn canonicalize_on_char_device_returns_path() {
let dev_null = std::path::Path::new("/dev/null");
if !dev_null.exists() {
return;
}
let fs_ = StdFilesystem::new();
let canon = fs_.canonicalize(dev_null).unwrap();
assert_eq!(canon.as_path(), fs::canonicalize(dev_null).unwrap());
}
#[test]
fn multi_hop_symlink_chain_resolves() {
let dir = td();
let target = dir.path().join("target");
fs::write(&target, b"payload").unwrap();
let a = dir.path().join("a");
let b = dir.path().join("b");
let c = dir.path().join("c");
symlink(&target, &a).unwrap();
symlink(&a, &b).unwrap();
symlink(&b, &c).unwrap();
let fs_ = StdFilesystem::new();
assert_eq!(fs_.metadata(&c).unwrap().kind, EntryKind::File);
assert_eq!(fs_.read(&c).unwrap(), b"payload");
assert_eq!(
fs_.canonicalize(&c).unwrap().as_path(),
fs::canonicalize(&target).unwrap()
);
assert_eq!(fs_.read_link(&c).unwrap(), b);
assert_eq!(fs_.symlink_metadata(&c).unwrap().kind, EntryKind::Symlink);
}
#[test]
fn metadata_through_intermediate_symlink() {
let dir = td();
let real = dir.path().join("real");
fs::create_dir(&real).unwrap();
fs::write(real.join("data"), b"x").unwrap();
let link = dir.path().join("link");
symlink(&real, &link).unwrap();
let fs_ = StdFilesystem::new();
let m = fs_.metadata(&link.join("data")).unwrap();
assert_eq!(m.kind, EntryKind::File);
}
#[test]
fn read_through_intermediate_symlink() {
let dir = td();
let real = dir.path().join("real");
fs::create_dir(&real).unwrap();
fs::write(real.join("data"), b"hello").unwrap();
let link = dir.path().join("link");
symlink(&real, &link).unwrap();
let fs_ = StdFilesystem::new();
assert_eq!(fs_.read(&link.join("data")).unwrap(), b"hello");
}
#[test]
fn read_dir_through_intermediate_symlink() {
let dir = td();
let real = dir.path().join("real");
fs::create_dir(&real).unwrap();
fs::write(real.join("a"), b"").unwrap();
fs::write(real.join("b"), b"").unwrap();
let link = dir.path().join("link");
symlink(&real, &link).unwrap();
let fs_ = StdFilesystem::new();
let entries = fs_.read_dir(&link).unwrap();
assert_eq!(entries.len(), 2);
}
#[test]
fn read_on_empty_file_returns_empty_bytes() {
let dir = td();
let f = dir.path().join("empty");
fs::write(&f, b"").unwrap();
let fs_ = StdFilesystem::new();
assert_eq!(fs_.read(&f).unwrap(), b"");
}
#[test]
fn metadata_size_matches_file_byte_length() {
let dir = td();
let f = dir.path().join("data");
fs::write(&f, vec![0u8; 1024]).unwrap();
let fs_ = StdFilesystem::new();
assert_eq!(fs_.metadata(&f).unwrap().size, 1024);
}
#[test]
fn metadata_size_zero_for_empty_file() {
let dir = td();
let f = dir.path().join("empty");
fs::write(&f, b"").unwrap();
let fs_ = StdFilesystem::new();
assert_eq!(fs_.metadata(&f).unwrap().size, 0);
}
#[test]
fn read_dir_entries_carry_file_sizes() {
let dir = td();
fs::write(dir.path().join("a"), vec![0u8; 7]).unwrap();
fs::write(dir.path().join("b"), vec![0u8; 100]).unwrap();
let fs_ = StdFilesystem::new();
let entries = fs_.read_dir(dir.path()).unwrap();
let by_name: std::collections::BTreeMap<_, _> = entries
.into_iter()
.map(|e| (e.path.file_name().unwrap().to_owned(), e.metadata.size))
.collect();
assert_eq!(by_name[std::ffi::OsStr::new("a")], 7);
assert_eq!(by_name[std::ffi::OsStr::new("b")], 100);
}
mod writable {
use std::fs;
use tempfile::TempDir;
use crate::std_impl::StdFilesystem;
use crate::traits::{EntryKind, Filesystem, FsError, WritableFilesystem};
fn td() -> TempDir {
tempfile::tempdir().expect("create tempdir")
}
#[test]
fn create_dir_all_creates_chain() {
let dir = td();
let target = dir.path().join("a/b/c");
let fs_ = StdFilesystem::new();
fs_.create_dir_all(&target).unwrap();
assert_eq!(fs_.metadata(&target).unwrap().kind, EntryKind::Dir);
}
#[test]
fn create_dir_all_is_idempotent() {
let dir = td();
let target = dir.path().join("a/b");
let fs_ = StdFilesystem::new();
fs_.create_dir_all(&target).unwrap();
fs_.create_dir_all(&target).unwrap();
assert_eq!(fs_.metadata(&target).unwrap().kind, EntryKind::Dir);
}
#[test]
fn create_dir_all_rejects_relative() {
let fs_ = StdFilesystem::new();
let err = fs_
.create_dir_all(std::path::Path::new("relative"))
.unwrap_err();
assert!(matches!(err, FsError::NotAbsolute { .. }));
}
#[test]
fn write_file_creates_new_file() {
let dir = td();
let f = dir.path().join("new.txt");
let fs_ = StdFilesystem::new();
fs_.write_file(&f, b"hello").unwrap();
assert_eq!(fs::read(&f).unwrap(), b"hello");
}
#[test]
fn write_file_overwrites_existing() {
let dir = td();
let f = dir.path().join("existing.txt");
fs::write(&f, b"old").unwrap();
let fs_ = StdFilesystem::new();
fs_.write_file(&f, b"new").unwrap();
assert_eq!(fs::read(&f).unwrap(), b"new");
}
#[test]
fn write_file_rejects_relative() {
let fs_ = StdFilesystem::new();
let err = fs_
.write_file(std::path::Path::new("relative"), b"")
.unwrap_err();
assert!(matches!(err, FsError::NotAbsolute { .. }));
}
#[test]
fn write_file_rejects_missing_parent() {
let dir = td();
let f = dir.path().join("missing_parent/file");
let fs_ = StdFilesystem::new();
let err = fs_.write_file(&f, b"").unwrap_err();
assert!(matches!(err, FsError::NotFound { .. }));
}
#[test]
fn rename_moves_file() {
let dir = td();
let from = dir.path().join("from");
let to = dir.path().join("to");
fs::write(&from, b"payload").unwrap();
let fs_ = StdFilesystem::new();
fs_.rename(&from, &to).unwrap();
assert_eq!(fs::read(&to).unwrap(), b"payload");
assert!(!from.exists());
}
#[test]
fn rename_moves_directory() {
let dir = td();
let src = dir.path().join("src");
let dst = dir.path().join("dst");
fs::create_dir(&src).unwrap();
fs::write(src.join("a"), b"a").unwrap();
let fs_ = StdFilesystem::new();
fs_.rename(&src, &dst).unwrap();
assert_eq!(fs::read(dst.join("a")).unwrap(), b"a");
assert!(!src.exists());
}
#[test]
fn rename_missing_source_errors_not_found() {
let dir = td();
let fs_ = StdFilesystem::new();
let err = fs_
.rename(&dir.path().join("ghost"), &dir.path().join("to"))
.unwrap_err();
assert!(matches!(err, FsError::NotFound { .. }));
}
#[test]
fn rename_rejects_relative_paths() {
let dir = td();
let fs_ = StdFilesystem::new();
let err = fs_
.rename(std::path::Path::new("relative"), &dir.path().join("to"))
.unwrap_err();
assert!(matches!(err, FsError::NotAbsolute { .. }));
let err = fs_
.rename(&dir.path().join("from"), std::path::Path::new("relative"))
.unwrap_err();
assert!(matches!(err, FsError::NotAbsolute { .. }));
}
#[test]
fn remove_dir_all_drops_subtree() {
let dir = td();
let sub = dir.path().join("sub");
fs::create_dir(&sub).unwrap();
fs::write(sub.join("a"), b"").unwrap();
fs::create_dir(sub.join("nested")).unwrap();
fs::write(sub.join("nested/b"), b"").unwrap();
let fs_ = StdFilesystem::new();
fs_.remove_dir_all(&sub).unwrap();
assert!(!sub.exists());
}
#[test]
fn remove_dir_all_on_missing_errors_not_found() {
let dir = td();
let fs_ = StdFilesystem::new();
let err = fs_.remove_dir_all(&dir.path().join("ghost")).unwrap_err();
assert!(matches!(err, FsError::NotFound { .. }));
}
#[test]
#[cfg(unix)]
fn set_permissions_changes_mode() {
use std::os::unix::fs::PermissionsExt;
let dir = td();
let f = dir.path().join("file");
fs::write(&f, b"").unwrap();
let fs_ = StdFilesystem::new();
fs_.set_permissions(&f, 0o755).unwrap();
let mode = fs::metadata(&f).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o755);
}
#[test]
fn set_permissions_on_missing_errors_not_found() {
let dir = td();
let fs_ = StdFilesystem::new();
let err = fs_
.set_permissions(&dir.path().join("ghost"), 0o644)
.unwrap_err();
assert!(matches!(err, FsError::NotFound { .. }));
}
#[test]
fn fsync_file_succeeds_on_real_file() {
let dir = td();
let f = dir.path().join("f");
fs::write(&f, b"").unwrap();
let fs_ = StdFilesystem::new();
fs_.fsync_file(&f).unwrap();
}
#[test]
fn fsync_file_on_missing_errors_not_found() {
let dir = td();
let fs_ = StdFilesystem::new();
let err = fs_.fsync_file(&dir.path().join("ghost")).unwrap_err();
assert!(matches!(err, FsError::NotFound { .. }));
}
#[test]
#[cfg(unix)]
fn fsync_dir_succeeds_on_real_directory() {
let dir = td();
let fs_ = StdFilesystem::new();
fs_.fsync_dir(dir.path()).unwrap();
}
#[test]
fn fsync_dir_on_missing_errors_not_found() {
let dir = td();
let fs_ = StdFilesystem::new();
let err = fs_.fsync_dir(&dir.path().join("ghost")).unwrap_err();
assert!(matches!(err, FsError::NotFound { .. }));
}
#[test]
fn two_phase_store_pattern_round_trips() {
let dir = td();
let shard = dir.path().join("shard");
let tmp = shard.join(".tmp-abc");
let final_entry = shard.join("abc");
let fs_ = StdFilesystem::new();
fs_.create_dir_all(&tmp.join("outputs")).unwrap();
fs_.write_file(&tmp.join("stdout"), b"out").unwrap();
fs_.write_file(&tmp.join("stderr"), b"err").unwrap();
fs_.write_file(&tmp.join("outputs/deadbeef"), b"blob")
.unwrap();
fs_.fsync_file(&tmp.join("stdout")).unwrap();
fs_.fsync_file(&tmp.join("stderr")).unwrap();
fs_.fsync_file(&tmp.join("outputs/deadbeef")).unwrap();
fs_.write_file(&tmp.join("manifest.json"), b"{}").unwrap();
fs_.fsync_file(&tmp.join("manifest.json")).unwrap();
fs_.rename(&tmp, &final_entry).unwrap();
fs_.fsync_dir(&shard).unwrap();
assert_eq!(fs::read(final_entry.join("stdout")).unwrap(), b"out");
assert_eq!(fs::read(final_entry.join("manifest.json")).unwrap(), b"{}");
assert!(!tmp.exists());
}
#[test]
fn crash_before_rename_leaves_entry_invisible() {
let dir = td();
let shard = dir.path().join("shard");
let tmp = shard.join(".tmp-abc");
let final_entry = shard.join("abc");
let fs_ = StdFilesystem::new();
fs_.create_dir_all(&tmp).unwrap();
fs_.write_file(&tmp.join("manifest.json"), b"{}").unwrap();
assert!(!final_entry.exists());
}
}
}