simple_path/simple_path.rs
1#![cfg_attr(not(target_os = "windows"), allow(unused))]
2use crate::Display;
3#[cfg(windows)]
4use crate::{LongUnc, PathExt, Volumes};
5use std::{
6 borrow::Cow,
7 fs, io,
8 path::{Path, PathBuf, StripPrefixError},
9};
10
11/// Simplifies [Win32 File Namespaces] paths (the "`\\?\`" prefix)
12/// for better readability and compatibility.
13///
14/// The following code is a snap-in replacement of [`fs::canonicalize`].
15/// ```no_run
16/// # use simple_path::SimplePath;
17/// # let path = "";
18/// SimplePath::default().canonicalize(path);
19/// ```
20///
21/// If you have `net use Z: \\server\share`:
22/// | | `C:\dir` | `Z:\x` |
23/// | --- | --- | --- |
24/// | [`fs::canonicalize`] | `\\?\C:\dir` | `\\?\UNC\server\share\x` |
25/// | `SimplePath` | `C:\dir` | `\\server\share\x` |
26/// | `SimplePath` with [`map_to_drive`] | `C:\dir` | `Z:\x` |
27///
28/// [`map_to_drive`]: `SimplePath::map_to_drive`
29/// [Win32 File Namespaces]: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#win32-file-namespaces
30#[derive(Debug, Default)]
31pub struct SimplePath {
32 /// When set to `true`,
33 /// the simplification is disabled
34 /// if the result is a "long path" (longer than 260 characters).
35 ///
36 /// Long paths may not be supported by some programs and APIs.
37 /// In such cases, the [Win32 File Namespaces] (the "`\\?\`" prefix)
38 /// may be able to work around the limitation.
39 ///
40 /// On the other hand,
41 /// other programs such as PowerShell v7 can't handle the "`\\?\`" prefix,
42 /// but it can handle long paths.
43 ///
44 /// [Win32 File Namespaces]: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#win32-file-namespaces
45 pub disallow_long: bool,
46
47 /// Map to network share drive names when possible.
48 ///
49 /// # Examples
50 ///
51 /// ```
52 /// # use simple_path::SimplePath;
53 /// # fn test() -> std::io::Result<()> {
54 /// let path = "file.txt";
55 /// let simple = SimplePath { map_to_drive: true, ..Default::default() };
56 /// let canonicalized = simple.canonicalize(path)?;
57 /// # Ok(())
58 /// # }
59 /// ```
60 /// If the `file.txt` is in a network drive,
61 /// the result is `Z:\dir\file.txt`
62 /// instead of `\\server\share\dir\file.txt`.
63 ///
64 /// The following code tries to preserve the original form of the `path`.
65 /// ```
66 /// # use simple_path::SimplePath;
67 /// # fn test(path: &std::path::Path) -> std::io::Result<()> {
68 /// SimplePath {
69 /// map_to_drive: !path.as_os_str().as_encoded_bytes().starts_with(br"\\"),
70 /// ..Default::default()
71 /// }.canonicalize(path)?;
72 /// # Ok(())
73 /// # }
74 /// ```
75 pub map_to_drive: bool,
76
77 /// The [`dunce`] simplification is applied by default.
78 /// Set to `true` to skip it.
79 ///
80 /// [`dunce`]: https://crates.io/crates/dunce
81 pub skip_dunce: bool,
82
83 /// It is highly recommended to always use `, ..Default::default()`.
84 /// Otherwise builds fail when new fields are added.
85 ///
86 /// This field is not used in any ways,
87 /// but exists to allow using `, ..Default::default()`
88 /// even when all other fields are specified.
89 pub _unused: bool,
90
91 #[cfg(all(test, windows))]
92 volumes: Option<Volumes>,
93}
94
95impl SimplePath {
96 #[cfg(all(test, windows))]
97 pub(crate) fn mock() -> SimplePath {
98 SimplePath {
99 volumes: Some(Volumes::mock()),
100 ..Default::default()
101 }
102 }
103
104 /// Calls [`fs::canonicalize`] and [`simplify`].
105 ///
106 /// On other platforms than Windows,
107 /// this is equivalent to [`fs::canonicalize`].
108 ///
109 /// [`fs::canonicalize`]: https://doc.rust-lang.org/std/fs/fn.canonicalize.html
110 /// [`simplify`]: SimplePath::simplify
111 pub fn canonicalize(&self, path: impl AsRef<Path>) -> io::Result<PathBuf> {
112 let canonicalized = fs::canonicalize(path)?;
113 #[cfg(windows)]
114 if let Some(simplified) = self.simplify(&canonicalized)? {
115 return Ok(simplified.into_owned());
116 }
117 Ok(canonicalized)
118 }
119
120 /// Try to simplify the given `path`.
121 ///
122 /// Returns `Ok(None)`
123 /// if no simplification is applied,
124 /// or on other platforms than Windows.
125 pub fn simplify<'a>(&self, path: &'a Path) -> io::Result<Option<Cow<'a, Path>>> {
126 #[cfg(windows)]
127 return self._simplify(path).map_err(io_error_from_anyhow);
128 #[cfg(not(windows))]
129 Ok(None)
130 }
131
132 #[cfg(windows)]
133 fn _simplify<'a>(&self, path: &'a Path) -> anyhow::Result<Option<Cow<'a, Path>>> {
134 // Try mapped network share drives if long UNC.
135 if let Ok(long_unc) = LongUnc::try_from(path)
136 && let Some(drive_path) = self.drive_path(path)?
137 {
138 if self.map_to_drive
139 && drive_path.has_drive()
140 && !drive_path.has_invalid_chars()
141 && (!self.disallow_long || !drive_path.is_longer_than_max_path())
142 {
143 return Ok(Some(Cow::Owned(drive_path.to_path_buf())));
144 }
145 if long_unc.is_sub_prefix_unc()
146 && !long_unc.has_invalid_chars()
147 && (!self.disallow_long || !long_unc.is_short_unc_longer_than_max_path())
148 {
149 return Ok(Some(Cow::Owned(long_unc.to_short_unc())));
150 }
151 }
152
153 if !self.skip_dunce {
154 // Try `dunce::simplified`.
155 let simplified = dunce::simplified(path);
156 if !std::ptr::eq(path, simplified) {
157 return Ok(Some(Cow::Borrowed(simplified)));
158 }
159 }
160 Ok(None)
161 }
162
163 #[cfg(windows)]
164 fn drive_path<'a>(&self, path: &'a Path) -> anyhow::Result<Option<crate::DrivePath<'a>>> {
165 #[cfg(test)]
166 if let Some(volumes) = &self.volumes {
167 return Ok(volumes._drive_path(path));
168 }
169 Volumes::drive_path(path)
170 }
171
172 /// Refreshes the cached information.
173 pub fn refresh() -> io::Result<()> {
174 #[cfg(windows)]
175 Volumes::refresh().map_err(io_error_from_anyhow)?;
176 Ok(())
177 }
178
179 /// Returns an object that implements [`Display`][`core::fmt::Display`]
180 /// for printing simplified paths.
181 ///
182 /// # Examples
183 ///
184 /// ```
185 /// # use std::path::Path;
186 /// # use simple_path::SimplePath;
187 /// # fn test() -> std::io::Result<()> {
188 /// let path = Path::new("file").canonicalize()?;
189 /// println!("{}", SimplePath::default().display(&path));
190 /// # Ok(())
191 /// # }
192 /// ```
193 pub fn display<'a>(&'a self, path: &'a Path) -> Display<'a> {
194 Display::new(self, path)
195 }
196
197 /// A snap-in replacement for [`Path::strip_prefix`]
198 /// with a fix for [a leading directory separator "`\`" left for UNC paths
199 /// on Windows](https://github.com/rust-lang/rust/issues/155183).
200 ///
201 /// # Examples
202 ///
203 /// ```
204 /// # use std::path::{Path, StripPrefixError};
205 /// # use simple_path::SimplePath;
206 /// # fn t<'a>(path: &'a Path, base: &'a Path) -> Result<&'a Path, StripPrefixError> {
207 /// SimplePath::strip_prefix(path, base)
208 /// # }
209 /// ```
210 pub fn strip_prefix(path: &Path, base: impl AsRef<Path>) -> Result<&Path, StripPrefixError> {
211 #[cfg(windows)]
212 return PathExt::strip_prefix_fix(path, base);
213 #[cfg(not(windows))]
214 path.strip_prefix(base)
215 }
216}
217
218fn io_error_from_anyhow(error: anyhow::Error) -> io::Error {
219 match error.downcast::<io::Error>() {
220 Ok(io_error) => io_error,
221 Err(other_error) => io::Error::other(other_error),
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn simplify_not_simplified() {
231 let simple = SimplePath::default();
232 assert_eq!(simple.simplify(Path::new(r"C:\foo")).unwrap(), None);
233 }
234
235 #[cfg(windows)]
236 #[test]
237 fn simplify_drive_not_simplified() {
238 let simple = SimplePath::mock();
239 assert_eq!(simple.simplify(Path::new(r"C:\foo")).unwrap(), None);
240 }
241
242 #[cfg(windows)]
243 #[test]
244 fn simplify_drive_unc() {
245 let mut simple = SimplePath::mock();
246 let path = Path::new(r"\\?\UNC\server\share\foo");
247 let path2 = Path::new(r"\\?\UNC\server2\share2\foo2");
248 assert_eq!(
249 simple.simplify(path).unwrap(),
250 Some(Cow::Owned(PathBuf::from(r"\\server\share\foo")))
251 );
252 assert_eq!(
253 simple.simplify(path2).unwrap(),
254 Some(Cow::Owned(PathBuf::from(r"\\server2\share2\foo2")))
255 );
256
257 simple.map_to_drive = true;
258 assert_eq!(
259 simple.simplify(path).unwrap(),
260 Some(Cow::Owned(PathBuf::from(r"X:\foo")))
261 );
262 assert_eq!(
263 simple.simplify(path2).unwrap(),
264 Some(Cow::Owned(PathBuf::from(r"Z:\foo2")))
265 );
266 }
267
268 #[cfg(windows)]
269 #[test]
270 fn simplify_dunce() {
271 let simple = SimplePath::default();
272 assert_eq!(
273 simple.simplify(Path::new(r"\\?\C:\foo")).unwrap(),
274 Some(Cow::Borrowed(Path::new(r"C:\foo")))
275 );
276 }
277
278 #[cfg(windows)]
279 #[test]
280 fn simplify_dunce_skip() {
281 let simple = SimplePath {
282 skip_dunce: true,
283 ..Default::default()
284 };
285 assert_eq!(simple.simplify(Path::new(r"\\?\C:\foo")).unwrap(), None);
286 }
287
288 #[cfg(windows)]
289 #[test]
290 fn simplify_unmapped_connected_share() {
291 let mut simple = SimplePath::mock();
292 let path = Path::new(r"\\?\UNC\server0\share0\foo");
293 assert_eq!(
294 simple.simplify(path).unwrap(),
295 Some(Cow::Owned(PathBuf::from(r"\\server0\share0\foo")))
296 );
297
298 // Even with map_to_drive = true, it should simplify to the UNC path,
299 // because the drive letter is '\0'.
300 simple.map_to_drive = true;
301 assert_eq!(
302 simple.simplify(path).unwrap(),
303 Some(Cow::Owned(PathBuf::from(r"\\server0\share0\foo")))
304 );
305 }
306}