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