use std::borrow::Cow;
use std::fs::File;
use std::fs::OpenOptions;
use std::io::Read;
use std::io::Seek;
use std::io::Write;
use std::path::Component;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Mutex;
use std::time::SystemTime;
use crate::Error;
use crate::Result;
use crate::env::SharedEnv;
macro_rules! fs_error {
($kind:ident; $($arg:tt)*) => {
$crate::Error::fs(format!($($arg)*))
.with_source(std::io::Error::from(std::io::ErrorKind::$kind))
};
($kind:expr => $($arg:tt)*) => {
$crate::Error::fs(format!($($arg)*)).with_source(std::io::Error::from($kind))
};
($($arg:tt)*) => {
$crate::Error::fs(format!($($arg)*))
};
}
#[cfg(unix)]
mod unix;
#[cfg(windows)]
mod windows;
#[cfg(unix)]
pub(crate) use unix::UnixAttributes;
#[cfg(unix)]
pub(crate) use unix::native_symlink_target;
#[cfg(unix)]
use unix::{
handle_file_id, handle_link_count, metadata_file_id, native_attributes, native_handle_metadata,
native_metadata, native_remove_dir_all, native_remove_symlink, native_symlink,
native_symlink_metadata, open_read_error, opened_file, opened_id, opened_len, path_file_id,
set_native_attributes,
};
#[cfg(windows)]
pub(crate) use windows::native_symlink_target;
#[cfg(windows)]
use windows::{
handle_file_id, handle_link_count, metadata_file_id, native_attributes, native_handle_metadata,
native_metadata, native_remove_dir_all, native_remove_symlink, native_symlink,
native_symlink_metadata, open_read_error, opened_file, opened_id, opened_len, path_file_id,
set_native_attributes,
};
pub trait Fs: Send + Sync {
type ReadHandle: ReadFile + Send;
type WriteHandle: WriteFile + Send;
type Dir: Iterator<Item = Result<DirEntry>> + Send;
fn open_read<P: AsRef<Path>>(&self, path: P) -> Result<Self::ReadHandle>;
fn open_write<P: AsRef<Path>>(&self, path: P) -> Result<Self::WriteHandle> {
self.open_write_options(path, OpenWriteOptions::new().create())
}
fn open_write_options<P: AsRef<Path>>(
&self,
path: P,
options: OpenWriteOptions,
) -> Result<Self::WriteHandle>;
fn read<P: AsRef<Path>>(&self, path: P) -> Result<Vec<u8>> {
let path = path.as_ref();
self.open_read(path).and_then(|mut file| {
let mut buffer = Vec::new();
file.read_to_end(&mut buffer)
.map(|_| buffer)
.map_err(fs_error_at(path))
})
}
fn read_to_string<P: AsRef<Path>>(&self, path: P) -> Result<String> {
let path = path.as_ref();
self.read(path)
.and_then(|data| String::from_utf8(data).map_err(fs_error_at(path)))
}
fn write<P: AsRef<Path>, C: AsRef<[u8]>>(&self, path: P, data: C) -> Result<()> {
let error_path = path.as_ref().to_path_buf();
self.open_write_options(path, OpenWriteOptions::new().create().truncate())
.and_then(|mut file| {
file.write_all(data.as_ref())
.map_err(fs_error_at(&error_path))
})
}
fn read_dir<P: AsRef<Path>>(&self, path: P) -> Result<Self::Dir>;
fn create_dir<P: AsRef<Path>>(&self, path: P) -> Result;
fn create_dir_all<P: AsRef<Path>>(&self, path: P) -> Result;
fn exists<P: AsRef<Path>>(&self, path: P) -> Result<bool>;
fn metadata<P: AsRef<Path>>(&self, path: P) -> Result<Metadata>;
fn symlink_metadata<P: AsRef<Path>>(&self, path: P) -> Result<Metadata>;
fn create_symlink<P: AsRef<Path>, T: AsRef<Path>>(&self, path: P, target: T) -> Result;
fn read_link<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf>;
fn canonicalize<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf>;
fn create_hard_link<P: AsRef<Path>, T: AsRef<Path>>(&self, path: P, target: T) -> Result;
fn copy<S: AsRef<Path>, T: AsRef<Path>>(&self, source: S, target: T) -> Result;
fn copy_dir_all<S: AsRef<Path>, T: AsRef<Path>>(&self, source: S, target: T) -> Result;
fn remove<P: AsRef<Path>>(&self, path: P) -> Result;
fn remove_dir_all<P: AsRef<Path>>(&self, path: P) -> Result;
fn rename<S: AsRef<Path>, T: AsRef<Path>>(&self, source: S, target: T) -> Result;
fn set_readonly<P: AsRef<Path>>(&self, path: P, readonly: bool) -> Result;
#[cfg(unix)]
fn set_executable<P: AsRef<Path>>(&self, path: P, executable: bool) -> Result;
#[cfg(windows)]
fn set_hidden<P: AsRef<Path>>(&self, path: P, hidden: bool) -> Result;
fn set_modified<P: AsRef<Path>>(&self, path: P, modified: SystemTime) -> Result;
#[cfg(unix)]
fn set_mode<P: AsRef<Path>>(&self, path: P, mode: u32) -> Result;
}
pub trait ReadFile: Read + Seek {
fn metadata(&self) -> Result<Metadata>;
fn lock_shared(&self) -> Result;
fn try_lock_shared(&self) -> Result<bool>;
fn unlock(&self) -> Result;
}
pub trait WriteFile: ReadFile + Write {
fn set_len(&self, len: u64) -> Result;
fn sync_all(&self) -> Result;
fn sync_data(&self) -> Result;
fn lock(&self) -> Result;
fn try_lock(&self) -> Result<bool>;
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct OpenWriteOptions {
pub append: bool,
pub truncate: bool,
pub create: bool,
pub create_new: bool,
pub write_only: bool,
#[cfg(unix)]
pub mode: Option<u32>,
}
impl OpenWriteOptions {
pub const fn new() -> Self {
OpenWriteOptions {
append: false,
truncate: false,
create: false,
create_new: false,
write_only: false,
#[cfg(unix)]
mode: None,
}
}
pub const fn append(mut self) -> Self {
self.append = true;
self
}
pub const fn create(mut self) -> Self {
self.create = true;
self
}
pub const fn create_new(mut self) -> Self {
self.create_new = true;
self
}
pub const fn write_only(mut self) -> Self {
self.write_only = true;
self
}
pub const fn truncate(mut self) -> Self {
self.truncate = true;
self
}
#[cfg(unix)]
pub const fn mode(mut self, mode: u32) -> Self {
self.mode = Some(mode);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FileType {
File,
Dir,
Symlink,
}
impl FileType {
pub fn is_dir(&self) -> bool {
matches!(self, FileType::Dir)
}
pub fn is_file(&self) -> bool {
matches!(self, FileType::File)
}
pub fn is_symlink(&self) -> bool {
matches!(self, FileType::Symlink)
}
}
#[derive(Debug, Clone)]
pub struct DirEntry {
pub(crate) path: PathBuf,
pub(crate) file_type: FileType,
}
impl DirEntry {
pub(crate) fn new(path: PathBuf, file_type: FileType) -> Option<Self> {
path.file_name().and_then(|name| name.to_str())?;
Some(Self { path, file_type })
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn file_name(&self) -> &str {
self.path
.file_name()
.and_then(|n| n.to_str())
.expect("DirEntry path has no UTF-8 file name component")
}
pub fn file_type(&self) -> FileType {
self.file_type
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Attributes {
pub(crate) readonly: bool,
pub(crate) executable: bool,
pub(crate) hidden: bool,
}
impl Attributes {
pub(crate) fn set(&mut self, attribute: Attribute, value: bool) {
match attribute {
Attribute::Readonly => self.readonly = value,
#[cfg(unix)]
Attribute::Executable => self.executable = value,
#[cfg(windows)]
Attribute::Hidden => self.hidden = value,
}
}
#[cfg(windows)]
pub(crate) fn attributes(&self) -> Attributes {
*self
}
}
#[cfg(unix)]
pub(crate) type NativeAttributes = UnixAttributes;
#[cfg(windows)]
pub(crate) type NativeAttributes = Attributes;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Metadata {
pub file_type: FileType,
pub len: u64,
pub modified: SystemTime,
pub created: SystemTime,
pub readonly: bool,
pub executable: bool,
pub hidden: bool,
#[cfg(unix)]
pub mode: u32,
#[cfg(unix)]
pub uid: u32,
#[cfg(unix)]
pub gid: u32,
}
impl Metadata {
pub(crate) fn new(
path: &Path,
file_type: FileType,
len: u64,
modified: SystemTime,
created: SystemTime,
native: NativeAttributes,
) -> Self {
let attributes = platform_attributes(path, file_type, native.attributes());
Self {
file_type,
len,
modified,
created,
readonly: attributes.readonly,
executable: attributes.executable,
hidden: attributes.hidden,
#[cfg(unix)]
mode: native.mode,
#[cfg(unix)]
uid: native.uid,
#[cfg(unix)]
gid: native.gid,
}
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum Attribute {
Readonly,
#[cfg(unix)]
Executable,
#[cfg(windows)]
Hidden,
}
fn platform_attributes(path: &Path, file_type: FileType, stored: Attributes) -> Attributes {
#[cfg(unix)]
let attributes = Attributes {
readonly: stored.readonly,
executable: file_type.is_file() && stored.executable,
hidden: path
.file_name()
.is_some_and(|name| name.as_encoded_bytes().starts_with(b".")),
};
#[cfg(windows)]
let attributes = Attributes {
readonly: stored.readonly,
executable: file_type.is_file()
&& path.extension().is_some_and(|extension| {
["exe", "com", "bat", "cmd"]
.iter()
.any(|executable| extension.eq_ignore_ascii_case(executable))
}),
hidden: stored.hidden,
};
attributes
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BorrowState {
Read(u64),
Write,
}
pub struct FileHandle {
borrows: Arc<Mutex<Borrows>>,
key: u64,
file: Arc<File>,
path: Arc<Path>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct FileId {
volume: u64,
index: u128,
}
pub(crate) struct OpenedFile {
#[cfg(unix)]
id: FileId,
#[cfg(unix)]
is_dir: bool,
#[cfg(unix)]
len: u64,
}
#[derive(Default)]
pub(crate) struct Borrows {
next_key: u64,
handles: Vec<(u64, Borrow)>,
changing: Vec<(u64, PathBuf)>,
}
struct Borrow {
file: Arc<File>,
id: Option<FileId>,
write: bool,
path: Arc<Path>,
}
struct Borrowed {
file: Arc<File>,
id: FileId,
write: bool,
path: Arc<Path>,
}
impl Borrows {
fn is_empty(&self) -> bool {
self.handles.is_empty()
}
fn insert(&mut self, file: Arc<File>, id: Option<FileId>, write: bool, path: Arc<Path>) -> u64 {
let key = self.next_key();
self.handles.push((
key,
Borrow {
file,
id,
write,
path,
},
));
key
}
fn remove(&mut self, key: u64) {
if let Some(index) = self.handles.iter().position(|(k, _)| *k == key) {
self.handles.swap_remove(index);
}
}
fn next_key(&mut self) -> u64 {
let key = self.next_key;
self.next_key += 1;
key
}
fn state(&mut self, id: FileId) -> Option<BorrowState> {
self.identify();
borrow_state(
self.handles
.iter()
.filter_map(|(_, borrow)| borrow.id.map(|id| (id, borrow.write))),
id,
)
}
fn borrowed(&mut self) -> Vec<Borrowed> {
self.identify();
self.handles
.iter()
.filter_map(|(_, borrow)| {
borrow.id.map(|id| Borrowed {
file: borrow.file.clone(),
id,
write: borrow.write,
path: borrow.path.clone(),
})
})
.collect()
}
fn identify(&mut self) {
for (_, borrow) in &mut self.handles {
if borrow.id.is_none() {
borrow.id = handle_file_id(&borrow.file).ok();
}
}
}
fn check_not_changing(&self, paths: &[&Path], action: &str) -> Result {
match paths.iter().find(|path| {
self.changing
.iter()
.any(|(_, changing)| path.starts_with(changing) || changing.starts_with(path))
}) {
Some(path) => Err(fs_error!(
ResourceBusy;
"Path could not be {action}, it is being removed or renamed: {}",
path.display()
)),
None => Ok(()),
}
}
fn start_change(&mut self, paths: &[&Path]) -> u64 {
let key = self.next_key();
self.changing
.extend(paths.iter().map(|path| (key, path.to_path_buf())));
key
}
}
fn borrow_state(handles: impl Iterator<Item = (FileId, bool)>, id: FileId) -> Option<BorrowState> {
let mut reads = 0;
for (handle_id, write) in handles {
if handle_id == id {
if write {
return Some(BorrowState::Write);
}
reads += 1;
}
}
(reads > 0).then_some(BorrowState::Read(reads))
}
struct Change<'a> {
borrows: &'a Mutex<Borrows>,
key: u64,
}
impl Drop for Change<'_> {
fn drop(&mut self) {
lock_handles(self.borrows)
.changing
.retain(|(key, _)| *key != self.key);
}
}
pub(crate) fn lock_handles<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
mutex.lock().unwrap_or_else(|e| {
mutex.clear_poison();
e.into_inner()
})
}
impl Drop for FileHandle {
fn drop(&mut self) {
lock_handles(&self.borrows).remove(self.key);
}
}
pub struct FileReadHandle(FileHandle);
impl FileReadHandle {
fn acquire(
borrows: Arc<Mutex<Borrows>>,
path: &Path,
file: File,
opened: &OpenedFile,
) -> Result<Self> {
let path = Arc::<Path>::from(path);
let file = Arc::new(file);
let key = {
let mut handles = lock_handles(&borrows);
handles.check_not_changing(&[&path], "opened")?;
let id = opened_id(&file, opened, !handles.is_empty());
if let Some(BorrowState::Write) = id.and_then(|id| handles.state(id)) {
return Err(fs_error!(
ResourceBusy;
"File could not be opened, it is already borrowed for writing: {}",
path.display()
));
}
handles.insert(file.clone(), id, false, path.clone())
};
Ok(Self(FileHandle {
borrows,
key,
file,
path,
}))
}
}
impl Read for FileReadHandle {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
(&*self.0.file).read(buf)
}
fn read_to_end(&mut self, buf: &mut Vec<u8>) -> std::io::Result<usize> {
(&*self.0.file).read_to_end(buf)
}
fn read_to_string(&mut self, buf: &mut String) -> std::io::Result<usize> {
(&*self.0.file).read_to_string(buf)
}
}
impl Seek for FileReadHandle {
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
(&*self.0.file).seek(pos)
}
}
pub struct FileWriteHandle(FileHandle);
impl FileWriteHandle {
fn open(
borrows: Arc<Mutex<Borrows>>,
path: &Path,
options: &OpenOptions,
truncate: bool,
) -> Result<Self> {
let file = options.open(path).map_err(fs_error_at(path))?;
Self::acquire(borrows, path, file, truncate)
}
fn acquire(
borrows: Arc<Mutex<Borrows>>,
path: &Path,
file: File,
truncate: bool,
) -> Result<Self> {
let path = Arc::<Path>::from(path);
let file = Arc::new(file);
let key = {
let mut handles = lock_handles(&borrows);
handles.check_not_changing(&[&path], "opened")?;
let id = (!handles.is_empty())
.then(|| handle_file_id(&file).ok())
.flatten();
if let Some(state) = id.and_then(|id| handles.state(id)) {
return Err(borrowed_error(&path, state, "opened"));
}
handles.insert(file.clone(), id, true, path.clone())
};
let handle = Self(FileHandle {
borrows,
key,
file,
path,
});
let truncated = if truncate {
truncate_file(&handle.0.file)
} else {
Ok(())
};
truncated
.map_err(fs_error_at(&handle.0.path))
.map(|()| handle)
}
}
fn truncate_file(file: &File) -> std::io::Result<()> {
file.set_len(0).or_else(|error| {
file.metadata()
.is_ok_and(|metadata| !metadata.is_file())
.then_some(())
.ok_or(error)
})
}
impl Read for FileWriteHandle {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
(&*self.0.file).read(buf)
}
fn read_to_end(&mut self, buf: &mut Vec<u8>) -> std::io::Result<usize> {
(&*self.0.file).read_to_end(buf)
}
fn read_to_string(&mut self, buf: &mut String) -> std::io::Result<usize> {
(&*self.0.file).read_to_string(buf)
}
}
impl Seek for FileWriteHandle {
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
(&*self.0.file).seek(pos)
}
}
impl Write for FileWriteHandle {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
(&*self.0.file).write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
(&*self.0.file).flush()
}
}
impl FileHandle {
fn metadata(&self) -> Result<Metadata> {
native_handle_metadata(&self.path, &self.file).map(|native| to_metadata(&self.path, native))
}
fn lock_shared(&self) -> Result {
self.file.lock_shared().map_err(fs_error_at(&self.path))
}
fn try_lock_shared(&self) -> Result<bool> {
self.locked(self.file.try_lock_shared())
}
fn unlock(&self) -> Result {
self.file.unlock().map_err(fs_error_at(&self.path))
}
fn locked(&self, result: std::result::Result<(), std::fs::TryLockError>) -> Result<bool> {
result
.map(|()| true)
.or_else(|error| {
let error = std::io::Error::from(error);
(error.kind() == std::io::ErrorKind::WouldBlock)
.then_some(false)
.ok_or(error)
})
.map_err(fs_error_at(&self.path))
}
}
impl ReadFile for FileReadHandle {
fn metadata(&self) -> Result<Metadata> {
self.0.metadata()
}
fn lock_shared(&self) -> Result {
self.0.lock_shared()
}
fn try_lock_shared(&self) -> Result<bool> {
self.0.try_lock_shared()
}
fn unlock(&self) -> Result {
self.0.unlock()
}
}
impl ReadFile for FileWriteHandle {
fn metadata(&self) -> Result<Metadata> {
self.0.metadata()
}
fn lock_shared(&self) -> Result {
self.0.lock_shared()
}
fn try_lock_shared(&self) -> Result<bool> {
self.0.try_lock_shared()
}
fn unlock(&self) -> Result {
self.0.unlock()
}
}
impl WriteFile for FileWriteHandle {
fn set_len(&self, len: u64) -> Result {
self.0.file.set_len(len).map_err(fs_error_at(&self.0.path))
}
fn sync_all(&self) -> Result {
self.0.file.sync_all().map_err(fs_error_at(&self.0.path))
}
fn sync_data(&self) -> Result {
self.0.file.sync_data().map_err(fs_error_at(&self.0.path))
}
fn lock(&self) -> Result {
self.0.file.lock().map_err(fs_error_at(&self.0.path))
}
fn try_lock(&self) -> Result<bool> {
self.0.locked(self.0.file.try_lock())
}
}
fn to_metadata(path: &Path, (metadata, file_type): (std::fs::Metadata, FileType)) -> Metadata {
Metadata::new(
path,
file_type,
metadata.len(),
metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH),
metadata.created().unwrap_or(SystemTime::UNIX_EPOCH),
native_attributes(&metadata),
)
}
pub struct ReadDir {
path: PathBuf,
dir: std::fs::ReadDir,
}
impl Iterator for ReadDir {
type Item = Result<DirEntry>;
fn next(&mut self) -> Option<Self::Item> {
let to_entry = |path: PathBuf, file_type: std::fs::FileType| {
file_type
.is_dir()
.then_some(FileType::Dir)
.or(file_type.is_file().then_some(FileType::File))
.or(file_type.is_symlink().then_some(FileType::Symlink))
.and_then(|file_type| DirEntry::new(path, file_type))
};
self.dir.find_map(|result| {
result
.and_then(|entry| entry.file_type().map(|t| to_entry(entry.path(), t)))
.map_err(fs_error_at(&self.path))
.transpose()
})
}
}
pub struct FsImpl {
env: SharedEnv,
handles: Arc<Mutex<Borrows>>,
}
impl FsImpl {
#[cfg(test)]
pub(crate) fn new(current_dir: PathBuf) -> Self {
Self::with_env(SharedEnv::new(Vec::new(), current_dir))
}
pub(crate) fn with_env(env: SharedEnv) -> Self {
FsImpl {
env,
handles: Arc::new(Mutex::new(Borrows::default())),
}
}
fn current_dir(&self) -> Arc<Path> {
self.env.current_dir()
}
}
impl Fs for FsImpl {
type ReadHandle = FileReadHandle;
type WriteHandle = FileWriteHandle;
type Dir = ReadDir;
fn open_read<P: AsRef<Path>>(&self, path: P) -> Result<Self::ReadHandle> {
self.open_read_file(&normalize_path(self.current_dir(), &path))
.map(|(handle, _)| handle)
}
fn write<P: AsRef<Path>, C: AsRef<[u8]>>(&self, path: P, data: C) -> Result<()> {
let normalized_path = normalize_path(self.current_dir(), &path);
FileWriteHandle::open(
self.handles.clone(),
&normalized_path,
File::options().write(true).create(true),
true,
)
.and_then(|handle| {
(&*handle.0.file)
.write_all(data.as_ref())
.map_err(fs_error_at(&handle.0.path))
})
}
fn read<P: AsRef<Path>>(&self, path: P) -> Result<Vec<u8>> {
let (handle, opened) = self.open_read_file(&normalize_path(self.current_dir(), &path))?;
opened_len(&handle.0.file, &opened)
.and_then(|len| {
let mut buffer = Vec::with_capacity(usize::try_from(len).unwrap_or_default());
Read::take(&*handle.0.file, u64::MAX)
.read_to_end(&mut buffer)
.map(|_| buffer)
})
.map_err(fs_error_at(&handle.0.path))
}
fn open_write_options<P: AsRef<Path>>(
&self,
path: P,
options: OpenWriteOptions,
) -> Result<Self::WriteHandle> {
let normalized_path = normalize_path(self.current_dir(), &path);
let mut open_options = File::options();
open_options
.read(!options.write_only)
.write(true)
.create(options.create)
.create_new(options.create_new)
.append(options.append)
.truncate(options.truncate && options.append);
#[cfg(unix)]
if let Some(mode) = options.mode {
std::os::unix::fs::OpenOptionsExt::mode(&mut open_options, mode);
}
FileWriteHandle::open(
self.handles.clone(),
&normalized_path,
&open_options,
options.truncate && !options.create_new,
)
}
fn read_dir<P: AsRef<Path>>(&self, path: P) -> Result<Self::Dir> {
let normalized_path = normalize_path(self.current_dir(), &path);
let dir = std::fs::read_dir(&normalized_path).map_err(fs_error_at(&normalized_path))?;
Ok(ReadDir {
path: normalized_path.into_owned(),
dir,
})
}
fn create_dir<P: AsRef<Path>>(&self, path: P) -> Result {
let normalized_path = normalize_path(self.current_dir(), &path);
std::fs::create_dir(&normalized_path).map_err(fs_error_at(&normalized_path))
}
fn create_dir_all<P: AsRef<Path>>(&self, path: P) -> Result {
let normalized_path = normalize_path(self.current_dir(), &path);
std::fs::create_dir_all(&normalized_path).map_err(fs_error_at(&normalized_path))
}
fn exists<P: AsRef<Path>>(&self, path: P) -> Result<bool> {
let normalized_path = normalize_path(self.current_dir(), &path);
std::fs::exists(&normalized_path).map_err(fs_error_at(&normalized_path))
}
fn metadata<P: AsRef<Path>>(&self, path: P) -> Result<Metadata> {
let normalized_path = normalize_path(self.current_dir(), &path);
native_metadata(&normalized_path).map(|native| to_metadata(&normalized_path, native))
}
fn symlink_metadata<P: AsRef<Path>>(&self, path: P) -> Result<Metadata> {
let normalized_path = normalize_path(self.current_dir(), &path);
native_symlink_metadata(&normalized_path)
.map(|native| to_metadata(&normalized_path, native))
}
fn create_symlink<P: AsRef<Path>, T: AsRef<Path>>(&self, path: P, target: T) -> Result {
let normalized_path = normalize_path(self.current_dir(), &path);
let target = native_symlink_target(target.as_ref());
if target.as_os_str().is_empty() {
return Err(fs_error!(
NotFound;
"Symlink could not be created, target not found: {}",
target.display()
));
}
let parent = normalized_path.parent().unwrap_or(&normalized_path);
let (_, file_type) =
native_metadata(&parent.join(target.components().as_path())).map_err(|error| {
if parent_dir(&normalized_path).is_none() {
fs_error!(
NotFound;
"Symlink could not be created, parent directory does not exist: {}",
normalized_path.display()
)
} else {
error
}
})?;
if ends_with_separator(&target) && !file_type.is_dir() {
return Err(fs_error!(
NotADirectory;
"Symlink could not be created, target is not a directory: {}",
target.display()
));
}
native_symlink(&normalized_path, &target, file_type).map_err(fs_error_at(&normalized_path))
}
fn read_link<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf> {
let normalized_path = normalize_path(self.current_dir(), &path);
std::fs::read_link(&normalized_path).map_err(fs_error_at(&normalized_path))
}
fn canonicalize<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf> {
let normalized_path = normalize_path(self.current_dir(), &path);
std::fs::canonicalize(&normalized_path).map_err(fs_error_at(&normalized_path))
}
fn create_hard_link<P: AsRef<Path>, T: AsRef<Path>>(&self, path: P, target: T) -> Result {
let current_dir = self.current_dir();
let normalized_path = normalize_path(¤t_dir, &path);
let normalized_target = normalize_path(¤t_dir, &target);
if native_metadata(&normalized_target)?.1.is_dir() {
return Err(fs_error!(
PermissionDenied;
"Hard link could not be created, target is a directory: {}",
normalized_target.display()
));
}
std::fs::canonicalize(&normalized_target)
.and_then(|target| std::fs::hard_link(target, &normalized_path))
.map_err(fs_error_at(&normalized_path))
}
fn copy<S: AsRef<Path>, T: AsRef<Path>>(&self, source: S, target: T) -> Result {
let current_dir = self.current_dir();
let normalized_source = normalize_path(¤t_dir, &source);
let normalized_target = normalize_path(¤t_dir, &target);
let (metadata, file_type) = native_symlink_metadata(&normalized_source)?;
if !matches!(file_type, FileType::File)
&& let Some(error) = copy_target_error(&normalized_target)
{
return Err(error);
}
match file_type {
FileType::File => self.copy_file(
&normalized_source,
&normalized_target,
metadata.permissions(),
),
FileType::Dir => copy_dir(
&normalized_source,
&normalized_target,
metadata.permissions(),
),
FileType::Symlink => std::fs::read_link(&normalized_source)
.map_err(fs_error_at(&normalized_source))
.and_then(|link| self.create_symlink(&normalized_target, link)),
}
}
fn copy_dir_all<S: AsRef<Path>, T: AsRef<Path>>(&self, source: S, target: T) -> Result {
let current_dir = self.current_dir();
copy_tree(
self,
&normalize_path(¤t_dir, &source),
&normalize_path(¤t_dir, &target),
)
}
fn remove<P: AsRef<Path>>(&self, path: P) -> Result {
let normalized_path = normalize_path(self.current_dir(), &path);
let (_change, metadata, file_type) = {
let mut borrows = lock_handles(&self.handles);
borrows.check_not_changing(&[&normalized_path], "removed")?;
let (metadata, file_type) = native_symlink_metadata(&normalized_path)?;
if matches!(file_type, FileType::File) {
check_not_borrowed(&normalized_path, &metadata, &mut borrows, "removed")?;
}
(
self.change(&mut borrows, &[&normalized_path]),
metadata,
file_type,
)
};
match file_type {
FileType::Dir => std::fs::remove_dir(&normalized_path),
FileType::Symlink => native_remove_symlink(&normalized_path, &metadata),
FileType::File => std::fs::remove_file(&normalized_path),
}
.map_err(fs_error_at(&normalized_path))
}
fn remove_dir_all<P: AsRef<Path>>(&self, path: P) -> Result {
let normalized_path = normalize_path(self.current_dir(), &path);
check_not_root(&normalized_path).and_then(|()| self.remove_tree(&normalized_path))
}
fn rename<S: AsRef<Path>, T: AsRef<Path>>(&self, source: S, target: T) -> Result {
let current_dir = self.current_dir();
let normalized_source = normalize_path(¤t_dir, &source);
let normalized_target = normalize_path(¤t_dir, &target);
let paths = [&*normalized_source, &*normalized_target];
let _change = {
let mut borrows = lock_handles(&self.handles);
borrows.check_not_changing(&paths, "renamed")?;
let source = Entry::new(&normalized_source);
let target = Entry::new(&normalized_target);
check_rename_types(&source, &target)?;
#[cfg(unix)]
if other_link_of_same_file(&source, &target) {
drop(borrows);
return std::fs::remove_file(&normalized_source)
.map_err(fs_error_at(&normalized_source));
}
if let Some(metadata) = replaced_other_file(&source, &target, &borrows) {
check_not_borrowed(&normalized_target, metadata, &mut borrows, "replaced")?;
}
self.change(&mut borrows, &paths)
};
std::fs::rename(&normalized_source, &normalized_target).map_err(|error| {
fs_error!(
"{error}: {} -> {}",
normalized_source.display(),
normalized_target.display()
)
.with_source(error)
})
}
fn set_readonly<P: AsRef<Path>>(&self, path: P, readonly: bool) -> Result {
self.set_attribute(path.as_ref(), Attribute::Readonly, readonly)
}
#[cfg(unix)]
fn set_executable<P: AsRef<Path>>(&self, path: P, executable: bool) -> Result {
self.set_attribute(path.as_ref(), Attribute::Executable, executable)
}
#[cfg(windows)]
fn set_hidden<P: AsRef<Path>>(&self, path: P, hidden: bool) -> Result {
self.set_attribute(path.as_ref(), Attribute::Hidden, hidden)
}
fn set_modified<P: AsRef<Path>>(&self, path: P, modified: SystemTime) -> Result {
let normalized_path = normalize_path(self.current_dir(), &path);
native_metadata(&normalized_path)?;
std::fs::set_times(
&normalized_path,
std::fs::FileTimes::new().set_modified(modified),
)
.map_err(fs_error_at(&normalized_path))
}
#[cfg(unix)]
fn set_mode<P: AsRef<Path>>(&self, path: P, mode: u32) -> Result {
use std::os::unix::fs::PermissionsExt;
let normalized_path = normalize_path(self.current_dir(), &path);
std::fs::set_permissions(
&normalized_path,
std::fs::Permissions::from_mode(mode & 0o7777),
)
.map_err(fs_error_at(&normalized_path))
}
}
impl FsImpl {
fn open_read_file(&self, path: &Path) -> Result<(FileReadHandle, OpenedFile)> {
let (file, opened) = File::open(path)
.map_err(|error| open_read_error(path, error))
.and_then(|file| {
opened_file(&file)
.map(|opened| (file, opened))
.map_err(fs_error_at(path))
})?;
#[cfg(unix)]
if opened.is_dir {
return Err(directory_error(path));
}
FileReadHandle::acquire(self.handles.clone(), path, file, &opened)
.map(|handle| (handle, opened))
}
fn copy_file(&self, source: &Path, target: &Path, permissions: std::fs::Permissions) -> Result {
let (source, _) = self.open_read_file(source)?;
let (target_file, existed) = open_copy_target(target, &permissions)?;
let target_handle =
FileWriteHandle::acquire(self.handles.clone(), target, target_file, existed)?;
std::io::copy(&mut &*source.0.file, &mut &*target_handle.0.file)
.and_then(|_| target_handle.0.file.set_permissions(permissions))
.map_err(fs_error_at(target))
}
fn remove_tree(&self, path: &Path) -> Result {
let (_change, borrowed) = {
let mut borrows = lock_handles(&self.handles);
borrows.check_not_changing(&[path], "removed")?;
let borrowed = borrows.borrowed();
(self.change(&mut borrows, &[path]), borrowed)
};
if !borrowed.is_empty()
&& std::fs::symlink_metadata(path).is_ok_and(|metadata| metadata.is_dir())
&& !all_outside_tree(path, &borrowed)
{
check_tree_not_borrowed(path, &borrowed)?;
}
native_remove_dir_all(path).map_err(fs_error_at(path))
}
fn change(&self, borrows: &mut Borrows, paths: &[&Path]) -> Change<'_> {
Change {
borrows: &self.handles,
key: borrows.start_change(paths),
}
}
fn set_attribute(&self, path: &Path, attribute: Attribute, value: bool) -> Result {
let normalized_path = normalize_path(self.current_dir(), &path);
let (metadata, _) = native_metadata(&normalized_path)?;
#[cfg(unix)]
if matches!(attribute, Attribute::Executable) && metadata.is_dir() {
return Err(fs_error!(
IsADirectory;
"Executable attribute cannot be set, path is a directory: {}",
normalized_path.display()
));
}
let mut native = native_attributes(&metadata);
native.set(attribute, value);
set_native_attributes(&normalized_path, &metadata, native)
.map_err(fs_error_at(&normalized_path))
}
}
#[derive(Default)]
struct Tree {
dirs: Vec<(PathBuf, PathBuf)>,
files: Vec<(PathBuf, PathBuf)>,
symlinks: Vec<(PathBuf, PathBuf)>,
}
pub(crate) fn copy_tree<F: Fs>(fs: &F, source: &Path, target: &Path) -> Result {
fs.metadata(source)
.and_then(|metadata| {
if !metadata.file_type.is_dir() {
return Err(fs_error!(
NotADirectory;
"Directory could not be copied, source is not a directory: {}",
source.display()
));
}
if fs.symlink_metadata(target).is_ok() {
return Err(fs_error!(
AlreadyExists;
"Directory could not be copied, target already exists: {}",
target.display()
));
}
if !target.parent().is_some_and(|parent| {
fs.metadata(parent)
.is_ok_and(|metadata| metadata.file_type.is_dir())
}) {
return Err(fs_error!(
NotFound;
"Directory could not be copied, parent directory does not exist: {}",
target.display()
));
}
let mut tree = Tree::default();
tree.dirs.push((source.to_path_buf(), target.to_path_buf()));
list_tree(fs, source, target, &mut tree).map(|()| tree)
})
.and_then(|tree| {
tree.dirs
.iter()
.try_for_each(|(_, target)| fs.create_dir_all(target))
.and_then(|()| {
tree.files
.iter()
.try_for_each(|(source, target)| fs.copy(source, target))
})
.and_then(|()| copy_symlinks(fs, tree.symlinks))
.and_then(|()| {
tree.dirs
.iter()
.rev()
.try_for_each(|(source, target)| copy_permissions(fs, source, target))
})
})
}
fn list_tree<F: Fs>(fs: &F, source: &Path, target: &Path, tree: &mut Tree) -> Result {
fs.read_dir(source).and_then(|mut entries| {
entries.try_for_each(|entry| {
entry.and_then(|entry| {
let paths = (entry.path().to_path_buf(), target.join(entry.file_name()));
match entry.file_type() {
FileType::Dir => {
tree.dirs.push(paths.clone());
list_tree(fs, &paths.0, &paths.1, tree)
}
FileType::File => {
tree.files.push(paths);
Ok(())
}
FileType::Symlink => {
tree.symlinks.push(paths);
Ok(())
}
}
})
})
})
}
fn copy_symlinks<F: Fs>(fs: &F, mut pending: Vec<(PathBuf, PathBuf)>) -> Result {
while !pending.is_empty() {
let count = pending.len();
let mut error = None;
pending.retain(|(source, target)| {
fs.copy(source, target)
.map_err(|e| error = Some(e))
.is_err()
});
if let Some(error) = error.filter(|_| pending.len() == count) {
return Err(error);
}
}
Ok(())
}
fn copy_permissions<F: Fs>(fs: &F, source: &Path, target: &Path) -> Result {
fs.metadata(source).and_then(|metadata| {
#[cfg(unix)]
let result = fs.set_mode(target, metadata.mode);
#[cfg(windows)]
let result = fs.set_readonly(target, metadata.readonly);
result
})
}
pub(crate) fn directory_error(path: &Path) -> Error {
fs_error!(
IsADirectory;
"File could not be opened, path is a directory: {}",
path.display()
)
}
fn open_copy_target(
target: &Path,
#[cfg_attr(windows, allow(unused_variables))] permissions: &std::fs::Permissions,
) -> Result<(File, bool)> {
let mut options = File::options();
options.write(true);
#[cfg(unix)]
std::os::unix::fs::OpenOptionsExt::mode(
&mut options,
std::os::unix::fs::PermissionsExt::mode(permissions),
);
match options.clone().create_new(true).open(target) {
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
options.create(true).open(target).map(|file| (file, true))
}
result => result.map(|file| (file, false)),
}
.map_err(|error| missing_parent_error(target).unwrap_or_else(|| fs_error_at(target)(error)))
}
fn copy_target_error(target: &Path) -> Option<Error> {
if std::fs::symlink_metadata(target).is_ok() {
Some(fs_error!(
AlreadyExists;
"Path could not be copied, target already exists: {}",
target.display()
))
} else {
missing_parent_error(target)
}
}
fn missing_parent_error(target: &Path) -> Option<Error> {
parent_dir(target).is_none().then(|| {
fs_error!(
NotFound;
"Path could not be copied, parent directory does not exist: {}",
target.display()
)
})
}
fn copy_dir(source: &Path, target: &Path, permissions: std::fs::Permissions) -> Result {
std::fs::read_dir(source)
.map(|mut entries| entries.next().is_none())
.map_err(fs_error_at(source))
.and_then(|empty| {
if !empty {
return Err(fs_error!(
DirectoryNotEmpty;
"Directory could not be copied, it is not empty: {}",
source.display()
));
}
std::fs::create_dir(target)
.and_then(|()| std::fs::set_permissions(target, permissions))
.map_err(fs_error_at(target))
})
}
pub(crate) fn check_not_root(path: &Path) -> Result {
if path.parent().is_none() && !path.as_os_str().is_empty() {
return Err(fs_error!(
ResourceBusy;
"Directory could not be removed, path is a root: {}",
path.display()
));
}
Ok(())
}
struct Entry<'a> {
path: &'a Path,
metadata: Option<std::fs::Metadata>,
}
impl<'a> Entry<'a> {
fn new(path: &'a Path) -> Self {
Self {
path,
metadata: std::fs::symlink_metadata(path).ok(),
}
}
fn is_dir(&self) -> Option<bool> {
self.metadata.as_ref().map(std::fs::Metadata::is_dir)
}
fn file(&self) -> Option<&std::fs::Metadata> {
self.metadata.as_ref().filter(|metadata| metadata.is_file())
}
fn file_id(&self) -> Option<FileId> {
self.file()
.and_then(|metadata| metadata_file_id(self.path, metadata).ok())
}
}
fn check_rename_types(source: &Entry, target: &Entry) -> Result {
match (source.is_dir(), target.is_dir()) {
(Some(true), Some(false)) => Err(fs_error!(
NotADirectory;
"Directory could not be renamed, target is not a directory: {}",
target.path.display()
)),
(Some(false), Some(true)) => Err(fs_error!(
IsADirectory;
"Path could not be renamed, target is a directory: {}",
target.path.display()
)),
_ => Ok(()),
}
}
#[cfg(unix)]
fn other_link_of_same_file(source: &Entry, target: &Entry) -> bool {
fn entry(path: &Path) -> Option<(&Path, &std::ffi::OsStr)> {
path.parent().zip(path.file_name())
}
source.file_id().is_some()
&& source.file_id() == target.file_id()
&& entry(source.path).zip(entry(target.path)).is_some_and(
|((source_dir, source_name), (target_dir, target_name))| {
path_file_id(source_dir).ok() != path_file_id(target_dir).ok()
|| (source_name != target_name
&& std::fs::read_dir(source_dir).is_ok_and(|entries| {
let names: Vec<_> = entries
.filter_map(|entry| entry.ok().map(|entry| entry.file_name()))
.collect();
names.iter().any(|name| name == source_name)
&& names.iter().any(|name| name == target_name)
}))
},
)
}
fn replaced_other_file<'a>(
source: &Entry,
target: &'a Entry,
borrows: &Borrows,
) -> Option<&'a std::fs::Metadata> {
target.file().filter(|_| {
!borrows.is_empty() && !(source.file().is_some() && source.file_id() == target.file_id())
})
}
fn check_not_borrowed(
path: &Path,
metadata: &std::fs::Metadata,
borrows: &mut Borrows,
action: &str,
) -> Result {
if borrows.is_empty() {
return Ok(());
}
match metadata_file_id(path, metadata)
.ok()
.and_then(|id| borrows.state(id))
{
Some(state) => Err(borrowed_error(path, state, action)),
None => Ok(()),
}
}
fn borrowed_error(path: &Path, state: BorrowState, action: &str) -> Error {
match state {
BorrowState::Read(count) => fs_error!(
ResourceBusy;
"File could not be {action}, it is already borrowed for reading {count} times: {}",
path.display()
),
BorrowState::Write => fs_error!(
ResourceBusy;
"File could not be {action}, it is already borrowed for writing: {}",
path.display()
),
}
}
fn all_outside_tree(dir: &Path, borrowed: &[Borrowed]) -> bool {
std::fs::canonicalize(dir).is_ok_and(|dir| {
borrowed.iter().all(|borrowed| {
handle_link_count(&borrowed.file).is_ok_and(|links| links == 1)
&& std::fs::canonicalize(&borrowed.path).is_ok_and(|path| {
!path.starts_with(&dir) && path_file_id(&path).is_ok_and(|id| id == borrowed.id)
})
})
})
}
fn check_tree_not_borrowed(dir: &Path, borrowed: &[Borrowed]) -> Result {
std::fs::read_dir(dir)
.map_err(fs_error_at(dir))
.and_then(|mut entries| {
entries.try_for_each(|entry| {
entry
.and_then(|entry| entry.metadata().map(|metadata| (entry.path(), metadata)))
.map_err(fs_error_at(dir))
.and_then(|(path, metadata)| {
if metadata.is_dir() {
check_tree_not_borrowed(&path, borrowed)
} else if metadata.is_file()
&& let Some(state) =
metadata_file_id(&path, &metadata).ok().and_then(|id| {
borrow_state(
borrowed
.iter()
.map(|borrowed| (borrowed.id, borrowed.write)),
id,
)
})
{
Err(borrowed_error(&path, state, "removed"))
} else {
Ok(())
}
})
})
})
}
fn parent_dir(path: &Path) -> Option<&Path> {
path.parent()
.filter(|parent| std::fs::metadata(parent).is_ok_and(|metadata| metadata.is_dir()))
}
pub(crate) fn ends_with_separator(path: &Path) -> bool {
path.as_os_str()
.as_encoded_bytes()
.last()
.is_some_and(|&byte| std::path::is_separator(byte.into()))
}
pub(crate) fn fs_error_at<T: std::error::Error + Send + Sync + 'static>(
path: &Path,
) -> impl FnOnce(T) -> Error + '_ {
move |error| fs_error!("{error}: {}", path.display()).with_source(error)
}
pub(crate) fn normalize_path<'a, C: AsRef<Path>, P: AsRef<Path> + ?Sized>(
current_dir: C,
path: &'a P,
) -> Cow<'a, Path> {
cannonicalize_path(current_dir.as_ref(), path.as_ref())
}
#[cfg(unix)]
fn is_normal(path: &Path) -> bool {
let bytes = path.as_os_str().as_encoded_bytes();
!bytes.is_empty()
&& bytes
.split(|&byte| byte == b'/')
.enumerate()
.all(|(index, part)| match part {
b"" => index == 0,
b"." | b".." => false,
_ => true,
})
}
fn cannonicalize_path<'a>(current_dir: &Path, path: &'a Path) -> Cow<'a, Path> {
if path.as_os_str().is_empty() {
return Cow::Borrowed(path);
}
#[cfg(unix)]
if is_normal(path) {
return if path.is_absolute() {
Cow::Borrowed(path)
} else {
Cow::Owned(current_dir.join(path))
};
}
let mut base = if path.is_relative() {
current_dir.to_path_buf()
} else {
PathBuf::new()
};
for component in path.components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
if matches!(
base.components().next_back(),
None | Some(Component::ParentDir)
) {
base.push(component);
} else {
base.pop();
}
}
Component::RootDir | Component::Prefix(_) => {
base.push(component);
}
Component::Normal(part) => {
base.push(part);
}
}
}
if ends_with_separator(path) {
base.push("");
}
Cow::Owned(base)
}
#[cfg(test)]
mod tests {
use std::io::SeekFrom;
use super::*;
use crate::TestDir;
#[cfg(unix)]
fn mode(path: &Path) -> u32 {
use std::os::unix::fs::PermissionsExt;
std::fs::metadata(path).unwrap().permissions().mode() & 0o7777
}
#[cfg(unix)]
fn set_mode(path: &Path, mode: u32) {
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode)).unwrap();
}
#[track_caller]
fn two_fs_with_file() -> (TestDir, FsImpl, FsImpl) {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
let other = FsImpl::new(test_dir.path().to_path_buf());
(test_dir, fs, other)
}
fn changing<'a>(fs: &'a FsImpl, path: &str) -> Change<'a> {
let path = normalize_path(fs.current_dir(), path);
fs.change(&mut lock_handles(&fs.handles), &[&path])
}
fn changing_error(fs: &FsImpl, path: &str, action: &str) -> Error {
fs_error!(
ResourceBusy;
"Path could not be {action}, it is being removed or renamed: {}",
normalize_path(fs.current_dir(), path).display()
)
}
fn assert_os_error(error: Error, path: &Path) {
let suffix = format!(": {}", path.display());
let os_message = error.description().strip_suffix(&suffix);
assert!(
os_message.is_some_and(|message| !message.is_empty()),
"error should be the OS message followed by the path, got: {error}"
);
}
fn assert_rename_os_error(error: Error, source: &Path, target: &Path) {
assert_os_error(
error,
Path::new(&format!("{} -> {}", source.display(), target.display())),
);
}
fn io_kind(error: &crate::Error) -> Option<std::io::ErrorKind> {
error
.source_as::<std::io::Error>()
.map(std::io::Error::kind)
}
fn described(error: &crate::Error) -> (String, Option<std::io::ErrorKind>) {
(error.to_string(), io_kind(error))
}
#[track_caller]
fn assert_error(error: &crate::Error, expected: crate::Error) {
assert_eq!(described(error), described(&expected));
}
mod fs_impl {
use super::*;
#[test]
fn fs_impl_should_enforce_one_writer_or_many_readers() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
let normalized_path = normalize_path(fs.current_dir(), "Cargo.toml");
let read1 = fs.open_read("Cargo.toml").unwrap();
let read2 = fs.open_read("Cargo.toml").unwrap();
let err = fs.open_write("Cargo.toml").err().unwrap().to_string();
assert_eq!(
err,
fs_error!(
"File could not be opened, it is already borrowed for reading 2 times: {}",
normalized_path.display()
)
.to_string()
);
drop(read1);
drop(read2);
let write_handle = fs.open_write("Cargo.toml").unwrap();
let err = fs.open_read("Cargo.toml").err().unwrap().to_string();
assert_eq!(
err,
fs_error!(
"File could not be opened, it is already borrowed for writing: {}",
normalized_path.display()
)
.to_string()
);
assert!(fs.read("Cargo.toml").is_err());
assert!(fs.write("Cargo.toml", "data").is_err());
let err = fs.open_write("Cargo.toml").err().unwrap().to_string();
assert_eq!(
err,
fs_error!(
"File could not be opened, it is already borrowed for writing: {}",
normalized_path.display()
)
.to_string()
);
drop(write_handle);
let _read = fs.open_read("Cargo.toml").unwrap();
}
#[test]
fn fs_impl_should_borrow_symlinked_file_under_its_target() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
fs.create_symlink("link", "a.txt").unwrap();
let _write = fs.open_write("link").unwrap();
assert_error(
&fs.open_read("a.txt").err().unwrap(),
fs_error!(
ResourceBusy;
"File could not be opened, it is already borrowed for writing: {}",
normalize_path(fs.current_dir(), "a.txt").display()
),
);
}
#[test]
fn fs_impl_should_borrow_hard_linked_file_once() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
fs.create_hard_link("b.txt", "a.txt").unwrap();
let _read = fs.open_read("a.txt").unwrap();
assert_error(
&fs.open_write("b.txt").err().unwrap(),
fs_error!(
ResourceBusy;
"File could not be opened, it is already borrowed for reading 1 times: {}",
normalize_path(fs.current_dir(), "b.txt").display()
),
);
assert_eq!(fs.read_to_string("b.txt").unwrap(), "aaa");
}
#[test]
fn fs_impl_should_borrow_file_created_through_dangling_symlink_under_its_target() {
let test_dir = TestDir::new();
let dir = test_dir.path();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "").unwrap();
fs.create_symlink("link", "a.txt").unwrap();
std::fs::remove_file(dir.join("a.txt")).unwrap();
let _write = fs.open_write("link").unwrap();
assert_error(
&fs.open_read("a.txt").err().unwrap(),
fs_error!(
ResourceBusy;
"File could not be opened, it is already borrowed for writing: {}",
normalize_path(fs.current_dir(), "a.txt").display()
),
);
}
mod with_env {
use super::*;
use crate::Env;
#[test]
fn with_env_relative_should_resolve_against_changed_current_dir() {
let test_dir = TestDir::new();
std::fs::create_dir_all(test_dir.path().join("a/b")).unwrap();
std::fs::write(test_dir.path().join("top.txt"), "").unwrap();
let env = crate::env::EnvImpl::new();
env.shared.replace_current_dir(test_dir.path().join("a/b"));
let fs = FsImpl::with_env(env.shared.clone());
env.set_current_dir("..");
assert_eq!(*fs.current_dir(), *test_dir.path().join("a"));
assert!(fs.exists("../top.txt").unwrap());
}
}
mod fs {
use super::*;
mod open_read {
use super::*;
#[cfg(unix)]
#[test]
fn open_read_with_trailing_separator_should_fail_for_file() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
assert_eq!(
io_kind(&fs.open_read("a.txt/").err().unwrap()),
Some(std::io::ErrorKind::NotADirectory)
);
}
#[test]
fn open_read_parent_dir_in_symlink_target_should_leave_resolved_dir() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("a/b").unwrap();
fs.write("a/b/c.txt", "ccc").unwrap();
fs.create_symlink("a/b/up", "../b/c.txt").unwrap();
fs.create_symlink("dir_link", "a/b").unwrap();
let content = fs.read_to_string("dir_link/up");
assert_eq!(content.unwrap(), "ccc");
}
#[test]
fn open_read_should_return_readable_handle() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
let mut file = fs.open_read("Cargo.toml").unwrap();
let len = file.seek(SeekFrom::End(0)).unwrap() as usize;
assert!(len > 0);
file.seek(SeekFrom::Start(0)).unwrap();
let mut content = vec![0_u8; len];
let read = file.read(&mut content).unwrap();
assert_eq!(read, len);
}
#[test]
fn open_read_path_in_dir_being_changed_should_fail_until_changed() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir("dir").unwrap();
fs.write("dir/a.txt", "aaa").unwrap();
let change = changing(&fs, "dir");
assert_error(
&fs.open_read("dir/a.txt").err().unwrap(),
changing_error(&fs, "dir/a.txt", "opened"),
);
drop(change);
fs.open_read("dir/a.txt").unwrap();
}
#[test]
fn open_read_should_fail_for_nonexistent_file() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
assert_os_error(
fs.open_read("nonexistent_file.txt").err().unwrap(),
&normalize_path(fs.current_dir(), "nonexistent_file.txt"),
);
}
#[test]
fn open_read_dir_should_fail() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
assert_error(
&fs.open_read("src").err().unwrap(),
fs_error!(
IsADirectory;
"File could not be opened, path is a directory: {}",
fs.current_dir().join("src").display()
),
);
}
#[test]
fn open_read_should_fail_when_file_is_write_borrowed() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
let _write_handle = fs.open_write("Cargo.toml").unwrap();
assert!(fs.open_read("Cargo.toml").is_err());
}
}
mod open_write {
use super::*;
#[test]
fn open_write_should_return_writable_handle_to_new_or_existent_file() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
let content = b"content";
let mut file = fs.open_write("fs_test_open_write.tmp").unwrap();
let written = file.write(content).unwrap();
assert_eq!(written, content.len());
drop(file);
let mut file = fs.open_write("fs_test_open_write.tmp").unwrap();
let len = file.seek(SeekFrom::End(0)).unwrap() as usize;
assert_eq!(len, content.len());
file.seek(SeekFrom::Start(0)).unwrap();
let mut read_content = vec![0_u8; content.len()];
let read = file.read(&mut read_content).unwrap();
assert_eq!(read, len);
assert_eq!(read_content, content);
}
}
mod open_write_options {
use super::*;
#[test]
fn open_write_options_write_only_should_write_but_not_read() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
let mut file = fs
.open_write_options("a.txt", OpenWriteOptions::new().create().write_only())
.unwrap();
file.write_all(b"aaa").unwrap();
file.rewind().unwrap();
assert!(file.read(&mut [0_u8; 3]).is_err());
drop(file);
assert_eq!(fs.read_to_string("a.txt").unwrap(), "aaa");
}
#[test]
fn open_write_options_append_should_only_append_to_existing_content() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
let content = b"content";
fs.write("fs_test_append.tmp", content).unwrap();
let mut file = fs
.open_write_options("fs_test_append.tmp", OpenWriteOptions::new().append())
.unwrap();
file.seek(SeekFrom::Start(0)).unwrap();
let written = file.write(content).unwrap();
assert_eq!(written, content.len());
let current = file.stream_position().unwrap() as usize;
assert_eq!(current, 2 * written);
file.flush().unwrap();
}
#[test]
fn open_write_options_truncate_should_clear_existing_content() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("fs_test_truncate.tmp", "content").unwrap();
let mut file = fs
.open_write_options(
"fs_test_truncate.tmp",
OpenWriteOptions::new().truncate(),
)
.unwrap();
let len = file.seek(SeekFrom::End(0)).unwrap() as usize;
assert_eq!(len, 0);
}
#[test]
fn open_write_options_truncate_borrowed_file_should_fail_and_keep_content() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
let _read = fs.open_read("a.txt").unwrap();
assert_error(
&fs.open_write_options("a.txt", OpenWriteOptions::new().truncate())
.err()
.unwrap(),
fs_error!(
ResourceBusy;
"File could not be opened, it is already borrowed for reading 1 times: {}",
normalize_path(fs.current_dir(), "a.txt").display()
),
);
assert_eq!(fs.read_to_string("a.txt").unwrap(), "aaa");
}
#[test]
fn open_write_options_truncate_with_create_new_should_create_file() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
let mut file = fs
.open_write_options(
"a.txt",
OpenWriteOptions::new().create_new().truncate(),
)
.unwrap();
file.write_all(b"aaa").unwrap();
drop(file);
assert_eq!(fs.read_to_string("a.txt").unwrap(), "aaa");
}
#[test]
fn open_write_options_create_new_should_fail_on_existing_file() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("fs_test_create_new.tmp", "content").unwrap();
assert_os_error(
fs.open_write_options(
"fs_test_create_new.tmp",
OpenWriteOptions::new().create_new(),
)
.err()
.unwrap(),
&normalize_path(fs.current_dir(), "fs_test_create_new.tmp"),
);
}
#[cfg(unix)]
#[test]
fn open_write_options_mode_should_apply_to_new_file_only() {
let test_dir = TestDir::new();
let path = test_dir.path().join("fs_test_open_write_mode.tmp");
let fs = FsImpl::new(test_dir.path().to_path_buf());
drop(
fs.open_write_options(
"fs_test_open_write_mode.tmp",
OpenWriteOptions::new().create_new().mode(0o600),
)
.unwrap(),
);
assert_eq!(mode(&path), 0o600);
drop(
fs.open_write_options(
"fs_test_open_write_mode.tmp",
OpenWriteOptions::new().create().mode(0o644),
)
.unwrap(),
);
assert_eq!(mode(&path), 0o600);
}
#[test]
fn open_write_options_append_with_truncate_should_be_rejected() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
assert!(
fs.open_write_options(
"fs_test_reject_append_truncate.tmp",
OpenWriteOptions::new().append().truncate()
)
.is_err()
);
assert!(
fs.open_write_options(
"fs_test_reject_append_truncate.tmp",
OpenWriteOptions::new().append().truncate().create()
)
.is_err()
);
assert!(
fs.open_write_options(
"fs_test_reject_append_truncate.tmp",
OpenWriteOptions::new().append().truncate().create_new()
)
.is_ok()
);
assert!(
fs.open_write_options(
"fs_test_reject_append_truncate.tmp",
OpenWriteOptions::new().append().truncate().create_new()
)
.is_err()
);
}
}
mod read {
use super::*;
#[test]
fn read_should_return_file_contents_as_bytes() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
let path = fs
.current_dir()
.join("Cargo.toml")
.to_string_lossy()
.to_string();
let result = fs.read(path.as_str()).unwrap();
assert!(!result.is_empty());
}
}
mod read_to_string {
use super::*;
#[test]
fn read_to_string_should_return_file_contents_as_string() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
let result = fs.read_to_string("./src/../Cargo.toml").unwrap();
assert!(!result.is_empty());
}
#[test]
fn read_to_string_should_fail_for_nonexistent_file() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
assert!(fs.read_to_string("Cargo.toml2").is_err());
}
}
mod write {
use super::*;
#[test]
fn write_with_trailing_separator_should_fail_without_creating() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
let result = fs.write("a.txt/", "aaa");
assert!(!test_dir.path().join("a.txt").exists());
#[cfg(unix)]
assert_eq!(
result.err().as_ref().and_then(io_kind),
Some(std::io::ErrorKind::IsADirectory)
);
#[cfg(windows)]
assert!(result.is_err());
}
#[cfg(unix)]
#[test]
fn write_should_write_file_that_cannot_be_read() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
fs.set_mode("a.txt", 0o200).unwrap();
fs.write("a.txt", "bbb").unwrap();
fs.set_mode("a.txt", 0o600).unwrap();
assert_eq!(fs.read_to_string("a.txt").unwrap(), "bbb");
}
#[cfg(unix)]
#[test]
fn write_should_write_to_file_that_cannot_be_truncated() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
fs.write("/dev/null", "aaa").unwrap();
}
#[test]
fn write_path_being_changed_should_fail_and_keep_content() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
let _change = changing(&fs, "a.txt");
assert_error(
&fs.write("a.txt", "bbb").err().unwrap(),
changing_error(&fs, "a.txt", "opened"),
);
assert_eq!(
std::fs::read_to_string(test_dir.path().join("a.txt")).unwrap(),
"aaa"
);
}
#[test]
fn write_should_create_file_and_write_content() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("fs_test_write_create.tmp", "hello").unwrap();
assert_eq!(
fs.read_to_string("fs_test_write_create.tmp").unwrap(),
"hello"
);
}
#[test]
fn write_should_overwrite_existing_content() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("fs_test_write_overwrite.tmp", "hello").unwrap();
fs.write("fs_test_write_overwrite.tmp", "a").unwrap();
assert_eq!(
fs.read_to_string("fs_test_write_overwrite.tmp").unwrap(),
"a"
);
}
#[test]
fn write_should_fail_when_file_is_write_borrowed() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("fs_test_write_while_write_borrowed.tmp", "hello")
.unwrap();
let _write_handle = fs
.open_write("fs_test_write_while_write_borrowed.tmp")
.unwrap();
assert!(
fs.write("fs_test_write_while_write_borrowed.tmp", "world")
.is_err()
);
}
#[test]
fn write_should_fail_when_file_is_read_borrowed() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("fs_test_write_while_read_borrowed.tmp", "hello")
.unwrap();
let _read_handle = fs
.open_read("fs_test_write_while_read_borrowed.tmp")
.unwrap();
fs.write("fs_test_write_while_read_borrowed.tmp", "world")
.unwrap_err();
assert_eq!(
fs.read_to_string("fs_test_write_while_read_borrowed.tmp")
.unwrap(),
"hello"
);
}
}
mod read_dir {
use super::*;
#[test]
fn read_dir_should_list_directory_contents() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
let entries: Vec<DirEntry> =
fs.read_dir("src").unwrap().collect::<Result<_>>().unwrap();
let names: Vec<String> =
entries.iter().map(|e| e.file_name().to_string()).collect();
assert!(names.contains(&"fs.rs".to_string()));
assert!(names.contains(&"lib.rs".to_string()));
}
#[test]
fn read_dir_should_distinguish_files_and_directories() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
let entries: Vec<DirEntry> =
fs.read_dir("src").unwrap().collect::<Result<_>>().unwrap();
let fs_entry = entries.iter().find(|e| e.file_name() == "fs.rs").unwrap();
assert!(
fs_entry.file_type().is_file(),
"expected fs.rs to be a file"
);
let platform_entry = entries
.iter()
.find(|e| e.file_name() == "test_platform")
.unwrap();
assert!(
platform_entry.file_type().is_dir(),
"expected 'test_platform' to be a directory"
);
}
#[test]
fn read_dir_should_fail_for_nonexistent_path() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
assert_os_error(
fs.read_dir("nonexistent_dir").err().unwrap(),
&normalize_path(fs.current_dir(), "nonexistent_dir"),
);
}
#[test]
fn read_dir_should_list_symlinks_without_following() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
fs.create_symlink("link", "a.txt").unwrap();
let mut entries: Vec<(String, FileType)> = fs
.read_dir(".")
.unwrap()
.map(|e| e.unwrap())
.map(|e| (e.file_name().to_string(), e.file_type()))
.collect();
entries.sort_by(|a, b| a.0.cmp(&b.0));
assert_eq!(
entries,
vec![
("a.txt".to_string(), FileType::File),
("link".to_string(), FileType::Symlink)
]
);
}
#[cfg(unix)]
#[test]
fn read_dir_should_skip_non_utf8_names() {
use std::os::unix::ffi::OsStrExt;
let test_dir = TestDir::new();
let dir = test_dir.path();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
std::fs::write(dir.join(std::ffi::OsStr::from_bytes(b"\xff.txt")), "bbb")
.unwrap();
let names: Vec<String> = fs
.read_dir(".")
.unwrap()
.map(|e| e.unwrap().file_name().to_string())
.collect();
assert_eq!(names, vec!["a.txt"]);
}
}
mod create_dir {
use super::*;
#[test]
fn create_dir_should_create_empty_dir() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir("dir").unwrap();
assert!(test_dir.path().join("dir").is_dir());
assert!(fs.read_dir("dir").unwrap().next().is_none());
}
#[test]
fn create_dir_concurrently_should_succeed_once() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
let created = std::thread::scope(|scope| {
let threads: Vec<_> = (0..8)
.map(|_| scope.spawn(|| fs.create_dir("lock").is_ok()))
.collect();
threads
.into_iter()
.map(|thread| thread.join().unwrap())
.filter(|&created| created)
.count()
});
assert_eq!(created, 1);
}
#[test]
fn create_dir_existing_path_should_fail() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir").unwrap();
fs.write("a.txt", "aaa").unwrap();
fs.write("b.txt", "").unwrap();
fs.create_symlink("dangling", "b.txt").unwrap();
fs.remove("b.txt").unwrap();
for path in ["dir", "a.txt", "dangling"] {
assert_os_error(
fs.create_dir(path).err().unwrap(),
&normalize_path(fs.current_dir(), path),
);
}
assert!(!fs.exists("b.txt").unwrap());
}
#[test]
fn create_dir_without_parent_dir_should_fail() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
for path in ["missing/dir", "a.txt/dir"] {
assert_os_error(
fs.create_dir(path).err().unwrap(),
&normalize_path(fs.current_dir(), path),
);
}
}
}
mod create_dir_all {
use super::*;
#[test]
fn create_dir_all_empty_path_should_create_nothing() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("").unwrap();
assert_eq!(std::fs::read_dir(test_dir.path()).unwrap().count(), 0);
}
#[test]
fn create_dir_all_should_create_directory_and_parents() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("a/b").unwrap();
assert!(test_dir.path().join("a/b").is_dir());
}
#[test]
fn create_dir_all_should_succeed_if_directory_already_exists() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
fs.create_dir_all("src").unwrap();
assert!(
fs.current_dir().join("src").is_dir(),
"src should still be a directory"
);
}
#[test]
fn create_dir_all_should_fail_if_file_exists_at_path() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
assert_os_error(
fs.create_dir_all("Cargo.toml").err().unwrap(),
&normalize_path(fs.current_dir(), "Cargo.toml"),
);
}
}
mod exists {
use super::*;
#[test]
fn exists_empty_path_should_return_false() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
assert!(!fs.exists("").unwrap());
}
#[test]
fn exists_should_return_true_for_existing_file() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
assert!(fs.exists("Cargo.toml").unwrap());
}
#[test]
fn exists_should_return_true_for_existing_directory() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
assert!(fs.exists("src").unwrap());
}
#[test]
fn exists_should_return_false_for_non_existing_file() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
assert!(!fs.exists("non_existing_file.tmp").unwrap());
}
#[test]
fn exists_should_return_false_for_non_existing_directory() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
assert!(!fs.exists("non_existing_directory").unwrap());
}
#[test]
fn exists_should_return_error_for_invalid_path() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
let result = fs.exists("\0");
assert!(result.is_err());
}
}
mod metadata {
use super::*;
#[test]
fn metadata_should_return_file_metadata() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("fs_test_metadata_file.tmp", "abc").unwrap();
let metadata = fs.metadata("fs_test_metadata_file.tmp").unwrap();
assert_eq!(metadata.file_type, FileType::File);
assert_eq!(metadata.len, 3);
assert!(metadata.modified <= SystemTime::now());
assert!(!metadata.readonly && !metadata.executable && !metadata.hidden);
}
#[test]
fn metadata_should_return_dir_metadata() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
let metadata = fs.metadata("src").unwrap();
assert_eq!(metadata.file_type, FileType::Dir);
assert!(!metadata.executable);
}
#[cfg(unix)]
#[test]
fn metadata_should_return_mode_and_owner() {
use std::os::unix::fs::MetadataExt;
let test_dir = TestDir::new();
let path = test_dir.path().join("fs_test_metadata_mode.tmp");
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("fs_test_metadata_mode.tmp", "").unwrap();
set_mode(&path, 0o4640);
let native = std::fs::metadata(&path).unwrap();
let metadata = fs.metadata("fs_test_metadata_mode.tmp").unwrap();
assert_eq!(metadata.mode, 0o4640);
assert_eq!((metadata.uid, metadata.gid), (native.uid(), native.gid()));
}
#[cfg(unix)]
#[test]
fn metadata_dot_name_should_be_hidden() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write(".fs_test_metadata_hidden.tmp", "").unwrap();
assert!(fs.metadata(".fs_test_metadata_hidden.tmp").unwrap().hidden);
}
#[cfg(windows)]
#[test]
fn metadata_executable_extension_should_be_executable() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("fs_test_metadata_executable.bat", "").unwrap();
assert!(
fs.metadata("fs_test_metadata_executable.bat")
.unwrap()
.executable
);
}
#[test]
fn metadata_should_fail_for_nonexistent_path() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
assert_os_error(
fs.metadata("nonexistent.tmp").err().unwrap(),
&normalize_path(fs.current_dir(), "nonexistent.tmp"),
);
}
#[cfg(unix)]
#[test]
fn metadata_should_fail_for_unsupported_file_type() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
assert_error(
&fs.metadata("/dev/null").err().unwrap(),
fs_error!(
Unsupported;
"Unsupported file type, path is neither a file, a directory nor a symlink: /dev/null"
),
);
}
#[test]
fn metadata_should_follow_symlinks() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "abc").unwrap();
fs.create_symlink("link", "a.txt").unwrap();
let metadata = fs.metadata("link").unwrap();
assert_eq!((metadata.file_type, metadata.len), (FileType::File, 3));
}
}
mod symlink_metadata {
use super::*;
#[cfg(unix)]
#[test]
fn symlink_metadata_with_trailing_separator_should_follow_symlink() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir("dir").unwrap();
fs.create_symlink("link", "dir").unwrap();
assert_eq!(
fs.symlink_metadata("link/").unwrap().file_type,
FileType::Dir
);
}
#[test]
fn symlink_metadata_should_not_follow_symlink() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "abcdef").unwrap();
fs.create_symlink("link", "a.txt").unwrap();
let metadata = fs.symlink_metadata("link").unwrap();
assert_eq!(metadata.file_type, FileType::Symlink);
#[cfg(unix)]
assert_eq!((metadata.len, metadata.mode), (5, 0o777));
}
#[test]
fn symlink_metadata_file_should_return_file_metadata() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
assert_eq!(
fs.symlink_metadata("Cargo.toml").unwrap().file_type,
FileType::File
);
}
#[test]
fn symlink_metadata_should_fail_for_nonexistent_path() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
assert_os_error(
fs.symlink_metadata("nonexistent.tmp").err().unwrap(),
&normalize_path(fs.current_dir(), "nonexistent.tmp"),
);
}
}
mod create_symlink {
use super::*;
#[test]
fn create_symlink_should_link_file_and_dir() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir").unwrap();
fs.write("a.txt", "aaa").unwrap();
fs.write("dir/b.txt", "bbb").unwrap();
fs.create_symlink("file_link", "a.txt").unwrap();
fs.create_symlink("dir_link", "dir").unwrap();
assert_eq!(fs.read_to_string("file_link").unwrap(), "aaa");
assert_eq!(fs.read_to_string("dir_link/b.txt").unwrap(), "bbb");
}
#[test]
fn create_symlink_existing_path_should_fail() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
assert_os_error(
fs.create_symlink("a.txt", "a.txt").err().unwrap(),
&normalize_path(fs.current_dir(), "a.txt"),
);
}
#[test]
fn create_symlink_nested_relative_target_should_resolve_against_symlink_parent() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir").unwrap();
fs.write("dir/a.txt", "aaa").unwrap();
fs.create_symlink("link", "dir/a.txt").unwrap();
assert_eq!(fs.read_to_string("link").unwrap(), "aaa");
}
#[test]
fn create_symlink_empty_target_should_fail() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
assert_eq!(
fs.create_symlink("fs_test_create_symlink_empty.tmp", "")
.err()
.unwrap()
.to_string(),
fs_error!("Symlink could not be created, target not found: ").to_string()
);
assert!(
fs.symlink_metadata("fs_test_create_symlink_empty.tmp")
.is_err()
);
}
#[test]
fn create_symlink_target_with_trailing_separator_should_require_dir() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir").unwrap();
fs.write("a.txt", "aaa").unwrap();
fs.create_symlink("dir_link", "dir/").unwrap();
assert!(fs.metadata("dir_link").unwrap().file_type.is_dir());
assert_error(
&fs.create_symlink("file_link", "a.txt/").err().unwrap(),
fs_error!(
NotADirectory;
"Symlink could not be created, target is not a directory: {}",
native_symlink_target(Path::new("a.txt/")).display()
),
);
}
#[test]
fn create_symlink_nonexistent_target_should_fail() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
assert_os_error(
fs.create_symlink("fs_test_create_symlink_dangling.tmp", "missing.tmp")
.err()
.unwrap(),
&normalize_path(fs.current_dir(), "missing.tmp"),
);
assert!(
fs.symlink_metadata("fs_test_create_symlink_dangling.tmp")
.is_err()
);
}
#[test]
fn create_symlink_without_parent_dir_should_fail() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
for (path, target) in [
("fs_test_create_symlink_missing.tmp/link", "/"),
("Cargo.toml/link", "/"),
] {
assert_os_error(
fs.create_symlink(path, target).err().unwrap(),
&normalize_path(fs.current_dir(), path),
);
}
let path = "fs_test_create_symlink_missing.tmp/link";
assert_error(
&fs.create_symlink(path, "missing.tmp").err().unwrap(),
fs_error!(
NotFound;
"Symlink could not be created, parent directory does not exist: {}",
normalize_path(fs.current_dir(), path).display()
),
);
}
}
mod canonicalize {
use super::*;
#[test]
fn canonicalize_should_resolve_symlinks_and_dots() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir").unwrap();
fs.write("dir/a.txt", "aaa").unwrap();
fs.create_symlink("link", "dir").unwrap();
assert_eq!(
fs.canonicalize("link/./a.txt").unwrap(),
std::fs::canonicalize(test_dir.path().join("dir").join("a.txt")).unwrap()
);
}
#[test]
fn canonicalize_nonexistent_path_should_fail() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
assert_os_error(
fs.canonicalize("missing").err().unwrap(),
&normalize_path(fs.current_dir(), "missing"),
);
}
}
mod read_link {
use super::*;
#[test]
fn read_link_should_return_target_as_given() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
fs.create_symlink("link", "a.txt").unwrap();
assert_eq!(fs.read_link("link").unwrap(), Path::new("a.txt"));
}
#[test]
fn read_link_file_should_fail() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
assert_os_error(
fs.read_link("Cargo.toml").err().unwrap(),
&normalize_path(fs.current_dir(), "Cargo.toml"),
);
}
}
mod create_hard_link {
use super::*;
#[test]
fn create_hard_link_should_share_file() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
fs.create_hard_link("b.txt", "a.txt").unwrap();
fs.write("b.txt", "bbb").unwrap();
fs.set_readonly("b.txt", true).unwrap();
let metadata = fs.metadata("a.txt").unwrap();
assert_eq!(fs.read_to_string("a.txt").unwrap(), "bbb");
assert!(metadata.readonly);
fs.set_readonly("a.txt", false).unwrap();
}
#[test]
fn create_hard_link_symlink_target_should_link_file() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
fs.create_symlink("link", "a.txt").unwrap();
fs.create_hard_link("b.txt", "link").unwrap();
assert_eq!(
fs.symlink_metadata("b.txt").unwrap().file_type,
FileType::File
);
}
#[test]
fn create_hard_link_existing_path_should_fail() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
assert_os_error(
fs.create_hard_link("a.txt", "a.txt").err().unwrap(),
&normalize_path(fs.current_dir(), "a.txt"),
);
}
#[test]
fn create_hard_link_without_parent_dir_should_fail() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
for path in [
"fs_test_create_hard_link_missing.tmp/link",
"Cargo.toml/link",
] {
assert_os_error(
fs.create_hard_link(path, "Cargo.toml").err().unwrap(),
&normalize_path(fs.current_dir(), path),
);
}
}
#[test]
fn create_hard_link_nonexistent_target_should_fail() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
assert_os_error(
fs.create_hard_link("fs_test_create_hard_link_dangling.tmp", "missing.tmp")
.err()
.unwrap(),
&normalize_path(fs.current_dir(), "missing.tmp"),
);
}
#[test]
fn create_hard_link_dir_target_should_fail() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
assert_error(
&fs.create_hard_link("fs_test_create_hard_link_dir.tmp", "src")
.err()
.unwrap(),
fs_error!(
PermissionDenied;
"Hard link could not be created, target is a directory: {}",
normalize_path(fs.current_dir(), "src").display()
),
);
}
}
mod copy {
use super::*;
#[cfg(target_os = "linux")]
#[test]
fn copy_failing_should_not_make_target_more_accessible_than_source() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
assert!(fs.copy("/proc/self/mem", "copy").is_err());
assert_eq!(mode(&test_dir.path().join("copy")) & 0o077, 0);
}
#[test]
fn copy_hard_linked_file_should_create_independent_file() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
fs.create_hard_link("b.txt", "a.txt").unwrap();
fs.copy("b.txt", "c.txt").unwrap();
fs.write("c.txt", "ccc").unwrap();
assert_eq!(fs.read_to_string("a.txt").unwrap(), "aaa");
assert_eq!(fs.read_to_string("c.txt").unwrap(), "ccc");
}
#[test]
fn copy_empty_dir_should_create_empty_dir() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir").unwrap();
fs.copy("dir", "copy").unwrap();
assert!(fs.metadata("copy").unwrap().file_type.is_dir());
assert!(fs.read_dir("copy").unwrap().next().is_none());
}
#[cfg(unix)]
#[test]
fn copy_should_copy_mode() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
fs.create_dir_all("dir").unwrap();
fs.set_mode("a.txt", 0o4750).unwrap();
fs.set_mode("dir", 0o700).unwrap();
fs.copy("a.txt", "b.txt").unwrap();
fs.copy("dir", "copy").unwrap();
assert_eq!(fs.metadata("b.txt").unwrap().mode, 0o4750);
assert_eq!(fs.metadata("copy").unwrap().mode, 0o700);
}
#[cfg(windows)]
#[test]
fn copy_should_copy_readonly_attribute() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
fs.set_readonly("a.txt", true).unwrap();
fs.copy("a.txt", "b.txt").unwrap();
assert!(fs.metadata("b.txt").unwrap().readonly);
fs.set_readonly("a.txt", false).unwrap();
fs.set_readonly("b.txt", false).unwrap();
}
#[test]
fn copy_symlink_should_create_symlink_with_same_target() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir").unwrap();
fs.write("a.txt", "aaa").unwrap();
fs.create_symlink("file_link", "a.txt").unwrap();
fs.create_symlink("dir_link", "dir").unwrap();
for (source, target) in [("file_link", "file_copy"), ("dir_link", "dir_copy")] {
fs.copy(source, target).unwrap();
assert_eq!(
fs.read_link(target).unwrap(),
fs.read_link(source).unwrap(),
"source: {source}"
);
}
assert_eq!(fs.read_to_string("file_copy").unwrap(), "aaa");
assert!(fs.metadata("dir_copy").unwrap().file_type.is_dir());
}
#[test]
fn copy_symlink_should_resolve_against_target_parent() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir").unwrap();
fs.write("dir/a.txt", "aaa").unwrap();
fs.create_symlink("dir/link", "a.txt").unwrap();
assert_os_error(
fs.copy("dir/link", "link").err().unwrap(),
&normalize_path(fs.current_dir(), "a.txt"),
);
}
#[test]
fn copy_should_allow_read_borrowed_source() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
let _read = fs.open_read("a.txt").unwrap();
fs.copy("a.txt", "b.txt").unwrap();
assert_eq!(fs.read_to_string("b.txt").unwrap(), "aaa");
}
#[test]
fn copy_write_borrowed_source_should_fail() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
let _write = fs.open_write("a.txt").unwrap();
assert_error(
&fs.copy("a.txt", "b.txt").err().unwrap(),
fs_error!(
ResourceBusy;
"File could not be opened, it is already borrowed for writing: {}",
normalize_path(fs.current_dir(), "a.txt").display()
),
);
assert!(!fs.exists("b.txt").unwrap());
}
#[test]
fn copy_file_into_dir_being_changed_should_fail() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir("dir").unwrap();
fs.write("a.txt", "aaa").unwrap();
let _change = changing(&fs, "dir");
assert_error(
&fs.copy("a.txt", "dir/a.txt").err().unwrap(),
changing_error(&fs, "dir/a.txt", "opened"),
);
}
#[test]
fn copy_file_should_overwrite_existing_file() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
fs.write("b.txt", "longer content").unwrap();
fs.copy("a.txt", "b.txt").unwrap();
assert_eq!(fs.read_to_string("b.txt").unwrap(), "aaa");
}
#[test]
fn copy_file_to_symlink_should_overwrite_its_target() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
fs.write("b.txt", "bbb").unwrap();
fs.create_symlink("link", "b.txt").unwrap();
fs.write("c.txt", "").unwrap();
fs.create_symlink("dangling", "c.txt").unwrap();
fs.remove("c.txt").unwrap();
fs.copy("a.txt", "link").unwrap();
fs.copy("a.txt", "dangling").unwrap();
assert_eq!(fs.read_to_string("b.txt").unwrap(), "aaa");
assert_eq!(fs.read_to_string("c.txt").unwrap(), "aaa");
assert!(fs.symlink_metadata("link").unwrap().file_type.is_symlink());
}
#[test]
fn copy_file_to_dir_should_fail() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
fs.create_dir("dir").unwrap();
assert_os_error(
fs.copy("a.txt", "dir").err().unwrap(),
&normalize_path(fs.current_dir(), "dir"),
);
}
#[test]
fn copy_file_to_other_name_of_itself_should_fail() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
fs.create_hard_link("b.txt", "a.txt").unwrap();
assert_error(
&fs.copy("a.txt", "b.txt").err().unwrap(),
fs_error!(
ResourceBusy;
"File could not be opened, it is already borrowed for reading 1 times: {}",
normalize_path(fs.current_dir(), "b.txt").display()
),
);
assert_eq!(fs.read_to_string("a.txt").unwrap(), "aaa");
}
#[test]
fn copy_dir_or_symlink_to_existing_target_should_fail() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
fs.write("b.txt", "bbb").unwrap();
fs.create_dir("dir").unwrap();
fs.create_symlink("link", "a.txt").unwrap();
for source in ["dir", "link"] {
assert_eq!(
described(&fs.copy(source, "b.txt").err().unwrap()),
described(&fs_error!(
AlreadyExists;
"Path could not be copied, target already exists: {}",
normalize_path(fs.current_dir(), "b.txt").display()
)),
"source: {source}"
);
}
assert_eq!(fs.read_to_string("b.txt").unwrap(), "bbb");
}
#[test]
fn copy_without_target_parent_dir_should_fail() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
for target in ["missing/b.txt", "a.txt/b.txt"] {
assert_eq!(
described(&fs.copy("a.txt", target).err().unwrap()),
described(&fs_error!(
NotFound;
"Path could not be copied, parent directory does not exist: {}",
normalize_path(fs.current_dir(), target).display()
)),
"target: {target}"
);
}
}
#[test]
fn copy_nonexistent_source_should_fail() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
assert_os_error(
fs.copy("missing.txt", "b.txt").err().unwrap(),
&normalize_path(fs.current_dir(), "missing.txt"),
);
}
#[test]
fn copy_non_empty_dir_should_fail() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir/sub").unwrap();
assert_error(
&fs.copy("dir", "copy").err().unwrap(),
fs_error!(
DirectoryNotEmpty;
"Directory could not be copied, it is not empty: {}",
normalize_path(fs.current_dir(), "dir").display()
),
);
assert!(!fs.exists("copy").unwrap());
}
#[cfg(unix)]
#[test]
fn copy_should_fail_for_unsupported_file_type() {
let test_dir = TestDir::temp();
let fs = FsImpl::new(test_dir.path().to_path_buf());
let path = normalize_path(fs.current_dir(), "socket");
let _listener = std::os::unix::net::UnixListener::bind(&path).unwrap();
assert_error(
&fs.copy("socket", "copy").err().unwrap(),
fs_error!(
Unsupported;
"Unsupported file type, path is neither a file, a directory nor a symlink: {}",
path.display()
),
);
}
}
mod copy_dir_all {
use super::*;
#[test]
fn copy_dir_all_should_copy_tree() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir/sub").unwrap();
fs.create_dir_all("dir/empty").unwrap();
fs.write("dir/a.txt", "aaa").unwrap();
fs.write("dir/sub/b.txt", "bbb").unwrap();
fs.create_symlink("dir/link", "sub/b.txt").unwrap();
fs.create_symlink("dir/sub/up", "../empty").unwrap();
fs.copy_dir_all("dir", "copy").unwrap();
fs.write("copy/sub/b.txt", "ccc").unwrap();
assert_eq!(fs.read_to_string("copy/a.txt").unwrap(), "aaa");
assert_eq!(fs.read_to_string("copy/link").unwrap(), "ccc");
assert_eq!(fs.read_to_string("dir/sub/b.txt").unwrap(), "bbb");
assert_eq!(
fs.read_link("copy/sub/up").unwrap(),
native_symlink_target(Path::new("../empty"))
);
assert!(fs.read_dir("copy/empty").unwrap().next().is_none());
}
#[test]
fn copy_dir_all_symlink_to_symlink_should_be_copied_after_it() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir").unwrap();
fs.write("dir/z.txt", "zzz").unwrap();
fs.create_symlink("dir/b_link", "z.txt").unwrap();
fs.create_symlink("dir/a_link", "b_link").unwrap();
fs.create_symlink("dir/c_link", "a_link").unwrap();
fs.copy_dir_all("dir", "copy").unwrap();
assert_eq!(fs.read_to_string("copy/c_link").unwrap(), "zzz");
assert_eq!(fs.read_link("copy/c_link").unwrap(), Path::new("a_link"));
}
#[cfg(unix)]
#[test]
fn copy_dir_all_should_copy_dir_mode_after_content() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir/sub").unwrap();
fs.write("dir/sub/a.txt", "aaa").unwrap();
fs.set_mode("dir/sub", 0o555).unwrap();
fs.set_mode("dir", 0o750).unwrap();
let result = fs.copy_dir_all("dir", "copy");
let modes = (
fs.metadata("copy").map(|metadata| metadata.mode),
fs.metadata("copy/sub").map(|metadata| metadata.mode),
);
for path in ["dir/sub", "copy/sub"] {
let _ = fs.set_mode(path, 0o755);
}
result.unwrap();
assert_eq!((modes.0.unwrap(), modes.1.unwrap()), (0o750, 0o555));
assert_eq!(fs.read_to_string("copy/sub/a.txt").unwrap(), "aaa");
}
#[cfg(windows)]
#[test]
fn copy_dir_all_should_copy_dir_readonly_attribute() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir/sub").unwrap();
fs.write("dir/sub/a.txt", "aaa").unwrap();
fs.set_readonly("dir/sub", true).unwrap();
let result = fs.copy_dir_all("dir", "copy");
let readonly = fs.metadata("copy/sub").map(|metadata| metadata.readonly);
for path in ["dir/sub", "copy/sub"] {
let _ = fs.set_readonly(path, false);
}
result.unwrap();
assert!(readonly.unwrap());
}
#[test]
fn copy_dir_all_target_inside_source_should_copy_source_as_listed() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir").unwrap();
fs.write("dir/a.txt", "aaa").unwrap();
fs.copy_dir_all("dir", "dir/copy").unwrap();
assert_eq!(fs.read_to_string("dir/copy/a.txt").unwrap(), "aaa");
assert!(!fs.exists("dir/copy/copy").unwrap());
}
#[test]
fn copy_dir_all_should_check_parent_of_normalized_target() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir").unwrap();
fs.write("dir/a.txt", "aaa").unwrap();
fs.copy_dir_all("dir", "copy/missing/..").unwrap();
assert_eq!(fs.read_to_string("copy/a.txt").unwrap(), "aaa");
}
#[test]
fn copy_dir_all_file_source_should_fail() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
assert_error(
&fs.copy_dir_all("a.txt", "copy").err().unwrap(),
fs_error!(
NotADirectory;
"Directory could not be copied, source is not a directory: {}",
normalize_path(fs.current_dir(), "a.txt").display()
),
);
}
#[test]
fn copy_dir_all_nonexistent_source_should_fail() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
assert_os_error(
fs.copy_dir_all("missing", "copy").err().unwrap(),
&normalize_path(fs.current_dir(), "missing"),
);
}
#[test]
fn copy_dir_all_existing_target_should_fail() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir").unwrap();
fs.write("a.txt", "aaa").unwrap();
for target in ["a.txt", "dir"] {
assert_eq!(
described(&fs.copy_dir_all("dir", target).err().unwrap()),
described(&fs_error!(
AlreadyExists;
"Directory could not be copied, target already exists: {}",
normalize_path(fs.current_dir(), target).display()
)),
"target: {target}"
);
}
}
#[test]
fn copy_dir_all_without_target_parent_dir_should_fail() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir").unwrap();
fs.write("a.txt", "aaa").unwrap();
for target in ["missing/copy", "a.txt/copy"] {
assert_eq!(
described(&fs.copy_dir_all("dir", target).err().unwrap()),
described(&fs_error!(
NotFound;
"Directory could not be copied, parent directory does not exist: {}",
normalize_path(fs.current_dir(), target).display()
)),
"target: {target}"
);
}
}
#[test]
fn copy_dir_all_unresolvable_symlink_should_fail() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir").unwrap();
fs.create_dir_all("other").unwrap();
fs.write("a.txt", "aaa").unwrap();
fs.create_symlink("dir/link", "../a.txt").unwrap();
assert_os_error(
fs.copy_dir_all("dir", "other/copy").err().unwrap(),
&normalize_path(fs.current_dir(), "other/copy")
.join(native_symlink_target(Path::new("../a.txt"))),
);
assert!(fs.exists("other/copy").unwrap());
}
}
mod remove {
use super::*;
#[cfg(unix)]
#[test]
fn remove_with_trailing_separator_should_keep_symlink_to_dir() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir("dir").unwrap();
fs.create_symlink("link", "dir").unwrap();
assert_eq!(
io_kind(&fs.remove("link/").err().unwrap()),
Some(std::io::ErrorKind::NotADirectory)
);
assert!(test_dir.path().join("dir").is_dir());
assert!(test_dir.path().join("link").is_symlink());
}
#[test]
fn remove_should_remove_file_and_empty_dir() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir").unwrap();
fs.write("a.txt", "aaa").unwrap();
fs.set_readonly("a.txt", true).unwrap();
for path in ["a.txt", "dir"] {
fs.remove(path).unwrap();
assert!(fs.symlink_metadata(path).is_err(), "path: {path}");
}
}
#[test]
fn remove_symlink_should_keep_target() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir").unwrap();
fs.write("dir/a.txt", "aaa").unwrap();
fs.create_symlink("file_link", "dir/a.txt").unwrap();
fs.create_symlink("dir_link", "dir").unwrap();
for path in ["file_link", "dir_link"] {
fs.remove(path).unwrap();
assert!(fs.symlink_metadata(path).is_err(), "path: {path}");
}
assert_eq!(fs.read_to_string("dir/a.txt").unwrap(), "aaa");
}
#[test]
fn remove_hard_link_should_keep_other_name() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
fs.create_hard_link("b.txt", "a.txt").unwrap();
fs.remove("a.txt").unwrap();
assert!(!fs.exists("a.txt").unwrap());
assert_eq!(fs.read_to_string("b.txt").unwrap(), "aaa");
}
#[test]
fn remove_borrowed_file_should_fail() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
fs.create_hard_link("b.txt", "a.txt").unwrap();
let path = normalize_path(fs.current_dir(), "b.txt");
let read = fs.open_read("a.txt").unwrap();
assert_error(
&fs.remove("b.txt").err().unwrap(),
fs_error!(
ResourceBusy;
"File could not be removed, it is already borrowed for reading 1 times: {}",
path.display()
),
);
drop(read);
let _write = fs.open_write("a.txt").unwrap();
assert_error(
&fs.remove("b.txt").err().unwrap(),
fs_error!(
ResourceBusy;
"File could not be removed, it is already borrowed for writing: {}",
path.display()
),
);
assert!(fs.exists("b.txt").unwrap());
}
#[test]
fn remove_should_ignore_files_borrowed_elsewhere() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
fs.write("b.txt", "bbb").unwrap();
let _read = fs.open_read("b.txt").unwrap();
fs.remove("a.txt").unwrap();
assert!(!fs.exists("a.txt").unwrap());
}
#[test]
fn remove_path_being_changed_should_fail() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
let _change = changing(&fs, "a.txt");
assert_error(
&fs.remove("a.txt").err().unwrap(),
changing_error(&fs, "a.txt", "removed"),
);
assert!(fs.exists("a.txt").unwrap());
}
#[test]
fn remove_non_empty_dir_should_fail() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
assert_os_error(
fs.remove(".").err().unwrap(),
&normalize_path(fs.current_dir(), "."),
);
}
#[test]
fn remove_should_fail_for_nonexistent_path() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
assert_os_error(
fs.remove("nonexistent.tmp").err().unwrap(),
&normalize_path(fs.current_dir(), "nonexistent.tmp"),
);
}
#[cfg(unix)]
#[test]
fn remove_should_fail_for_unsupported_file_type() {
let test_dir = TestDir::temp();
let fs = FsImpl::new(test_dir.path().to_path_buf());
let path = normalize_path(fs.current_dir(), "socket");
let _listener = std::os::unix::net::UnixListener::bind(&path).unwrap();
assert_error(
&fs.remove("socket").err().unwrap(),
fs_error!(
Unsupported;
"Unsupported file type, path is neither a file, a directory nor a symlink: {}",
path.display()
),
);
}
}
mod remove_dir_all {
use super::*;
#[test]
fn remove_dir_all_empty_path_should_fail_and_keep_current_dir() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
assert!(fs.remove_dir_all("").is_err());
assert_eq!(fs.read_to_string("a.txt").unwrap(), "aaa");
}
#[test]
fn remove_dir_all_should_remove_tree_but_keep_symlink_targets() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir/sub/empty").unwrap();
fs.create_dir_all("outside").unwrap();
fs.write("dir/a.txt", "aaa").unwrap();
fs.write("dir/sub/b.txt", "bbb").unwrap();
fs.write("outside/c.txt", "ccc").unwrap();
fs.set_readonly("dir/a.txt", true).unwrap();
fs.create_symlink("dir/file_link", "../outside/c.txt")
.unwrap();
fs.create_symlink("dir/sub/dir_link", "../../outside")
.unwrap();
fs.remove_dir_all("dir").unwrap();
assert!(fs.symlink_metadata("dir").is_err());
assert_eq!(fs.read_to_string("outside/c.txt").unwrap(), "ccc");
}
#[test]
fn remove_dir_all_symlink_should_remove_only_symlink() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir").unwrap();
fs.write("dir/a.txt", "aaa").unwrap();
fs.create_symlink("link", "dir").unwrap();
fs.remove_dir_all("link").unwrap();
assert!(fs.symlink_metadata("link").is_err());
assert_eq!(fs.read_to_string("dir/a.txt").unwrap(), "aaa");
}
#[cfg(windows)]
#[test]
fn remove_dir_all_symlink_to_file_should_remove_only_symlink() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
fs.create_symlink("link", "a.txt").unwrap();
fs.remove_dir_all("link").unwrap();
assert!(fs.symlink_metadata("link").is_err());
assert_eq!(fs.read_to_string("a.txt").unwrap(), "aaa");
}
#[test]
fn remove_dir_all_file_should_fail() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
assert_os_error(
fs.remove_dir_all("a.txt").err().unwrap(),
&normalize_path(fs.current_dir(), "a.txt"),
);
assert!(fs.exists("a.txt").unwrap());
}
#[test]
fn remove_dir_all_borrowed_file_should_fail() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir").unwrap();
fs.write("dir/a.txt", "aaa").unwrap();
let _write = fs.open_write("dir/a.txt").unwrap();
assert_error(
&fs.remove_dir_all("dir").err().unwrap(),
fs_error!(
ResourceBusy;
"File could not be removed, it is already borrowed for writing: {}",
normalize_path(fs.current_dir(), "dir")
.join("a.txt")
.display()
),
);
assert!(fs.exists("dir/a.txt").unwrap());
}
#[test]
fn remove_dir_all_should_fail_for_nonexistent_path() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
assert_os_error(
fs.remove_dir_all("dir").err().unwrap(),
&normalize_path(fs.current_dir(), "dir"),
);
}
#[test]
fn remove_dir_all_should_ignore_files_borrowed_outside_tree() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir/sub").unwrap();
fs.write("dir/sub/a.txt", "aaa").unwrap();
fs.write("b.txt", "bbb").unwrap();
fs.create_symlink("dir/link", "../b.txt").unwrap();
let _read = fs.open_read("b.txt").unwrap();
fs.remove_dir_all("dir").unwrap();
assert!(!fs.exists("dir").unwrap());
}
#[test]
fn remove_dir_all_should_find_borrowed_file_renamed_into_tree() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir("dir").unwrap();
fs.write("a.txt", "aaa").unwrap();
let _read = fs.open_read("a.txt").unwrap();
fs.rename("a.txt", "dir/a.txt").unwrap();
assert_error(
&fs.remove_dir_all("dir").err().unwrap(),
fs_error!(
ResourceBusy;
"File could not be removed, it is already borrowed for reading 1 times: {}",
normalize_path(fs.current_dir(), "dir/a.txt").display()
),
);
assert!(fs.exists("dir/a.txt").unwrap());
}
#[test]
fn remove_dir_all_dir_with_path_being_changed_should_fail() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir/sub").unwrap();
let _change = changing(&fs, "dir/sub");
assert_error(
&fs.remove_dir_all("dir").err().unwrap(),
changing_error(&fs, "dir", "removed"),
);
assert!(fs.exists("dir/sub").unwrap());
}
#[test]
fn remove_dir_all_hard_link_to_borrowed_file_should_fail() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir/sub").unwrap();
fs.write("dir/a.txt", "aaa").unwrap();
fs.write("b.txt", "bbb").unwrap();
fs.create_hard_link("dir/sub/link", "b.txt").unwrap();
let _read = fs.open_read("b.txt").unwrap();
assert_error(
&fs.remove_dir_all("dir").err().unwrap(),
fs_error!(
ResourceBusy;
"File could not be removed, it is already borrowed for reading 1 times: {}",
normalize_path(fs.current_dir(), "dir")
.join("sub")
.join("link")
.display()
),
);
assert!(fs.exists("dir/a.txt").unwrap());
}
#[cfg(unix)]
#[test]
fn remove_dir_all_should_remove_unsupported_entries() {
let test_dir = TestDir::temp();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir").unwrap();
let _listener = std::os::unix::net::UnixListener::bind(normalize_path(
fs.current_dir(),
"dir/socket",
))
.unwrap();
fs.remove_dir_all("dir").unwrap();
assert!(!fs.exists("dir").unwrap());
}
}
mod rename {
use super::*;
#[test]
fn rename_borrowed_file_should_keep_borrow_under_new_name() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
let mut read = fs.open_read("a.txt").unwrap();
fs.rename("a.txt", "b.txt").unwrap();
assert!(!fs.exists("a.txt").unwrap());
assert!(fs.open_write("b.txt").is_err());
let mut content = String::new();
read.read_to_string(&mut content).unwrap();
assert_eq!(content, "aaa");
}
#[test]
fn rename_dir_should_move_tree() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir/sub").unwrap();
fs.create_dir_all("other").unwrap();
fs.write("dir/sub/a.txt", "aaa").unwrap();
fs.create_symlink("dir/link", "sub/a.txt").unwrap();
fs.rename("dir", "other/moved").unwrap();
assert!(!fs.exists("dir").unwrap());
assert_eq!(fs.read_to_string("other/moved/link").unwrap(), "aaa");
}
#[test]
fn rename_symlink_should_move_only_symlink() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir").unwrap();
fs.write("a.txt", "aaa").unwrap();
fs.create_symlink("link", "a.txt").unwrap();
fs.rename("link", "dir/link").unwrap();
assert_eq!(fs.read_link("dir/link").unwrap(), Path::new("a.txt"));
assert_eq!(fs.read_to_string("a.txt").unwrap(), "aaa");
}
#[test]
fn rename_file_should_replace_file() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
fs.write("b.txt", "bbb").unwrap();
fs.rename("a.txt", "b.txt").unwrap();
assert!(!fs.exists("a.txt").unwrap());
assert_eq!(fs.read_to_string("b.txt").unwrap(), "aaa");
}
#[test]
fn rename_hard_link_onto_same_file_should_remove_source() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir").unwrap();
fs.write("a.txt", "aaa").unwrap();
fs.create_hard_link("b.txt", "a.txt").unwrap();
fs.create_hard_link("dir/c.txt", "a.txt").unwrap();
let mut read = fs.open_read("a.txt").unwrap();
for (source, target) in [("a.txt", "b.txt"), ("b.txt", "dir/c.txt")] {
fs.rename(source, target).unwrap();
assert!(!fs.exists(source).unwrap(), "source: {source}");
}
assert_eq!(fs.read_to_string("dir/c.txt").unwrap(), "aaa");
let mut content = String::new();
read.read_to_string(&mut content).unwrap();
assert_eq!(content, "aaa");
}
#[test]
fn rename_to_itself_should_do_nothing() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir").unwrap();
fs.write("dir/a.txt", "aaa").unwrap();
fs.create_symlink("link", "dir").unwrap();
let _read = fs.open_read("dir/a.txt").unwrap();
for (source, target) in [
("dir/a.txt", "dir/a.txt"),
("dir/a.txt", "dir/../dir/a.txt"),
("link/a.txt", "dir/a.txt"),
] {
fs.rename(source, target).unwrap();
assert_eq!(
fs.read_to_string("dir/a.txt").unwrap(),
"aaa",
"source: {source}, target: {target}"
);
}
}
#[test]
fn rename_should_replace_target_as_std() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir/sub").unwrap();
fs.create_dir_all("empty").unwrap();
fs.write("a.txt", "aaa").unwrap();
fs.write("b.txt", "bbb").unwrap();
fs.write("c.txt", "").unwrap();
fs.create_symlink("link", "b.txt").unwrap();
fs.create_symlink("dangling", "c.txt").unwrap();
fs.remove("c.txt").unwrap();
fs.rename("a.txt", "dangling").unwrap();
fs.rename("link", "b.txt").unwrap();
fs.rename("dir", "empty").unwrap();
assert_eq!(fs.read_to_string("dangling").unwrap(), "aaa");
assert_eq!(fs.read_link("b.txt").unwrap(), Path::new("b.txt"));
assert!(fs.exists("empty/sub").unwrap());
}
#[test]
fn rename_incompatible_target_should_fail() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir").unwrap();
fs.create_dir_all("full/sub").unwrap();
fs.write("a.txt", "aaa").unwrap();
fs.write("b.txt", "").unwrap();
fs.create_symlink("dangling", "b.txt").unwrap();
fs.remove("b.txt").unwrap();
for (source, target, error) in [
(
"dir",
"a.txt",
"Directory could not be renamed, target is not a directory",
),
(
"dir",
"dangling",
"Directory could not be renamed, target is not a directory",
),
(
"a.txt",
"dir",
"Path could not be renamed, target is a directory",
),
(
"dangling",
"dir",
"Path could not be renamed, target is a directory",
),
] {
assert_eq!(
fs.rename(source, target)
.expect_err(&format!("source: {source}, target: {target}"))
.to_string(),
fs_error!(
"{error}: {}",
normalize_path(fs.current_dir(), target).display()
)
.to_string(),
"source: {source}, target: {target}"
);
}
assert_rename_os_error(
fs.rename("dir", "full")
.expect_err("source: dir, target: full"),
&normalize_path(fs.current_dir(), "dir"),
&normalize_path(fs.current_dir(), "full"),
);
}
#[test]
fn rename_borrowed_target_should_fail() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
fs.write("b.txt", "bbb").unwrap();
let _read = fs.open_read("b.txt").unwrap();
assert_error(
&fs.rename("a.txt", "b.txt").err().unwrap(),
fs_error!(
ResourceBusy;
"File could not be replaced, it is already borrowed for reading 1 times: {}",
normalize_path(fs.current_dir(), "b.txt").display()
),
);
assert!(fs.exists("a.txt").unwrap());
}
#[test]
fn rename_into_dir_being_changed_should_fail() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir("dir").unwrap();
fs.write("a.txt", "aaa").unwrap();
let _change = changing(&fs, "dir");
assert_error(
&fs.rename("a.txt", "dir/a.txt").err().unwrap(),
changing_error(&fs, "dir/a.txt", "renamed"),
);
assert!(fs.exists("a.txt").unwrap());
}
#[test]
fn rename_onto_symlink_to_borrowed_file_should_replace_symlink() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
fs.write("b.txt", "bbb").unwrap();
fs.create_symlink("link", "b.txt").unwrap();
let _read = fs.open_read("b.txt").unwrap();
fs.rename("a.txt", "link").unwrap();
assert_eq!(fs.read_to_string("link").unwrap(), "aaa");
assert_eq!(fs.read_to_string("b.txt").unwrap(), "bbb");
}
#[test]
fn rename_without_target_parent_dir_should_fail() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
for target in ["missing/b.txt", "a.txt/b.txt"] {
assert_rename_os_error(
fs.rename("a.txt", target).err().unwrap(),
&normalize_path(fs.current_dir(), "a.txt"),
&normalize_path(fs.current_dir(), target),
);
}
}
#[test]
fn rename_dir_into_itself_should_fail() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.create_dir_all("dir/sub").unwrap();
assert_rename_os_error(
fs.rename("dir", "dir/sub/moved").err().unwrap(),
&normalize_path(fs.current_dir(), "dir"),
&normalize_path(fs.current_dir(), "dir/sub/moved"),
);
assert!(fs.exists("dir/sub").unwrap());
}
#[test]
fn rename_nonexistent_source_should_fail() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
assert_rename_os_error(
fs.rename("a.txt", "b.txt").err().unwrap(),
&normalize_path(fs.current_dir(), "a.txt"),
&normalize_path(fs.current_dir(), "b.txt"),
);
}
}
mod set_readonly {
use super::*;
#[cfg(unix)]
#[test]
fn set_readonly_should_map_to_write_bits() {
let test_dir = TestDir::new();
let path = test_dir.path().join("fs_test_set_readonly.tmp");
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("fs_test_set_readonly.tmp", "").unwrap();
for (initial, readonly, expected) in [
(0o644, true, 0o444),
(0o755, true, 0o555),
(0o444, false, 0o644),
(0o664, false, 0o664),
] {
set_mode(&path, initial);
fs.set_readonly("fs_test_set_readonly.tmp", readonly)
.unwrap();
assert_eq!(
mode(&path),
expected,
"initial: {initial:o}, readonly: {readonly}"
);
}
}
#[cfg(windows)]
#[test]
fn set_readonly_unchanged_should_succeed() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("fs_test_set_readonly_unchanged.tmp", "").unwrap();
fs.set_readonly("fs_test_set_readonly_unchanged.tmp", false)
.unwrap();
assert!(
!fs.metadata("fs_test_set_readonly_unchanged.tmp")
.unwrap()
.readonly
);
}
#[cfg(windows)]
#[test]
fn set_readonly_false_without_other_attributes_should_set_normal() {
use super::windows::FILE_ATTRIBUTE_NORMAL;
use super::windows::FILE_ATTRIBUTE_READONLY;
use std::os::windows::ffi::OsStrExt;
use std::os::windows::fs::MetadataExt;
#[link(name = "kernel32")]
unsafe extern "system" {
fn SetFileAttributesW(file_name: *const u16, file_attributes: u32) -> i32;
}
let test_dir = TestDir::new();
let path = test_dir.path().join("fs_test_set_readonly_normal.tmp");
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("fs_test_set_readonly_normal.tmp", "").unwrap();
let file_name: Vec<u16> = path
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect();
assert_ne!(
unsafe { SetFileAttributesW(file_name.as_ptr(), FILE_ATTRIBUTE_READONLY) },
0
);
fs.set_readonly("fs_test_set_readonly_normal.tmp", false)
.unwrap();
assert_eq!(
std::fs::metadata(&path).unwrap().file_attributes(),
FILE_ATTRIBUTE_NORMAL
);
}
#[cfg(windows)]
#[test]
fn set_readonly_should_set_readonly_attribute() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("fs_test_set_readonly.tmp", "").unwrap();
for readonly in [true, false] {
fs.set_readonly("fs_test_set_readonly.tmp", readonly)
.unwrap();
assert_eq!(
fs.metadata("fs_test_set_readonly.tmp").unwrap().readonly,
readonly
);
}
}
}
#[cfg(unix)]
mod set_executable {
use super::*;
#[test]
fn set_executable_file_should_map_to_execute_bits() {
let test_dir = TestDir::new();
let path = test_dir.path().join("fs_test_set_executable_file.tmp");
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("fs_test_set_executable_file.tmp", "").unwrap();
for (initial, executable, expected) in [
(0o644, true, 0o755),
(0o600, true, 0o700),
(0o000, true, 0o100),
(0o744, true, 0o744),
(0o755, false, 0o644),
] {
set_mode(&path, initial);
fs.set_executable("fs_test_set_executable_file.tmp", executable)
.unwrap();
assert_eq!(
mode(&path),
expected,
"initial: {initial:o}, executable: {executable}"
);
}
}
#[test]
fn set_executable_dir_should_fail() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
assert_error(
&fs.set_executable(".", true).err().unwrap(),
fs_error!(
IsADirectory;
"Executable attribute cannot be set, path is a directory: {}",
normalize_path(fs.current_dir(), ".").display()
),
);
}
}
#[cfg(windows)]
mod set_hidden {
use super::*;
#[test]
fn set_hidden_should_set_hidden_attribute() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("fs_test_set_hidden.tmp", "").unwrap();
for hidden in [true, false] {
fs.set_hidden("fs_test_set_hidden.tmp", hidden).unwrap();
assert_eq!(
fs.metadata("fs_test_set_hidden.tmp").unwrap().hidden,
hidden
);
}
}
}
mod set_modified {
use super::*;
#[cfg(unix)]
#[test]
fn set_modified_should_set_time_of_file_that_cannot_be_read() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "").unwrap();
fs.set_mode("a.txt", 0o200).unwrap();
let modified = SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1000);
fs.set_modified("a.txt", modified).unwrap();
assert_eq!(fs.metadata("a.txt").unwrap().modified, modified);
}
#[test]
fn set_modified_should_set_modified_time() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "").unwrap();
let modified = SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1000);
for path in ["a.txt", "."] {
fs.set_modified(path, modified).unwrap();
assert_eq!(
fs.metadata(path).unwrap().modified,
modified,
"path: {path}"
);
}
}
#[test]
fn set_modified_should_fail_for_nonexistent_path() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
assert_os_error(
fs.set_modified("nonexistent.tmp", SystemTime::UNIX_EPOCH)
.err()
.unwrap(),
&normalize_path(fs.current_dir(), "nonexistent.tmp"),
);
}
#[cfg(unix)]
#[test]
fn set_modified_should_fail_for_unsupported_file_type() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
assert_error(
&fs.set_modified("/dev/null", SystemTime::UNIX_EPOCH)
.err()
.unwrap(),
fs_error!(
Unsupported;
"Unsupported file type, path is neither a file, a directory nor a symlink: /dev/null"
),
);
}
}
#[cfg(unix)]
mod set_mode {
use super::*;
#[test]
fn set_mode_should_set_permission_bits() {
let test_dir = TestDir::new();
let path = test_dir.path().join("fs_test_set_mode.tmp");
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("fs_test_set_mode.tmp", "").unwrap();
for (requested, expected) in
[(0o600, 0o600), (0o4750, 0o4750), (0o100644, 0o644)]
{
fs.set_mode("fs_test_set_mode.tmp", requested).unwrap();
assert_eq!(mode(&path), expected, "requested: {requested:o}");
}
}
#[test]
fn set_mode_should_fail_for_nonexistent_path() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
assert_os_error(
fs.set_mode("nonexistent.tmp", 0o600).err().unwrap(),
&normalize_path(fs.current_dir(), "nonexistent.tmp"),
);
}
#[test]
fn set_mode_should_set_mode_of_special_file() {
let test_dir = TestDir::temp();
let fs = FsImpl::new(test_dir.path().to_path_buf());
let path = test_dir.path().join("socket");
let _socket = std::os::unix::net::UnixListener::bind(&path).unwrap();
fs.set_mode("socket", 0o600).unwrap();
assert_eq!(mode(&path), 0o600);
}
}
}
mod set_attribute {
use super::*;
#[test]
fn set_attribute_should_fail_for_nonexistent_path() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
assert_os_error(
fs.set_attribute(Path::new("nonexistent.tmp"), Attribute::Readonly, true)
.err()
.unwrap(),
&normalize_path(fs.current_dir(), "nonexistent.tmp"),
);
}
#[cfg(unix)]
#[test]
fn set_attribute_should_fail_for_unsupported_file_type() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
assert_error(
&fs.set_attribute(Path::new("/dev/null"), Attribute::Readonly, true)
.err()
.unwrap(),
fs_error!(
Unsupported;
"Unsupported file type, path is neither a file, a directory nor a symlink: /dev/null"
),
);
}
}
}
mod file_handle {
use super::*;
mod drop {
use super::*;
#[test]
fn drop_should_not_panic_when_file_not_in_handles() {
let handle = FileHandle {
borrows: Arc::new(Mutex::new(Borrows::default())),
key: 0,
file: Arc::new(std::fs::File::open("Cargo.toml").unwrap()),
path: Path::new("Cargo.toml").into(),
};
drop(handle);
}
}
}
mod file_read_handle {
use super::*;
#[test]
fn file_read_handle_should_be_usable_as_struct_field() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
struct S<Handle: Read + Seek> {
file: Handle,
}
let mut s = S {
file: fs.open_read("Cargo.toml").unwrap(),
};
let mut buffer = vec![0_u8; 1];
let read = s.file.read(&mut buffer).unwrap();
assert_eq!(read, buffer.len());
}
mod read_file {
use super::*;
mod metadata {
use super::*;
#[test]
fn metadata_should_return_metadata_of_open_file() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write(".a.txt", "content").unwrap();
let metadata = fs.open_read(".a.txt").unwrap().metadata().unwrap();
assert_eq!(metadata, fs.metadata(".a.txt").unwrap());
assert_eq!(metadata.len, 7);
}
}
mod lock_shared {
use super::*;
#[test]
fn lock_shared_should_allow_only_other_shared_locks() {
let (_test_dir, fs, other) = two_fs_with_file();
let file = fs.open_read("a.txt").unwrap();
file.lock_shared().unwrap();
assert!(other.open_read("a.txt").unwrap().try_lock_shared().unwrap());
assert!(!other.open_write("a.txt").unwrap().try_lock().unwrap());
}
}
mod try_lock_shared {
use super::*;
#[test]
fn try_lock_shared_should_fail_while_exclusively_locked() {
let (_test_dir, fs, other) = two_fs_with_file();
let file = fs.open_write("a.txt").unwrap();
file.lock().unwrap();
let other_file = other.open_read("a.txt").unwrap();
assert!(!other_file.try_lock_shared().unwrap());
file.unlock().unwrap();
assert!(other_file.try_lock_shared().unwrap());
}
}
mod unlock {
use super::*;
#[test]
fn unlock_should_release_lock() {
let (_test_dir, fs, other) = two_fs_with_file();
let file = fs.open_read("a.txt").unwrap();
file.lock_shared().unwrap();
let other_file = other.open_write("a.txt").unwrap();
file.unlock().unwrap();
assert!(other_file.try_lock().unwrap());
}
}
}
mod read {
use super::*;
mod read_to_end {
use super::*;
#[test]
fn read_to_end_should_append_whole_file() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "content").unwrap();
let mut buffer = b"old ".to_vec();
let read = fs.open_read("a.txt").unwrap().read_to_end(&mut buffer);
assert_eq!(read.unwrap(), 7);
assert_eq!(buffer, b"old content");
}
}
mod read_to_string {
use super::*;
#[test]
fn read_to_string_should_append_whole_file() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "content").unwrap();
let mut buffer = "old ".to_string();
let read = fs.open_read("a.txt").unwrap().read_to_string(&mut buffer);
assert_eq!(read.unwrap(), 7);
assert_eq!(buffer, "old content");
}
}
}
}
mod file_write_handle {
use super::*;
mod read_file {
use super::*;
mod metadata {
use super::*;
#[test]
fn metadata_should_return_metadata_of_open_file() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
let mut file = fs.open_write("a.txt").unwrap();
file.write_all(b"content").unwrap();
let metadata = file.metadata().unwrap();
assert_eq!(metadata.len, 7);
assert!(metadata.file_type.is_file());
}
}
mod lock_shared {
use super::*;
#[test]
fn lock_shared_should_exclude_exclusive_locks() {
let (_test_dir, fs, other) = two_fs_with_file();
let file = fs.open_write("a.txt").unwrap();
file.lock_shared().unwrap();
assert!(!other.open_write("a.txt").unwrap().try_lock().unwrap());
}
}
mod try_lock_shared {
use super::*;
#[test]
fn try_lock_shared_should_lock_unlocked_file() {
let (_test_dir, fs, other) = two_fs_with_file();
let file = fs.open_write("a.txt").unwrap();
assert!(file.try_lock_shared().unwrap());
assert!(!other.open_write("a.txt").unwrap().try_lock().unwrap());
}
}
mod unlock {
use super::*;
#[test]
fn unlock_should_release_lock() {
let (_test_dir, fs, other) = two_fs_with_file();
let file = fs.open_write("a.txt").unwrap();
file.lock().unwrap();
let other_file = other.open_read("a.txt").unwrap();
file.unlock().unwrap();
assert!(other_file.try_lock_shared().unwrap());
}
}
}
mod write_file {
use super::*;
mod lock {
use super::*;
#[test]
fn lock_should_exclude_other_locks_until_dropped() {
let (_test_dir, fs, other) = two_fs_with_file();
let file = fs.open_write("a.txt").unwrap();
file.lock().unwrap();
let other_file = other.open_read("a.txt").unwrap();
assert!(!other_file.try_lock_shared().unwrap());
drop(file);
assert!(other_file.try_lock_shared().unwrap());
}
}
mod try_lock {
use super::*;
#[test]
fn try_lock_should_fail_while_locked_by_other_handle() {
let (_test_dir, fs, other) = two_fs_with_file();
let file = fs.open_read("a.txt").unwrap();
file.lock_shared().unwrap();
let other_file = other.open_write("a.txt").unwrap();
assert!(!other_file.try_lock().unwrap());
drop(file);
assert!(other_file.try_lock().unwrap());
}
}
mod set_len {
use super::*;
#[test]
fn set_len_should_truncate_or_extend_with_zeros_keeping_cursor() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "content").unwrap();
let mut file = fs.open_write("a.txt").unwrap();
file.seek(SeekFrom::Start(3)).unwrap();
file.set_len(2).unwrap();
file.set_len(4).unwrap();
assert_eq!(file.stream_position().unwrap(), 3);
drop(file);
assert_eq!(fs.read("a.txt").unwrap(), b"co\0\0");
}
}
mod sync_all {
use super::*;
#[test]
fn sync_all_should_succeed() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
let mut file = fs.open_write("a.txt").unwrap();
file.write_all(b"content").unwrap();
file.sync_all().unwrap();
}
}
mod sync_data {
use super::*;
#[test]
fn sync_data_should_succeed() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
let mut file = fs.open_write("a.txt").unwrap();
file.write_all(b"content").unwrap();
file.sync_data().unwrap();
}
}
}
mod read {
use super::*;
mod read_to_end {
use super::*;
#[test]
fn read_to_end_should_append_rest_of_file() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "content").unwrap();
let mut buffer = Vec::new();
let read = fs.open_write("a.txt").unwrap().read_to_end(&mut buffer);
assert_eq!(read.unwrap(), 7);
assert_eq!(buffer, b"content");
}
}
mod read_to_string {
use super::*;
#[test]
fn read_to_string_should_append_rest_of_file() {
let test_dir = TestDir::new();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "content").unwrap();
let mut buffer = String::new();
let read = fs.open_write("a.txt").unwrap().read_to_string(&mut buffer);
assert_eq!(read.unwrap(), 7);
assert_eq!(buffer, "content");
}
}
}
}
mod borrows {
use super::*;
mod state {
use super::*;
#[test]
fn state_should_identify_handles_regardless_of_path_once_needed() {
let test_dir = TestDir::new();
let dir = test_dir.path();
let fs = FsImpl::new(test_dir.path().to_path_buf());
fs.write("a.txt", "aaa").unwrap();
fs.write("b.txt", "bbb").unwrap();
fs.create_symlink("symlink", "a.txt").unwrap();
fs.create_hard_link("hard_link", "a.txt").unwrap();
let mut borrows = Borrows::default();
let file = Arc::new(File::open(dir.join("symlink")).unwrap());
borrows.insert(file, None, false, dir.join("symlink").into());
let id = |name: &str| path_file_id(&dir.join(name)).unwrap();
let states = [
borrows.state(id("a.txt")),
borrows.state(id("hard_link")),
borrows.state(id("b.txt")),
];
assert_eq!(
states,
[Some(BorrowState::Read(1)), Some(BorrowState::Read(1)), None]
);
}
#[test]
fn state_should_count_reads_and_report_write() {
let mut borrows = Borrows::default();
let path = Path::new("Cargo.toml");
let id = path_file_id(path).unwrap();
let open = || Arc::new(File::open("Cargo.toml").unwrap());
borrows.insert(open(), Some(id), false, path.into());
let second = borrows.insert(open(), None, false, path.into());
assert_eq!(borrows.state(id), Some(BorrowState::Read(2)));
borrows.remove(second);
borrows.insert(open(), Some(id), true, path.into());
assert_eq!(borrows.state(id), Some(BorrowState::Write));
}
}
}
mod lock_handles {
use super::*;
#[test]
fn lock_handles_should_recover_from_poison_and_return_guard() {
let fs = FsImpl::new(std::env::current_dir().unwrap());
let poisoned_handles = fs.handles.clone();
let _ = std::panic::catch_unwind(|| {
std::thread::spawn(move || {
let _guard = poisoned_handles.lock().unwrap();
panic!("Poison the mutex");
})
.join()
.ok()
});
fs.open_read("Cargo.toml").unwrap();
fs.open_write("Cargo.toml").unwrap();
}
}
mod fs_error {
use crate::ErrorKind;
#[test]
fn fs_error_should_format_arguments_into_fs_error() {
let error = fs_error!("{} {}", "some", "error");
assert_eq!(error.kind(), ErrorKind::Fs);
assert_eq!(error.description(), "some error");
}
}
mod check_not_root {
use super::*;
#[test]
fn check_not_root_should_fail_only_for_root() {
#[cfg(unix)]
let (root, dir) = ("/", "/dir");
#[cfg(windows)]
let (root, dir) = ("C:\\", "C:\\dir");
assert_eq!(
check_not_root(Path::new(root)).err().unwrap().to_string(),
fs_error!("Directory could not be removed, path is a root: {root}").to_string()
);
assert!(check_not_root(Path::new(dir)).is_ok());
}
}
mod ends_with_separator {
use super::*;
#[test]
fn ends_with_separator_should_detect_trailing_separator() {
for (path, expected) in [("a/", true), ("a/b", false), ("", false), ("/", true)] {
assert_eq!(
ends_with_separator(Path::new(path)),
expected,
"path: {path}"
);
}
}
}
#[cfg(unix)]
mod is_normal {
use super::*;
#[test]
fn is_normal_should_reject_dot_and_empty_components() {
for (path, expected) in [
("a", true),
("a/b", true),
("/a/b", true),
(".a/b..", true),
("", false),
("/", false),
("a/", false),
("a//b", false),
("./a", false),
("a/./b", false),
("a/..", false),
("../a", false),
] {
assert_eq!(is_normal(Path::new(path)), expected, "path: {path}");
}
}
}
#[cfg(unix)]
mod normalize_path {
use super::*;
#[test]
fn normalize_path_with_trailing_separator_should_keep_it() {
for (path, expected) in [("a/", "/cwd/a"), ("a/b/../", "/cwd/a"), ("/", "/")] {
let normalized = normalize_path("/cwd", path);
assert_eq!(normalized, PathBuf::from(expected), "path: {path}");
assert!(ends_with_separator(&normalized), "path: {path}");
}
}
#[test]
fn normalize_path_relative_base_should_keep_leading_parent_references() {
for (current_dir, path, expected) in [
("", "../a", "../a"),
("..", "../a", "../../a"),
("a", "../../b", "../b"),
] {
assert_eq!(
normalize_path(current_dir, path),
PathBuf::from(expected),
"current_dir: {current_dir}, path: {path}"
);
}
}
#[test]
fn normalize_path_should_resolve_against_current_dir_with_or_without_dots() {
for (path, expected) in [
("a/b", "/cwd/a/b"),
("/a/b", "/a/b"),
("a/./b/../c/", "/cwd/a/c"),
("a//b", "/cwd/a/b"),
("../a", "/a"),
("", ""),
] {
assert_eq!(
normalize_path("/cwd", path),
PathBuf::from(expected),
"path: {path}"
);
}
}
}
#[cfg(windows)]
mod normalize_path {
use super::*;
#[test]
fn normalize_path_with_trailing_separator_should_keep_it() {
for (path, expected) in [
("a\\", "C:\\cwd\\a"),
("a\\b\\..\\", "C:\\cwd\\a"),
("C:\\", "C:\\"),
] {
let normalized = normalize_path("C:\\cwd", path);
assert_eq!(normalized, PathBuf::from(expected), "path: {path}");
assert!(ends_with_separator(&normalized), "path: {path}");
}
}
#[test]
fn normalize_path_relative_base_should_keep_leading_parent_references() {
for (current_dir, path, expected) in [
("", "..\\a", "..\\a"),
("..", "..\\a", "..\\..\\a"),
("a", "..\\..\\b", "..\\b"),
] {
assert_eq!(
normalize_path(current_dir, path),
PathBuf::from(expected),
"current_dir: {current_dir}, path: {path}"
);
}
}
#[test]
fn normalize_path_should_resolve_against_current_dir_with_or_without_dots() {
for (path, expected) in [
("a\\b", "C:\\cwd\\a\\b"),
("C:\\a\\b", "C:\\a\\b"),
("a\\.\\b\\..\\c\\", "C:\\cwd\\a\\c"),
("a\\\\b", "C:\\cwd\\a\\b"),
("..\\a", "C:\\a"),
("", ""),
] {
assert_eq!(
normalize_path("C:\\cwd", path),
PathBuf::from(expected),
"path: {path}"
);
}
}
}
#[cfg(windows)]
mod truncate_file {
use super::*;
#[test]
fn truncate_file_directory_handle_should_succeed() {
let test_dir = TestDir::new();
let file = windows::open_for_metadata(test_dir.path()).unwrap();
assert!(truncate_file(&file).is_ok());
}
}
mod fs_error_at {
use super::*;
#[test]
fn fs_error_at_should_map_into_fs_error_for_path_with_source() {
use std::error::Error as _;
let path = Path::new("/some/file.txt");
let result: std::result::Result<(), std::io::Error> = Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"fs error",
));
let error = result.map_err(fs_error_at(path)).unwrap_err();
assert_eq!(
error.to_string(),
fs_error!("fs error: {}", path.display()).to_string()
);
assert_eq!(
error
.source()
.and_then(|source| source.downcast_ref::<std::io::Error>())
.map(std::io::Error::kind),
Some(std::io::ErrorKind::NotFound)
);
}
}
mod file_type {
use super::*;
mod is_dir {
use super::*;
#[test]
fn is_dir_should_return_true_for_dir() {
assert!(FileType::Dir.is_dir());
assert!(!FileType::File.is_dir());
}
}
mod is_file {
use super::*;
#[test]
fn is_file_should_return_true_for_file() {
assert!(FileType::File.is_file());
assert!(!FileType::Dir.is_file());
}
}
mod is_symlink {
use super::*;
#[test]
fn is_symlink_should_return_true_for_symlink() {
assert!(FileType::Symlink.is_symlink());
assert!(!FileType::File.is_symlink());
}
}
}
mod dir_entry {
use super::*;
#[test]
fn dir_entry_should_expose_path_file_name_and_file_type() {
let entry = DirEntry {
path: PathBuf::from("/some/dir/file.txt"),
file_type: FileType::File,
};
assert_eq!(entry.path(), Path::new("/some/dir/file.txt"));
assert_eq!(entry.file_name(), "file.txt");
assert_eq!(entry.file_type(), FileType::File);
}
#[cfg(windows)]
#[test]
fn dir_entry_new_invalid_utf16_name_should_return_none() {
use std::os::windows::ffi::OsStringExt;
let invalid = std::ffi::OsString::from_wide(&[0xD800]);
let path = PathBuf::from(invalid);
assert!(DirEntry::new(path, FileType::File).is_none());
}
}
mod attributes {
use super::*;
mod set {
use super::*;
#[test]
fn set_should_change_only_selected_flag() {
for (attribute, expected) in [
(
Attribute::Readonly,
Attributes {
readonly: true,
..Attributes::default()
},
),
#[cfg(unix)]
(
Attribute::Executable,
Attributes {
executable: true,
..Attributes::default()
},
),
#[cfg(windows)]
(
Attribute::Hidden,
Attributes {
hidden: true,
..Attributes::default()
},
),
] {
let mut attributes = Attributes::default();
attributes.set(attribute, true);
assert_eq!(attributes, expected, "attribute: {attribute:?}");
}
}
}
}
mod platform_attributes {
use super::*;
#[cfg(unix)]
#[test]
fn platform_attributes_should_derive_hidden_and_restrict_executable_to_files() {
let all = Attributes {
readonly: true,
executable: true,
hidden: true,
};
for (path, file_type, stored, expected) in [
(
"/a/file",
FileType::File,
all,
Attributes {
readonly: true,
executable: true,
..Attributes::default()
},
),
(
"/a/.file",
FileType::File,
Attributes::default(),
Attributes {
hidden: true,
..Attributes::default()
},
),
(
"/a/dir",
FileType::Dir,
all,
Attributes {
readonly: true,
..Attributes::default()
},
),
(
"/",
FileType::Dir,
Attributes::default(),
Attributes::default(),
),
] {
assert_eq!(
platform_attributes(Path::new(path), file_type, stored),
expected,
"path: {path}, stored: {stored:?}"
);
}
}
#[cfg(windows)]
#[test]
fn platform_attributes_should_derive_executable_from_file_extension() {
let all = Attributes {
readonly: true,
executable: true,
hidden: true,
};
for (path, file_type, stored, expected) in [
(
"C:\\a\\file",
FileType::File,
all,
Attributes {
readonly: true,
hidden: true,
..Attributes::default()
},
),
(
"C:\\a\\.file",
FileType::File,
Attributes::default(),
Attributes::default(),
),
(
"C:\\a\\run.exe",
FileType::File,
Attributes::default(),
Attributes {
executable: true,
..Attributes::default()
},
),
(
"C:\\a\\run.CMD",
FileType::File,
Attributes::default(),
Attributes {
executable: true,
..Attributes::default()
},
),
(
"C:\\a\\run.com",
FileType::File,
Attributes::default(),
Attributes {
executable: true,
..Attributes::default()
},
),
(
"C:\\a\\run.bat",
FileType::File,
Attributes::default(),
Attributes {
executable: true,
..Attributes::default()
},
),
(
"C:\\a\\dir.exe",
FileType::Dir,
Attributes::default(),
Attributes::default(),
),
] {
assert_eq!(
platform_attributes(Path::new(path), file_type, stored),
expected,
"path: {path}, stored: {stored:?}"
);
}
}
}
}