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, 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(Debug, Default)]
31pub struct SimplePath {
32    /// When set to `true`,
33    /// the simplification is disabled
34    /// if the result is a "long path" (longer than 260 characters).
35    ///
36    /// Long paths may not be supported by some programs and APIs.
37    /// In such cases, the [Win32 File Namespaces] (the "`\\?\`" prefix)
38    /// may be able to work around the limitation.
39    ///
40    /// On the other hand,
41    /// other programs such as PowerShell v7 can't handle the "`\\?\`" prefix,
42    /// but it can handle long paths.
43    ///
44    /// [Win32 File Namespaces]: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#win32-file-namespaces
45    pub disallow_long: bool,
46
47    /// Map to network share drive names when possible.
48    ///
49    /// # Examples
50    ///
51    /// ```
52    /// # use simple_path::SimplePath;
53    /// # fn test() -> std::io::Result<()> {
54    /// let path = "file.txt";
55    /// let simple = SimplePath { map_to_drive: true, ..Default::default() };
56    /// let canonicalized = simple.canonicalize(path)?;
57    /// # Ok(())
58    /// # }
59    /// ```
60    /// If the `file.txt` is in a network drive,
61    /// the result is `Z:\dir\file.txt`
62    /// instead of `\\server\share\dir\file.txt`.
63    ///
64    /// The following code tries to preserve the original form of the `path`.
65    /// ```
66    /// # use simple_path::SimplePath;
67    /// # fn test(path: &std::path::Path) -> std::io::Result<()> {
68    /// SimplePath {
69    ///     map_to_drive: !path.as_os_str().as_encoded_bytes().starts_with(br"\\"),
70    ///     ..Default::default()
71    /// }.canonicalize(path)?;
72    /// # Ok(())
73    /// # }
74    /// ```
75    pub map_to_drive: bool,
76
77    /// The [`dunce`] simplification is applied by default.
78    /// Set to `true` to skip it.
79    ///
80    /// [`dunce`]: https://crates.io/crates/dunce
81    pub skip_dunce: bool,
82
83    /// It is highly recommended to always use `, ..Default::default()`.
84    /// Otherwise builds fail when new fields are added.
85    ///
86    /// This field is not used in any ways,
87    /// but exists to allow using `, ..Default::default()`
88    /// even when all other fields are specified.
89    pub _unused: bool,
90
91    #[cfg(all(test, windows))]
92    volumes: Option<Volumes>,
93}
94
95impl SimplePath {
96    #[cfg(all(test, windows))]
97    pub(crate) fn mock() -> SimplePath {
98        SimplePath {
99            volumes: Some(Volumes::mock()),
100            ..Default::default()
101        }
102    }
103
104    /// Calls [`fs::canonicalize`] and [`simplify`].
105    ///
106    /// On other platforms than Windows,
107    /// this is equivalent to [`fs::canonicalize`].
108    ///
109    /// [`fs::canonicalize`]: https://doc.rust-lang.org/std/fs/fn.canonicalize.html
110    /// [`simplify`]: SimplePath::simplify
111    pub fn canonicalize(&self, path: impl AsRef<Path>) -> io::Result<PathBuf> {
112        let canonicalized = fs::canonicalize(path)?;
113        #[cfg(windows)]
114        if let Some(simplified) = self.simplify(&canonicalized)? {
115            return Ok(simplified.into_owned());
116        }
117        Ok(canonicalized)
118    }
119
120    /// Try to simplify the given `path`.
121    ///
122    /// Returns `Ok(None)`
123    /// if no simplification is applied,
124    /// or on other platforms than Windows.
125    pub fn simplify<'a>(&self, path: &'a Path) -> io::Result<Option<Cow<'a, Path>>> {
126        #[cfg(windows)]
127        return self._simplify(path).map_err(io_error_from_anyhow);
128        #[cfg(not(windows))]
129        Ok(None)
130    }
131
132    #[cfg(windows)]
133    fn _simplify<'a>(&self, path: &'a Path) -> anyhow::Result<Option<Cow<'a, Path>>> {
134        // Try mapped network share drives.
135        if path.is_win32_file_namespace_unc()
136            && let Some(drive_path) = self.drive_path(path)?
137        {
138            if self.map_to_drive
139                && drive_path.has_drive()
140                && (!self.disallow_long || !drive_path.is_win32_long_path())
141            {
142                return Ok(Some(Cow::Owned(drive_path.to_path_buf())));
143            }
144            if let Some(unc) = path.unc_from_win32_file_namespace(self.disallow_long) {
145                return Ok(Some(Cow::Owned(unc)));
146            }
147        }
148
149        if !self.skip_dunce {
150            // Try `dunce::simplified`.
151            let simplified = dunce::simplified(path);
152            if !std::ptr::eq(path, simplified) {
153                return Ok(Some(Cow::Borrowed(simplified)));
154            }
155        }
156        Ok(None)
157    }
158
159    #[cfg(windows)]
160    fn drive_path<'a>(&self, path: &'a Path) -> anyhow::Result<Option<crate::DrivePath<'a>>> {
161        #[cfg(test)]
162        if let Some(volumes) = &self.volumes {
163            return Ok(volumes._drive_path(path));
164        }
165        Volumes::drive_path(path)
166    }
167
168    /// Refreshes the cached information.
169    pub fn refresh() -> io::Result<()> {
170        #[cfg(windows)]
171        Volumes::refresh().map_err(io_error_from_anyhow)?;
172        Ok(())
173    }
174
175    /// Returns an object that implements [`Display`][`core::fmt::Display`]
176    /// for printing simplified paths.
177    ///
178    /// # Examples
179    ///
180    /// ```
181    /// # use std::path::Path;
182    /// # use simple_path::SimplePath;
183    /// # fn test() -> std::io::Result<()> {
184    /// let path = Path::new("file").canonicalize()?;
185    /// println!("{}", SimplePath::default().display(&path));
186    /// # Ok(())
187    /// # }
188    /// ```
189    pub fn display<'a>(&'a self, path: &'a Path) -> Display<'a> {
190        Display::new(self, path)
191    }
192
193    /// A snap-in replacement for [`Path::strip_prefix`]
194    /// with a fix for [a leading "`\`" left for UNC paths
195    /// on Windows](https://github.com/rust-lang/rust/issues/155183).
196    ///
197    /// # Examples
198    ///
199    /// ```
200    /// # use std::path::{Path, StripPrefixError};
201    /// # use simple_path::SimplePath;
202    /// # fn t<'a>(path: &'a Path, base: &'a Path) -> Result<&'a Path, StripPrefixError> {
203    /// SimplePath::strip_prefix(path, base)
204    /// # }
205    /// ```
206    pub fn strip_prefix(path: &Path, base: impl AsRef<Path>) -> Result<&Path, StripPrefixError> {
207        #[cfg(windows)]
208        return PathExt::strip_prefix_fix(path, base);
209        #[cfg(not(windows))]
210        path.strip_prefix(base)
211    }
212}
213
214fn io_error_from_anyhow(error: anyhow::Error) -> io::Error {
215    match error.downcast::<io::Error>() {
216        Ok(io_error) => io_error,
217        Err(other_error) => io::Error::other(other_error),
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    #[test]
226    fn simplify_not_simplified() {
227        let simple = SimplePath::default();
228        assert_eq!(simple.simplify(Path::new(r"C:\foo")).unwrap(), None);
229    }
230
231    #[cfg(windows)]
232    #[test]
233    fn simplify_drive_not_simplified() {
234        let simple = SimplePath::mock();
235        assert_eq!(simple.simplify(Path::new(r"C:\foo")).unwrap(), None);
236    }
237
238    #[cfg(windows)]
239    #[test]
240    fn simplify_drive_unc() {
241        let mut simple = SimplePath::mock();
242        let path = Path::new(r"\\?\UNC\server\share\foo");
243        let path2 = Path::new(r"\\?\UNC\server2\share2\foo2");
244        assert_eq!(
245            simple.simplify(path).unwrap(),
246            Some(Cow::Owned(PathBuf::from(r"\\server\share\foo")))
247        );
248        assert_eq!(
249            simple.simplify(path2).unwrap(),
250            Some(Cow::Owned(PathBuf::from(r"\\server2\share2\foo2")))
251        );
252
253        simple.map_to_drive = true;
254        assert_eq!(
255            simple.simplify(path).unwrap(),
256            Some(Cow::Owned(PathBuf::from(r"X:\foo")))
257        );
258        assert_eq!(
259            simple.simplify(path2).unwrap(),
260            Some(Cow::Owned(PathBuf::from(r"Z:\foo2")))
261        );
262    }
263
264    #[cfg(windows)]
265    #[test]
266    fn simplify_dunce() {
267        let simple = SimplePath::default();
268        assert_eq!(
269            simple.simplify(Path::new(r"\\?\C:\foo")).unwrap(),
270            Some(Cow::Borrowed(Path::new(r"C:\foo")))
271        );
272    }
273
274    #[cfg(windows)]
275    #[test]
276    fn simplify_dunce_skip() {
277        let simple = SimplePath {
278            skip_dunce: true,
279            ..Default::default()
280        };
281        assert_eq!(simple.simplify(Path::new(r"\\?\C:\foo")).unwrap(), None);
282    }
283
284    #[cfg(windows)]
285    #[test]
286    fn simplify_unmapped_connected_share() {
287        let mut simple = SimplePath::mock();
288        let path = Path::new(r"\\?\UNC\server0\share0\foo");
289        assert_eq!(
290            simple.simplify(path).unwrap(),
291            Some(Cow::Owned(PathBuf::from(r"\\server0\share0\foo")))
292        );
293
294        // Even with map_to_drive = true, it should simplify to the UNC path,
295        // because the drive letter is '\0'.
296        simple.map_to_drive = true;
297        assert_eq!(
298            simple.simplify(path).unwrap(),
299            Some(Cow::Owned(PathBuf::from(r"\\server0\share0\foo")))
300        );
301    }
302}