Skip to main content

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