#[cfg(unix)]
use libc::{
mode_t, S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFREG, S_IFSOCK, S_IRGRP,
S_IROTH, S_IRUSR, S_ISGID, S_ISUID, S_ISVTX, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP, S_IXOTH,
S_IXUSR,
};
use std::borrow::Cow;
use std::collections::HashSet;
use std::collections::VecDeque;
use std::env;
use std::ffi::{OsStr, OsString};
use std::fs;
use std::fs::read_dir;
use std::hash::Hash;
use std::io::{Error, ErrorKind, Result as IOResult};
#[cfg(unix)]
use std::os::unix::{fs::MetadataExt, io::AsRawFd};
use std::path::{Component, Path, PathBuf, MAIN_SEPARATOR};
#[cfg(target_os = "windows")]
use winapi_util::AsHandleRef;
#[cfg(unix)]
#[macro_export]
macro_rules! has {
($mode:expr, $perm:expr) => {
$mode & $perm != 0
};
}
pub struct FileInformation(
#[cfg(unix)] nix::sys::stat::FileStat,
#[cfg(windows)] winapi_util::file::Information,
);
impl FileInformation {
#[cfg(unix)]
pub fn from_file(file: &impl AsRawFd) -> IOResult<Self> {
let stat = nix::sys::stat::fstat(file.as_raw_fd())?;
Ok(Self(stat))
}
#[cfg(target_os = "windows")]
pub fn from_file(file: &impl AsHandleRef) -> IOResult<Self> {
let info = winapi_util::file::information(file.as_handle_ref())?;
Ok(Self(info))
}
pub fn from_path(path: impl AsRef<Path>, dereference: bool) -> IOResult<Self> {
#[cfg(unix)]
{
let stat = if dereference {
nix::sys::stat::stat(path.as_ref())
} else {
nix::sys::stat::lstat(path.as_ref())
};
Ok(Self(stat?))
}
#[cfg(target_os = "windows")]
{
use std::fs::OpenOptions;
use std::os::windows::prelude::*;
let mut open_options = OpenOptions::new();
let mut custom_flags = 0;
if !dereference {
custom_flags |=
windows_sys::Win32::Storage::FileSystem::FILE_FLAG_OPEN_REPARSE_POINT;
}
custom_flags |= windows_sys::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS;
open_options.custom_flags(custom_flags);
let file = open_options.read(true).open(path.as_ref())?;
Self::from_file(&file)
}
}
pub fn file_size(&self) -> u64 {
#[cfg(unix)]
{
assert!(self.0.st_size >= 0, "File size is negative");
self.0.st_size.try_into().unwrap()
}
#[cfg(target_os = "windows")]
{
self.0.file_size()
}
}
#[cfg(windows)]
pub fn file_index(&self) -> u64 {
self.0.file_index()
}
pub fn number_of_links(&self) -> u64 {
#[cfg(all(
unix,
not(target_vendor = "apple"),
not(target_os = "android"),
not(target_os = "freebsd"),
not(target_arch = "aarch64"),
not(target_arch = "riscv64"),
target_pointer_width = "64"
))]
return self.0.st_nlink;
#[cfg(all(
unix,
any(
target_vendor = "apple",
target_os = "android",
target_os = "freebsd",
target_arch = "aarch64",
target_arch = "riscv64",
not(target_pointer_width = "64")
)
))]
return self.0.st_nlink.into();
#[cfg(windows)]
return self.0.number_of_links();
}
#[cfg(unix)]
pub fn inode(&self) -> u64 {
#[cfg(all(not(target_os = "freebsd"), target_pointer_width = "64"))]
return self.0.st_ino;
#[cfg(any(target_os = "freebsd", not(target_pointer_width = "64")))]
return self.0.st_ino.into();
}
}
#[cfg(unix)]
impl PartialEq for FileInformation {
fn eq(&self, other: &Self) -> bool {
self.0.st_dev == other.0.st_dev && self.0.st_ino == other.0.st_ino
}
}
#[cfg(target_os = "windows")]
impl PartialEq for FileInformation {
fn eq(&self, other: &Self) -> bool {
self.0.volume_serial_number() == other.0.volume_serial_number()
&& self.0.file_index() == other.0.file_index()
}
}
impl Eq for FileInformation {}
impl Hash for FileInformation {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
#[cfg(unix)]
{
self.0.st_dev.hash(state);
self.0.st_ino.hash(state);
}
#[cfg(target_os = "windows")]
{
self.0.volume_serial_number().hash(state);
self.0.file_index().hash(state);
}
}
}
pub fn resolve_relative_path(path: &Path) -> Cow<Path> {
if path.components().all(|e| e != Component::ParentDir) {
return path.into();
}
let root = Component::RootDir.as_os_str();
let mut result = env::current_dir().unwrap_or_else(|_| PathBuf::from(root));
for comp in path.components() {
match comp {
Component::ParentDir => {
if let Ok(p) = result.read_link() {
result = p;
}
result.pop();
}
Component::CurDir => (),
Component::RootDir | Component::Normal(_) | Component::Prefix(_) => {
result.push(comp.as_os_str());
}
}
}
result.into()
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MissingHandling {
Normal,
Existing,
Missing,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ResolveMode {
None,
Physical,
Logical,
}
pub fn normalize_path(path: &Path) -> PathBuf {
let mut components = path.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};
for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(component.as_os_str());
}
Component::CurDir => {}
Component::ParentDir => {
ret.pop();
}
Component::Normal(c) => {
ret.push(c);
}
}
}
ret
}
fn resolve_symlink<P: AsRef<Path>>(path: P) -> IOResult<Option<PathBuf>> {
let result = if fs::symlink_metadata(&path)?.file_type().is_symlink() {
Some(fs::read_link(&path)?)
} else {
None
};
Ok(result)
}
enum OwningComponent {
Prefix(OsString),
RootDir,
CurDir,
ParentDir,
Normal(OsString),
}
impl OwningComponent {
fn as_os_str(&self) -> &OsStr {
match self {
Self::Prefix(s) => s.as_os_str(),
Self::RootDir => Component::RootDir.as_os_str(),
Self::CurDir => Component::CurDir.as_os_str(),
Self::ParentDir => Component::ParentDir.as_os_str(),
Self::Normal(s) => s.as_os_str(),
}
}
}
impl<'a> From<Component<'a>> for OwningComponent {
fn from(comp: Component<'a>) -> Self {
match comp {
Component::Prefix(_) => Self::Prefix(comp.as_os_str().to_os_string()),
Component::RootDir => Self::RootDir,
Component::CurDir => Self::CurDir,
Component::ParentDir => Self::ParentDir,
Component::Normal(s) => Self::Normal(s.to_os_string()),
}
}
}
#[allow(clippy::cognitive_complexity)]
pub fn canonicalize<P: AsRef<Path>>(
original: P,
miss_mode: MissingHandling,
res_mode: ResolveMode,
) -> IOResult<PathBuf> {
const SYMLINKS_TO_LOOK_FOR_LOOPS: i32 = 20;
let original = original.as_ref();
let has_to_be_directory =
(miss_mode == MissingHandling::Normal || miss_mode == MissingHandling::Existing) && {
let path_str = original.to_string_lossy();
path_str.ends_with(MAIN_SEPARATOR) || path_str.ends_with('/')
};
let original = if original.is_absolute() {
original.to_path_buf()
} else {
let current_dir = env::current_dir()?;
dunce::canonicalize(current_dir)?.join(original)
};
let path = if res_mode == ResolveMode::Logical {
normalize_path(&original)
} else {
original
};
let mut parts: VecDeque<OwningComponent> = path.components().map(|part| part.into()).collect();
let mut result = PathBuf::new();
let mut followed_symlinks = 0;
let mut visited_files = HashSet::new();
while let Some(part) = parts.pop_front() {
match part {
OwningComponent::Prefix(s) => {
result.push(s);
continue;
}
OwningComponent::RootDir | OwningComponent::Normal(..) => {
result.push(part.as_os_str());
}
OwningComponent::CurDir => {}
OwningComponent::ParentDir => {
result.pop();
}
}
if res_mode == ResolveMode::None {
continue;
}
match resolve_symlink(&result) {
Ok(Some(link_path)) => {
for link_part in link_path.components().rev() {
parts.push_front(link_part.into());
}
if followed_symlinks < SYMLINKS_TO_LOOK_FOR_LOOPS {
followed_symlinks += 1;
} else {
let file_info =
FileInformation::from_path(result.parent().unwrap(), false).unwrap();
let mut path_to_follow = PathBuf::new();
for part in &parts {
path_to_follow.push(part.as_os_str());
}
if !visited_files.insert((file_info, path_to_follow)) {
return Err(Error::new(
ErrorKind::InvalidInput,
"Too many levels of symbolic links",
)); }
}
result.pop();
}
Err(e) => {
if miss_mode == MissingHandling::Existing
|| (miss_mode == MissingHandling::Normal && !parts.is_empty())
{
return Err(e);
}
}
_ => {}
}
}
match miss_mode {
MissingHandling::Existing => {
if has_to_be_directory {
read_dir(&result)?;
}
}
MissingHandling::Normal => {
if result.exists() {
if has_to_be_directory {
read_dir(&result)?;
}
} else if let Some(parent) = result.parent() {
read_dir(parent)?;
}
}
MissingHandling::Missing => {}
}
Ok(result)
}
#[cfg(not(unix))]
pub fn display_permissions(metadata: &fs::Metadata, display_file_type: bool) -> String {
let write = if metadata.permissions().readonly() {
'-'
} else {
'w'
};
if display_file_type {
let file_type = if metadata.is_symlink() {
'l'
} else if metadata.is_dir() {
'd'
} else {
'-'
};
format!("{file_type}r{write}xr{write}xr{write}x")
} else {
format!("r{write}xr{write}xr{write}x")
}
}
#[cfg(unix)]
pub fn display_permissions(metadata: &fs::Metadata, display_file_type: bool) -> String {
let mode: mode_t = metadata.mode() as mode_t;
display_permissions_unix(mode, display_file_type)
}
#[allow(clippy::if_not_else)]
#[allow(clippy::cognitive_complexity)]
#[cfg(unix)]
pub fn display_permissions_unix(mode: mode_t, display_file_type: bool) -> String {
let mut result;
if display_file_type {
result = String::with_capacity(10);
result.push(match mode & S_IFMT {
S_IFDIR => 'd',
S_IFCHR => 'c',
S_IFBLK => 'b',
S_IFREG => '-',
S_IFIFO => 'p',
S_IFLNK => 'l',
S_IFSOCK => 's',
_ => '?',
});
} else {
result = String::with_capacity(9);
}
result.push(if has!(mode, S_IRUSR) { 'r' } else { '-' });
result.push(if has!(mode, S_IWUSR) { 'w' } else { '-' });
result.push(if has!(mode, S_ISUID as mode_t) {
if has!(mode, S_IXUSR) {
's'
} else {
'S'
}
} else if has!(mode, S_IXUSR) {
'x'
} else {
'-'
});
result.push(if has!(mode, S_IRGRP) { 'r' } else { '-' });
result.push(if has!(mode, S_IWGRP) { 'w' } else { '-' });
result.push(if has!(mode, S_ISGID as mode_t) {
if has!(mode, S_IXGRP) {
's'
} else {
'S'
}
} else if has!(mode, S_IXGRP) {
'x'
} else {
'-'
});
result.push(if has!(mode, S_IROTH) { 'r' } else { '-' });
result.push(if has!(mode, S_IWOTH) { 'w' } else { '-' });
result.push(if has!(mode, S_ISVTX as mode_t) {
if has!(mode, S_IXOTH) {
't'
} else {
'T'
}
} else if has!(mode, S_IXOTH) {
'x'
} else {
'-'
});
result
}
pub fn dir_strip_dot_for_creation(path: &Path) -> PathBuf {
if path.to_string_lossy().ends_with("/.") {
Path::new(&path).components().collect::<PathBuf>()
} else {
path.to_path_buf()
}
}
pub fn paths_refer_to_same_file<P: AsRef<Path>>(p1: P, p2: P, dereference: bool) -> bool {
infos_refer_to_same_file(
FileInformation::from_path(p1, dereference),
FileInformation::from_path(p2, dereference),
)
}
pub fn infos_refer_to_same_file(
info1: IOResult<FileInformation>,
info2: IOResult<FileInformation>,
) -> bool {
if let Ok(info1) = info1 {
if let Ok(info2) = info2 {
return info1 == info2;
}
}
false
}
pub fn make_path_relative_to<P1: AsRef<Path>, P2: AsRef<Path>>(path: P1, to: P2) -> PathBuf {
let path = path.as_ref();
let to = to.as_ref();
let common_prefix_size = path
.components()
.zip(to.components())
.take_while(|(first, second)| first == second)
.count();
let path_suffix = path
.components()
.skip(common_prefix_size)
.map(|x| x.as_os_str());
let mut components: Vec<_> = to
.components()
.skip(common_prefix_size)
.map(|_| Component::ParentDir.as_os_str())
.chain(path_suffix)
.collect();
if components.is_empty() {
components.push(Component::CurDir.as_os_str());
}
components.iter().collect()
}
pub fn is_symlink_loop(path: &Path) -> bool {
let mut visited_symlinks = HashSet::new();
let mut current_path = path.to_path_buf();
while let (Ok(metadata), Ok(link)) = (
current_path.symlink_metadata(),
fs::read_link(¤t_path),
) {
if !metadata.file_type().is_symlink() {
return false;
}
if !visited_symlinks.insert(current_path.clone()) {
return true;
}
current_path = link;
}
false
}
#[cfg(not(unix))]
pub fn are_hardlinks_to_same_file(_source: &Path, _target: &Path) -> bool {
false
}
#[cfg(unix)]
pub fn are_hardlinks_to_same_file(source: &Path, target: &Path) -> bool {
let source_metadata = match fs::symlink_metadata(source) {
Ok(metadata) => metadata,
Err(_) => return false,
};
let target_metadata = match fs::symlink_metadata(target) {
Ok(metadata) => metadata,
Err(_) => return false,
};
source_metadata.ino() == target_metadata.ino() && source_metadata.dev() == target_metadata.dev()
}
#[cfg(not(unix))]
pub fn are_hardlinks_or_one_way_symlink_to_same_file(_source: &Path, _target: &Path) -> bool {
false
}
#[cfg(unix)]
pub fn are_hardlinks_or_one_way_symlink_to_same_file(source: &Path, target: &Path) -> bool {
let source_metadata = match fs::metadata(source) {
Ok(metadata) => metadata,
Err(_) => return false,
};
let target_metadata = match fs::symlink_metadata(target) {
Ok(metadata) => metadata,
Err(_) => return false,
};
source_metadata.ino() == target_metadata.ino() && source_metadata.dev() == target_metadata.dev()
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(unix)]
use std::io::Write;
#[cfg(unix)]
use std::os::unix;
#[cfg(unix)]
use tempfile::{tempdir, NamedTempFile};
struct NormalizePathTestCase<'a> {
path: &'a str,
test: &'a str,
}
const NORMALIZE_PATH_TESTS: [NormalizePathTestCase; 8] = [
NormalizePathTestCase {
path: "./foo/bar.txt",
test: "foo/bar.txt",
},
NormalizePathTestCase {
path: "bar/../foo/bar.txt",
test: "foo/bar.txt",
},
NormalizePathTestCase {
path: "foo///bar.txt",
test: "foo/bar.txt",
},
NormalizePathTestCase {
path: "foo///bar",
test: "foo/bar",
},
NormalizePathTestCase {
path: "foo//./bar",
test: "foo/bar",
},
NormalizePathTestCase {
path: "/foo//./bar",
test: "/foo/bar",
},
NormalizePathTestCase {
path: r"C:/you/later/",
test: "C:/you/later",
},
NormalizePathTestCase {
path: "\\networkShare/a//foo//./bar",
test: "\\networkShare/a/foo/bar",
},
];
#[test]
fn test_normalize_path() {
for test in &NORMALIZE_PATH_TESTS {
let path = Path::new(test.path);
let normalized = normalize_path(path);
assert_eq!(
test.test
.replace('/', std::path::MAIN_SEPARATOR.to_string().as_str()),
normalized.to_str().expect("Path is not valid utf-8!")
);
}
}
#[cfg(unix)]
#[test]
fn test_display_permissions() {
assert_eq!(
"drwxr-xr-x",
display_permissions_unix(S_IFDIR | 0o755, true)
);
assert_eq!(
"rwxr-xr-x",
display_permissions_unix(S_IFDIR | 0o755, false)
);
assert_eq!(
"-rw-r--r--",
display_permissions_unix(S_IFREG | 0o644, true)
);
assert_eq!(
"srw-r-----",
display_permissions_unix(S_IFSOCK | 0o640, true)
);
assert_eq!(
"lrw-r-xr-x",
display_permissions_unix(S_IFLNK | 0o655, true)
);
assert_eq!("?rw-r-xr-x", display_permissions_unix(0o655, true));
assert_eq!(
"brwSr-xr-x",
display_permissions_unix(S_IFBLK | S_ISUID as mode_t | 0o655, true)
);
assert_eq!(
"brwsr-xr-x",
display_permissions_unix(S_IFBLK | S_ISUID as mode_t | 0o755, true)
);
assert_eq!(
"prw---sr--",
display_permissions_unix(S_IFIFO | S_ISGID as mode_t | 0o614, true)
);
assert_eq!(
"prw---Sr--",
display_permissions_unix(S_IFIFO | S_ISGID as mode_t | 0o604, true)
);
assert_eq!(
"c---r-xr-t",
display_permissions_unix(S_IFCHR | S_ISVTX as mode_t | 0o055, true)
);
assert_eq!(
"c---r-xr-T",
display_permissions_unix(S_IFCHR | S_ISVTX as mode_t | 0o054, true)
);
}
#[cfg(unix)]
#[test]
fn test_is_symlink_loop_no_loop() {
let temp_dir = tempdir().unwrap();
let file_path = temp_dir.path().join("file.txt");
let symlink_path = temp_dir.path().join("symlink");
fs::write(&file_path, "test content").unwrap();
unix::fs::symlink(&file_path, &symlink_path).unwrap();
assert!(!is_symlink_loop(&symlink_path));
}
#[cfg(unix)]
#[test]
fn test_is_symlink_loop_direct_loop() {
let temp_dir = tempdir().unwrap();
let symlink_path = temp_dir.path().join("loop");
unix::fs::symlink(&symlink_path, &symlink_path).unwrap();
assert!(is_symlink_loop(&symlink_path));
}
#[cfg(unix)]
#[test]
fn test_is_symlink_loop_indirect_loop() {
let temp_dir = tempdir().unwrap();
let symlink1_path = temp_dir.path().join("symlink1");
let symlink2_path = temp_dir.path().join("symlink2");
unix::fs::symlink(&symlink1_path, &symlink2_path).unwrap();
unix::fs::symlink(&symlink2_path, &symlink1_path).unwrap();
assert!(is_symlink_loop(&symlink1_path));
}
#[cfg(unix)]
#[test]
fn test_are_hardlinks_to_same_file_same_file() {
let mut temp_file = NamedTempFile::new().unwrap();
writeln!(temp_file, "Test content").unwrap();
let path1 = temp_file.path();
let path2 = temp_file.path();
assert_eq!(are_hardlinks_to_same_file(&path1, &path2), true);
}
#[cfg(unix)]
#[test]
fn test_are_hardlinks_to_same_file_different_files() {
let mut temp_file1 = NamedTempFile::new().unwrap();
writeln!(temp_file1, "Test content 1").unwrap();
let mut temp_file2 = NamedTempFile::new().unwrap();
writeln!(temp_file2, "Test content 2").unwrap();
let path1 = temp_file1.path();
let path2 = temp_file2.path();
assert_eq!(are_hardlinks_to_same_file(&path1, &path2), false);
}
#[cfg(unix)]
#[test]
fn test_are_hardlinks_to_same_file_hard_link() {
let mut temp_file = NamedTempFile::new().unwrap();
writeln!(temp_file, "Test content").unwrap();
let path1 = temp_file.path();
let path2 = temp_file.path().with_extension("hardlink");
fs::hard_link(&path1, &path2).unwrap();
assert_eq!(are_hardlinks_to_same_file(&path1, &path2), true);
}
}