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