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}