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