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}