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