Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,68 @@ let path = path.with_unix_encoding();
assert_eq!(path, "/path/to/file.txt");
```

### Converting to `std::path`

There are times when you need to hand a `typed-path` back to `std`, for
example to pass it to `std::fs::File::open`. Because `std::path::Path`
silently means "Windows-style" on Windows and "Unix-style" on Unix, this
library is careful about *which* typed paths it lets you convert directly,
to prevent code that compiles on one host but produces garbage on the
other.

The conversions are available on the types whose encoding is guaranteed
to match (or be resolved against) the host platform:

- **[`Utf8PlatformPath`][Utf8PlatformPath] / [`Utf8PlatformPathBuf`][Utf8PlatformPathBuf]** —
infallible, since the encoding matches the host and the bytes are
valid UTF-8.

```rust
use std::path::PathBuf;
use typed_path::Utf8PlatformPathBuf;

let platform_path_buf = Utf8PlatformPathBuf::from("some_file.txt");
let std_path_buf: PathBuf = platform_path_buf.into_std_path_buf();
assert_eq!(std_path_buf, PathBuf::from("some_file.txt"));
```

- **[`PlatformPath`][PlatformPath] / [`PlatformPathBuf`][PlatformPathBuf]** —
bytes are reinterpreted via the host's `OsStr` on Unix-family targets
(lossless), or routed through `to_string_lossy` on Windows.

```rust
use typed_path::PlatformPath;

let platform_path = PlatformPath::new(b"some_file.txt");
let std_path_buf = platform_path.to_std_path_buf_lossy();
assert_eq!(std_path_buf, std::path::PathBuf::from("some_file.txt"));
```

- **[`TypedPath`][TypedPath] / [`TypedPathBuf`][TypedPathBuf] /
[`Utf8TypedPath`][Utf8TypedPath] / [`Utf8TypedPathBuf`][Utf8TypedPathBuf]** —
fallible: succeed only when the runtime variant matches the host
platform.

```rust
use typed_path::Utf8TypedPathBuf;

let native_path_buf = if cfg!(windows) {
Utf8TypedPathBuf::from(r"C:\some\path")
} else {
Utf8TypedPathBuf::from("/some/path")
};
assert!(native_path_buf.into_std_path_buf().is_ok());
```

If you're holding a [`Utf8WindowsPath`][Utf8WindowsPath] /
[`Utf8UnixPath`][Utf8UnixPath] (or their non-UTF-8 counterparts) and need
to reach `std::path`, the intentional path is to first move through
[`Utf8PlatformPath`][Utf8PlatformPath] (with
`with_platform_encoding`) or through [`Utf8TypedPath`][Utf8TypedPath].
This is a deliberate two-step process: cross-encoding conversion isn't
generally safe — a `Utf8WindowsPath` on a Linux host has no meaningful
representation as a Linux `std::path::PathBuf`.

### Normalization

Alongside implementing the standard methods associated with [`Path`][StdPath]
Expand Down Expand Up @@ -374,4 +436,8 @@ Apache License, Version 2.0, (LICENSE-APACHE or
[Utf8TypedPathBuf]: https://docs.rs/typed-path/latest/typed_path/enum.Utf8TypedPathBuf.html
[NativePathBuf]: https://docs.rs/typed-path/latest/typed_path/type.NativePathBuf.html
[Utf8NativePathBuf]: https://docs.rs/typed-path/latest/typed_path/type.Utf8NativePathBuf.html
[PlatformPath]: https://docs.rs/typed-path/latest/typed_path/type.PlatformPath.html
[PlatformPathBuf]: https://docs.rs/typed-path/latest/typed_path/type.PlatformPathBuf.html
[Utf8PlatformPath]: https://docs.rs/typed-path/latest/typed_path/type.Utf8PlatformPath.html
[Utf8PlatformPathBuf]: https://docs.rs/typed-path/latest/typed_path/type.Utf8PlatformPathBuf.html
[utils]: https://docs.rs/typed-path/latest/typed_path/utils/index.html
105 changes: 105 additions & 0 deletions src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,47 @@ mod non_utf8 {
self.with_encoding_checked()
}
}

#[cfg(feature = "std")]
impl PlatformPath {
/// Converts a [`PlatformPath`] into a [`std::path::PathBuf`], performing a lossy
/// conversion only when the underlying bytes are not representable.
///
/// On Unix-family hosts, this is a lossless byte-for-byte conversion (the bytes are
/// reinterpreted via [`std::os::unix::ffi::OsStrExt`]). On Windows it falls back to
/// [`Path::to_string_lossy`], which replaces invalid UTF-8 sequences with
/// `U+FFFD REPLACEMENT CHARACTER`.
///
/// For a fallible conversion that preserves the bytes exactly on every platform, use
/// the [`TryFrom<PlatformPathBuf> for std::path::PathBuf`] impl (which errors when the
/// bytes are not valid UTF-8).
///
/// [`TryFrom<PlatformPathBuf> for std::path::PathBuf`]: std::convert::TryFrom
///
/// # Examples
///
/// ```
/// use typed_path::PlatformPath;
///
/// let path = PlatformPath::new("some/path");
/// let std_path_buf = path.to_std_path_buf_lossy();
/// assert_eq!(std_path_buf, std::path::PathBuf::from("some/path"));
/// ```
pub fn to_std_path_buf_lossy(&self) -> std::path::PathBuf {
#[cfg(unix)]
{
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
std::path::PathBuf::from(
<OsStr as OsStrExt>::from_bytes(self.as_bytes()).to_owned(),
)
}
#[cfg(windows)]
{
std::path::PathBuf::from(self.to_string_lossy().into_owned())
}
}
}
}

