ts_io/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/// Extension trait to call [`crate::normalize_path`] on a path.
10pub trait NormalizePath {
11    /// Normalize a path using only the components of the path.
12    ///
13    /// This will ignore any symbolic links, and strip the verbatim `\\?\` prefixes, so should only be
14    /// used when that can be tolerated.
15    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/// Custom component as [`std::path::Component`] is difficult to construct.
24#[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    /// Convert the component to an [`OsStr`]
35    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
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}