path_slash/lib.rs
1//! A library for converting file paths to and from "slash paths".
2//!
3//! A "slash path" is a path whose components are always separated by `/` and never `\`.
4//!
5//! On Unix-like OS, the path separator is `/`. So any conversion is not necessary.
6//! But on Windows, the file path separator is `\`, and needs to be replaced with `/` for converting
7//! the paths to "slash paths". Of course, `\`s used for escaping characters should not be replaced.
8//!
9//! For example, a file path `foo\bar\piyo.txt` can be converted to/from a slash path `foo/bar/piyo.txt`.
10//!
11//! Supported Rust version is 1.38.0 or later.
12//!
13//! This package was inspired by Go's [`path/filepath.FromSlash`](https://golang.org/pkg/path/filepath/#FromSlash)
14//! and [`path/filepath.ToSlash`](https://golang.org/pkg/path/filepath/#ToSlash).
15//!
16//! ```rust
17//! use std::path::{Path, PathBuf};
18//! use std::borrow::Cow;
19//!
20//! // Trait for extending std::path::Path
21//! use path_slash::PathExt as _;
22//! // Trait for extending std::path::PathBuf
23//! use path_slash::PathBufExt as _;
24//! // Trait for extending std::borrow::Cow
25//! use path_slash::CowExt as _;
26//!
27//! #[cfg(target_os = "windows")]
28//! {
29//! // Convert from `Path`
30//! assert_eq!(
31//! Path::new(r"foo\bar\piyo.txt").to_slash().unwrap(),
32//! "foo/bar/piyo.txt",
33//! );
34//!
35//! // Convert to/from PathBuf
36//! let p = PathBuf::from_slash("foo/bar/piyo.txt");
37//! assert_eq!(p, PathBuf::from(r"foo\bar\piyo.txt"));
38//! assert_eq!(p.to_slash().unwrap(), "foo/bar/piyo.txt");
39//!
40//! // Convert to/from Cow<'_, Path>
41//! let p = Cow::from_slash("foo/bar/piyo.txt");
42//! assert_eq!(p, Cow::<Path>::Owned(PathBuf::from(r"foo\bar\piyo.txt")));
43//! assert_eq!(p.to_slash().unwrap(), "foo/bar/piyo.txt");
44//! }
45//!
46//! #[cfg(not(target_os = "windows"))]
47//! {
48//! // Convert from `Path`
49//! assert_eq!(
50//! Path::new("foo/bar/piyo.txt").to_slash().unwrap(),
51//! "foo/bar/piyo.txt",
52//! );
53//!
54//! // Convert to/from PathBuf
55//! let p = PathBuf::from_slash("foo/bar/piyo.txt");
56//! assert_eq!(p, PathBuf::from("foo/bar/piyo.txt"));
57//! assert_eq!(p.to_slash().unwrap(), "foo/bar/piyo.txt");
58//!
59//! // Convert to/from Cow<'_, Path>
60//! let p = Cow::from_slash("foo/bar/piyo.txt");
61//! assert_eq!(p, Cow::Borrowed(Path::new("foo/bar/piyo.txt")));
62//! assert_eq!(p.to_slash().unwrap(), "foo/bar/piyo.txt");
63//! }
64//! ```
65#![forbid(unsafe_code)]
66#![warn(clippy::dbg_macro, clippy::print_stdout)]
67
68use std::borrow::Cow;
69use std::ffi::OsStr;
70use std::path::{Path, PathBuf, MAIN_SEPARATOR};
71
72#[cfg(target_os = "windows")]
73mod windows {
74 use super::*;
75 use std::os::windows::ffi::OsStrExt as _;
76
77 // Workaround for Windows. There is no way to extract raw byte sequence from `OsStr` (in `Path`).
78 // And `OsStr::to_string_lossy` may cause extra heap allocation.
79 pub(crate) fn ends_with_main_sep(p: &Path) -> bool {
80 p.as_os_str().encode_wide().last() == Some(MAIN_SEPARATOR as u16)
81 }
82}
83
84fn str_to_path(s: &str, sep: char) -> Cow<'_, Path> {
85 let mut buf = String::new();
86
87 for (i, c) in s.char_indices() {
88 if c == sep {
89 if buf.is_empty() {
90 buf.reserve(s.len());
91 buf.push_str(&s[..i]);
92 }
93 buf.push(MAIN_SEPARATOR);
94 } else if !buf.is_empty() {
95 buf.push(c);
96 }
97 }
98
99 if buf.is_empty() {
100 Cow::Borrowed(Path::new(s))
101 } else {
102 Cow::Owned(PathBuf::from(buf))
103 }
104}
105
106fn str_to_pathbuf<S: AsRef<str>>(s: S, sep: char) -> PathBuf {
107 let s = s
108 .as_ref()
109 .chars()
110 .map(|c| if c == sep { MAIN_SEPARATOR } else { c })
111 .collect::<String>();
112 PathBuf::from(s)
113 // Note: When MAIN_SEPARATOR_STR is stabilized, replace this implementation with the following:
114 // PathBuf::from(s.as_ref().replace(sep, MAIN_SEPARATOR_STR))
115}
116
117/// Trait to extend [`Path`].
118///
119/// ```
120/// # use std::path::Path;
121/// # use std::borrow::Cow;
122/// use path_slash::PathExt as _;
123///
124/// assert_eq!(
125/// Path::new("foo").to_slash(),
126/// Some(Cow::Borrowed("foo")),
127/// );
128/// ```
129pub trait PathExt {
130 /// Convert the file path into slash path as UTF-8 string. This method is similar to
131 /// [`Path::to_str`], but the path separator is fixed to '/'.
132 ///
133 /// Any file path separators in the file path are replaced with '/'. Only when the replacement
134 /// happens, heap allocation happens and `Cow::Owned` is returned.
135 /// When the path contains non-Unicode sequence, this method returns None.
136 ///
137 /// ```
138 /// # use std::path::Path;
139 /// # use std::borrow::Cow;
140 /// use path_slash::PathExt as _;
141 ///
142 /// #[cfg(target_os = "windows")]
143 /// let s = Path::new(r"foo\bar\piyo.txt");
144 ///
145 /// #[cfg(not(target_os = "windows"))]
146 /// let s = Path::new("foo/bar/piyo.txt");
147 ///
148 /// assert_eq!(s.to_slash(), Some(Cow::Borrowed("foo/bar/piyo.txt")));
149 /// ```
150 fn to_slash(&self) -> Option<Cow<'_, str>>;
151 /// Convert the file path into slash path as UTF-8 string. This method is similar to
152 /// [`Path::to_string_lossy`], but the path separator is fixed to '/'.
153 ///
154 /// Any file path separators in the file path are replaced with '/'.
155 /// Any non-Unicode sequences are replaced with U+FFFD.
156 ///
157 /// ```
158 /// # use std::path::Path;
159 /// use path_slash::PathExt as _;
160 ///
161 /// #[cfg(target_os = "windows")]
162 /// let s = Path::new(r"foo\bar\piyo.txt");
163 ///
164 /// #[cfg(not(target_os = "windows"))]
165 /// let s = Path::new("foo/bar/piyo.txt");
166 ///
167 /// assert_eq!(s.to_slash_lossy(), "foo/bar/piyo.txt");
168 /// ```
169 fn to_slash_lossy(&self) -> Cow<'_, str>;
170}
171
172impl PathExt for Path {
173 #[cfg(not(target_os = "windows"))]
174 fn to_slash_lossy(&self) -> Cow<'_, str> {
175 self.to_string_lossy()
176 }
177 #[cfg(target_os = "windows")]
178 fn to_slash_lossy(&self) -> Cow<'_, str> {
179 use std::path::Component;
180
181 let mut buf = String::new();
182 for c in self.components() {
183 match c {
184 Component::RootDir => { /* empty */ }
185 Component::CurDir => buf.push('.'),
186 Component::ParentDir => buf.push_str(".."),
187 Component::Prefix(prefix) => {
188 buf.push_str(&prefix.as_os_str().to_string_lossy());
189 // C:\foo is [Prefix, RootDir, Normal]. Avoid C://
190 continue;
191 }
192 Component::Normal(s) => buf.push_str(&s.to_string_lossy()),
193 }
194 buf.push('/');
195 }
196
197 if !windows::ends_with_main_sep(self) && buf != "/" && buf.ends_with('/') {
198 buf.pop(); // Pop last '/'
199 }
200
201 Cow::Owned(buf)
202 }
203
204 #[cfg(not(target_os = "windows"))]
205 fn to_slash(&self) -> Option<Cow<'_, str>> {
206 self.to_str().map(Cow::Borrowed)
207 }
208 #[cfg(target_os = "windows")]
209 fn to_slash(&self) -> Option<Cow<'_, str>> {
210 use std::path::Component;
211
212 let mut buf = String::new();
213 for c in self.components() {
214 match c {
215 Component::RootDir => { /* empty */ }
216 Component::CurDir => buf.push('.'),
217 Component::ParentDir => buf.push_str(".."),
218 Component::Prefix(prefix) => {
219 buf.push_str(prefix.as_os_str().to_str()?);
220 // C:\foo is [Prefix, RootDir, Normal]. Avoid C://
221 continue;
222 }
223 Component::Normal(s) => buf.push_str(s.to_str()?),
224 }
225 buf.push('/');
226 }
227
228 if !windows::ends_with_main_sep(self) && buf != "/" && buf.ends_with('/') {
229 buf.pop(); // Pop last '/'
230 }
231
232 Some(Cow::Owned(buf))
233 }
234}
235
236/// Trait to extend [`PathBuf`].
237///
238/// ```
239/// # use std::path::PathBuf;
240/// use path_slash::PathBufExt as _;
241///
242/// assert_eq!(
243/// PathBuf::from_slash("foo/bar/piyo.txt").to_slash().unwrap(),
244/// "foo/bar/piyo.txt",
245/// );
246/// ```
247pub trait PathBufExt {
248 /// Convert the slash path (path separated with '/') to [`PathBuf`].
249 ///
250 /// Any '/' in the slash path is replaced with the file path separator.
251 /// The replacements only happen on Windows since the file path separators on Unix-like OS are
252 /// the same as '/'.
253 ///
254 /// On non-Windows OS, it is simply equivalent to [`PathBuf::from`].
255 ///
256 /// ```
257 /// # use std::path::PathBuf;
258 /// use path_slash::PathBufExt as _;
259 ///
260 /// let p = PathBuf::from_slash("foo/bar/piyo.txt");
261 ///
262 /// #[cfg(target_os = "windows")]
263 /// assert_eq!(p, PathBuf::from(r"foo\bar\piyo.txt"));
264 ///
265 /// #[cfg(not(target_os = "windows"))]
266 /// assert_eq!(p, PathBuf::from("foo/bar/piyo.txt"));
267 /// ```
268 fn from_slash<S: AsRef<str>>(s: S) -> Self;
269 /// Convert the [`OsStr`] slash path (path separated with '/') to [`PathBuf`].
270 ///
271 /// Any '/' in the slash path is replaced with the file path separator.
272 /// The replacements only happen on Windows since the file path separators on Unix-like OS are
273 /// the same as '/'.
274 ///
275 /// On Windows, any non-Unicode sequences are replaced with U+FFFD while the conversion.
276 /// On non-Windows OS, it is simply equivalent to [`PathBuf::from`] and there is no
277 /// loss while conversion.
278 ///
279 /// ```
280 /// # use std::path::PathBuf;
281 /// # use std::ffi::OsStr;
282 /// use path_slash::PathBufExt as _;
283 ///
284 /// let s: &OsStr = "foo/bar/piyo.txt".as_ref();
285 /// let p = PathBuf::from_slash_lossy(s);
286 ///
287 /// #[cfg(target_os = "windows")]
288 /// assert_eq!(p, PathBuf::from(r"foo\bar\piyo.txt"));
289 ///
290 /// #[cfg(not(target_os = "windows"))]
291 /// assert_eq!(p, PathBuf::from("foo/bar/piyo.txt"));
292 /// ```
293 fn from_slash_lossy<S: AsRef<OsStr>>(s: S) -> Self;
294 /// Convert the backslash path (path separated with '\\') to [`PathBuf`].
295 ///
296 /// Any '\\' in the slash path is replaced with the file path separator.
297 /// The replacements only happen on non-Windows.
298 fn from_backslash<S: AsRef<str>>(s: S) -> Self;
299 /// Convert the [`OsStr`] backslash path (path separated with '\\') to [`PathBuf`].
300 ///
301 /// Any '\\' in the slash path is replaced with the file path separator.
302 fn from_backslash_lossy<S: AsRef<OsStr>>(s: S) -> Self;
303 /// Convert the file path into slash path as UTF-8 string. This method is similar to
304 /// [`Path::to_str`], but the path separator is fixed to '/'.
305 ///
306 /// Any file path separators in the file path are replaced with '/'. Only when the replacement
307 /// happens, heap allocation happens and `Cow::Owned` is returned.
308 /// When the path contains non-Unicode sequence, this method returns None.
309 ///
310 /// ```
311 /// # use std::path::PathBuf;
312 /// # use std::borrow::Cow;
313 /// use path_slash::PathBufExt as _;
314 ///
315 /// #[cfg(target_os = "windows")]
316 /// let s = PathBuf::from(r"foo\bar\piyo.txt");
317 ///
318 /// #[cfg(not(target_os = "windows"))]
319 /// let s = PathBuf::from("foo/bar/piyo.txt");
320 ///
321 /// assert_eq!(s.to_slash(), Some(Cow::Borrowed("foo/bar/piyo.txt")));
322 /// ```
323 fn to_slash(&self) -> Option<Cow<'_, str>>;
324 /// Convert the file path into slash path as UTF-8 string. This method is similar to
325 /// [`Path::to_string_lossy`], but the path separator is fixed to '/'.
326 ///
327 /// Any file path separators in the file path are replaced with '/'.
328 /// Any non-Unicode sequences are replaced with U+FFFD.
329 ///
330 /// ```
331 /// # use std::path::PathBuf;
332 /// use path_slash::PathBufExt as _;
333 ///
334 /// #[cfg(target_os = "windows")]
335 /// let s = PathBuf::from(r"foo\bar\piyo.txt");
336 ///
337 /// #[cfg(not(target_os = "windows"))]
338 /// let s = PathBuf::from("foo/bar/piyo.txt");
339 ///
340 /// assert_eq!(s.to_slash_lossy(), "foo/bar/piyo.txt");
341 /// ```
342 fn to_slash_lossy(&self) -> Cow<'_, str>;
343}
344
345impl PathBufExt for PathBuf {
346 #[cfg(not(target_os = "windows"))]
347 fn from_slash<S: AsRef<str>>(s: S) -> Self {
348 PathBuf::from(s.as_ref())
349 }
350 #[cfg(target_os = "windows")]
351 fn from_slash<S: AsRef<str>>(s: S) -> Self {
352 str_to_pathbuf(s, '/')
353 }
354
355 #[cfg(not(target_os = "windows"))]
356 fn from_slash_lossy<S: AsRef<OsStr>>(s: S) -> Self {
357 PathBuf::from(s.as_ref())
358 }
359 #[cfg(target_os = "windows")]
360 fn from_slash_lossy<S: AsRef<OsStr>>(s: S) -> Self {
361 Self::from_slash(&s.as_ref().to_string_lossy())
362 }
363
364 #[cfg(not(target_os = "windows"))]
365 fn from_backslash<S: AsRef<str>>(s: S) -> Self {
366 str_to_pathbuf(s, '\\')
367 }
368 #[cfg(target_os = "windows")]
369 fn from_backslash<S: AsRef<str>>(s: S) -> Self {
370 PathBuf::from(s.as_ref())
371 }
372
373 #[cfg(not(target_os = "windows"))]
374 fn from_backslash_lossy<S: AsRef<OsStr>>(s: S) -> Self {
375 str_to_pathbuf(&s.as_ref().to_string_lossy(), '\\')
376 }
377 #[cfg(target_os = "windows")]
378 fn from_backslash_lossy<S: AsRef<OsStr>>(s: S) -> Self {
379 PathBuf::from(s.as_ref())
380 }
381
382 fn to_slash(&self) -> Option<Cow<'_, str>> {
383 self.as_path().to_slash()
384 }
385
386 fn to_slash_lossy(&self) -> Cow<'_, str> {
387 self.as_path().to_slash_lossy()
388 }
389}
390
391/// Trait to extend [`Cow`].
392///
393/// ```
394/// # use std::borrow::Cow;
395/// use path_slash::CowExt as _;
396///
397/// assert_eq!(
398/// Cow::from_slash("foo/bar/piyo.txt").to_slash_lossy(),
399/// "foo/bar/piyo.txt",
400/// );
401/// ```
402pub trait CowExt<'a> {
403 /// Convert the slash path (path separated with '/') to [`Cow`].
404 ///
405 /// Any '/' in the slash path is replaced with the file path separator.
406 /// Heap allocation may only happen on Windows since the file path separators on Unix-like OS
407 /// are the same as '/'.
408 ///
409 /// ```
410 /// # use std::borrow::Cow;
411 /// # use std::path::Path;
412 /// use path_slash::CowExt as _;
413 ///
414 /// #[cfg(not(target_os = "windows"))]
415 /// assert_eq!(
416 /// Cow::from_slash("foo/bar/piyo.txt"),
417 /// Path::new("foo/bar/piyo.txt"),
418 /// );
419 ///
420 /// #[cfg(target_os = "windows")]
421 /// assert_eq!(
422 /// Cow::from_slash("foo/bar/piyo.txt"),
423 /// Path::new(r"foo\\bar\\piyo.txt"),
424 /// );
425 /// ```
426 fn from_slash(s: &'a str) -> Self;
427 /// Convert the [`OsStr`] slash path (path separated with '/') to [`Cow`].
428 ///
429 /// Any '/' in the slash path is replaced with the file path separator.
430 /// Heap allocation may only happen on Windows since the file path separators on Unix-like OS
431 /// are the same as '/'.
432 ///
433 /// On Windows, any non-Unicode sequences are replaced with U+FFFD while the conversion.
434 /// On non-Windows OS, there is no loss while conversion.
435 fn from_slash_lossy(s: &'a OsStr) -> Self;
436 /// Convert the backslash path (path separated with '\\') to [`Cow`].
437 ///
438 /// Any '\\' in the slash path is replaced with the file path separator. Heap allocation may
439 /// only happen on non-Windows.
440 ///
441 /// ```
442 /// # use std::borrow::Cow;
443 /// # use std::path::Path;
444 /// use path_slash::CowExt as _;
445 ///
446 /// #[cfg(not(target_os = "windows"))]
447 /// assert_eq!(
448 /// Cow::from_backslash(r"foo\\bar\\piyo.txt"),
449 /// Path::new("foo/bar/piyo.txt"),
450 /// );
451 ///
452 /// #[cfg(target_os = "windows")]
453 /// assert_eq!(
454 /// Cow::from_backslash(r"foo\\bar\\piyo.txt"),
455 /// Path::new(r"foo\\bar\\piyo.txt"),
456 /// );
457 /// ```
458 fn from_backslash(s: &'a str) -> Self;
459 /// Convert the [`OsStr`] backslash path (path separated with '\\') to [`Cow`].
460 ///
461 /// Any '\\' in the slash path is replaced with the file path separator. Heap allocation may
462 /// only happen on non-Windows.
463 fn from_backslash_lossy(s: &'a OsStr) -> Self;
464 /// Convert the file path into slash path as UTF-8 string. This method is similar to
465 /// [`Path::to_str`], but the path separator is fixed to '/'.
466 ///
467 /// Any file path separators in the file path are replaced with '/'. Only when the replacement
468 /// happens, heap allocation happens and `Cow::Owned` is returned.
469 /// When the path contains non-Unicode sequences, this method returns `None`.
470 ///
471 /// ```
472 /// # use std::path::Path;
473 /// # use std::borrow::Cow;
474 /// use path_slash::CowExt as _;
475 ///
476 /// #[cfg(target_os = "windows")]
477 /// let s = Cow::Borrowed(Path::new(r"foo\bar\piyo.txt"));
478 ///
479 /// #[cfg(not(target_os = "windows"))]
480 /// let s = Cow::Borrowed(Path::new("foo/bar/piyo.txt"));
481 ///
482 /// assert_eq!(s.to_slash(), Some(Cow::Borrowed("foo/bar/piyo.txt")));
483 /// ```
484 fn to_slash(&self) -> Option<Cow<'_, str>>;
485 /// Convert the file path into slash path as UTF-8 string. This method is similar to
486 /// [`Path::to_string_lossy`], but the path separator is fixed to '/'.
487 ///
488 /// Any file path separators in the file path are replaced with '/'.
489 /// Any non-Unicode sequences are replaced with U+FFFD.
490 ///
491 /// ```
492 /// # use std::path::Path;
493 /// # use std::borrow::Cow;
494 /// use path_slash::CowExt as _;
495 ///
496 /// #[cfg(target_os = "windows")]
497 /// let s = Cow::Borrowed(Path::new(r"foo\bar\piyo.txt"));
498 ///
499 /// #[cfg(not(target_os = "windows"))]
500 /// let s = Cow::Borrowed(Path::new("foo/bar/piyo.txt"));
501 ///
502 /// assert_eq!(s.to_slash_lossy(), "foo/bar/piyo.txt");
503 /// ```
504 fn to_slash_lossy(&self) -> Cow<'_, str>;
505}
506
507impl<'a> CowExt<'a> for Cow<'a, Path> {
508 #[cfg(not(target_os = "windows"))]
509 fn from_slash(s: &'a str) -> Self {
510 Cow::Borrowed(Path::new(s))
511 }
512 #[cfg(target_os = "windows")]
513 fn from_slash(s: &'a str) -> Self {
514 str_to_path(s, '/')
515 }
516
517 #[cfg(not(target_os = "windows"))]
518 fn from_slash_lossy(s: &'a OsStr) -> Self {
519 Cow::Borrowed(Path::new(s))
520 }
521 #[cfg(target_os = "windows")]
522 fn from_slash_lossy(s: &'a OsStr) -> Self {
523 match s.to_string_lossy() {
524 Cow::Borrowed(s) => str_to_path(s, '/'),
525 Cow::Owned(s) => Cow::Owned(str_to_pathbuf(&s, '/')),
526 }
527 }
528
529 #[cfg(not(target_os = "windows"))]
530 fn from_backslash(s: &'a str) -> Self {
531 str_to_path(s, '\\')
532 }
533 #[cfg(target_os = "windows")]
534 fn from_backslash(s: &'a str) -> Self {
535 Cow::Borrowed(Path::new(s))
536 }
537
538 #[cfg(not(target_os = "windows"))]
539 fn from_backslash_lossy(s: &'a OsStr) -> Self {
540 match s.to_string_lossy() {
541 Cow::Borrowed(s) => str_to_path(s, '\\'),
542 Cow::Owned(s) => Cow::Owned(str_to_pathbuf(&s, '\\')),
543 }
544 }
545 #[cfg(target_os = "windows")]
546 fn from_backslash_lossy(s: &'a OsStr) -> Self {
547 Cow::Borrowed(Path::new(s))
548 }
549
550 fn to_slash(&self) -> Option<Cow<'_, str>> {
551 self.as_ref().to_slash()
552 }
553
554 fn to_slash_lossy(&self) -> Cow<'_, str> {
555 self.as_ref().to_slash_lossy()
556 }
557}