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}