Skip to main content

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        // "." becomes empty path, so we reinsert "."
74        if ret.as_os_str().is_empty() {
75            ret.push(".")
76        }
77
78        ret
79    }
80
81    fn try_normalize(&self) -> Option<PathBuf> {
82        let mut ret = PathBuf::new();
83
84        for component in self.components() {
85            match component {
86                Component::Prefix(..) | Component::RootDir => return None,
87                Component::CurDir => {}
88                Component::ParentDir => {
89                    if !ret.pop() {
90                        return None;
91                    }
92                }
93                Component::Normal(c) => {
94                    ret.push(c);
95                }
96            }
97        }
98
99        // "." becomes empty path, so we reinsert "."
100        if ret.as_os_str().is_empty() {
101            ret.push(".")
102        }
103
104        Some(ret)
105    }
106
107    fn is_normalized(&self) -> bool {
108        for component in self.components() {
109            match component {
110                Component::CurDir | Component::ParentDir => {
111                    return false;
112                }
113                _ => continue,
114            }
115        }
116
117        true
118    }
119}