simple_path/simple_path.rs
1#![cfg_attr(not(target_os = "windows"), allow(unused))]
2use crate::Display;
3#[cfg(windows)]
4use crate::{LongUnc, 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(Clone, Debug, Default)]
31pub struct SimplePath {
32 /// Disallow simplifications
33 /// if the result is a "long path" (longer than 260 characters).
34 /// Initially `false`.
35 ///
36 /// Long paths may not be supported by some programs and APIs.
37 /// In such cases, using the [Win32 File Namespaces] (the "`\\?\`" prefix)
38 /// can often work around the limitation.
39 /// Setting this option to `true` can improve
40 /// the compatibility with such cases.
41 ///
42 /// On the other hand, some other programs such as PowerShell v7
43 /// can handle long paths,
44 /// but they can't handle the "`\\?\`" prefix.
45 /// They work best with `false`.
46 ///
47 /// [Win32 File Namespaces]: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#win32-file-namespaces
48 pub disallow_long: bool,
49
50 /// Simplify all long UNC paths (prefixed by "`\\?\UNC\`").
51 /// Initially `false`.
52 ///
53 /// Technically speaking,
54 /// since the "`\\?\`" prefix ([Win32 File Namespaces])
55 /// disables all string parsing and
56 /// sends the following string directly to the file system,
57 /// simplifying the path is not always guaranteed to be safe or equivalent.
58 ///
59 /// For this reason,
60 /// the `SimplePath` simplifies connected network shares only by default.
61 /// Set this option to `true`
62 /// to simplify all paths prefixed by "`\\?\UNC\`".
63 ///
64 /// Please also see the [safety] note.
65 ///
66 /// # Examples
67 /// ```
68 /// # use simple_path::SimplePath;
69 /// # use std::path::Path;
70 /// let path = Path::new(r"\\?\UNC\server\share\dir");
71 /// let simple = SimplePath { allow_unknown_unc: true, ..Default::default() };
72 /// #[cfg(windows)]
73 /// assert_eq!(&*simple.simplify(path).unwrap().unwrap(), r"\\server\share\dir");
74 /// ```
75 ///
76 /// [safety]: https://github.com/kojiishi/simple-path#safety
77 /// [Win32 File Namespaces]: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#win32-file-namespaces
78 pub allow_unknown_unc: bool,
79
80 /// Map to network share drive names when possible.
81 /// Initially `false`.
82 ///
83 /// # Examples
84 ///
85 /// ```
86 /// # use simple_path::SimplePath;
87 /// # fn test() -> std::io::Result<()> {
88 /// let path = "file.txt";
89 /// let simple = SimplePath { map_to_drive: true, ..Default::default() };
90 /// let canonicalized = simple.canonicalize(path)?;
91 /// # Ok(())
92 /// # }
93 /// ```
94 /// If the `file.txt` is in a network drive,
95 /// the result is `Z:\dir\file.txt`
96 /// instead of `\\server\share\dir\file.txt`.
97 ///
98 /// The following code tries to preserve the original form of the `path`.
99 /// ```
100 /// # use simple_path::SimplePath;
101 /// # fn test(path: &std::path::Path) -> std::io::Result<()> {
102 /// SimplePath {
103 /// map_to_drive: !path.as_os_str().as_encoded_bytes().starts_with(br"\\"),
104 /// ..Default::default()
105 /// }.canonicalize(path)?;
106 /// # Ok(())
107 /// # }
108 /// ```
109 pub map_to_drive: bool,
110
111 /// Skip the [`dunce`] simplification.
112 /// Initially `false`.
113 ///
114 /// [`dunce`]: https://crates.io/crates/dunce
115 pub skip_dunce: bool,
116
117 /// It is highly recommended to always use `, ..Default::default()`.
118 /// Otherwise builds fail when new fields are added.
119 ///
120 /// This field is not used in any ways,
121 /// but exists to allow using `, ..Default::default()`
122 /// even when all other fields are specified.
123 pub _unused: bool,
124
125 #[cfg(all(test, windows))]
126 volumes: Option<Volumes>,
127}
128
129impl SimplePath {
130 #[cfg(all(test, windows))]
131 pub(crate) fn mock() -> SimplePath {
132 SimplePath {
133 volumes: Some(Volumes::mock()),
134 ..Default::default()
135 }
136 }
137
138 /// A snap-in replacement for [`fs::canonicalize`].
139 /// It calls [`fs::canonicalize`] and [`simplify`].
140 ///
141 /// On other platforms than Windows,
142 /// this is equivalent to [`fs::canonicalize`].
143 ///
144 /// # Examples
145 /// ```
146 /// # fn test(path: &std::path::Path) -> std::io::Result<()> {
147 /// use simple_path::SimplePath;
148 /// let canonicalized = SimplePath::default().canonicalize(path)?;
149 /// println!("{}", canonicalized.display());
150 /// # Ok(()) }
151 /// ```
152 ///
153 /// [`fs::canonicalize`]: https://doc.rust-lang.org/std/fs/fn.canonicalize.html
154 /// [`simplify`]: SimplePath::simplify
155 pub fn canonicalize(&self, path: impl AsRef<Path>) -> io::Result<PathBuf> {
156 let canonicalized = fs::canonicalize(path)?;
157 #[cfg(windows)]
158 if let Some(simplified) = self.simplify(&canonicalized)? {
159 return Ok(simplified.into_owned());
160 }
161 Ok(canonicalized)
162 }
163
164 /// Try to simplify the given `path`.
165 ///
166 /// Returns `Ok(None)`
167 /// if no simplification is applied,
168 /// or on other platforms than Windows.
169 pub fn simplify<'a>(&self, path: &'a Path) -> io::Result<Option<Cow<'a, Path>>> {
170 #[cfg(windows)]
171 return self._simplify(path).map_err(io_error_from_anyhow);
172 #[cfg(not(windows))]
173 Ok(None)
174 }
175
176 #[cfg(windows)]
177 fn _simplify<'a>(&self, path: &'a Path) -> anyhow::Result<Option<Cow<'a, Path>>> {
178 if let Ok(long_unc) = LongUnc::try_from(path)
179 && long_unc.is_sub_prefix_unc()
180 {
181 // Try mapped network drives.
182 let drive_path = if !self.allow_unknown_unc || self.map_to_drive {
183 self.drive_path(path)?
184 } else {
185 None
186 };
187 if self.map_to_drive
188 && let Some(drive_path) = &drive_path
189 && drive_path.has_drive()
190 && !drive_path.has_invalid_chars()
191 && (!self.disallow_long || !drive_path.is_longer_than_max_path())
192 {
193 return Ok(Some(Cow::Owned(drive_path.to_path_buf())));
194 }
195
196 // Try short UNC (`\\server\share`).
197 if (self.allow_unknown_unc || drive_path.is_some())
198 && !long_unc.has_invalid_chars()
199 && (!self.disallow_long || !long_unc.is_short_unc_longer_than_max_path())
200 {
201 return Ok(Some(Cow::Owned(long_unc.to_short_unc())));
202 }
203 }
204
205 // Try `dunce::simplified`.
206 if !self.skip_dunce {
207 let simplified = dunce::simplified(path);
208 if !std::ptr::eq(path, simplified) {
209 return Ok(Some(Cow::Borrowed(simplified)));
210 }
211 }
212 Ok(None)
213 }
214
215 #[cfg(windows)]
216 fn drive_path<'a>(&self, path: &'a Path) -> anyhow::Result<Option<crate::DrivePath<'a>>> {
217 #[cfg(test)]
218 if let Some(volumes) = &self.volumes {
219 return Ok(volumes._drive_path(path));
220 }
221 Volumes::drive_path(path)
222 }
223
224 /// Refreshes the cached information.
225 pub fn refresh() -> io::Result<()> {
226 #[cfg(windows)]
227 Volumes::refresh().map_err(io_error_from_anyhow)?;
228 Ok(())
229 }
230
231 /// Returns an object that implements [`Display`][`core::fmt::Display`]
232 /// for printing simplified paths.
233 ///
234 /// # Examples
235 ///
236 /// ```
237 /// # use std::path::Path;
238 /// # use simple_path::SimplePath;
239 /// # fn test() -> std::io::Result<()> {
240 /// let path = Path::new("file").canonicalize()?;
241 /// println!("{}", SimplePath::default().display(&path));
242 /// # Ok(())
243 /// # }
244 /// ```
245 pub fn display<'a>(&'a self, path: &'a Path) -> Display<'a> {
246 Display::new(self, path)
247 }
248
249 /// A snap-in replacement for [`Path::strip_prefix`]
250 /// with a fix for [a leading directory separator "`\`" left for UNC paths
251 /// on Windows](https://github.com/rust-lang/rust/issues/155183).
252 ///
253 /// # Examples
254 ///
255 /// ```
256 /// # use std::path::{Path, StripPrefixError};
257 /// # use simple_path::SimplePath;
258 /// # fn t<'a>(path: &'a Path, base: &'a Path) -> Result<&'a Path, StripPrefixError> {
259 /// SimplePath::strip_prefix(path, base)
260 /// # }
261 /// ```
262 pub fn strip_prefix(path: &Path, base: impl AsRef<Path>) -> Result<&Path, StripPrefixError> {
263 #[cfg(windows)]
264 return PathExt::strip_prefix_fix(path, base);
265 #[cfg(not(windows))]
266 path.strip_prefix(base)
267 }
268}
269
270fn io_error_from_anyhow(error: anyhow::Error) -> io::Error {
271 match error.downcast::<io::Error>() {
272 Ok(io_error) => io_error,
273 Err(other_error) => io::Error::other(other_error),
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[cfg(windows)]
282 #[test]
283 fn simplify_drive() {
284 let mut simple = SimplePath::mock();
285 assert_eq!(simple.simplify(Path::new(r"C:\foo")).unwrap(), None);
286 simple.allow_unknown_unc = true;
287 assert_eq!(simple.simplify(Path::new(r"C:\foo")).unwrap(), None);
288 }
289
290 #[cfg(windows)]
291 #[test]
292 fn simplify_drive_unc() {
293 let mut simple = SimplePath::mock();
294 let path = Path::new(r"\\?\UNC\server\share\foo");
295 let path2 = Path::new(r"\\?\UNC\server2\share2\foo2");
296 assert_eq!(
297 simple.simplify(path).unwrap(),
298 Some(Cow::Owned(PathBuf::from(r"\\server\share\foo")))
299 );
300 assert_eq!(
301 simple.simplify(path2).unwrap(),
302 Some(Cow::Owned(PathBuf::from(r"\\server2\share2\foo2")))
303 );
304
305 simple.map_to_drive = true;
306 assert_eq!(
307 simple.simplify(path).unwrap(),
308 Some(Cow::Owned(PathBuf::from(r"X:\foo")))
309 );
310 assert_eq!(
311 simple.simplify(path2).unwrap(),
312 Some(Cow::Owned(PathBuf::from(r"Z:\foo2")))
313 );
314 }
315
316 #[cfg(windows)]
317 #[test]
318 fn simplify_dunce() {
319 let simple = SimplePath::default();
320 assert_eq!(
321 simple.simplify(Path::new(r"\\?\C:\foo")).unwrap(),
322 Some(Cow::Borrowed(Path::new(r"C:\foo")))
323 );
324 }
325
326 #[cfg(windows)]
327 #[test]
328 fn simplify_dunce_skip() {
329 let simple = SimplePath {
330 skip_dunce: true,
331 ..Default::default()
332 };
333 assert_eq!(simple.simplify(Path::new(r"\\?\C:\foo")).unwrap(), None);
334 }
335
336 #[cfg(windows)]
337 #[test]
338 fn simplify_unmapped_connected_share() {
339 let mut simple = SimplePath::mock();
340 let path = Path::new(r"\\?\UNC\server0\share0\foo");
341 assert_eq!(
342 simple.simplify(path).unwrap(),
343 Some(Cow::Owned(PathBuf::from(r"\\server0\share0\foo")))
344 );
345
346 // Even with map_to_drive = true, it should simplify to the UNC path,
347 // because the drive letter is '\0'.
348 simple.map_to_drive = true;
349 assert_eq!(
350 simple.simplify(path).unwrap(),
351 Some(Cow::Owned(PathBuf::from(r"\\server0\share0\foo")))
352 );
353 }
354
355 #[cfg(windows)]
356 #[test]
357 fn simplify_unknown_unc() -> anyhow::Result<()> {
358 let mut simple = SimplePath::mock();
359 let unknown = Path::new(r"\\?\UNC\server\unknown\foo");
360 let mapped = Path::new(r"\\?\UNC\server\share\foo");
361 assert_eq!(simple.simplify(unknown)?, None);
362
363 // `unknown` should be simplified if `allow_unknown_unc`.
364 simple.allow_unknown_unc = true;
365 assert_eq!(
366 simple.simplify(unknown)?,
367 Some(Cow::Owned(PathBuf::from(r"\\server\unknown\foo")))
368 );
369 assert_eq!(
370 simple.simplify(mapped)?,
371 Some(Cow::Owned(PathBuf::from(r"\\server\share\foo")))
372 );
373
374 // `map_to_drive` should still be in effect.
375 simple.map_to_drive = true;
376 assert_eq!(
377 simple.simplify(mapped)?,
378 Some(Cow::Owned(PathBuf::from(r"X:\foo")))
379 );
380
381 // `allow_unknown_unc` should simplify only for "`\\?\UNC\`".
382 assert_eq!(simple.simplify(Path::new(r"\\.\COM1:"))?, None);
383 simple.skip_dunce = true;
384 assert_eq!(simple.simplify(Path::new(r"\\?\C:\foo"))?, None);
385 Ok(())
386 }
387}