#[cfg(any(feature = "full-resolve", feature = "expand-user"))]
use std::path::PathBuf;
use std::{
fs::{
File, OpenOptions, Permissions, copy, create_dir, create_dir_all, hard_link, read,
read_to_string, remove_dir, remove_dir_all, remove_file, rename, set_permissions, write,
},
io::{self},
ops::{Deref, DerefMut},
path::Path,
};
#[derive(Debug)]
pub struct FileLockGuard {
file: File,
}
impl Drop for FileLockGuard {
fn drop(&mut self) {
drop(self.file.unlock())
}
}
impl Deref for FileLockGuard {
type Target = File;
fn deref(&self) -> &Self::Target {
&self.file
}
}
impl DerefMut for FileLockGuard {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.file
}
}
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)]
pub enum MkdirOptions {
WithoutParents,
WithParents,
}
mod sealed {
use std::path::Path;
pub trait Sealed {}
impl Sealed for Path {}
}
#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)]
pub enum ShouldBlock {
No,
Yes,
}
pub trait PathExt: sealed::Sealed {
fn touch(&self) -> io::Result<File>;
fn mkdir(&self, opts: MkdirOptions) -> io::Result<()>;
fn lock(&self, should_block: ShouldBlock) -> io::Result<FileLockGuard>;
fn lock_shared(&self, should_block: ShouldBlock) -> io::Result<FileLockGuard>;
#[cfg(feature = "full-resolve")]
#[cfg_attr(docsrs, doc(cfg(feature = "full-resolve")))]
fn resolve(&self) -> io::Result<PathBuf>;
#[cfg(feature = "expand-user")]
#[cfg_attr(docsrs, doc(cfg(feature = "expand-user")))]
fn expand_user(&self) -> io::Result<PathBuf>;
#[cfg(feature = "expand-user")]
#[cfg_attr(docsrs, doc(cfg(feature = "expand-user")))]
fn expand_user_with(&self, home: impl AsRef<str>) -> io::Result<PathBuf>;
#[cfg(feature = "expand-user")]
#[cfg_attr(docsrs, doc(cfg(feature = "expand-user")))]
fn expand_user_with_fn<F, H>(&self, home: F) -> io::Result<PathBuf>
where
H: AsRef<str>,
F: FnOnce() -> H;
fn is_executable(&self) -> bool;
fn copy_to(&self, to: impl AsRef<Path>) -> io::Result<u64>;
fn hard_link_to(&self, to: impl AsRef<Path>) -> io::Result<()>;
fn read(&self) -> io::Result<Vec<u8>>;
fn read_to_string(&self) -> io::Result<String>;
fn rename_to(&self, to: impl AsRef<Path>) -> io::Result<()>;
fn rm(&self) -> io::Result<()>;
fn rmdir(&self) -> io::Result<()>;
fn rmtree(&self) -> io::Result<()>;
fn set_permissions(&self, permissions: Permissions) -> io::Result<()>;
fn write(&self, contents: impl AsRef<[u8]>) -> io::Result<()>;
}
impl PathExt for Path {
fn touch(&self) -> io::Result<File> {
if let Some(parent) = self.parent() {
parent.mkdir(MkdirOptions::WithParents)?;
}
let mut opts = OpenOptions::new();
opts.read(true).write(true).create(true).truncate(false);
#[cfg(unix)]
{
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
if let Ok(metadata) = self.metadata() {
const MASK: u32 = 0o666;
let permissions = metadata.permissions().mode();
opts.mode(permissions & MASK);
}
}
opts.open(self)
}
fn mkdir(&self, opts: MkdirOptions) -> io::Result<()> {
let result = match opts {
MkdirOptions::WithoutParents => create_dir(self),
MkdirOptions::WithParents => create_dir_all(self),
};
match result {
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => Ok(()),
_ => result,
}
}
fn lock(&self, should_block: ShouldBlock) -> io::Result<FileLockGuard> {
let file = self.touch()?;
let result = if matches!(should_block, ShouldBlock::Yes) {
file.lock()
} else {
file.try_lock().map_err(|err| match err {
std::fs::TryLockError::Error(error) => error,
std::fs::TryLockError::WouldBlock => io::Error::from(io::ErrorKind::WouldBlock),
})
};
result.map(|_| FileLockGuard { file })
}
fn lock_shared(&self, should_block: ShouldBlock) -> io::Result<FileLockGuard> {
let file = self.touch()?;
let result = if matches!(should_block, ShouldBlock::Yes) {
file.lock_shared()
} else {
file.try_lock_shared().map_err(|err| match err {
std::fs::TryLockError::Error(error) => error,
std::fs::TryLockError::WouldBlock => io::Error::from(io::ErrorKind::WouldBlock),
})
};
result.map(|_| FileLockGuard { file })
}
#[cfg(unix)]
fn is_executable(&self) -> bool {
use std::os::unix::prelude::*;
self.metadata()
.map(|metadata| {
#[allow(clippy::unnecessary_cast)] const EXEC_MASK: u32 = (libc::S_IXUSR | libc::S_IXGRP | libc::S_IXOTH) as u32;
const _: () = assert!(EXEC_MASK == 0o111, "bits mismatch");
metadata.is_file() && metadata.permissions().mode() & EXEC_MASK != 0
})
.unwrap_or(false)
}
#[cfg(windows)]
fn is_executable(&self) -> bool {
self.is_file()
}
#[cfg(not(any(unix, windows)))]
fn is_executable(&self) -> bool {
false
}
fn copy_to(&self, to: impl AsRef<Path>) -> io::Result<u64> {
copy(self, to)
}
fn hard_link_to(&self, to: impl AsRef<Path>) -> io::Result<()> {
hard_link(self, to)
}
fn read(&self) -> io::Result<Vec<u8>> {
read(self)
}
fn read_to_string(&self) -> io::Result<String> {
read_to_string(self)
}
fn rename_to(&self, to: impl AsRef<Path>) -> io::Result<()> {
rename(self, to)
}
fn rm(&self) -> io::Result<()> {
remove_file(self)
}
fn rmdir(&self) -> io::Result<()> {
remove_dir(self)
}
fn rmtree(&self) -> io::Result<()> {
remove_dir_all(self)
}
fn set_permissions(&self, permissions: Permissions) -> io::Result<()> {
set_permissions(self, permissions)
}
fn write(&self, contents: impl AsRef<[u8]>) -> io::Result<()> {
write(self, contents)
}
#[cfg(feature = "full-resolve")]
fn resolve(&self) -> io::Result<PathBuf> {
use soft_canonicalize::soft_canonicalize;
soft_canonicalize(self)
}
#[cfg(feature = "expand-user")]
fn expand_user(&self) -> io::Result<PathBuf> {
use shellexpand::tilde;
let Some(as_str) = self.to_str() else {
return Err(io::Error::other("path is not an UTF-8 string"));
};
Ok(PathBuf::from(tilde(as_str).into_owned()))
}
#[cfg(feature = "expand-user")]
fn expand_user_with(&self, home: impl AsRef<str>) -> io::Result<PathBuf> {
self.expand_user_with_fn(|| home)
}
#[cfg(feature = "expand-user")]
fn expand_user_with_fn<F, H>(&self, home: F) -> io::Result<PathBuf>
where
H: AsRef<str>,
F: FnOnce() -> H,
{
use shellexpand::tilde_with_context;
let Some(as_str) = self.to_str() else {
return Err(io::Error::other("path is not an UTF-8 string"));
};
Ok(PathBuf::from(
tilde_with_context(as_str, || Some(home())).into_owned(),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use claim::{assert_err, assert_ok};
use tempfile::tempdir;
use std::io::{Read, Write};
use std::sync::{Arc, Barrier};
use std::thread;
use std::time::Duration;
use tempfile::NamedTempFile;
#[test]
fn create_new_file_should_work() {
let tmp = tempdir().expect("needed for tests");
let mut new_file = tmp.path().to_path_buf();
new_file.push("x");
assert_ok!(new_file.touch());
}
#[test]
fn multiple_touch() {
let tmp = tempdir().expect("needed for tests");
let mut new_file = tmp.path().to_path_buf();
new_file.push("x");
let mut file = new_file.touch().unwrap();
file.write_all("test".as_bytes()).unwrap();
let mut new_handle = new_file.touch().unwrap();
let mut content = String::new();
let read_bytes = new_handle.read_to_string(&mut content).unwrap();
assert_eq!(read_bytes, 4);
assert_eq!(content, "test");
}
#[test]
fn create_dirs() {
{
let tmp = tempdir().expect("needed for tests");
let mut new_file = tmp.path().to_path_buf();
new_file.push("x");
new_file.push("y");
assert_ok!(new_file.mkdir(MkdirOptions::WithParents));
}
{
let tmp = tempdir().expect("needed for tests");
let mut new_file = tmp.path().to_path_buf();
new_file.push("x");
assert_ok!(new_file.mkdir(MkdirOptions::WithParents));
}
{
let tmp = tempdir().expect("needed for tests");
let mut new_file = tmp.path().to_path_buf();
new_file.push("x");
assert_ok!(new_file.mkdir(MkdirOptions::WithoutParents));
}
{
let tmp = tempdir().expect("needed for tests");
let mut new_file = tmp.path().to_path_buf();
new_file.push("x");
new_file.push("y");
assert_err!(new_file.mkdir(MkdirOptions::WithoutParents));
}
}
#[test]
fn mkdir_doesnt_screw_paths() {
let tmp = tempdir().expect("needed for tests");
let mut new_file = tmp.path().to_path_buf();
let mut copy = new_file.clone();
let mut copy2 = new_file.clone();
new_file.push("x");
new_file.push("y");
assert_ok!(new_file.mkdir(MkdirOptions::WithParents));
assert_ok!(new_file.mkdir(MkdirOptions::WithoutParents));
copy.push("x");
copy.push("file");
copy2.push("x");
assert_ok!(copy.touch());
assert_ok!(copy2.mkdir(MkdirOptions::WithParents));
assert_ok!(copy2.mkdir(MkdirOptions::WithoutParents));
assert!(copy.exists());
assert!(copy2.exists());
assert!(new_file.exists());
}
#[test]
fn lock_blocking_should_work() {
use tempfile::NamedTempFile;
let lockfile = NamedTempFile::new().expect("needed for tests");
let path = lockfile.path();
let lock = assert_ok!(path.lock(ShouldBlock::Yes));
drop(lock);
}
#[test]
fn lock_non_blocking_should_work() {
use tempfile::NamedTempFile;
let lockfile = NamedTempFile::new().expect("needed for tests");
let path = lockfile.path();
let lock = assert_ok!(path.lock(ShouldBlock::No));
drop(lock);
}
#[test]
fn lock_shared_blocking_should_work() {
use tempfile::NamedTempFile;
let lockfile = NamedTempFile::new().expect("needed for tests");
let path = lockfile.path();
let lock = assert_ok!(path.lock_shared(ShouldBlock::Yes));
drop(lock);
}
#[test]
fn lock_shared_non_blocking_should_work() {
use tempfile::NamedTempFile;
let lockfile = NamedTempFile::new().expect("needed for tests");
let path = lockfile.path();
let lock = assert_ok!(path.lock_shared(ShouldBlock::No));
drop(lock);
}
#[test]
fn multiple_shared_locks_can_coexist() {
use tempfile::NamedTempFile;
let lockfile = NamedTempFile::new().expect("needed for tests");
let path = lockfile.path();
let lock1 = assert_ok!(path.lock_shared(ShouldBlock::Yes));
let lock2 = assert_ok!(path.lock_shared(ShouldBlock::No));
let lock3 = assert_ok!(path.lock_shared(ShouldBlock::No));
drop(lock1);
drop(lock2);
drop(lock3);
}
#[test]
fn exclusive_lock_prevents_another_exclusive_lock() {
use tempfile::NamedTempFile;
let lockfile = NamedTempFile::new().expect("needed for tests");
let path = lockfile.path();
let _lock1 = assert_ok!(path.lock(ShouldBlock::Yes));
let result = path.lock(ShouldBlock::No);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::WouldBlock);
}
#[test]
fn exclusive_lock_prevents_shared_lock() {
use tempfile::NamedTempFile;
let lockfile = NamedTempFile::new().expect("needed for tests");
let path = lockfile.path();
let _lock1 = assert_ok!(path.lock(ShouldBlock::Yes));
let result = path.lock_shared(ShouldBlock::No);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::WouldBlock);
}
#[test]
fn shared_lock_prevents_exclusive_lock() {
use tempfile::NamedTempFile;
let lockfile = NamedTempFile::new().expect("needed for tests");
let path = lockfile.path();
let _lock1 = assert_ok!(path.lock_shared(ShouldBlock::Yes));
let result = path.lock(ShouldBlock::No);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::WouldBlock);
}
#[test]
fn multithreaded_exclusive_locks_are_mutually_exclusive() {
let lockfile = NamedTempFile::new().expect("needed for tests");
let lockpath = Arc::new(lockfile.path().to_path_buf());
let barrier = Arc::new(Barrier::new(2));
let lockpath_clone = Arc::clone(&lockpath);
let barrier_clone = Arc::clone(&barrier);
let handle = thread::spawn(move || {
let lock = assert_ok!(lockpath_clone.as_path().lock(ShouldBlock::Yes));
barrier_clone.wait();
thread::sleep(Duration::from_millis(100));
drop(lock);
});
barrier.wait();
thread::sleep(Duration::from_millis(10));
let result = lockpath.as_path().lock(ShouldBlock::No);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::WouldBlock);
handle.join().expect("thread should not panic");
assert_ok!(lockpath.as_path().lock(ShouldBlock::No));
}
#[test]
fn multithreaded_shared_locks_can_coexist() {
let lockfile = NamedTempFile::new().expect("needed for tests");
let lockpath = Arc::new(lockfile.path().to_path_buf());
let barrier = Arc::new(Barrier::new(3));
let mut handles = vec![];
for _ in 0..2 {
let lockpath_clone = Arc::clone(&lockpath);
let barrier_clone = Arc::clone(&barrier);
let handle = thread::spawn(move || {
let _lock = assert_ok!(lockpath_clone.as_path().lock_shared(ShouldBlock::Yes));
barrier_clone.wait();
});
handles.push(handle);
}
let _lock = assert_ok!(lockpath.as_path().lock_shared(ShouldBlock::Yes));
barrier.wait();
for handle in handles {
handle.join().expect("thread should not panic");
}
}
#[test]
fn multithreaded_exclusive_lock_blocks_shared_locks() {
let lockfile = NamedTempFile::new().expect("needed for tests");
let lockpath = Arc::new(lockfile.path().to_path_buf());
let barrier = Arc::new(Barrier::new(2));
let lockpath_clone = Arc::clone(&lockpath);
let barrier_clone = Arc::clone(&barrier);
let handle = thread::spawn(move || {
let lock = assert_ok!(lockpath_clone.as_path().lock(ShouldBlock::Yes));
barrier_clone.wait();
thread::sleep(Duration::from_millis(100));
drop(lock);
});
barrier.wait();
thread::sleep(Duration::from_millis(10));
let result = lockpath.as_path().lock_shared(ShouldBlock::No);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::WouldBlock);
handle.join().expect("thread should not panic");
assert_ok!(lockpath.as_path().lock_shared(ShouldBlock::No));
}
#[test]
fn multithreaded_shared_lock_blocks_exclusive_lock() {
let lockfile = NamedTempFile::new().expect("needed for tests");
let lockpath = Arc::new(lockfile.path().to_path_buf());
let barrier = Arc::new(Barrier::new(2));
let lockpath_clone = Arc::clone(&lockpath);
let barrier_clone = Arc::clone(&barrier);
let handle = thread::spawn(move || {
let lock = assert_ok!(lockpath_clone.as_path().lock_shared(ShouldBlock::Yes));
barrier_clone.wait();
thread::sleep(Duration::from_millis(100));
drop(lock);
});
barrier.wait();
thread::sleep(Duration::from_millis(10));
let result = lockpath.as_path().lock(ShouldBlock::No);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::WouldBlock);
handle.join().expect("thread should not panic");
assert_ok!(lockpath.as_path().lock(ShouldBlock::No));
}
#[test]
fn multithreaded_multiple_shared_then_exclusive() {
let lockfile = NamedTempFile::new().expect("needed for tests");
let lockpath = Arc::new(lockfile.path().to_path_buf());
let barrier = Arc::new(Barrier::new(4));
let mut handles = vec![];
for _ in 0..3 {
let lockpath_clone = Arc::clone(&lockpath);
let barrier_clone = Arc::clone(&barrier);
let handle = thread::spawn(move || {
let lock = assert_ok!(lockpath_clone.as_path().lock_shared(ShouldBlock::Yes));
barrier_clone.wait();
thread::sleep(Duration::from_millis(50));
drop(lock);
});
handles.push(handle);
}
barrier.wait();
thread::sleep(Duration::from_millis(10));
let result = lockpath.as_path().lock(ShouldBlock::No);
assert_err!(&result);
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::WouldBlock);
for handle in handles {
handle.join().expect("thread should not panic");
}
assert_ok!(lockpath.as_path().lock(ShouldBlock::Yes));
}
}