simple_path/simple_path.rs
1#![cfg_attr(not(target_os = "windows"), allow(unused))]
2use crate::Display;
3#[cfg(windows)]
4use crate::{PathExt, UncPath, 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(Clone, Debug, Default)]
31pub struct SimplePath {
32 /// Disallow simplifications
33 /// if the result is a "long path" (longer than 260 characters).
34 /// Initially `false`.
35 ///
36 /// Long paths may not be supported by some programs and APIs.
37 /// In such cases, using the [Win32 File Namespaces] (the "`\\?\`" prefix)
38 /// can often work around the limitation.
39 /// Setting this option to `true` can improve
40 /// the compatibility with such cases.
41 ///
42 /// On the other hand, some other programs such as PowerShell v7
43 /// can handle long paths,
44 /// but they can't handle the "`\\?\`" prefix.
45 /// They work best with `false`.
46 ///
47 /// Please also see the [Maximum Path Length Limitation].
48 ///
49 /// [Maximum Path Length Limitation]: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
50 /// [Win32 File Namespaces]: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#win32-file-namespaces
51 pub disallow_long: bool,
52
53 /// Disallow simplifications
54 /// if the path is not connected.
55 /// Initially `false`.
56 ///
57 /// Technically speaking,
58 /// since the "`\\?\`" prefix ([Win32 File Namespaces])
59 /// disables all string parsing and
60 /// sends the following string directly to the file system,
61 /// simplifying the path is not always guaranteed to be safe or equivalent.
62 ///
63 /// Enable this option
64 /// to restrict simplification to verified paths,
65 /// providing an extra layer of safety.
66 ///
67 /// Please also see the [safety] note.
68 ///
69 /// # Examples
70 /// ```
71 /// # use simple_path::SimplePath;
72 /// # use std::path::Path;
73 /// let path = Path::new(r"\\?\UNC\server\share\dir");
74 /// let simple = SimplePath { disallow_unknown_unc: true, ..Default::default() };
75 /// #[cfg(windows)]
76 /// assert!(simple.simplify(path).unwrap().is_none());
77 /// ```
78 ///
79 /// [safety]: https://github.com/kojiishi/simple-path#safety-and-equivalence
80 /// [Win32 File Namespaces]: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#win32-file-namespaces
81 pub disallow_unknown_unc: bool,
82
83 /// Map to network share drive names when possible.
84 /// Initially `false`.
85 ///
86 /// # Examples
87 ///
88 /// ```
89 /// # use simple_path::SimplePath;
90 /// # fn test() -> std::io::Result<()> {
91 /// let path = "file.txt";
92 /// let simple = SimplePath { map_to_drive: true, ..Default::default() };
93 /// let canonicalized = simple.canonicalize(path)?;
94 /// # Ok(())
95 /// # }
96 /// ```
97 /// If the `file.txt` is in a network drive,
98 /// the result is `Z:\dir\file.txt`
99 /// instead of `\\server\share\dir\file.txt`.
100 ///
101 /// The following code tries to preserve the original form of the `path`.
102 /// ```
103 /// # use simple_path::SimplePath;
104 /// # fn test(path: &std::path::Path) -> std::io::Result<()> {
105 /// SimplePath {
106 /// map_to_drive: !SimplePath::is_unc(path),
107 /// ..Default::default()
108 /// }.canonicalize(path)?;
109 /// # Ok(())
110 /// # }
111 /// ```
112 pub map_to_drive: bool,
113
114 /// Skip the [`dunce`] simplification.
115 /// Initially `false`.
116 ///
117 /// [`dunce`]: https://crates.io/crates/dunce
118 pub skip_dunce: bool,
119
120 /// It is highly recommended to always use `, ..Default::default()`.
121 /// Otherwise builds fail when new fields are added.
122 ///
123 /// This field is not used in any ways,
124 /// but exists to allow using `, ..Default::default()`
125 /// even when all other fields are specified.
126 pub _unused: bool,
127
128 #[cfg(all(test, windows))]
129 volumes: Option<Volumes>,
130}
131
132impl SimplePath {
133 #[cfg(all(test, windows))]
134 pub(crate) fn mock() -> SimplePath {
135 SimplePath {
136 volumes: Some(Volumes::mock()),
137 ..Default::default()
138 }
139 }
140
141 /// A snap-in replacement for [`fs::canonicalize`].
142 /// It calls [`fs::canonicalize`] and [`simplify`].
143 ///
144 /// On other platforms than Windows,
145 /// this is equivalent to [`fs::canonicalize`].
146 ///
147 /// # Examples
148 /// ```
149 /// # fn test(path: &std::path::Path) -> std::io::Result<()> {
150 /// use simple_path::SimplePath;
151 /// let canonicalized = SimplePath::default().canonicalize(path)?;
152 /// println!("{}", canonicalized.display());
153 /// # Ok(()) }
154 /// ```
155 ///
156 /// [`fs::canonicalize`]: https://doc.rust-lang.org/std/fs/fn.canonicalize.html
157 /// [`simplify`]: SimplePath::simplify
158 #[inline]
159 pub fn canonicalize(&self, path: impl AsRef<Path>) -> io::Result<PathBuf> {
160 let canonicalized = fs::canonicalize(path)?;
161 #[cfg(windows)]
162 if let Some(simplified) = self.simplify(&canonicalized)? {
163 return Ok(simplified.into_owned());
164 }
165 Ok(canonicalized)
166 }
167
168 /// Try to simplify the given `path`.
169 ///
170 /// Returns `Ok(None)`
171 /// if no simplification is applied,
172 /// or on other platforms than Windows.
173 #[inline]
174 pub fn simplify<'a>(&self, path: &'a Path) -> io::Result<Option<Cow<'a, Path>>> {
175 #[cfg(windows)]
176 return self._simplify(path).map_err(io_error_from_anyhow);
177 #[cfg(not(windows))]
178 Ok(None)
179 }
180
181 #[cfg(windows)]
182 fn _simplify<'a>(&self, path: &'a Path) -> anyhow::Result<Option<Cow<'a, Path>>> {
183 // If it starts with the `\\?\UNC\` prefix.
184 if let Ok(unc) = UncPath::try_from(path)
185 && unc.is_file_namespace_unc()
186 {
187 // Try mapped network drives.
188 let drive_path = if self.disallow_unknown_unc || self.map_to_drive {
189 self.drive_path(path)?
190 } else {
191 None
192 };
193 if self.map_to_drive
194 && let Some(drive_path) = &drive_path
195 && drive_path.has_drive()
196 && !drive_path.has_invalid_chars()
197 && (!self.disallow_long || !drive_path.is_longer_than_max_path())
198 {
199 return Ok(Some(Cow::Owned(drive_path.to_path_buf())));
200 }
201
202 // Try short UNC (`\\server\share`).
203 if (!self.disallow_unknown_unc || drive_path.is_some())
204 && let Some(short_unc) = unc.to_short_unc()
205 && !short_unc.has_invalid_chars()
206 && (!self.disallow_long || !short_unc.is_longer_than_win_max_path())
207 {
208 return Ok(Some(Cow::Owned(short_unc)));
209 }
210 }
211
212 // Try `dunce::simplified`.
213 if !self.skip_dunce {
214 let simplified = dunce::simplified(path);
215 if !std::ptr::eq(path, simplified) {
216 return Ok(Some(Cow::Borrowed(simplified)));
217 }
218 }
219 Ok(None)
220 }
221
222 #[cfg(windows)]
223 #[inline]
224 fn drive_path<'a>(&self, path: &'a Path) -> anyhow::Result<Option<crate::DrivePath<'a>>> {
225 #[cfg(test)]
226 if let Some(volumes) = &self.volumes {
227 return Ok(volumes._drive_path(path));
228 }
229 Volumes::drive_path(path)
230 }
231
232 /// Refresh the cached information.
233 pub fn refresh() -> io::Result<()> {
234 #[cfg(windows)]
235 Volumes::refresh().map_err(io_error_from_anyhow)?;
236 Ok(())
237 }
238
239 /// Return an object that implements [`Display`][`core::fmt::Display`]
240 /// for printing simplified paths.
241 ///
242 /// # Examples
243 ///
244 /// ```
245 /// # use std::path::Path;
246 /// # use simple_path::SimplePath;
247 /// # fn test() -> std::io::Result<()> {
248 /// let path = Path::new("file").canonicalize()?;
249 /// println!("{}", SimplePath::default().display(&path));
250 /// # Ok(())
251 /// # }
252 /// ```
253 pub fn display<'a>(&'a self, path: &'a Path) -> Display<'a> {
254 Display::new(self, path)
255 }
256
257 /// Return `true` if the given `path` is a UNC path.
258 /// A UNC path starts with a "`\\`" prefix.
259 ///
260 /// Always `false` on non-Windows platforms.
261 ///
262 /// # Examples
263 /// ```
264 /// # use simple_path::SimplePath;
265 /// #[cfg(windows)]
266 /// {
267 /// assert!(SimplePath::is_unc(r"\\unc"));
268 /// assert!(SimplePath::is_unc(r"//unc"));
269 /// assert!(!SimplePath::is_unc(r"\not-unc"));
270 /// }
271 /// assert!(!SimplePath::is_unc("/not-unc"));
272 /// assert!(!SimplePath::is_unc("not-unc"));
273 /// ```
274 #[inline]
275 pub fn is_unc(path: impl AsRef<Path>) -> bool {
276 #[cfg(windows)]
277 return UncPath::is_unc(path);
278 #[cfg(not(windows))]
279 false
280 }
281
282 /// A snap-in replacement for [`Path::strip_prefix`]
283 /// with a fix for [a leading directory separator "`\`" left for UNC paths
284 /// on Windows](https://github.com/rust-lang/rust/issues/155183).
285 ///
286 /// # Examples
287 ///
288 /// ```
289 /// # use std::path::{Path, StripPrefixError};
290 /// # use simple_path::SimplePath;
291 /// # fn t<'a>(path: &'a Path, base: &'a Path) -> Result<&'a Path, StripPrefixError> {
292 /// SimplePath::strip_prefix(path, base)
293 /// # }
294 /// ```
295 #[inline]
296 pub fn strip_prefix(path: &Path, base: impl AsRef<Path>) -> Result<&Path, StripPrefixError> {
297 #[cfg(windows)]
298 return PathExt::strip_prefix_fix(path, base);
299 #[cfg(not(windows))]
300 path.strip_prefix(base)
301 }
302}
303
304fn io_error_from_anyhow(error: anyhow::Error) -> io::Error {
305 match error.downcast::<io::Error>() {
306 Ok(io_error) => io_error,
307 Err(other_error) => io::Error::other(other_error),
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 #[cfg(windows)]
316 #[test]
317 fn simplify_drive() {
318 let mut simple = SimplePath::mock();
319 assert_eq!(simple.simplify(Path::new(r"C:\foo")).unwrap(), None);
320 simple.disallow_unknown_unc = true;
321 assert_eq!(simple.simplify(Path::new(r"C:\foo")).unwrap(), None);
322 }
323
324 #[cfg(windows)]
325 #[test]
326 fn simplify_drive_unc() {
327 let mut simple = SimplePath::mock();
328 let path = Path::new(r"\\?\UNC\server\share\foo");
329 let path2 = Path::new(r"\\?\UNC\server2\share2\foo2");
330 assert_eq!(
331 simple.simplify(path).unwrap(),
332 Some(Cow::Owned(PathBuf::from(r"\\server\share\foo")))
333 );
334 assert_eq!(
335 simple.simplify(path2).unwrap(),
336 Some(Cow::Owned(PathBuf::from(r"\\server2\share2\foo2")))
337 );
338
339 simple.map_to_drive = true;
340 assert_eq!(
341 simple.simplify(path).unwrap(),
342 Some(Cow::Owned(PathBuf::from(r"X:\foo")))
343 );
344 assert_eq!(
345 simple.simplify(path2).unwrap(),
346 Some(Cow::Owned(PathBuf::from(r"Z:\foo2")))
347 );
348 }
349
350 #[cfg(windows)]
351 #[test]
352 fn simplify_dunce() {
353 let simple = SimplePath::default();
354 assert_eq!(
355 simple.simplify(Path::new(r"\\?\C:\foo")).unwrap(),
356 Some(Cow::Borrowed(Path::new(r"C:\foo")))
357 );
358 }
359
360 #[cfg(windows)]
361 #[test]
362 fn simplify_dunce_skip() {
363 let simple = SimplePath {
364 skip_dunce: true,
365 ..Default::default()
366 };
367 assert_eq!(simple.simplify(Path::new(r"\\?\C:\foo")).unwrap(), None);
368 }
369
370 #[cfg(windows)]
371 #[test]
372 fn simplify_unmapped_connected_share() {
373 let mut simple = SimplePath::mock();
374 let path = Path::new(r"\\?\UNC\server0\share0\foo");
375 assert_eq!(
376 simple.simplify(path).unwrap(),
377 Some(Cow::Owned(PathBuf::from(r"\\server0\share0\foo")))
378 );
379
380 // Even with map_to_drive = true, it should simplify to the UNC path,
381 // because the drive letter is '\0'.
382 simple.map_to_drive = true;
383 assert_eq!(
384 simple.simplify(path).unwrap(),
385 Some(Cow::Owned(PathBuf::from(r"\\server0\share0\foo")))
386 );
387 }
388
389 #[cfg(windows)]
390 #[test]
391 fn simplify_unknown_unc() -> anyhow::Result<()> {
392 let mut simple = SimplePath::mock();
393 let unknown = Path::new(r"\\?\UNC\server\unknown\foo");
394 let mapped = Path::new(r"\\?\UNC\server\share\foo");
395 assert_eq!(
396 simple.simplify(unknown)?,
397 Some(Cow::Owned(PathBuf::from(r"\\server\unknown\foo")))
398 );
399 assert_eq!(
400 simple.simplify(mapped)?,
401 Some(Cow::Owned(PathBuf::from(r"\\server\share\foo")))
402 );
403
404 // `unknown` should not be simplified if `disallow_unknown_unc`.
405 simple.disallow_unknown_unc = true;
406 assert_eq!(simple.simplify(unknown)?, None);
407
408 // `map_to_drive` should still be in effect.
409 simple.map_to_drive = true;
410 assert_eq!(
411 simple.simplify(mapped)?,
412 Some(Cow::Owned(PathBuf::from(r"X:\foo")))
413 );
414
415 // `disallow_unknown_unc` should simplify only for "`\\?\UNC\`".
416 assert_eq!(simple.simplify(Path::new(r"\\.\COM1:"))?, None);
417 simple.skip_dunce = true;
418 assert_eq!(simple.simplify(Path::new(r"\\?\C:\foo"))?, None);
419 Ok(())
420 }
421}