ts_path/
normalize.rs

1//! A simple normalization of a path.
2
3use core::slice;
4use std::{
5    ffi::{OsStr, OsString},
6    path::{Component, MAIN_SEPARATOR_STR, Path, PathBuf, Prefix},
7};
8
9/// Custom component as [`std::path::Component`] is difficult to construct.
10#[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    /// Convert the component to an [`OsStr`]
21    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
78/// Extension trait to call [`crate::normalize_path`] on a path.
79pub trait NormalizePath {
80    /// Normalize a path using only the components of the path.
81    ///
82    /// This will ignore any symbolic links, and strip the verbatim `\\?\` prefixes, so should only be
83    /// used when that can be tolerated.
84    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
92/// Normalize a path using only the components of the path.
93///
94/// This will ignore any symbolic links, and strip the verbatim `\\?\` prefixes, so should only be
95/// used when that can be tolerated.
96pub 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}