normalize_path/
lib.rs

1//! Normalizes paths similarly to canonicalize, but without performing I/O.
2//!
3//! This is like Python's `os.path.normpath`.
4//!
5//! Initially adapted from [Cargo's implementation][cargo-paths].
6//!
7//! [cargo-paths]: https://github.com/rust-lang/cargo/blob/fede83ccf973457de319ba6fa0e36ead454d2e20/src/cargo/util/paths.rs#L61
8//!
9//! # Example
10//!
11//! ```
12//! use normalize_path::NormalizePath;
13//! use std::path::Path;
14//!
15//! assert_eq!(
16//!     Path::new("/A/foo/../B/./").normalize(),
17//!     Path::new("/A/B")
18//! );
19//! ```
20
21use std::path::{Component, Path, PathBuf};
22
23/// Extension trait to add `normalize_path` to std's [`Path`].
24pub trait NormalizePath {
25    /// Normalize a path without performing I/O.
26    ///
27    /// All redundant separator and up-level references are collapsed.
28    ///
29    /// However, this does not resolve links.
30    fn normalize(&self) -> PathBuf;
31
32    /// Same as [`NormalizePath::normalize`] except that if
33    /// `Component::Prefix`/`Component::RootDir` is encountered,
34    /// or if the path points outside of current dir, returns `None`.
35    fn try_normalize(&self) -> Option<PathBuf>;
36
37    /// Return `true` if the path is normalized.
38    ///
39    /// # Quirk
40    ///
41    /// If the path does not start with `./` but contains `./` in the middle,
42    /// then this function might returns `true`.
43    fn is_normalized(&self) -> bool;
44}
45
46impl NormalizePath for Path {
47    fn normalize(&self) -> PathBuf {
48        let mut components = self.components().peekable();
49        let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek() {
50            let buf = PathBuf::from(c.as_os_str());
51            components.next();
52            buf
53        } else {
54            PathBuf::new()
55        };
56
57        for component in components {
58            match component {
59                Component::Prefix(..) => unreachable!(),
60                Component::RootDir => {
61                    ret.push(component.as_os_str());
62                }
63                Component::CurDir => {}
64                Component::ParentDir => {
65                    ret.pop();
66                }
67                Component::Normal(c) => {
68                    ret.push(c);
69                }
70            }
71        }
72
73        ret
74    }
75
76    fn try_normalize(&self) -> Option<PathBuf> {
77        let mut ret = PathBuf::new();
78
79        for component in self.components() {
80            match component {
81                Component::Prefix(..) | Component::RootDir => return None,
82                Component::CurDir => {}
83                Component::ParentDir => {
84                    if !ret.pop() {
85                        return None;
86                    }
87                }
88                Component::Normal(c) => {
89                    ret.push(c);
90                }
91            }
92        }
93
94        Some(ret)
95    }
96
97    fn is_normalized(&self) -> bool {
98        for component in self.components() {
99            match component {
100                Component::CurDir | Component::ParentDir => {
101                    return false;
102                }
103                _ => continue,
104            }
105        }
106
107        true
108    }
109}