#[cfg(unix)]
use libc::{
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,
mkfifo, mode_t,
};
use std::collections::HashSet;
use std::collections::VecDeque;
use std::env;
#[cfg(unix)]
use std::ffi::CString;
use std::ffi::{OsStr, OsString};
use std::fs;
use std::fs::read_dir;
use std::hash::Hash;
use std::io::Stdin;
use std::io::{Error, ErrorKind, Result as IOResult};
#[cfg(unix)]
use std::os::fd::AsFd;
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
use std::path::{Component, MAIN_SEPARATOR, Path, PathBuf};
#[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 AsFd) -> IOResult<Self> {
let stat = nix::sys::stat::fstat(file)?;
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 = "aix"),
not(target_os = "android"),
not(target_os = "freebsd"),
not(target_os = "netbsd"),
not(target_os = "openbsd"),
not(target_os = "illumos"),
not(target_os = "solaris"),
not(target_arch = "aarch64"),
not(target_arch = "riscv64"),
not(target_arch = "loongarch64"),
not(target_arch = "sparc64"),
target_pointer_width = "64"
))]
return self.0.st_nlink;
#[cfg(all(
unix,
any(
target_vendor = "apple",
target_os = "android",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "illumos",
target_os = "solaris",
target_arch = "aarch64",
target_arch = "riscv64",
target_arch = "loongarch64",
target_arch = "sparc64",
not(target_pointer_width = "64")
)
))]
return self.0.st_nlink.into();
#[cfg(target_os = "aix")]
return self.0.st_nlink.try_into().unwrap();
#[cfg(windows)]
return self.0.number_of_links();
}
#[cfg(unix)]
pub fn inode(&self) -> u64 {
#[cfg(all(
not(any(target_os = "freebsd", target_os = "netbsd")),
target_pointer_width = "64"
))]
return self.0.st_ino;
#[cfg(any(
target_os = "freebsd",
target_os = "netbsd",
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);
}
}
}
#[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().copied() {
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)
}
#[cfg(unix)]
fn get_file_display(mode: mode_t) -> char {
match mode & S_IFMT {
S_IFDIR => 'd',
S_IFCHR => 'c',
S_IFBLK => 'b',
S_IFREG => '-',
S_IFIFO => 'p',
S_IFLNK => 'l',
S_IFSOCK => 's',
_ => '?',
}
}
#[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(get_file_display(mode));
} 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 {
let path_str = path.to_string_lossy();
if path_str.ends_with("/.") || path_str.ends_with("/./") {
Path::new(&path).components().collect()
} 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 (Ok(source_metadata), Ok(target_metadata)) =
(fs::symlink_metadata(source), fs::symlink_metadata(target))
else {
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 (Ok(source_metadata), Ok(target_metadata)) =
(fs::metadata(source), fs::symlink_metadata(target))
else {
return false;
};
source_metadata.ino() == target_metadata.ino() && source_metadata.dev() == target_metadata.dev()
}
#[cfg(unix)]
pub fn path_ends_with_terminator(path: &Path) -> bool {
use std::os::unix::prelude::OsStrExt;
path.as_os_str()
.as_bytes()
.last()
.is_some_and(|&byte| byte == b'/')
}
#[cfg(windows)]
pub fn path_ends_with_terminator(path: &Path) -> bool {
use std::os::windows::prelude::OsStrExt;
path.as_os_str()
.encode_wide()
.last()
.is_some_and(|wide| wide == b'/'.into() || wide == b'\\'.into())
}
pub fn is_stdin_directory(stdin: &Stdin) -> bool {
#[cfg(unix)]
{
use nix::sys::stat::fstat;
let mode = fstat(stdin.as_fd()).unwrap().st_mode as mode_t;
mode & S_IFMT == S_IFDIR
}
#[cfg(windows)]
{
use std::os::windows::io::AsRawHandle;
let handle = stdin.as_raw_handle();
if let Ok(metadata) = fs::metadata(format!("{}", handle as usize)) {
return metadata.is_dir();
}
false
}
}
pub mod sane_blksize {
#[cfg(not(target_os = "windows"))]
use std::os::unix::fs::MetadataExt;
use std::{fs::metadata, path::Path};
pub const DEFAULT: u64 = 512;
pub const MAX: u64 = (u32::MAX / 8 + 1) as u64;
pub fn sane_blksize(st_blksize: u64) -> u64 {
match st_blksize {
0 => DEFAULT,
1..=MAX => st_blksize,
_ => DEFAULT,
}
}
pub fn sane_blksize_from_metadata(_metadata: &std::fs::Metadata) -> u64 {
#[cfg(not(target_os = "windows"))]
{
sane_blksize(_metadata.blksize())
}
#[cfg(target_os = "windows")]
{
DEFAULT
}
}
pub fn sane_blksize_from_path(path: &Path) -> u64 {
match metadata(path) {
Ok(metadata) => sane_blksize_from_metadata(&metadata),
Err(_) => DEFAULT,
}
}
}
pub fn get_filename(file: &Path) -> Option<&str> {
file.file_name().and_then(|filename| filename.to_str())
}
#[cfg(unix)]
pub fn make_fifo(path: &Path) -> std::io::Result<()> {
let name = CString::new(path.to_str().unwrap()).unwrap();
let err = unsafe { mkfifo(name.as_ptr(), 0o666) };
if err == -1 {
Err(std::io::Error::from_raw_os_error(err))
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(unix)]
use std::io::Write;
#[cfg(unix)]
use std::os::unix;
#[cfg(unix)]
use std::os::unix::fs::FileTypeExt;
#[cfg(unix)]
use tempfile::{NamedTempFile, tempdir};
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!(are_hardlinks_to_same_file(path1, path2));
}
#[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!(!are_hardlinks_to_same_file(path1, path2));
}
#[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!(are_hardlinks_to_same_file(path1, &path2));
}
#[cfg(unix)]
#[test]
fn test_get_file_display() {
assert_eq!(get_file_display(S_IFDIR | 0o755), 'd');
assert_eq!(get_file_display(S_IFCHR | 0o644), 'c');
assert_eq!(get_file_display(S_IFBLK | 0o600), 'b');
assert_eq!(get_file_display(S_IFREG | 0o777), '-');
assert_eq!(get_file_display(S_IFIFO | 0o666), 'p');
assert_eq!(get_file_display(S_IFLNK | 0o777), 'l');
assert_eq!(get_file_display(S_IFSOCK | 0o600), 's');
assert_eq!(get_file_display(0o777), '?');
}
#[test]
fn test_path_ends_with_terminator() {
assert!(path_ends_with_terminator(Path::new("/some/path/")));
#[cfg(windows)]
assert!(path_ends_with_terminator(Path::new("C:\\some\\path\\")));
assert!(!path_ends_with_terminator(Path::new("/some/path")));
assert!(!path_ends_with_terminator(Path::new("C:\\some\\path")));
assert!(!path_ends_with_terminator(Path::new("")));
assert!(path_ends_with_terminator(Path::new("/")));
#[cfg(windows)]
assert!(path_ends_with_terminator(Path::new("C:\\")));
}
#[test]
fn test_sane_blksize() {
assert_eq!(512, sane_blksize::sane_blksize(0));
assert_eq!(512, sane_blksize::sane_blksize(512));
assert_eq!(4096, sane_blksize::sane_blksize(4096));
assert_eq!(0x2000_0000, sane_blksize::sane_blksize(0x2000_0000));
assert_eq!(512, sane_blksize::sane_blksize(0x2000_0001));
}
#[test]
fn test_get_file_name() {
let file_path = PathBuf::from("~/foo.txt");
assert!(matches!(get_filename(&file_path), Some("foo.txt")));
}
#[cfg(unix)]
#[test]
fn test_make_fifo() {
let tempdir = tempdir().unwrap();
let path = tempdir.path().join("f");
assert!(make_fifo(&path).is_ok());
assert!(std::fs::metadata(&path).unwrap().file_type().is_fifo());
let path2 = path.clone();
std::thread::spawn(move || assert!(std::fs::write(&path2, b"foo").is_ok()));
assert_eq!(std::fs::read(&path).unwrap(), b"foo");
}
}