mod utf8 {
Expand Down Expand Up @@ -354,4 +395,68 @@ mod utf8 {
StdPathBuf::from(utf8_platform_path_buf.into_string())
}
}

#[cfg(all(feature = "std", not(target_family = "wasm")))]
impl Utf8PlatformPath {
/// Borrows this [`Utf8PlatformPath`] as a [`std::path::Path`].
///
/// Because the underlying bytes are guaranteed to be valid UTF-8 *and* in the host
/// platform's encoding, this conversion is infallible and zero-copy.
///
/// # Examples
///
/// ```
/// use typed_path::Utf8PlatformPath;
/// use std::path::Path;
///
/// let platform_path = Utf8PlatformPath::new("some_file.txt");
/// let std_path = platform_path.as_std_path();
/// assert_eq!(std_path, Path::new("some_file.txt"));
/// ```
pub fn as_std_path(&self) -> &StdPath {
StdPath::new(self.as_str())
}

/// Converts this [`Utf8PlatformPath`] into an owned [`std::path::PathBuf`].
///
/// Because the underlying bytes are guaranteed to be valid UTF-8 *and* in the host
/// platform's encoding, this conversion is infallible.
///
/// # Examples
///
/// ```
/// use typed_path::Utf8PlatformPath;
/// use std::path::PathBuf;
///
/// let platform_path = Utf8PlatformPath::new("some_file.txt");
/// let std_path_buf = platform_path.to_std_path_buf();
/// assert_eq!(std_path_buf, PathBuf::from("some_file.txt"));
/// ```
pub fn to_std_path_buf(&self) -> StdPathBuf {
StdPathBuf::from(self.as_str())
}
}

#[cfg(all(feature = "std", not(target_family = "wasm")))]
impl Utf8PlatformPathBuf {
/// Consumes this [`Utf8PlatformPathBuf`] and returns the underlying
/// [`std::path::PathBuf`].
///
/// Because the underlying bytes are guaranteed to be valid UTF-8 *and* in the host
/// platform's encoding, this conversion is infallible.
///
/// # Examples
///
/// ```
/// use typed_path::Utf8PlatformPathBuf;
/// use std::path::PathBuf;
///
/// let platform_path_buf = Utf8PlatformPathBuf::from("some_file.txt");
/// let std_path_buf = platform_path_buf.into_std_path_buf();
/// assert_eq!(std_path_buf, PathBuf::from("some_file.txt"));
/// ```
pub fn into_std_path_buf(self) -> StdPathBuf {
StdPathBuf::from(self.into_string())
}
}
}
51 changes: 51 additions & 0 deletions src/typed/non_utf8/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,57 @@ impl<'a> TypedPath<'a> {
Self::Windows(p) => TypedPathBuf::Windows(p.with_windows_encoding_checked()?),
})
}

/// Converts this [`TypedPath`] into an owned [`std::path::PathBuf`], returning [`None`]
/// if the path's encoding does not match the host platform or if the bytes are not valid
/// UTF-8.
///
/// Conversion is only attempted when the underlying variant matches the compilation target
/// (`TypedPath::Unix` on Unix-family hosts, `TypedPath::Windows` on Windows). A
/// `TypedPath::Windows` on a Unix host (or vice versa) does not have a meaningful
/// representation as a host `std::path::PathBuf`, and would silently produce a path that
/// fails at the filesystem layer; this method returns [`None`] for those cases.
///
/// For non-UTF-8 byte paths on Unix-family hosts, [`std::ffi::OsString::from`] provides a
/// lossless conversion through [`OsStrExt`].
///
/// [`OsStrExt`]: std::os::unix::ffi::OsStrExt
///
/// # Examples
///
/// ```
/// use typed_path::TypedPath;
///
/// // Succeeds when the path's encoding matches the host platform and bytes are valid UTF-8
/// let native_path = if cfg!(windows) {
/// TypedPath::derive(br"C:\some\path")
/// } else {
/// TypedPath::derive(b"/some/path")
/// };
/// assert!(native_path.to_std_path_buf().is_some());
///
/// // Returns None for the mismatched encoding
/// let foreign_path = if cfg!(windows) {
/// TypedPath::derive(b"/some/path")
/// } else {
/// TypedPath::derive(br"C:\some\path")
/// };
/// assert_eq!(foreign_path.to_std_path_buf(), None);
/// ```
#[cfg(all(feature = "std", not(target_family = "wasm")))]
pub fn to_std_path_buf(&self) -> Option<std::path::PathBuf> {
match self {
#[cfg(unix)]
Self::Unix(p) => std::str::from_utf8(p.as_bytes())
.ok()
.map(std::path::PathBuf::from),
#[cfg(windows)]
Self::Windows(p) => std::str::from_utf8(p.as_bytes())
.ok()
.map(std::path::PathBuf::from),
_ => None,
}
}
}

