1use core::slice;
4use std::{
5 ffi::{OsStr, OsString},
6 path::{Component, MAIN_SEPARATOR_STR, Path, PathBuf, Prefix},
7};
8
9#[derive(Debug)]
11#[allow(clippy::missing_docs_in_private_items)]
12enum CustomComponent<'a> {
13 Prefix(OsString),
14 CurDir,
15 ParentDir,
16 RootDir,
17 Normal(&'a OsStr),
18}
19impl<'a> CustomComponent<'a> {
20 pub fn as_os_str(&'a self) -> &'a OsStr {
22 match self {
23 Self::Prefix(p) => p.as_os_str(),
24 Self::RootDir => OsStr::new(MAIN_SEPARATOR_STR),
25 Self::CurDir => OsStr::new("."),
26 Self::ParentDir => OsStr::new(".."),
27 Self::Normal(path) => path,
28 }
29 }
30}
31impl<'a> From<Component<'a>> for CustomComponent<'a> {
32 fn from(value: Component<'a>) -> Self {
33 match value {
34 Component::Prefix(prefix_component) => match prefix_component.kind() {
35 Prefix::Verbatim(os_str) => Self::Normal(os_str),
36 Prefix::DeviceNS(os_str) => {
37 let mut prefix = OsString::with_capacity(4 + os_str.len());
38 prefix.push(r"\\.\");
39 prefix.push(os_str);
40 Self::Prefix(prefix)
41 }
42 Prefix::VerbatimUNC(server, share) | Prefix::UNC(server, share) => {
43 let mut prefix = OsString::with_capacity(2 + server.len() + share.len());
44 prefix.push(r"\\");
45 prefix.push(server);
46 prefix.push(r"\");
47 prefix.push(share);
48 Self::Prefix(prefix)
49 }
50 Prefix::VerbatimDisk(disk) | Prefix::Disk(disk) => {
51 let mut prefix = OsString::with_capacity(2);
52 let letter = str::from_utf8(slice::from_ref(&disk)).unwrap_or("C");
53 prefix.push(letter);
54 prefix.push(":");
55 Self::Prefix(prefix)
56 }
57 },
58 Component::RootDir => Self::RootDir,
59 Component::CurDir => Self::CurDir,
60 Component::ParentDir => Self::ParentDir,
61 Component::Normal(os_str) => Self::Normal(os_str),
62 }
63 }
64}
65impl AsRef<OsStr> for CustomComponent<'_> {
66 #[inline]
67 fn as_ref(&self) -> &OsStr {
68 self.as_os_str()
69 }
70}
71impl AsRef<Path> for CustomComponent<'_> {
72 #[inline]
73 fn as_ref(&self) -> &Path {
74 self.as_os_str().as_ref()
75 }
76}
77
78pub trait NormalizePath {
80 fn normalized(&self) -> PathBuf;
85}
86impl<P: AsRef<Path>> NormalizePath for P {
87 fn normalized(&self) -> PathBuf {
88 normalize_path(self.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}