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::{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}