Skip to main content

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}