uucore/features/
fs.rs

1// This file is part of the uutils coreutils package.
2//
3// For the full copyright and license information, please view the LICENSE
4// file that was distributed with this source code.
5
6//! Set of functions to manage regular files, special files, and links.
7
8// spell-checker:ignore backport
9
10#[cfg(unix)]
11use libc::{
12    S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFREG, S_IFSOCK, S_IRGRP, S_IROTH,
13    S_IRUSR, S_ISGID, S_ISUID, S_ISVTX, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP, S_IXOTH, S_IXUSR,
14    mkfifo, mode_t,
15};
16use std::collections::HashSet;
17use std::collections::VecDeque;
18use std::env;
19#[cfg(unix)]
20use std::ffi::CString;
21use std::ffi::{OsStr, OsString};
22use std::fs;
23use std::fs::read_dir;
24use std::hash::Hash;
25use std::io::Stdin;
26use std::io::{Error, ErrorKind, Result as IOResult};
27#[cfg(unix)]
28use std::os::fd::AsFd;
29#[cfg(unix)]
30use std::os::unix::fs::MetadataExt;
31use std::path::{Component, MAIN_SEPARATOR, Path, PathBuf};
32#[cfg(target_os = "windows")]
33use winapi_util::AsHandleRef;
34
35/// Used to check if the `mode` has its `perm` bit set.
36///
37/// This macro expands to `mode & perm != 0`.
38#[cfg(unix)]
39#[macro_export]
40macro_rules! has {
41    ($mode:expr, $perm:expr) => {
42        $mode & $perm != 0
43    };
44}
45
46/// Information to uniquely identify a file
47pub struct FileInformation(
48    #[cfg(unix)] nix::sys::stat::FileStat,
49    #[cfg(windows)] winapi_util::file::Information,
50);
51
52impl FileInformation {
53    /// Get information from a currently open file
54    #[cfg(unix)]
55    pub fn from_file(file: &impl AsFd) -> IOResult<Self> {
56        let stat = nix::sys::stat::fstat(file)?;
57        Ok(Self(stat))
58    }
59
60    /// Get information from a currently open file
61    #[cfg(target_os = "windows")]
62    pub fn from_file(file: &impl AsHandleRef) -> IOResult<Self> {
63        let info = winapi_util::file::information(file.as_handle_ref())?;
64        Ok(Self(info))
65    }
66
67    /// Get information for a given path.
68    ///
69    /// If `path` points to a symlink and `dereference` is true, information about
70    /// the link's target will be returned.
71    pub fn from_path(path: impl AsRef<Path>, dereference: bool) -> IOResult<Self> {
72        #[cfg(unix)]
73        {
74            let stat = if dereference {
75                nix::sys::stat::stat(path.as_ref())
76            } else {
77                nix::sys::stat::lstat(path.as_ref())
78            };
79            Ok(Self(stat?))
80        }
81        #[cfg(target_os = "windows")]
82        {
83            use std::fs::OpenOptions;
84            use std::os::windows::prelude::*;
85            let mut open_options = OpenOptions::new();
86            let mut custom_flags = 0;
87            if !dereference {
88                custom_flags |=
89                    windows_sys::Win32::Storage::FileSystem::FILE_FLAG_OPEN_REPARSE_POINT;
90            }
91            custom_flags |= windows_sys::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS;
92            open_options.custom_flags(custom_flags);
93            let file = open_options.read(true).open(path.as_ref())?;
94            Self::from_file(&file)
95        }
96    }
97
98    pub fn file_size(&self) -> u64 {
99        #[cfg(unix)]
100        {
101            assert!(self.0.st_size >= 0, "File size is negative");
102            self.0.st_size.try_into().unwrap()
103        }
104        #[cfg(target_os = "windows")]
105        {
106            self.0.file_size()
107        }
108    }
109
110    #[cfg(windows)]
111    pub fn file_index(&self) -> u64 {
112        self.0.file_index()
113    }
114
115    pub fn number_of_links(&self) -> u64 {
116        #[cfg(all(
117            unix,
118            not(target_vendor = "apple"),
119            not(target_os = "aix"),
120            not(target_os = "android"),
121            not(target_os = "freebsd"),
122            not(target_os = "netbsd"),
123            not(target_os = "openbsd"),
124            not(target_os = "illumos"),
125            not(target_os = "solaris"),
126            not(target_arch = "aarch64"),
127            not(target_arch = "riscv64"),
128            not(target_arch = "loongarch64"),
129            not(target_arch = "sparc64"),
130            target_pointer_width = "64"
131        ))]
132        return self.0.st_nlink;
133        #[cfg(all(
134            unix,
135            any(
136                target_vendor = "apple",
137                target_os = "android",
138                target_os = "freebsd",
139                target_os = "netbsd",
140                target_os = "openbsd",
141                target_os = "illumos",
142                target_os = "solaris",
143                target_arch = "aarch64",
144                target_arch = "riscv64",
145                target_arch = "loongarch64",
146                target_arch = "sparc64",
147                not(target_pointer_width = "64")
148            )
149        ))]
150        return self.0.st_nlink.into();
151        #[cfg(target_os = "aix")]
152        return self.0.st_nlink.try_into().unwrap();
153        #[cfg(windows)]
154        return self.0.number_of_links();
155    }
156
157    #[cfg(unix)]
158    pub fn inode(&self) -> u64 {
159        #[cfg(all(
160            not(any(target_os = "freebsd", target_os = "netbsd")),
161            target_pointer_width = "64"
162        ))]
163        return self.0.st_ino;
164        #[cfg(any(
165            target_os = "freebsd",
166            target_os = "netbsd",
167            not(target_pointer_width = "64")
168        ))]
169        return self.0.st_ino.into();
170    }
171}
172
173#[cfg(unix)]
174impl PartialEq for FileInformation {
175    fn eq(&self, other: &Self) -> bool {
176        self.0.st_dev == other.0.st_dev && self.0.st_ino == other.0.st_ino
177    }
178}
179
180#[cfg(target_os = "windows")]
181impl PartialEq for FileInformation {
182    fn eq(&self, other: &Self) -> bool {
183        self.0.volume_serial_number() == other.0.volume_serial_number()
184            && self.0.file_index() == other.0.file_index()
185    }
186}
187
188impl Eq for FileInformation {}
189
190impl Hash for FileInformation {
191    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
192        #[cfg(unix)]
193        {
194            self.0.st_dev.hash(state);
195            self.0.st_ino.hash(state);
196        }
197        #[cfg(target_os = "windows")]
198        {
199            self.0.volume_serial_number().hash(state);
200            self.0.file_index().hash(state);
201        }
202    }
203}
204
205/// Controls how symbolic links should be handled when canonicalizing a path.
206#[derive(Clone, Copy, Debug, Eq, PartialEq)]
207pub enum MissingHandling {
208    /// Return an error if any part of the path is missing.
209    Normal,
210
211    /// Resolve symbolic links, ignoring errors on the final component.
212    Existing,
213
214    /// Resolve symbolic links, ignoring errors on the non-final components.
215    Missing,
216}
217
218/// Controls when symbolic links are resolved
219#[derive(Clone, Copy, Debug, Eq, PartialEq)]
220pub enum ResolveMode {
221    /// Do not resolve any symbolic links.
222    None,
223
224    /// Resolve symlinks as encountered when processing the path
225    Physical,
226
227    /// Resolve '..' elements before symlinks
228    Logical,
229}
230
231/// Normalize a path by removing relative information
232/// For example, convert 'bar/../foo/bar.txt' => 'foo/bar.txt'
233/// copied from `<https://github.com/rust-lang/cargo/blob/2e4cfc2b7d43328b207879228a2ca7d427d188bb/src/cargo/util/paths.rs#L65-L90>`
234/// both projects are MIT `<https://github.com/rust-lang/cargo/blob/master/LICENSE-MIT>`
235/// for std impl progress see rfc `<https://github.com/rust-lang/rfcs/issues/2208>`
236/// replace this once that lands
237pub fn normalize_path(path: &Path) -> PathBuf {
238    let mut components = path.components().peekable();
239    let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().copied() {
240        components.next();
241        PathBuf::from(c.as_os_str())
242    } else {
243        PathBuf::new()
244    };
245
246    for component in components {
247        match component {
248            Component::Prefix(..) => unreachable!(),
249            Component::RootDir => {
250                ret.push(component.as_os_str());
251            }
252            Component::CurDir => {}
253            Component::ParentDir => {
254                ret.pop();
255            }
256            Component::Normal(c) => {
257                ret.push(c);
258            }
259        }
260    }
261    ret
262}
263
264fn resolve_symlink<P: AsRef<Path>>(path: P) -> IOResult<Option<PathBuf>> {
265    let result = if fs::symlink_metadata(&path)?.file_type().is_symlink() {
266        Some(fs::read_link(&path)?)
267    } else {
268        None
269    };
270    Ok(result)
271}
272
273enum OwningComponent {
274    Prefix(OsString),
275    RootDir,
276    CurDir,
277    ParentDir,
278    Normal(OsString),
279}
280
281impl OwningComponent {
282    fn as_os_str(&self) -> &OsStr {
283        match self {
284            Self::Prefix(s) => s.as_os_str(),
285            Self::RootDir => Component::RootDir.as_os_str(),
286            Self::CurDir => Component::CurDir.as_os_str(),
287            Self::ParentDir => Component::ParentDir.as_os_str(),
288            Self::Normal(s) => s.as_os_str(),
289        }
290    }
291}
292
293impl<'a> From<Component<'a>> for OwningComponent {
294    fn from(comp: Component<'a>) -> Self {
295        match comp {
296            Component::Prefix(_) => Self::Prefix(comp.as_os_str().to_os_string()),
297            Component::RootDir => Self::RootDir,
298            Component::CurDir => Self::CurDir,
299            Component::ParentDir => Self::ParentDir,
300            Component::Normal(s) => Self::Normal(s.to_os_string()),
301        }
302    }
303}
304
305/// Return the canonical, absolute form of a path.
306///
307/// This function is a generalization of [`std::fs::canonicalize`] that
308/// allows controlling how symbolic links are resolved and how to deal
309/// with missing components. It returns the canonical, absolute form of
310/// a path.
311/// The `miss_mode` parameter controls how missing path elements are handled
312///
313/// * [`MissingHandling::Normal`] makes this function behave like
314///   [`std::fs::canonicalize`], resolving symbolic links and returning
315///   an error if the path does not exist.
316/// * [`MissingHandling::Missing`] makes this function ignore non-final
317///   components of the path that could not be resolved.
318/// * [`MissingHandling::Existing`] makes this function return an error
319///   if the final component of the path does not exist.
320///
321/// The `res_mode` parameter controls how symbolic links are
322/// resolved:
323///
324/// * [`ResolveMode::None`] makes this function not try to resolve
325///   any symbolic links.
326/// * [`ResolveMode::Physical`] makes this function resolve symlinks as they
327///   are encountered
328/// * [`ResolveMode::Logical`] makes this function resolve '..' components
329///   before symlinks
330///
331#[allow(clippy::cognitive_complexity)]
332pub fn canonicalize<P: AsRef<Path>>(
333    original: P,
334    miss_mode: MissingHandling,
335    res_mode: ResolveMode,
336) -> IOResult<PathBuf> {
337    const SYMLINKS_TO_LOOK_FOR_LOOPS: i32 = 20;
338    let original = original.as_ref();
339    let has_to_be_directory =
340        (miss_mode == MissingHandling::Normal || miss_mode == MissingHandling::Existing) && {
341            let path_str = original.to_string_lossy();
342            path_str.ends_with(MAIN_SEPARATOR) || path_str.ends_with('/')
343        };
344    let original = if original.is_absolute() {
345        original.to_path_buf()
346    } else {
347        let current_dir = env::current_dir()?;
348        dunce::canonicalize(current_dir)?.join(original)
349    };
350    let path = if res_mode == ResolveMode::Logical {
351        normalize_path(&original)
352    } else {
353        original
354    };
355    let mut parts: VecDeque<OwningComponent> = path.components().map(|part| part.into()).collect();
356    let mut result = PathBuf::new();
357    let mut followed_symlinks = 0;
358    let mut visited_files = HashSet::new();
359    while let Some(part) = parts.pop_front() {
360        match part {
361            OwningComponent::Prefix(s) => {
362                result.push(s);
363                continue;
364            }
365            OwningComponent::RootDir | OwningComponent::Normal(..) => {
366                result.push(part.as_os_str());
367            }
368            OwningComponent::CurDir => {}
369            OwningComponent::ParentDir => {
370                result.pop();
371            }
372        }
373        if res_mode == ResolveMode::None {
374            continue;
375        }
376        match resolve_symlink(&result) {
377            Ok(Some(link_path)) => {
378                for link_part in link_path.components().rev() {
379                    parts.push_front(link_part.into());
380                }
381                if followed_symlinks < SYMLINKS_TO_LOOK_FOR_LOOPS {
382                    followed_symlinks += 1;
383                } else {
384                    let file_info =
385                        FileInformation::from_path(result.parent().unwrap(), false).unwrap();
386                    let mut path_to_follow = PathBuf::new();
387                    for part in &parts {
388                        path_to_follow.push(part.as_os_str());
389                    }
390                    if !visited_files.insert((file_info, path_to_follow)) {
391                        return Err(Error::new(
392                            ErrorKind::InvalidInput,
393                            "Too many levels of symbolic links",
394                        )); // TODO use ErrorKind::FilesystemLoop when stable
395                    }
396                }
397                result.pop();
398            }
399            Err(e) => {
400                if miss_mode == MissingHandling::Existing
401                    || (miss_mode == MissingHandling::Normal && !parts.is_empty())
402                {
403                    return Err(e);
404                }
405            }
406            _ => {}
407        }
408    }
409    // raise Not a directory if required
410    match miss_mode {
411        MissingHandling::Existing => {
412            if has_to_be_directory {
413                read_dir(&result)?;
414            }
415        }
416        MissingHandling::Normal => {
417            if result.exists() {
418                if has_to_be_directory {
419                    read_dir(&result)?;
420                }
421            } else if let Some(parent) = result.parent() {
422                read_dir(parent)?;
423            }
424        }
425        MissingHandling::Missing => {}
426    }
427    Ok(result)
428}
429
430#[cfg(not(unix))]
431/// Display the permissions of a file
432pub fn display_permissions(metadata: &fs::Metadata, display_file_type: bool) -> String {
433    let write = if metadata.permissions().readonly() {
434        '-'
435    } else {
436        'w'
437    };
438
439    if display_file_type {
440        let file_type = if metadata.is_symlink() {
441            'l'
442        } else if metadata.is_dir() {
443            'd'
444        } else {
445            '-'
446        };
447
448        format!("{file_type}r{write}xr{write}xr{write}x")
449    } else {
450        format!("r{write}xr{write}xr{write}x")
451    }
452}
453
454#[cfg(unix)]
455/// Display the permissions of a file
456pub fn display_permissions(metadata: &fs::Metadata, display_file_type: bool) -> String {
457    let mode: mode_t = metadata.mode() as mode_t;
458    display_permissions_unix(mode, display_file_type)
459}
460
461/// Returns a character representation of the file type based on its mode.
462/// This function is specific to Unix-like systems.
463///
464/// - `mode`: The mode of the file, typically obtained from file metadata.
465///
466/// # Returns
467/// - 'd' for directories
468/// - 'c' for character devices
469/// - 'b' for block devices
470/// - '-' for regular files
471/// - 'p' for FIFOs (named pipes)
472/// - 'l' for symbolic links
473/// - 's' for sockets
474/// - '?' for any other unrecognized file types
475#[cfg(unix)]
476fn get_file_display(mode: mode_t) -> char {
477    match mode & S_IFMT {
478        S_IFDIR => 'd',
479        S_IFCHR => 'c',
480        S_IFBLK => 'b',
481        S_IFREG => '-',
482        S_IFIFO => 'p',
483        S_IFLNK => 'l',
484        S_IFSOCK => 's',
485        // TODO: Other file types
486        _ => '?',
487    }
488}
489
490// The logic below is more readable written this way.
491#[allow(clippy::if_not_else)]
492#[allow(clippy::cognitive_complexity)]
493#[cfg(unix)]
494/// Display the permissions of a file on a unix like system
495pub fn display_permissions_unix(mode: mode_t, display_file_type: bool) -> String {
496    let mut result;
497    if display_file_type {
498        result = String::with_capacity(10);
499        result.push(get_file_display(mode));
500    } else {
501        result = String::with_capacity(9);
502    }
503
504    result.push(if has!(mode, S_IRUSR) { 'r' } else { '-' });
505    result.push(if has!(mode, S_IWUSR) { 'w' } else { '-' });
506    result.push(if has!(mode, S_ISUID as mode_t) {
507        if has!(mode, S_IXUSR) { 's' } else { 'S' }
508    } else if has!(mode, S_IXUSR) {
509        'x'
510    } else {
511        '-'
512    });
513
514    result.push(if has!(mode, S_IRGRP) { 'r' } else { '-' });
515    result.push(if has!(mode, S_IWGRP) { 'w' } else { '-' });
516    result.push(if has!(mode, S_ISGID as mode_t) {
517        if has!(mode, S_IXGRP) { 's' } else { 'S' }
518    } else if has!(mode, S_IXGRP) {
519        'x'
520    } else {
521        '-'
522    });
523
524    result.push(if has!(mode, S_IROTH) { 'r' } else { '-' });
525    result.push(if has!(mode, S_IWOTH) { 'w' } else { '-' });
526    result.push(if has!(mode, S_ISVTX as mode_t) {
527        if has!(mode, S_IXOTH) { 't' } else { 'T' }
528    } else if has!(mode, S_IXOTH) {
529        'x'
530    } else {
531        '-'
532    });
533
534    result
535}
536
537/// For some programs like install or mkdir, dir/. or dir/./ can be provided
538/// Special case to match GNU's behavior:
539/// install -d foo/. (and foo/./) should work and just create foo/
540/// std::fs::create_dir("foo/."); fails in pure Rust
541pub fn dir_strip_dot_for_creation(path: &Path) -> PathBuf {
542    let path_str = path.to_string_lossy();
543
544    if path_str.ends_with("/.") || path_str.ends_with("/./") {
545        // Do a simple dance to strip the "/."
546        Path::new(&path).components().collect()
547    } else {
548        path.to_path_buf()
549    }
550}
551
552/// Checks if `p1` and `p2` are the same file.
553/// If error happens when trying to get files' metadata, returns false
554pub fn paths_refer_to_same_file<P: AsRef<Path>>(p1: P, p2: P, dereference: bool) -> bool {
555    infos_refer_to_same_file(
556        FileInformation::from_path(p1, dereference),
557        FileInformation::from_path(p2, dereference),
558    )
559}
560
561/// Checks if `p1` and `p2` are the same file information.
562/// If error happens when trying to get files' metadata, returns false
563pub fn infos_refer_to_same_file(
564    info1: IOResult<FileInformation>,
565    info2: IOResult<FileInformation>,
566) -> bool {
567    if let Ok(info1) = info1 {
568        if let Ok(info2) = info2 {
569            return info1 == info2;
570        }
571    }
572    false
573}
574
575/// Converts absolute `path` to be relative to absolute `to` path.
576pub fn make_path_relative_to<P1: AsRef<Path>, P2: AsRef<Path>>(path: P1, to: P2) -> PathBuf {
577    let path = path.as_ref();
578    let to = to.as_ref();
579    let common_prefix_size = path
580        .components()
581        .zip(to.components())
582        .take_while(|(first, second)| first == second)
583        .count();
584    let path_suffix = path
585        .components()
586        .skip(common_prefix_size)
587        .map(|x| x.as_os_str());
588    let mut components: Vec<_> = to
589        .components()
590        .skip(common_prefix_size)
591        .map(|_| Component::ParentDir.as_os_str())
592        .chain(path_suffix)
593        .collect();
594    if components.is_empty() {
595        components.push(Component::CurDir.as_os_str());
596    }
597    components.iter().collect()
598}
599
600/// Checks if there is a symlink loop in the given path.
601///
602/// A symlink loop is a chain of symlinks where the last symlink points back to one of the previous symlinks in the chain.
603///
604/// # Arguments
605///
606/// * `path` - A reference to a `Path` representing the starting path to check for symlink loops.
607///
608/// # Returns
609///
610/// * `bool` - Returns `true` if a symlink loop is detected, `false` otherwise.
611pub fn is_symlink_loop(path: &Path) -> bool {
612    let mut visited_symlinks = HashSet::new();
613    let mut current_path = path.to_path_buf();
614
615    while let (Ok(metadata), Ok(link)) = (
616        current_path.symlink_metadata(),
617        fs::read_link(&current_path),
618    ) {
619        if !metadata.file_type().is_symlink() {
620            return false;
621        }
622        if !visited_symlinks.insert(current_path.clone()) {
623            return true;
624        }
625        current_path = link;
626    }
627
628    false
629}
630
631#[cfg(not(unix))]
632// Hard link comparison is not supported on non-Unix platforms
633pub fn are_hardlinks_to_same_file(_source: &Path, _target: &Path) -> bool {
634    false
635}
636
637/// Checks if two paths are hard links to the same file.
638///
639/// # Arguments
640///
641/// * `source` - A reference to a `Path` representing the source path.
642/// * `target` - A reference to a `Path` representing the target path.
643///
644/// # Returns
645///
646/// * `bool` - Returns `true` if the paths are hard links to the same file, and `false` otherwise.
647#[cfg(unix)]
648pub fn are_hardlinks_to_same_file(source: &Path, target: &Path) -> bool {
649    let (Ok(source_metadata), Ok(target_metadata)) =
650        (fs::symlink_metadata(source), fs::symlink_metadata(target))
651    else {
652        return false;
653    };
654
655    source_metadata.ino() == target_metadata.ino() && source_metadata.dev() == target_metadata.dev()
656}
657
658#[cfg(not(unix))]
659pub fn are_hardlinks_or_one_way_symlink_to_same_file(_source: &Path, _target: &Path) -> bool {
660    false
661}
662
663/// Checks if either two paths are hard links to the same file or if the source path is a symbolic link which when fully resolved points to target path
664///
665/// # Arguments
666///
667/// * `source` - A reference to a `Path` representing the source path.
668/// * `target` - A reference to a `Path` representing the target path.
669///
670/// # Returns
671///
672/// * `bool` - Returns `true` if either of above conditions are true, and `false` otherwise.
673#[cfg(unix)]
674pub fn are_hardlinks_or_one_way_symlink_to_same_file(source: &Path, target: &Path) -> bool {
675    let (Ok(source_metadata), Ok(target_metadata)) =
676        (fs::metadata(source), fs::symlink_metadata(target))
677    else {
678        return false;
679    };
680
681    source_metadata.ino() == target_metadata.ino() && source_metadata.dev() == target_metadata.dev()
682}
683
684/// Returns true if the passed `path` ends with a path terminator.
685///
686/// This function examines the last character of the path to determine
687/// if it is a directory separator. It supports both Unix-style (`/`)
688/// and Windows-style (`\`) separators.
689///
690/// # Arguments
691///
692/// * `path` - A reference to the path to be checked.
693#[cfg(unix)]
694pub fn path_ends_with_terminator(path: &Path) -> bool {
695    use std::os::unix::prelude::OsStrExt;
696    path.as_os_str()
697        .as_bytes()
698        .last()
699        .is_some_and(|&byte| byte == b'/')
700}
701
702#[cfg(windows)]
703pub fn path_ends_with_terminator(path: &Path) -> bool {
704    use std::os::windows::prelude::OsStrExt;
705    path.as_os_str()
706        .encode_wide()
707        .last()
708        .is_some_and(|wide| wide == b'/'.into() || wide == b'\\'.into())
709}
710
711/// Checks if the standard input (stdin) is a directory.
712///
713/// # Arguments
714///
715/// * `stdin` - A reference to the standard input handle.
716///
717/// # Returns
718///
719/// * `bool` - Returns `true` if stdin is a directory, `false` otherwise.
720pub fn is_stdin_directory(stdin: &Stdin) -> bool {
721    #[cfg(unix)]
722    {
723        use nix::sys::stat::fstat;
724        let mode = fstat(stdin.as_fd()).unwrap().st_mode as mode_t;
725        // We use the S_IFMT mask ala S_ISDIR() to avoid mistaking
726        // sockets for directories.
727        mode & S_IFMT == S_IFDIR
728    }
729
730    #[cfg(windows)]
731    {
732        use std::os::windows::io::AsRawHandle;
733        let handle = stdin.as_raw_handle();
734        if let Ok(metadata) = fs::metadata(format!("{}", handle as usize)) {
735            return metadata.is_dir();
736        }
737        false
738    }
739}
740
741pub mod sane_blksize {
742
743    #[cfg(not(target_os = "windows"))]
744    use std::os::unix::fs::MetadataExt;
745    use std::{fs::metadata, path::Path};
746
747    pub const DEFAULT: u64 = 512;
748    pub const MAX: u64 = (u32::MAX / 8 + 1) as u64;
749
750    /// Provides sanity checked blksize value from the provided value.
751    ///
752    /// If the provided value is a invalid values a meaningful adaption
753    /// of that value is done.
754    pub fn sane_blksize(st_blksize: u64) -> u64 {
755        match st_blksize {
756            0 => DEFAULT,
757            1..=MAX => st_blksize,
758            _ => DEFAULT,
759        }
760    }
761
762    /// Provides the blksize information from the provided metadata.
763    ///
764    /// If the metadata contain invalid values a meaningful adaption
765    /// of that value is done.
766    pub fn sane_blksize_from_metadata(_metadata: &std::fs::Metadata) -> u64 {
767        #[cfg(not(target_os = "windows"))]
768        {
769            sane_blksize(_metadata.blksize())
770        }
771
772        #[cfg(target_os = "windows")]
773        {
774            DEFAULT
775        }
776    }
777
778    /// Provides the blksize information from given file path's filesystem.
779    ///
780    /// If the metadata can't be fetched or contain invalid values a
781    /// meaningful adaption of that value is done.
782    pub fn sane_blksize_from_path(path: &Path) -> u64 {
783        match metadata(path) {
784            Ok(metadata) => sane_blksize_from_metadata(&metadata),
785            Err(_) => DEFAULT,
786        }
787    }
788}
789
790/// Extracts the filename component from the given `file` path and returns it as an `Option<&str>`.
791///
792/// If the `file` path contains a filename, this function returns `Some(filename)` where `filename` is
793/// the extracted filename as a string slice (`&str`). If the `file` path does not have a filename
794/// component or if the filename is not valid UTF-8, it returns `None`.
795///
796/// # Arguments
797///
798/// * `file`: A reference to a `Path` representing the file path from which to extract the filename.
799///
800/// # Returns
801///
802/// * `Some(filename)`: If a valid filename exists in the `file` path, where `filename` is the
803///   extracted filename as a string slice (`&str`).
804/// * `None`: If the `file` path does not contain a valid filename or if the filename is not valid UTF-8.
805pub fn get_filename(file: &Path) -> Option<&str> {
806    file.file_name().and_then(|filename| filename.to_str())
807}
808
809/// Make a FIFO, also known as a named pipe.
810///
811/// This is a safe wrapper for the unsafe [`libc::mkfifo`] function,
812/// which makes a [named
813/// pipe](https://en.wikipedia.org/wiki/Named_pipe) on Unix systems.
814///
815/// # Errors
816///
817/// If the named pipe cannot be created.
818///
819/// # Examples
820///
821/// ```ignore
822/// use uucore::fs::make_fifo;
823///
824/// make_fifo("my-pipe").expect("failed to create the named pipe");
825///
826/// std::thread::spawn(|| { std::fs::write("my-pipe", b"hello").unwrap(); });
827/// assert_eq!(std::fs::read("my-pipe").unwrap(), b"hello");
828/// ```
829#[cfg(unix)]
830pub fn make_fifo(path: &Path) -> std::io::Result<()> {
831    let name = CString::new(path.to_str().unwrap()).unwrap();
832    let err = unsafe { mkfifo(name.as_ptr(), 0o666) };
833    if err == -1 {
834        Err(std::io::Error::from_raw_os_error(err))
835    } else {
836        Ok(())
837    }
838}
839
840#[cfg(test)]
841mod tests {
842    // Note this useful idiom: importing names from outer (for mod tests) scope.
843    use super::*;
844    #[cfg(unix)]
845    use std::io::Write;
846    #[cfg(unix)]
847    use std::os::unix;
848    #[cfg(unix)]
849    use std::os::unix::fs::FileTypeExt;
850    #[cfg(unix)]
851    use tempfile::{NamedTempFile, tempdir};
852
853    struct NormalizePathTestCase<'a> {
854        path: &'a str,
855        test: &'a str,
856    }
857
858    const NORMALIZE_PATH_TESTS: [NormalizePathTestCase; 8] = [
859        NormalizePathTestCase {
860            path: "./foo/bar.txt",
861            test: "foo/bar.txt",
862        },
863        NormalizePathTestCase {
864            path: "bar/../foo/bar.txt",
865            test: "foo/bar.txt",
866        },
867        NormalizePathTestCase {
868            path: "foo///bar.txt",
869            test: "foo/bar.txt",
870        },
871        NormalizePathTestCase {
872            path: "foo///bar",
873            test: "foo/bar",
874        },
875        NormalizePathTestCase {
876            path: "foo//./bar",
877            test: "foo/bar",
878        },
879        NormalizePathTestCase {
880            path: "/foo//./bar",
881            test: "/foo/bar",
882        },
883        NormalizePathTestCase {
884            path: r"C:/you/later/",
885            test: "C:/you/later",
886        },
887        NormalizePathTestCase {
888            path: "\\networkShare/a//foo//./bar",
889            test: "\\networkShare/a/foo/bar",
890        },
891    ];
892
893    #[test]
894    fn test_normalize_path() {
895        for test in &NORMALIZE_PATH_TESTS {
896            let path = Path::new(test.path);
897            let normalized = normalize_path(path);
898            assert_eq!(
899                test.test
900                    .replace('/', std::path::MAIN_SEPARATOR.to_string().as_str()),
901                normalized.to_str().expect("Path is not valid utf-8!")
902            );
903        }
904    }
905
906    #[cfg(unix)]
907    #[test]
908    fn test_display_permissions() {
909        // spell-checker:ignore (perms) brwsr drwxr rwxr
910        assert_eq!(
911            "drwxr-xr-x",
912            display_permissions_unix(S_IFDIR | 0o755, true)
913        );
914        assert_eq!(
915            "rwxr-xr-x",
916            display_permissions_unix(S_IFDIR | 0o755, false)
917        );
918        assert_eq!(
919            "-rw-r--r--",
920            display_permissions_unix(S_IFREG | 0o644, true)
921        );
922        assert_eq!(
923            "srw-r-----",
924            display_permissions_unix(S_IFSOCK | 0o640, true)
925        );
926        assert_eq!(
927            "lrw-r-xr-x",
928            display_permissions_unix(S_IFLNK | 0o655, true)
929        );
930        assert_eq!("?rw-r-xr-x", display_permissions_unix(0o655, true));
931
932        assert_eq!(
933            "brwSr-xr-x",
934            display_permissions_unix(S_IFBLK | S_ISUID as mode_t | 0o655, true)
935        );
936        assert_eq!(
937            "brwsr-xr-x",
938            display_permissions_unix(S_IFBLK | S_ISUID as mode_t | 0o755, true)
939        );
940
941        assert_eq!(
942            "prw---sr--",
943            display_permissions_unix(S_IFIFO | S_ISGID as mode_t | 0o614, true)
944        );
945        assert_eq!(
946            "prw---Sr--",
947            display_permissions_unix(S_IFIFO | S_ISGID as mode_t | 0o604, true)
948        );
949
950        assert_eq!(
951            "c---r-xr-t",
952            display_permissions_unix(S_IFCHR | S_ISVTX as mode_t | 0o055, true)
953        );
954        assert_eq!(
955            "c---r-xr-T",
956            display_permissions_unix(S_IFCHR | S_ISVTX as mode_t | 0o054, true)
957        );
958    }
959
960    #[cfg(unix)]
961    #[test]
962    fn test_is_symlink_loop_no_loop() {
963        let temp_dir = tempdir().unwrap();
964        let file_path = temp_dir.path().join("file.txt");
965        let symlink_path = temp_dir.path().join("symlink");
966
967        fs::write(&file_path, "test content").unwrap();
968        unix::fs::symlink(&file_path, &symlink_path).unwrap();
969
970        assert!(!is_symlink_loop(&symlink_path));
971    }
972
973    #[cfg(unix)]
974    #[test]
975    fn test_is_symlink_loop_direct_loop() {
976        let temp_dir = tempdir().unwrap();
977        let symlink_path = temp_dir.path().join("loop");
978
979        unix::fs::symlink(&symlink_path, &symlink_path).unwrap();
980
981        assert!(is_symlink_loop(&symlink_path));
982    }
983
984    #[cfg(unix)]
985    #[test]
986    fn test_is_symlink_loop_indirect_loop() {
987        let temp_dir = tempdir().unwrap();
988        let symlink1_path = temp_dir.path().join("symlink1");
989        let symlink2_path = temp_dir.path().join("symlink2");
990
991        unix::fs::symlink(&symlink1_path, &symlink2_path).unwrap();
992        unix::fs::symlink(&symlink2_path, &symlink1_path).unwrap();
993
994        assert!(is_symlink_loop(&symlink1_path));
995    }
996
997    #[cfg(unix)]
998    #[test]
999    fn test_are_hardlinks_to_same_file_same_file() {
1000        let mut temp_file = NamedTempFile::new().unwrap();
1001        writeln!(temp_file, "Test content").unwrap();
1002
1003        let path1 = temp_file.path();
1004        let path2 = temp_file.path();
1005
1006        assert!(are_hardlinks_to_same_file(path1, path2));
1007    }
1008
1009    #[cfg(unix)]
1010    #[test]
1011    fn test_are_hardlinks_to_same_file_different_files() {
1012        let mut temp_file1 = NamedTempFile::new().unwrap();
1013        writeln!(temp_file1, "Test content 1").unwrap();
1014
1015        let mut temp_file2 = NamedTempFile::new().unwrap();
1016        writeln!(temp_file2, "Test content 2").unwrap();
1017
1018        let path1 = temp_file1.path();
1019        let path2 = temp_file2.path();
1020
1021        assert!(!are_hardlinks_to_same_file(path1, path2));
1022    }
1023
1024    #[cfg(unix)]
1025    #[test]
1026    fn test_are_hardlinks_to_same_file_hard_link() {
1027        let mut temp_file = NamedTempFile::new().unwrap();
1028        writeln!(temp_file, "Test content").unwrap();
1029        let path1 = temp_file.path();
1030
1031        let path2 = temp_file.path().with_extension("hardlink");
1032        fs::hard_link(path1, &path2).unwrap();
1033
1034        assert!(are_hardlinks_to_same_file(path1, &path2));
1035    }
1036
1037    #[cfg(unix)]
1038    #[test]
1039    fn test_get_file_display() {
1040        assert_eq!(get_file_display(S_IFDIR | 0o755), 'd');
1041        assert_eq!(get_file_display(S_IFCHR | 0o644), 'c');
1042        assert_eq!(get_file_display(S_IFBLK | 0o600), 'b');
1043        assert_eq!(get_file_display(S_IFREG | 0o777), '-');
1044        assert_eq!(get_file_display(S_IFIFO | 0o666), 'p');
1045        assert_eq!(get_file_display(S_IFLNK | 0o777), 'l');
1046        assert_eq!(get_file_display(S_IFSOCK | 0o600), 's');
1047        assert_eq!(get_file_display(0o777), '?');
1048    }
1049
1050    #[test]
1051    fn test_path_ends_with_terminator() {
1052        // Path ends with a forward slash
1053        assert!(path_ends_with_terminator(Path::new("/some/path/")));
1054
1055        // Path ends with a backslash
1056        #[cfg(windows)]
1057        assert!(path_ends_with_terminator(Path::new("C:\\some\\path\\")));
1058
1059        // Path does not end with a terminator
1060        assert!(!path_ends_with_terminator(Path::new("/some/path")));
1061        assert!(!path_ends_with_terminator(Path::new("C:\\some\\path")));
1062
1063        // Empty path
1064        assert!(!path_ends_with_terminator(Path::new("")));
1065
1066        // Root path
1067        assert!(path_ends_with_terminator(Path::new("/")));
1068        #[cfg(windows)]
1069        assert!(path_ends_with_terminator(Path::new("C:\\")));
1070    }
1071
1072    #[test]
1073    fn test_sane_blksize() {
1074        assert_eq!(512, sane_blksize::sane_blksize(0));
1075        assert_eq!(512, sane_blksize::sane_blksize(512));
1076        assert_eq!(4096, sane_blksize::sane_blksize(4096));
1077        assert_eq!(0x2000_0000, sane_blksize::sane_blksize(0x2000_0000));
1078        assert_eq!(512, sane_blksize::sane_blksize(0x2000_0001));
1079    }
1080    #[test]
1081    fn test_get_file_name() {
1082        let file_path = PathBuf::from("~/foo.txt");
1083        assert!(matches!(get_filename(&file_path), Some("foo.txt")));
1084    }
1085
1086    #[cfg(unix)]
1087    #[test]
1088    fn test_make_fifo() {
1089        // Create the FIFO in a temporary directory.
1090        let tempdir = tempdir().unwrap();
1091        let path = tempdir.path().join("f");
1092        assert!(make_fifo(&path).is_ok());
1093
1094        // Check that it is indeed a FIFO.
1095        assert!(std::fs::metadata(&path).unwrap().file_type().is_fifo());
1096
1097        // Check that we can write to it and read from it.
1098        //
1099        // Write and read need to happen in different threads,
1100        // otherwise `write` would block indefinitely while waiting
1101        // for the `read`.
1102        let path2 = path.clone();
1103        std::thread::spawn(move || assert!(std::fs::write(&path2, b"foo").is_ok()));
1104        assert_eq!(std::fs::read(&path).unwrap(), b"foo");
1105    }
1106}