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}