#[cfg(windows)]
use std::fs;
use std::fs::File;
use std::io;
use std::path::Path;
fn parent(path: &Path) -> &Path {
path.parent()
.filter(|path| !path.as_os_str().is_empty())
.unwrap_or(Path::new("."))
}
pub fn available_space(path: &Path) -> io::Result<u64> {
let absolute = std::path::absolute(path)?;
let mut path = absolute.as_path();
loop {
match std::fs::metadata(path) {
Ok(_) => return directory_available_space(path),
Err(error) if error.kind() == io::ErrorKind::NotFound => {
path = path.parent().ok_or(error)?;
}
Err(error) => return Err(error),
}
}
}
#[cfg(unix)]
fn directory_available_space(path: &Path) -> io::Result<u64> {
let stats = rustix::fs::statvfs(path)?;
stats
.f_bavail
.checked_mul(stats.f_frsize)
.ok_or_else(|| io::Error::other("available space overflow"))
}
#[cfg(windows)]
fn directory_available_space(path: &Path) -> io::Result<u64> {
use std::os::windows::ffi::OsStrExt;
use windows_sys::Win32::Storage::FileSystem::GetDiskFreeSpaceExW;
let path = path.join("");
let path: Vec<_> = path.as_os_str().encode_wide().chain(Some(0)).collect();
if path[..path.len() - 1].contains(&0) {
return Err(io::ErrorKind::InvalidInput.into());
}
let mut available = 0;
if unsafe {
GetDiskFreeSpaceExW(
path.as_ptr(),
&raw mut available,
std::ptr::null_mut(),
std::ptr::null_mut(),
)
} == 0
{
return Err(io::Error::last_os_error());
}
Ok(available)
}
#[cfg(unix)]
pub fn create_directory(path: &Path) -> io::Result<()> {
directory(path, true).map(|_| ())
}
#[cfg(unix)]
fn directory(path: &Path, create: bool) -> io::Result<File> {
use rustix::fs::{CWD, Mode, OFlags, mkdirat, openat};
let flags = OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC;
let mut directory = openat(CWD, "/", flags, Mode::empty())?;
for part in std::path::absolute(path)?.components() {
if matches!(part, std::path::Component::RootDir) {
continue;
}
if create {
match mkdirat(&directory, part.as_os_str(), Mode::RWXU) {
Ok(()) | Err(rustix::io::Errno::EXIST) => {}
Err(error) => return Err(error.into()),
}
}
directory = openat(&directory, part.as_os_str(), flags, Mode::empty())?;
}
Ok(File::from(directory))
}
#[cfg(windows)]
pub fn create_directory(path: &Path) -> io::Result<()> {
use std::os::windows::fs::MetadataExt;
use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT;
let mut parent = std::path::PathBuf::new();
let mut ancestors = Vec::new();
for component in path.components() {
parent.push(component);
if matches!(component, std::path::Component::Prefix(_)) {
continue;
}
match fs::symlink_metadata(&parent) {
Ok(metadata)
if metadata.is_dir()
&& metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT == 0 => {}
Ok(_) => return Err(io::Error::other("unsafe directory")),
Err(error) if error.kind() == io::ErrorKind::NotFound => fs::create_dir(&parent)?,
Err(error) => return Err(error),
}
ancestors.push(windows_directory(&parent)?);
}
Ok(())
}
#[cfg(windows)]
fn windows_directory(path: &Path) -> io::Result<File> {
use std::os::windows::fs::{MetadataExt, OpenOptionsExt};
use windows_sys::Win32::Storage::FileSystem::{
FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT,
FILE_SHARE_READ, FILE_SHARE_WRITE,
};
let dir = fs::OpenOptions::new()
.read(true)
.share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE)
.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS)
.open(path)?;
let meta = dir.metadata()?;
if !meta.is_dir() || meta.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return Err(io::Error::other("unsafe directory"));
}
Ok(dir)
}
#[cfg(windows)]
fn windows_ancestors(path: &Path) -> io::Result<Vec<File>> {
let mut parent = Some(path);
let mut ancestors = Vec::new();
while let Some(path) = parent {
if path.as_os_str().is_empty() {
break;
}
ancestors.push(windows_directory(path)?);
parent = path.parent();
}
Ok(ancestors)
}
pub fn check_directory(path: &Path) -> io::Result<()> {
#[cfg(unix)]
{
directory(path, false).map(|_| ())
}
#[cfg(windows)]
{
let _ancestors = windows_ancestors(path)?;
Ok(())
}
}
#[cfg(unix)]
pub fn open_file(path: &Path, create: bool, write: bool) -> io::Result<File> {
use rustix::fs::{Mode, OFlags, openat};
let parent = directory(parent(path), false)?;
let name = path
.file_name()
.ok_or_else(|| io::Error::other("missing file name"))?;
let flags = (if write { OFlags::RDWR } else { OFlags::RDONLY })
| if create {
OFlags::CREATE | OFlags::EXCL
} else {
OFlags::empty()
};
let file = File::from(openat(
&parent,
name,
flags | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
Mode::RUSR | Mode::WUSR,
)?);
if !file.metadata()?.is_file() {
return Err(io::Error::other("not a regular file"));
}
Ok(file)
}
#[cfg(windows)]
pub fn open_file(path: &Path, create: bool, write: bool) -> io::Result<File> {
use std::os::windows::fs::{MetadataExt, OpenOptionsExt};
use windows_sys::Win32::Storage::FileSystem::{
FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_OPEN_REPARSE_POINT,
};
let _ancestors = windows_ancestors(parent(path))?;
let file = fs::OpenOptions::new()
.read(true)
.write(write)
.create_new(create)
.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
.open(path)?;
let meta = file.metadata()?;
if !meta.is_file() || meta.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return Err(io::Error::other("not a regular file"));
}
Ok(file)
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub fn rename_no_replace(from: &Path, to: &Path) -> io::Result<()> {
use rustix::fs::{RenameFlags, renameat_with};
let source = directory(parent(from), false)?;
let destination = directory(parent(to), false)?;
renameat_with(
&source,
from.file_name()
.ok_or_else(|| io::Error::other("missing name"))?,
&destination,
to.file_name()
.ok_or_else(|| io::Error::other("missing name"))?,
RenameFlags::NOREPLACE,
)
.map_err(Into::into)
}
#[cfg(windows)]
pub fn rename_no_replace(from: &Path, to: &Path) -> io::Result<()> {
use std::os::windows::ffi::OsStrExt;
use windows_sys::Win32::Storage::FileSystem::MoveFileExW;
let _source = windows_ancestors(parent(from))?;
let _destination = windows_ancestors(parent(to))?;
let destination = to;
let from: Vec<_> = from.as_os_str().encode_wide().chain(Some(0)).collect();
let to: Vec<_> = to.as_os_str().encode_wide().chain(Some(0)).collect();
if unsafe { MoveFileExW(from.as_ptr(), to.as_ptr(), 0) } == 0 {
let error = io::Error::last_os_error();
if fs::symlink_metadata(destination).is_ok() {
return Err(io::ErrorKind::AlreadyExists.into());
}
return Err(error);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn available_space_missing_directory_queries_existing_ancestor() {
let temp = tempfile::tempdir().unwrap();
let missing = temp.path().join("missing/nested");
let result = available_space(&missing);
assert!(result.is_ok(), "{result:?}");
assert!(!missing.exists());
}
#[test]
fn rename_no_replace_existing_file_or_directory_preserves_both_entries() {
for directories in [false, true] {
let temp = tempfile::tempdir_in(std::env::temp_dir().canonicalize().unwrap()).unwrap();
let from = temp.path().join("from");
let to = temp.path().join("to");
if directories {
fs::create_dir(&from).unwrap();
fs::create_dir(&to).unwrap();
} else {
fs::write(&from, b"new").unwrap();
fs::write(&to, b"existing").unwrap();
}
let result = rename_no_replace(&from, &to);
assert_eq!(result.unwrap_err().kind(), io::ErrorKind::AlreadyExists);
assert!(from.exists());
if !directories {
assert_eq!(fs::read(to).unwrap(), b"existing");
}
}
}
#[test]
fn create_directory_canonical_path_creates_nested_directories() {
let temp = tempfile::tempdir_in(std::env::temp_dir().canonicalize().unwrap()).unwrap();
let nested = temp.path().join("stage").join("nested");
let result = create_directory(&nested);
assert!(result.is_ok(), "{result:?}");
assert!(nested.is_dir());
}
#[cfg(unix)]
#[test]
fn open_file_symlink_ancestor_or_leaf_is_rejected() {
let temp = tempfile::tempdir_in(std::env::temp_dir().canonicalize().unwrap()).unwrap();
let actual = temp.path().join("actual");
fs::create_dir(&actual).unwrap();
fs::write(actual.join("file"), b"private").unwrap();
let link = temp.path().join("link");
std::os::unix::fs::symlink(&actual, &link).unwrap();
std::os::unix::fs::symlink(actual.join("file"), temp.path().join("leaf")).unwrap();
let through_parent = open_file(&link.join("file"), false, false);
let through_leaf = open_file(&temp.path().join("leaf"), false, false);
let create_below_link = create_directory(&link.join("child"));
assert!(through_parent.is_err());
assert!(through_leaf.is_err());
assert!(create_below_link.is_err());
assert!(!actual.join("child").exists());
}
}