diff --git a/README.md b/README.md index 660c218..a5e443e 100644 --- a/README.md +++ b/README.md @@ -16,19 +16,24 @@ and [`path/filepath.ToSlash`](https://golang.org/pkg/path/filepath/#ToSlash). ## Usage -`path_slash::PathExt` and `path_slash::PathBufExt` traits are defined. By using them, `std::path::Path` -and `std::path::PathBuf` gains some methods and associated functions +`path_slash::PathExt` and `path_slash::PathBufExt` traits are defined. By using them, `std::path::Path`, +`std::path::PathBuf` and `Cow<'_, Path>` gain some methods and associated functions. - `PathExt` - - `Path::to_slash(&self) -> Option` - - `Path::to_slash_lossy(&self) -> String` + - `Path::to_slash(&self) -> Option>` + - `Path::to_slash_lossy(&self) -> Cow<'_, Path>` - `PathBufExt` - `PathBuf::from_slash>(s: S) -> PathBuf` - `PathBuf::from_slash_lossy>(s: S) -> PathBuf` - `PathBuf::from_backslash>(s: S) -> PathBuf` - `PathBuf::from_backslash_lossy>(s: S) -> PathBuf` - - `PathBuf::to_slash(&self) -> Option` - - `PathBuf::to_slash_lossy(&self) -> String` + - `PathBuf::to_slash(&self) -> Option>` + - `PathBuf::to_slash_lossy(&self) -> Cow<'_, Path>` +- `CowExt` + - `fn from_slash(s: &str) -> Self` + - `fn from_slash_lossy(s: &OsStr) -> Self` + - `fn from_backslash(s: &str) -> Self` + - `fn from_backslash_lossy(s: &OsStr) -> Self` ```rust fn example_path_ext() { @@ -40,10 +45,6 @@ fn example_path_ext() { Path::new(r"foo\bar\piyo.txt").to_slash(), Some("foo/bar/piyo.txt".to_string()), ); - assert_eq!( - Path::new(r"C:\foo\bar\piyo.txt").to_slash(), - Some("C:/foo/bar/piyo.txt".to_string()), - ); } fn example_pathbuf_ext() { @@ -53,7 +54,18 @@ fn example_pathbuf_ext() { // On Windows let p = PathBuf::from_slash("foo/bar/piyo.txt"); assert_eq!(p, PathBuf::from(r"foo\bar\piyo.txt")); - assert_eq!(p.to_slash(), Some("foo/bar/piyo.txt".to_string())); + assert_eq!(p.to_slash().unwrap(), "foo/bar/piyo.txt".to_string()); +} + +fn example_cow_ext() { + // Trait for extending std::borrow::Cow<'_, Path> + use path_slash::CowExt as _; + + let p = Cow::from_slash("foo/bar/piyo.txt"); + // On Windows + assert_eq!(p, Cow::Owned(PathBuf::from(r"foo\bar\piyo.txt"))); + // On non-Windows + assert_eq!(p, Cow::Borrowed(Path::new("foo/bar/piyo.txt"))); } ``` diff --git a/src/lib.rs b/src/lib.rs index 8e7a2d3..b6387bc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,10 @@ -//! A library for converting file paths to and from "slash paths." +//! A library for converting file paths to and from "slash paths". //! //! A "slash path" is a path whose components are always separated by `/` and never `\`. //! //! On Unix-like OSes, the path separator is `/`. So any conversion is not necessary. -//! But on Windows, the file path separator is `\`, and needs to be replaced with `/`. Of course, `\`s used -//! for escaping characters should not be replaced. +//! But on Windows, the file path separator is `\`, and needs to be replaced with `/` for converting +//! the paths to "slash paths". Of course, `\`s used for escaping characters should not be replaced. //! //! For example, a file path `foo\bar\piyo.txt` can be converted to/from a slash path `foo/bar/piyo.txt`. //! @@ -13,63 +13,104 @@ //! //! ```rust //! use std::path::{Path, PathBuf}; +//! use std::borrow::Cow; //! //! // Trait for extending std::path::Path -//! use path_slash::PathExt; +//! use path_slash::PathExt as _; //! // Trait for extending std::path::PathBuf -//! use path_slash::PathBufExt; +//! use path_slash::PathBufExt as _; +//! // Trait for extending std::borrow::Cow +//! use path_slash::CowExt as _; //! //! #[cfg(target_os = "windows")] //! { +//! // Convert from `Path` //! assert_eq!( -//! Path::new(r"foo\bar\piyo.txt").to_slash(), -//! Some("foo/bar/piyo.txt".to_string()), -//! ); -//! assert_eq!( -//! Path::new(r"C:\foo\bar\piyo.txt").to_slash(), -//! Some("C:/foo/bar/piyo.txt".to_string()), +//! Path::new(r"foo\bar\piyo.txt").to_slash().unwrap(), +//! "foo/bar/piyo.txt", //! ); //! +//! // Convert to/from PathBuf //! let p = PathBuf::from_slash("foo/bar/piyo.txt"); //! assert_eq!(p, PathBuf::from(r"foo\bar\piyo.txt")); -//! assert_eq!(p.to_slash(), Some("foo/bar/piyo.txt".to_string())); +//! assert_eq!(p.to_slash().unwrap(), "foo/bar/piyo.txt"); +//! +//! // Convert to Cow<'_, Path> +//! let p = Cow::from_slash("foo/bar/piyo.txt"); +//! assert_eq!(p, Cow::Owned(PathBuf::from(r"foo\bar\piyo.txt"))); //! } //! //! #[cfg(not(target_os = "windows"))] //! { +//! // Convert from `Path` //! assert_eq!( -//! Path::new("foo/bar/piyo.txt").to_slash(), -//! Some("foo/bar/piyo.txt".to_string()), -//! ); -//! assert_eq!( -//! Path::new("/foo/bar/piyo.txt").to_slash(), -//! Some("/foo/bar/piyo.txt".to_string()), +//! Path::new("foo/bar/piyo.txt").to_slash().unwrap(), +//! "foo/bar/piyo.txt", //! ); //! +//! // Convert to/from PathBuf //! let p = PathBuf::from_slash("foo/bar/piyo.txt"); -//! assert_eq!(p, PathBuf::from(r"foo/bar/piyo.txt")); -//! assert_eq!(p.to_slash(), Some("foo/bar/piyo.txt".to_string())); +//! assert_eq!(p, PathBuf::from("foo/bar/piyo.txt")); +//! assert_eq!(p.to_slash().unwrap(), "foo/bar/piyo.txt"); +//! +//! // Convert to Cow<'_, Path> +//! let p = Cow::from_slash("foo/bar/piyo.txt"); +//! assert_eq!(p, Cow::Borrowed(Path::new("foo/bar/piyo.txt"))); //! } //! ``` #![forbid(unsafe_code)] #![warn(clippy::dbg_macro, clippy::print_stdout)] +use std::borrow::Cow; use std::ffi::OsStr; -use std::path::{Path, PathBuf}; +use std::path::{Path, PathBuf, MAIN_SEPARATOR}; + +fn str_to_path(s: &str, sep: char) -> Cow<'_, Path> { + let mut buf = String::new(); + + for (i, c) in s.char_indices() { + if c == sep { + if buf.is_empty() { + buf.reserve(s.len()); + buf.push_str(&s[..i]); + } + buf.push(MAIN_SEPARATOR); + } else if !buf.is_empty() { + buf.push(c); + } + } + + if buf.is_empty() { + Cow::Borrowed(Path::new(s)) + } else { + Cow::Owned(PathBuf::from(buf)) + } +} + +fn str_to_pathbuf>(s: S, sep: char) -> PathBuf { + let s = s + .as_ref() + .chars() + .map(|c| if c == sep { MAIN_SEPARATOR } else { c }) + .collect::(); + PathBuf::from(s) +} /// Trait to extend [`std::path::Path`]. /// /// ``` -/// use path_slash::PathExt; +/// # use std::path::Path; +/// # use std::borrow::Cow; +/// use path_slash::PathExt as _; /// /// assert_eq!( -/// std::path::Path::new("foo").to_slash(), -/// Some("foo".to_string()), +/// Path::new("foo").to_slash(), +/// Some(Cow::Borrowed("foo")), /// ); /// ``` pub trait PathExt { - fn to_slash(&self) -> Option; - fn to_slash_lossy(&self) -> String; + fn to_slash(&self) -> Option>; + fn to_slash_lossy(&self) -> Cow<'_, str>; } impl PathExt for Path { @@ -78,11 +119,9 @@ impl PathExt for Path { /// Any file path separators in the file path is replaced with '/'. /// Any non-Unicode sequences are replaced with U+FFFD. /// - /// On non-Windows OS, it is equivalent to `to_string_lossy().to_string()` - /// /// ``` - /// use std::path::Path; - /// use path_slash::PathExt; + /// # use std::path::Path; + /// use path_slash::PathExt as _; /// /// #[cfg(target_os = "windows")] /// let s = Path::new(r"foo\bar\piyo.txt"); @@ -90,66 +129,37 @@ impl PathExt for Path { /// #[cfg(not(target_os = "windows"))] /// let s = Path::new("foo/bar/piyo.txt"); /// - /// assert_eq!(s.to_slash_lossy(), "foo/bar/piyo.txt".to_string()); + /// assert_eq!(s.to_slash_lossy(), "foo/bar/piyo.txt"); /// ``` #[cfg(not(target_os = "windows"))] - fn to_slash_lossy(&self) -> String { - self.to_string_lossy().to_string() + fn to_slash_lossy(&self) -> Cow<'_, str> { + self.to_string_lossy() } - - /// Convert the file path into slash path as UTF-8 string. - /// - /// Any file path separators in the file path is replaced with '/'. - /// Any non-Unicode sequences are replaced with U+FFFD. - /// - /// On non-Windows OS, it is equivalent to `.to_string_lossy().to_string()`. - /// - /// ``` - /// use std::path::Path; - /// use path_slash::PathExt; - /// - /// #[cfg(target_os = "windows")] - /// let s = Path::new(r"foo\bar\piyo.txt"); - /// - /// #[cfg(not(target_os = "windows"))] - /// let s = Path::new("foo/bar/piyo.txt"); - /// - /// assert_eq!(s.to_slash_lossy(), "foo/bar/piyo.txt".to_string()); - /// ``` #[cfg(target_os = "windows")] - fn to_slash_lossy(&self) -> String { - use std::path; + fn to_slash_lossy(&self) -> Cow<'_, str> { + use std::path::Component; let mut buf = String::new(); - let mut has_trailing_slash = false; for c in self.components() { match c { - path::Component::RootDir => { /* empty */ } - path::Component::CurDir => buf.push('.'), - path::Component::ParentDir => buf.push_str(".."), - path::Component::Prefix(ref prefix) => { - let s = prefix.as_os_str(); - match s.to_str() { - Some(ref s) => buf.push_str(s), - None => buf.push_str(&s.to_string_lossy()), - } + Component::RootDir => { /* empty */ } + Component::CurDir => buf.push('.'), + Component::ParentDir => buf.push_str(".."), + Component::Prefix(prefix) => { + buf.push_str(&prefix.as_os_str().to_string_lossy()); // C:\foo is [Prefix, RootDir, Normal]. Avoid C:// continue; } - path::Component::Normal(ref s) => match s.to_str() { - Some(ref s) => buf.push_str(s), - None => buf.push_str(&s.to_string_lossy()), - }, + Component::Normal(s) => buf.push_str(&s.to_string_lossy()), } buf.push('/'); - has_trailing_slash = true; } - if buf != "/" && has_trailing_slash { + if buf != "/" && buf.ends_with('/') { buf.pop(); // Pop last '/' } - buf + Cow::Owned(buf) } /// Convert the file path into slash path as UTF-8 string. @@ -157,11 +167,10 @@ impl PathExt for Path { /// Any file path separators in the file path is replaced with '/'. /// When the path contains non-Unicode sequence, this method returns None. /// - /// On non-Windows OS, it is equivalent to `.to_str().map(str::to_string)` - /// /// ``` - /// use std::path::Path; - /// use path_slash::PathExt; + /// # use std::path::Path; + /// # use std::borrow::Cow; + /// use path_slash::PathExt as _; /// /// #[cfg(target_os = "windows")] /// let s = Path::new(r"foo\bar\piyo.txt"); @@ -169,80 +178,49 @@ impl PathExt for Path { /// #[cfg(not(target_os = "windows"))] /// let s = Path::new("foo/bar/piyo.txt"); /// - /// assert_eq!(s.to_slash(), Some("foo/bar/piyo.txt".to_string())); + /// assert_eq!(s.to_slash(), Some(Cow::Borrowed("foo/bar/piyo.txt"))); /// ``` #[cfg(not(target_os = "windows"))] - fn to_slash(&self) -> Option { - self.to_str().map(str::to_string) + fn to_slash(&self) -> Option> { + self.to_str().map(Cow::Borrowed) } - - /// Convert the file path into slash path as UTF-8 string. - /// - /// Any file path separators in the file path is replaced with '/'. - /// When the path contains non-Unicode sequence, this method returns None. - /// - /// On non-Windows OS, it is equivalent to `.to_str().map(str::to_string)` - /// - /// ``` - /// use std::path::Path; - /// use path_slash::PathExt; - /// - /// #[cfg(target_os = "windows")] - /// let s = Path::new(r"foo\bar\piyo.txt"); - /// - /// #[cfg(not(target_os = "windows"))] - /// let s = Path::new("foo/bar/piyo.txt"); - /// - /// assert_eq!(s.to_slash(), Some("foo/bar/piyo.txt".to_string())); - /// ``` #[cfg(target_os = "windows")] - fn to_slash(&self) -> Option { - use std::path; + fn to_slash(&self) -> Option> { + use std::path::Component; let mut buf = String::new(); - let mut has_trailing_slash = false; for c in self.components() { match c { - path::Component::RootDir => { /* empty */ } - path::Component::CurDir => buf.push('.'), - path::Component::ParentDir => buf.push_str(".."), - path::Component::Prefix(ref prefix) => { - if let Some(s) = prefix.as_os_str().to_str() { - buf.push_str(s); - // C:\foo is [Prefix, RootDir, Normal]. Avoid C:// - continue; - } else { - return None; - } - } - path::Component::Normal(ref s) => { - if let Some(s) = s.to_str() { - buf.push_str(s); - } else { - return None; - } + Component::RootDir => { /* empty */ } + Component::CurDir => buf.push('.'), + Component::ParentDir => buf.push_str(".."), + Component::Prefix(prefix) => { + buf.push_str(prefix.as_os_str().to_str()?); + // C:\foo is [Prefix, RootDir, Normal]. Avoid C:// + continue; } + Component::Normal(s) => buf.push_str(s.to_str()?), } buf.push('/'); - has_trailing_slash = true; } - if buf != "/" && has_trailing_slash { + if buf != "/" && buf.ends_with('/') { buf.pop(); // Pop last '/' } - Some(buf) + Some(Cow::Owned(buf)) } } /// Trait to extend [`std::path::PathBuf`]. /// /// ``` +/// # use std::path::PathBuf; /// use path_slash::PathBufExt; /// /// assert_eq!( -/// std::path::PathBuf::from_slash("foo/bar/piyo.txt").to_slash(), -/// Some("foo/bar/piyo.txt".to_string()), +/// PathBuf::from_slash("foo/bar/piyo.txt").to_slash().unwrap(), +/// "foo/bar/piyo.txt", /// ); /// ``` pub trait PathBufExt { @@ -250,8 +228,8 @@ pub trait PathBufExt { fn from_slash_lossy>(s: S) -> Self; fn from_backslash>(s: S) -> Self; fn from_backslash_lossy>(s: S) -> Self; - fn to_slash(&self) -> Option; - fn to_slash_lossy(&self) -> String; + fn to_slash(&self) -> Option>; + fn to_slash_lossy(&self) -> Cow<'_, str>; } impl PathBufExt for PathBuf { @@ -264,7 +242,7 @@ impl PathBufExt for PathBuf { /// On non-Windows OS, it is simply equivalent to [`std::path::PathBuf::from`]. /// /// ``` - /// use std::path::PathBuf; + /// # use std::path::PathBuf; /// use path_slash::PathBufExt; /// /// let p = PathBuf::from_slash("foo/bar/piyo.txt"); @@ -279,20 +257,28 @@ impl PathBufExt for PathBuf { fn from_slash>(s: S) -> Self { PathBuf::from(s.as_ref()) } + #[cfg(target_os = "windows")] + fn from_slash>(s: S) -> Self { + str_to_pathbuf(s, '/') + } - /// Convert the slash path (path separated with '/') to [`std::path::PathBuf`]. + /// Convert the [`OsStr`] slash path (path separated with '/') to [`std::path::PathBuf`]. /// /// Any '/' in the slash path is replaced with the file path separator. /// The replacements only happen on Windows since the file path separators on other OSes are the /// same as '/'. /// - /// On non-Windows OS, it is simply equivalent to [`std::path::PathBuf::from`]. + /// On Windows, any non-Unicode sequences are replaced with U+FFFD while the conversion. + /// On non-Windows OS, it is simply equivalent to [`std::path::PathBuf::from`] and there is no + /// loss while conversion. /// /// ``` - /// use std::path::PathBuf; + /// # use std::path::PathBuf; + /// # use std::ffi::OsStr; /// use path_slash::PathBufExt; /// - /// let p = PathBuf::from_slash("foo/bar/piyo.txt"); + /// let s: &OsStr = "foo/bar/piyo.txt".as_ref(); + /// let p = PathBuf::from_slash_lossy(s); /// /// #[cfg(target_os = "windows")] /// assert_eq!(p, PathBuf::from(r"foo\bar\piyo.txt")); @@ -300,19 +286,13 @@ impl PathBufExt for PathBuf { /// #[cfg(not(target_os = "windows"))] /// assert_eq!(p, PathBuf::from("foo/bar/piyo.txt")); /// ``` + #[cfg(not(target_os = "windows"))] + fn from_slash_lossy>(s: S) -> Self { + PathBuf::from(s.as_ref()) + } #[cfg(target_os = "windows")] - fn from_slash>(s: S) -> Self { - use std::path; - - let s = s - .as_ref() - .chars() - .map(|c| match c { - '/' => path::MAIN_SEPARATOR, - c => c, - }) - .collect::(); - PathBuf::from(s) + fn from_slash_lossy>(s: S) -> Self { + Self::from_slash(&s.as_ref().to_string_lossy()) } /// Convert the backslash path (path separated with '\\') to [`std::path::PathBuf`]. @@ -321,144 +301,186 @@ impl PathBufExt for PathBuf { /// The replacements only happen on non-Windows. #[cfg(not(target_os = "windows"))] fn from_backslash>(s: S) -> Self { - use std::path; - - let s = s - .as_ref() - .chars() - .map(|c| match c { - '\\' => path::MAIN_SEPARATOR, - c => c, - }) - .collect::(); - PathBuf::from(s) + str_to_pathbuf(s, '\\') } - - /// Convert the backslash path (path separated with '\\') to [`std::path::PathBuf`]. - /// - /// Any '\\' in the slash path is replaced with the file path separator. - /// The replacements only happen on non-Windows. #[cfg(target_os = "windows")] fn from_backslash>(s: S) -> Self { PathBuf::from(s.as_ref()) } - /// Convert the backslash path (path separated with '\\') to [`std::path::PathBuf`]. + /// Convert the [`OsStr`] backslash path (path separated with '\\') to [`std::path::PathBuf`]. /// /// Any '\\' in the slash path is replaced with the file path separator. #[cfg(not(target_os = "windows"))] fn from_backslash_lossy>(s: S) -> Self { s.as_ref().to_string_lossy().replace('\\', "/").into() } - - /// Convert the backslash path (path separated with '\\') to [`std::path::PathBuf`]. - /// - /// Any '\\' in the slash path is replaced with the file path separator. #[cfg(target_os = "windows")] fn from_backslash_lossy>(s: S) -> Self { PathBuf::from(s.as_ref()) } - /// Convert the slash path (path separated with '/') to [`std::path::PathBuf`]. - /// - /// Any '/' in the slash path is replaced with the file path separator. - /// The replacements only happen on Windows since the file path separators on other OSes are the - /// same as '/'. + /// Convert the file path into slash path as UTF-8 string. /// - /// On Windows, any non-Unicode sequences are replaced with U+FFFD while the conversion. - /// On non-Windows OS, it is simply equivalent to [`std::path::PathBuf::from`] and there is no - /// loss while conversion. + /// Any file path separators in the file path is replaced with '/'. + /// Any non-Unicode sequences are replaced with U+FFFD. /// /// ``` - /// use std::ffi::OsStr; - /// use std::path::PathBuf; + /// # use std::path::PathBuf; /// use path_slash::PathBufExt; /// - /// let s: &OsStr = "foo/bar/piyo.txt".as_ref(); - /// let p = PathBuf::from_slash_lossy(s); - /// /// #[cfg(target_os = "windows")] - /// assert_eq!(p, PathBuf::from(r"foo\bar\piyo.txt")); + /// let s = PathBuf::from(r"foo\bar\piyo.txt"); /// /// #[cfg(not(target_os = "windows"))] - /// assert_eq!(p, PathBuf::from("foo/bar/piyo.txt")); + /// let s = PathBuf::from("foo/bar/piyo.txt"); + /// + /// assert_eq!(s.to_slash_lossy(), "foo/bar/piyo.txt"); /// ``` - #[cfg(not(target_os = "windows"))] - fn from_slash_lossy>(s: S) -> Self { - PathBuf::from(s.as_ref()) + fn to_slash_lossy(&self) -> Cow<'_, str> { + self.as_path().to_slash_lossy() } - /// Convert the slash path (path separated with '/') to [`std::path::PathBuf`]. - /// - /// Any '/' in the slash path is replaced with the file path separator. - /// The replacements only happen on Windows since the file path separators on other OSes are the - /// same as '/'. + /// Convert the file path into slash path as UTF-8 string. /// - /// On Windows, any non-Unicode sequences are replaced with U+FFFD while the conversion. - /// On non-Windows OS, it is simply equivalent to [`std::path::PathBuf::from`] and there is no - /// loss while conversion. + /// Any file path separators in the file path is replaced with '/'. + /// When the path contains non-Unicode sequence, this method returns None. /// /// ``` - /// use std::ffi::OsStr; - /// use std::path::PathBuf; + /// # use std::path::PathBuf; + /// # use std::borrow::Cow; /// use path_slash::PathBufExt; /// - /// let s: &OsStr = "foo/bar/piyo.txt".as_ref(); - /// let p = PathBuf::from_slash_lossy(s); - /// /// #[cfg(target_os = "windows")] - /// assert_eq!(p, PathBuf::from(r"foo\bar\piyo.txt")); + /// let s = PathBuf::from(r"foo\bar\piyo.txt"); /// /// #[cfg(not(target_os = "windows"))] - /// assert_eq!(p, PathBuf::from("foo/bar/piyo.txt")); + /// let s = PathBuf::from("foo/bar/piyo.txt"); + /// + /// assert_eq!(s.to_slash(), Some(Cow::Borrowed("foo/bar/piyo.txt"))); /// ``` - #[cfg(target_os = "windows")] - fn from_slash_lossy>(s: S) -> Self { - Self::from_slash(&s.as_ref().to_string_lossy()) + fn to_slash(&self) -> Option> { + self.as_path().to_slash() } +} - /// Convert the file path into slash path as UTF-8 string. - /// - /// Any file path separators in the file path is replaced with '/'. - /// Any non-Unicode sequences are replaced with U+FFFD. +/// Trait to extend [`std::borrow::Cow`]. +/// +/// ``` +/// # use std::path::Path; +/// # use std::borrow::Cow; +/// use path_slash::{CowExt as _, PathExt as _}; +/// +/// assert_eq!( +/// Cow::from_slash("foo/bar/piyo.txt").to_slash_lossy(), +/// "foo/bar/piyo.txt", +/// ); +/// ``` +pub trait CowExt<'a> { + fn from_slash(s: &'a str) -> Self; + fn from_slash_lossy(s: &'a OsStr) -> Self; + fn from_backslash(s: &'a str) -> Self; + fn from_backslash_lossy(s: &'a OsStr) -> Self; +} + +impl<'a> CowExt<'a> for Cow<'a, Path> { + /// Convert the slash path (path separated with '/') to [`Cow`]. /// - /// On non-Windows OS, it is equivalent to `to_string_lossy().to_string()` + /// Any '/' in the slash path is replaced with the file path separator. + /// Heap allocation may only happen on Windows since the file path separators on other OSes are + /// the same as '/'. /// /// ``` - /// use path_slash::PathBufExt; - /// - /// #[cfg(target_os = "windows")] - /// let s = std::path::PathBuf::from(r"foo\bar\piyo.txt"); + /// # use std::borrow::Cow; + /// # use std::path::Path; + /// use path_slash::CowExt; /// /// #[cfg(not(target_os = "windows"))] - /// let s = std::path::PathBuf::from("foo/bar/piyo.txt"); + /// assert_eq!( + /// Cow::from_slash("foo/bar/piyo.txt"), + /// Path::new("foo/bar/piyo.txt"), + /// ); /// - /// assert_eq!(s.to_slash_lossy(), "foo/bar/piyo.txt".to_string()); + /// #[cfg(target_os = "windows")] + /// assert_eq!( + /// Cow::from_slash("foo/bar/piyo.txt"), + /// Path::new(r"foo\\bar\\piyo.txt"), + /// ); /// ``` - fn to_slash_lossy(&self) -> String { - self.as_path().to_slash_lossy() + #[cfg(not(target_os = "windows"))] + fn from_slash(s: &'a str) -> Self { + Cow::Borrowed(Path::new(s)) + } + #[cfg(target_os = "windows")] + fn from_slash(s: &'a str) -> Self { + str_to_path(s, '/') } - /// Convert the file path into slash path as UTF-8 string. + /// Convert the [`OsStr`] slash path (path separated with '/') to [`Cow`]. /// - /// Any file path separators in the file path is replaced with '/'. - /// When the path contains non-Unicode sequence, this method returns None. + /// Any '/' in the slash path is replaced with the file path separator. + /// Heap allocation may only happen on Windows since the file path separators on other OSes are + /// the same as '/'. /// - /// On non-Windows OS, it is equivalent to `.to_str().map(std::to_string())` + /// On Windows, any non-Unicode sequences are replaced with U+FFFD while the conversion. + /// On non-Windows OS, there is no loss while conversion. + #[cfg(not(target_os = "windows"))] + fn from_slash_lossy(s: &'a OsStr) -> Self { + Cow::Borrowed(Path::new(s)) + } + #[cfg(target_os = "windows")] + fn from_slash_lossy(s: &'a OsStr) -> Self { + match s.to_string_lossy() { + Cow::Borrowed(s) => str_to_path(s, '/'), + Cow::Owned(s) => Cow::Owned(str_to_pathbuf(&s, '/')), + } + } + + /// Convert the backslash path (path separated with '\\') to [`Cow`]. /// - /// ``` - /// use path_slash::PathBufExt; + /// Any '\\' in the slash path is replaced with the file path separator. Heap allocation may + /// only happen on non-Windows. /// - /// #[cfg(target_os = "windows")] - /// let s = std::path::PathBuf::from(r"foo\bar\piyo.txt"); + /// ``` + /// # use std::borrow::Cow; + /// # use std::path::Path; + /// use path_slash::CowExt; /// /// #[cfg(not(target_os = "windows"))] - /// let s = std::path::PathBuf::from("foo/bar/piyo.txt"); + /// assert_eq!( + /// Cow::from_backslash(r"foo\\bar\\piyo.txt"), + /// Path::new("foo/bar/piyo.txt"), + /// ); /// - /// assert_eq!(s.to_slash(), Some("foo/bar/piyo.txt".to_string())); + /// #[cfg(target_os = "windows")] + /// assert_eq!( + /// Cow::from_backslash(r"foo\\bar\\piyo.txt"), + /// Path::new(r"foo\\bar\\piyo.txt"), + /// ); /// ``` - fn to_slash(&self) -> Option { - self.as_path().to_slash() + #[cfg(not(target_os = "windows"))] + fn from_backslash(s: &'a str) -> Self { + str_to_path(s, '\\') + } + #[cfg(target_os = "windows")] + fn from_backslash(s: &'a str) -> Self { + Cow::Borrowed(Path::new(s)) + } + + /// Convert the [`OsStr`] backslash path (path separated with '\\') to [`Cow`]. + /// + /// Any '\\' in the slash path is replaced with the file path separator. Heap allocation may + /// only happen on non-Windows. + #[cfg(not(target_os = "windows"))] + fn from_backslash_lossy(s: &'a OsStr) -> Self { + match s.to_string_lossy() { + Cow::Borrowed(s) => str_to_path(s, '\\'), + Cow::Owned(s) => Cow::Owned(str_to_pathbuf(&s, '\\')), + } + } + #[cfg(target_os = "windows")] + fn from_backslash_lossy(s: &'a OsStr) -> Self { + Cow::Borrowed(Path::new(s)) } } diff --git a/src/test.rs b/src/test.rs index a64b946..e2c3ad4 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,5 +1,6 @@ -use super::{PathBufExt as _, PathExt as _}; +use super::{CowExt as _, PathBufExt as _, PathExt as _}; use lazy_static::lazy_static; +use std::borrow::Cow; use std::ffi::OsStr; use std::path; use std::path::PathBuf; @@ -76,6 +77,38 @@ fn from_backslash_lossy() { } } +#[test] +fn cow_from_slash() { + for (input, expected) in FROM_SLASH_TESTS.iter() { + assert_eq!(&Cow::from_slash(input), expected); + } +} + +#[test] +fn cow_from_slash_lossy() { + for (input, expected) in FROM_SLASH_TESTS.iter() { + let input: &OsStr = input.as_ref(); + assert_eq!(&Cow::from_slash_lossy(input), expected); + } +} + +#[test] +fn cow_from_backslash() { + for (input, expected) in FROM_SLASH_TESTS.iter() { + let input = input.replace('/', r"\"); + assert_eq!(&Cow::from_backslash(&input), expected); + } +} + +#[test] +fn cow_from_backslash_lossy() { + for (input, expected) in FROM_SLASH_TESTS.iter() { + let input = input.replace('/', r"\"); + let input: &OsStr = input.as_ref(); + assert_eq!(&Cow::from_backslash_lossy(input), expected); + } +} + lazy_static! { static ref TO_SLASH_TESTS: Vec<(PathBuf, String)> = { [ @@ -114,14 +147,17 @@ lazy_static! { #[test] fn to_slash_path() { for (input, expected) in TO_SLASH_TESTS.iter() { - assert_eq!(input.as_path().to_slash(), Some(expected.clone())); + assert_eq!( + input.as_path().to_slash(), + Some(Cow::Borrowed(expected.as_str())) + ); } } #[test] fn to_slash_pathbuf() { for (input, expected) in TO_SLASH_TESTS.iter() { - assert_eq!(input.to_slash(), Some(expected.clone())); + assert_eq!(input.to_slash(), Some(Cow::Borrowed(expected.as_str()))); } } @@ -142,7 +178,10 @@ fn to_slash_lossy_pathbuf() { #[test] fn from_slash_to_slash() { for (_, path) in TO_SLASH_TESTS.iter() { - assert_eq!(PathBuf::from_slash(path).to_slash(), Some(path.clone())); + assert_eq!( + PathBuf::from_slash(path).to_slash(), + Some(Cow::Borrowed(path.as_str())) + ); } } @@ -154,8 +193,8 @@ mod windows { fn with_driver_letter_to_slash() { let path = PathBuf::from_slash("C:/foo/bar"); assert_eq!(path, PathBuf::from(r"C:\foo\bar")); - let slash = path.to_slash(); - assert_eq!(slash, Some("C:/foo/bar".to_string())); + let slash = path.to_slash().unwrap(); + assert_eq!(slash, "C:/foo/bar"); } #[test] @@ -163,7 +202,7 @@ mod windows { let path = PathBuf::from_slash("C:/foo/bar"); assert_eq!(path, PathBuf::from(r"C:\foo\bar")); let slash = path.to_slash_lossy(); - assert_eq!(slash, "C:/foo/bar".to_string()); + assert_eq!(slash, "C:/foo/bar"); } #[test] @@ -171,7 +210,7 @@ mod windows { let path = PathBuf::from_slash("C:"); assert_eq!(path, PathBuf::from(r"C:")); let slash = path.to_slash().unwrap(); - assert_eq!(slash, "C:".to_string()); + assert_eq!(slash, "C:"); } #[test] @@ -179,7 +218,7 @@ mod windows { let path = PathBuf::from_slash("C:"); assert_eq!(path, PathBuf::from(r"C:")); let slash = path.to_slash_lossy(); - assert_eq!(slash, "C:".to_string()); + assert_eq!(slash, "C:"); } #[test] @@ -187,7 +226,7 @@ mod windows { let path = PathBuf::from_slash(r"\\?\C:/foo/bar"); assert_eq!(path, PathBuf::from(r"\\?\C:\foo\bar")); let slash = path.to_slash().unwrap(); - assert_eq!(slash, r"\\?\C:/foo/bar".to_string()); + assert_eq!(slash, r"\\?\C:/foo/bar"); } #[test] @@ -195,7 +234,7 @@ mod windows { let path = PathBuf::from_slash(r"\\?\C:/foo/bar"); assert_eq!(path, PathBuf::from(r"\\?\C:\foo\bar")); let slash = path.to_slash_lossy(); - assert_eq!(slash, r"\\?\C:/foo/bar".to_string()); + assert_eq!(slash, r"\\?\C:/foo/bar"); } #[test] @@ -203,7 +242,7 @@ mod windows { let path = PathBuf::from_slash(r"\\server\share/foo/bar"); assert_eq!(path, PathBuf::from(r"\\server\share\foo\bar")); let slash = path.to_slash().unwrap(); - assert_eq!(slash, r"\\server\share/foo/bar".to_string()); + assert_eq!(slash, r"\\server\share/foo/bar"); } #[test] @@ -211,7 +250,7 @@ mod windows { let path = PathBuf::from_slash(r"\\server\share/foo/bar"); assert_eq!(path, PathBuf::from(r"\\server\share\foo\bar")); let slash = path.to_slash_lossy(); - assert_eq!(slash, r"\\server\share/foo/bar".to_string()); + assert_eq!(slash, r"\\server\share/foo/bar"); } #[test] @@ -219,7 +258,7 @@ mod windows { let path = PathBuf::from_slash(r"\\server\share"); assert_eq!(path, PathBuf::from(r"\\server\share")); let slash = path.to_slash().unwrap(); - assert_eq!(slash, r"\\server\share".to_string()); + assert_eq!(slash, r"\\server\share"); } #[test] @@ -227,7 +266,7 @@ mod windows { let path = PathBuf::from_slash(r"\\server\share"); assert_eq!(path, PathBuf::from(r"\\server\share")); let slash = path.to_slash_lossy(); - assert_eq!(slash, r"\\server\share".to_string()); + assert_eq!(slash, r"\\server\share"); } #[test] @@ -235,7 +274,7 @@ mod windows { let path = PathBuf::from_slash(r"\\?\UNC\server\share/foo/bar"); assert_eq!(path, PathBuf::from(r"\\?\UNC\server\share\foo\bar")); let slash = path.to_slash().unwrap(); - assert_eq!(slash, r"\\?\UNC\server\share/foo/bar".to_string()); + assert_eq!(slash, r"\\?\UNC\server\share/foo/bar"); } #[test] @@ -243,7 +282,7 @@ mod windows { let path = PathBuf::from_slash(r"\\?\UNC\server\share/foo/bar"); assert_eq!(path, PathBuf::from(r"\\?\UNC\server\share\foo\bar")); let slash = path.to_slash_lossy(); - assert_eq!(slash, r"\\?\UNC\server\share/foo/bar".to_string()); + assert_eq!(slash, r"\\?\UNC\server\share/foo/bar"); } #[test] @@ -251,7 +290,7 @@ mod windows { let path = PathBuf::from_slash(r"\\?\UNC\server\share"); assert_eq!(path, PathBuf::from(r"\\?\UNC\server\share")); let slash = path.to_slash().unwrap(); - assert_eq!(slash, r"\\?\UNC\server\share".to_string()); + assert_eq!(slash, r"\\?\UNC\server\share"); } #[test] @@ -259,6 +298,6 @@ mod windows { let path = PathBuf::from_slash(r"\\?\UNC\server\share"); assert_eq!(path, PathBuf::from(r"\\?\UNC\server\share")); let slash = path.to_slash_lossy(); - assert_eq!(slash, r"\\?\UNC\server\share".to_string()); + assert_eq!(slash, r"\\?\UNC\server\share"); } }