1use core::slice;
4use std::{
5 ffi::{OsStr, OsString},
6 path::{Component, MAIN_SEPARATOR_STR, Path, PathBuf, Prefix},
7};
8
9pub trait NormalizePath {
11 fn normalized(&self) -> PathBuf;
16}
17impl<P: AsRef<Path>> NormalizePath for P {
18 fn normalized(&self) -> PathBuf {
19 normalize_path(self.as_ref())
20 }
21}
22
23#[derive(Debug)]
25#[allow(clippy::missing_docs_in_private_items)]
26enum CustomComponent<'a> {
27 Prefix(OsString),
28 CurDir,
29 ParentDir,
30 RootDir,
31 Normal(&'a OsStr),
32}
33impl<'a> CustomComponent<'a> {
34 pub fn as_os_str(&'a self) -> &'a OsStr {
36 match self {
37 Self::Prefix(p) => p.as_os_str(),
38 Self::RootDir => OsStr::new(MAIN_SEPARATOR_STR),
39 Self::CurDir => OsStr::new("."),
40 Self::ParentDir => OsStr::new(".."),
41 Self::Normal(path) => path,
42 }
43 }
44}
45impl<'a> From<Component<'a>> for CustomComponent<'a> {
46 fn from(value: Component<'a>) -> Self {
47 match value {
48 Component::Prefix(prefix_component) => match prefix_component.kind() {
49 Prefix::Verbatim(os_str) => Self::Normal(os_str),
50 Prefix::DeviceNS(os_str) => {
51 let mut prefix = OsString::with_capacity(4 + os_str.len());
52 prefix.push(r"\\.\");
53 prefix.push(os_str);
54 Self::Prefix(prefix)
55 }
56 Prefix::VerbatimUNC(server, share) | Prefix::UNC(server, share) => {
57 let mut prefix = OsString::with_capacity(2 + server.len() + share.len());
58 prefix.push(r"\\");
59 prefix.push(server);
60 prefix.push(r"\");
61 prefix.push(share);
62 Self::Prefix(prefix)
63 }
64 Prefix::VerbatimDisk(disk) | Prefix::Disk(disk) => {
65 let mut prefix = OsString::with_capacity(2);
66 let letter = str::from_utf8(slice::from_ref(&disk)).unwrap_or("C");
67 prefix.push(letter);
68 prefix.push(":");
69 Self::Prefix(prefix)
70 }
71 },
72 Component::RootDir => Self::RootDir,
73 Component::CurDir => Self::CurDir,
74 Component::ParentDir => Self::ParentDir,
75 Component::Normal(os_str) => Self::Normal(os_str),
76 }
77 }
78}
79impl AsRef<OsStr> for CustomComponent<'_> {
80 #[inline]
81 fn as_ref(&self) -> &OsStr {
82 self.as_os_str()
83 }
84}
85impl AsRef<Path> for CustomComponent<'_> {
86 #[inline]
87 fn as_ref(&self) -> &Path {
88 self.as_os_str().as_ref()
89 }
90}
91
92pub fn normalize_path(path: &Path) -> PathBuf {
97 let mut output: Vec<CustomComponent> = Vec::with_capacity(path.components().count());
98
99 for component in path.components() {
100 match component {
101 Component::CurDir => {}
102 Component::ParentDir => {
103 if let Some(component) = output.last()
104 && matches!(component, CustomComponent::Normal(_))
105 {
106 output.pop();
107 } else {
108 output.push(component.into());
109 }
110 }
111 Component::RootDir => {
112 if output
113 .last()
114 .is_none_or(|component| matches!(component, CustomComponent::Prefix(_)))
115 {
116 output.push(component.into());
117 };
118 }
119 _ => output.push(component.into()),
120 }
121 }
122
123 PathBuf::from_iter(output)
124}
125
126#[cfg(test)]
127mod test {
128 use std::path::Path;
129
130 use crate::NormalizePath;
131
132 #[test]
133 fn handles_verbatim_prefixes() {
134 let expected = Path::new(r"some-verbatim-path\some-more-path");
135 let data = Path::new(r"\\?\some-verbatim-path\some-more-path");
136 assert_eq!(expected, data.normalized());
137
138 let expected = Path::new(r"T:\some-verbatim-path\some-more-path");
139 let data = Path::new(r"\\?\T:\some-verbatim-path\some-more-path");
140 assert_eq!(expected, data.normalized());
141
142 let expected = Path::new(r"\\server\share\some-more-path");
143 let data = Path::new(r"\\?\UNC\server\share\some-more-path");
144 assert_eq!(expected, data.normalized());
145 }
146
147 #[test]
148 fn handles_prefixes() {
149 let data = Path::new(r"\\server\share\some-more-path");
150 assert_eq!(data, data.normalized());
151
152 let data = Path::new(r"C:\path\some-more-path");
153 assert_eq!(data, data.normalized());
154
155 let data = Path::new(r"\\.\path\some-more-path");
156 assert_eq!(data, data.normalized());
157 }
158
159 #[test]
160 fn handles_parent() {
161 let expected = Path::new(r"../../path");
162 let data = Path::new(r"../../some-parent/../path");
163 assert_eq!(expected, data.normalized());
164
165 let expected = Path::new(r"../../../");
166 let data = Path::new(r"../../some-parent/../../path/..");
167 assert_eq!(expected, data.normalized());
168 }
169
170 #[test]
171 fn handles_current_dir() {
172 let expected = Path::new(r"some/annoying/path");
173 let data = Path::new(r"./some/./././annoying/path/.");
174 assert_eq!(expected, data.normalized());
175 }
176}