impl<'a> From<&'a [u8]> for TypedPath<'a> {
Expand Down
44 changes: 44 additions & 0 deletions src/typed/non_utf8/pathbuf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1096,6 +1096,14 @@ impl TryFrom<TypedPathBuf> for WindowsPathBuf {
impl TryFrom<TypedPathBuf> for PathBuf {
type Error = TypedPathBuf;

/// Attempts to convert a [`TypedPathBuf`] into a [`std::path::PathBuf`], succeeding only
/// when the runtime variant matches the host platform's encoding *and* the bytes are
/// valid UTF-8.
///
/// Cross-encoding conversion (e.g. a [`TypedPathBuf::Windows`] on a Unix host) is
/// intentionally rejected; the bytes would otherwise produce a host path that fails at
/// the filesystem layer. The original [`TypedPathBuf`] is returned on failure so the
/// caller can recover it.
fn try_from(path: TypedPathBuf) -> Result<Self, Self::Error> {
match path {
#[cfg(unix)]
Expand All @@ -1107,6 +1115,42 @@ impl TryFrom<TypedPathBuf> for PathBuf {
}
}

impl TypedPathBuf {
/// Consumes this [`TypedPathBuf`] and returns the underlying [`std::path::PathBuf`], or
/// the original [`TypedPathBuf`] if the path's encoding does not match the host platform
/// or if the bytes are not valid UTF-8.
///
/// See [`TryFrom<TypedPathBuf> for std::path::PathBuf`] for the rationale behind the
/// host-match requirement.
///
/// [`TryFrom<TypedPathBuf> for std::path::PathBuf`]: std::convert::TryFrom
///
/// # Examples
///
/// ```
/// use typed_path::TypedPathBuf;
///
/// let native_path_buf = if cfg!(windows) {
/// TypedPathBuf::from(br"C:\some\path".to_vec())
/// } else {
/// TypedPathBuf::from(b"/some/path".to_vec())
/// };
/// assert!(native_path_buf.into_std_path_buf().is_ok());
///
/// // The mismatched encoding is returned untouched
/// let foreign_path_buf = if cfg!(windows) {
/// TypedPathBuf::from(b"/some/path".to_vec())
/// } else {
/// TypedPathBuf::from(br"C:\some\path".to_vec())
/// };
/// assert!(foreign_path_buf.into_std_path_buf().is_err());
/// ```
#[cfg(all(feature = "std", not(target_family = "wasm")))]
pub fn into_std_path_buf(self) -> Result<PathBuf, Self> {
PathBuf::try_from(self)
}
}

impl PartialEq<TypedPath<'_>> for TypedPathBuf {
fn eq(&self, path: &TypedPath<'_>) -> bool {
path.eq(&self.to_path())
Expand Down
41 changes: 41 additions & 0 deletions src/typed/utf8/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,47 @@ impl<'a> Utf8TypedPath<'a> {
Self::Windows(p) => Utf8TypedPathBuf::Windows(p.with_windows_encoding_checked()?),
})
}

/// Converts this [`Utf8TypedPath`] into an owned [`std::path::PathBuf`], returning [`None`]
/// if the path's encoding does not match the host platform.
///
/// Conversion is only attempted when the underlying variant matches the compilation target
/// (`Utf8TypedPath::Unix` on Unix-family hosts, `Utf8TypedPath::Windows` on Windows). A
/// `Utf8TypedPath::Windows` on a Unix host (or vice versa) does not have a meaningful
/// representation as a host `std::path::PathBuf`, and would silently produce a path that
/// fails at the filesystem layer; this method returns [`None`] for those cases.
///
/// # Examples
///
/// ```
/// use typed_path::Utf8TypedPath;
///
/// // Succeeds when the path's encoding matches the host platform
/// let native_path = if cfg!(windows) {
/// Utf8TypedPath::derive(r"C:\some\path")
/// } else {
/// Utf8TypedPath::derive("/some/path")
/// };
/// assert!(native_path.to_std_path_buf().is_some());
///
/// // Returns None for the mismatched encoding
/// let foreign_path = if cfg!(windows) {
/// Utf8TypedPath::derive("/some/path")
/// } else {
/// Utf8TypedPath::derive(r"C:\some\path")
/// };
/// assert_eq!(foreign_path.to_std_path_buf(), None);
/// ```
#[cfg(all(feature = "std", not(target_family = "wasm")))]
pub fn to_std_path_buf(&self) -> Option<std::path::PathBuf> {
match self {
#[cfg(unix)]
Self::Unix(p) => Some(std::path::PathBuf::from(p.as_str())),
#[cfg(windows)]
Self::Windows(p) => Some(std::path::PathBuf::from(p.as_str())),
_ => None,
}
}
}

impl fmt::Display for Utf8TypedPath<'_> {
Expand Down
Loading
Loading