Skip to content

fix: lossless OsStr/Path round-trip on unix (sound on all platforms)#13

Open
ofek-sha wants to merge 2 commits into
mainfrom
fix/osstr-lossless
Open

fix: lossless OsStr/Path round-trip on unix (sound on all platforms)#13
ofek-sha wants to merge 2 commits into
mainfrom
fix/osstr-lossless

Conversation

@ofek-sha

Copy link
Copy Markdown
Collaborator

BorrowedInterned::as_os_str()/as_path() reconstructed via bstr's lossy conversion, which can replace bytes with U+FFFD — corrupting non-UTF-8 OS paths that were interned intact.

  • unix: now lossless + zero-copy via the safe OsStrExt::from_bytes (Cow::Borrowed).
  • non-unix (Windows/WASI): kept the existing safe lossy path. std has no safe lossless reconstruction of an OsStr from arbitrary bytes there — the only lossless option (from_encoded_bytes_unchecked) is unsafe and would be unsound here, because an Interned may hold arbitrary bytes (Interned::new(b"\xff\xff")) that aren't valid WTF-8 → UB. Documented, with a note that a lossless-on-Windows path could be a future explicit unsafe opt-in.
  • as_path() delegates to as_os_str(); both stay bstr-gated with Cow returns. No unsafe code.

Review gap finding #9. An earlier attempt used from_encoded_bytes_unchecked unconditionally (UB on Windows) — this version is sound. cargo test / --all-features / --features bstr green; non-UTF-8 unix round-trip test added; clippy/fmt clean.

🤖 Generated with Claude Code

ofek-sha and others added 2 commits June 28, 2026 11:26
`BorrowedInterned::as_os_str()`/`as_path()` reconstructed the value via
bstr's `to_os_str_lossy()`/`to_path_lossy()`. On non-unix platforms
(e.g. Windows) that path falls back to a UTF-8-lossy conversion, so
non-UTF-8 OS strings/paths came back corrupted with U+FFFD replacement
characters — even though the store side (`Interned`'s `From<&OsStr>` /
`From<&Path>` impls) preserves the exact `OsStr::as_encoded_bytes()`.

Reconstruct losslessly via `OsStr::from_encoded_bytes_unchecked` on the
stored bytes, with a SAFETY comment documenting that those bytes came
from `as_encoded_bytes()` on the same platform/build (the function's
documented precondition). Return types stay `Cow` (now always
`Cow::Borrowed`) so the public API is unchanged. Kept bstr-gated.

Adds a unix non-UTF-8 round-trip test asserting the raw bytes survive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The previous commit reconstructed the value via
`OsStr::from_encoded_bytes_unchecked(self.deref())`. That is unsound:
`as_os_str()` is public and callable on any `Interned`, including one
created from arbitrary bytes (e.g. `Interned::new(b"\xff\xff")`). On
Windows `from_encoded_bytes_unchecked` requires valid WTF-8; arbitrary
bytes are not, so the call is UB. There is no safe lossless
reconstruction from arbitrary bytes in std on Windows.

Fix without any `unsafe`, cfg-gated:
- unix: `OsStrExt::from_bytes(self.deref())` — a unix `OsStr` is just
  bytes, so this is lossless AND safe; returns `Cow::Borrowed`.
- non-unix (Windows/WASI): keep the existing safe lossy path
  (`as_bstr().to_os_str_lossy()`).
- `as_path()` is now implemented in terms of `as_os_str()`
  (Borrowed -> `Path::new`, Owned -> `PathBuf::from`) so the two stay
  consistent. Both remain bstr-gated and keep their `Cow` return types.

Document on both methods that the conversion is lossless + zero-copy on
unix and best-effort lossy on non-unix, with a note that a
lossless-on-Windows path could be added later as an explicit `unsafe`
opt-in whose caller guarantees `OsStr` provenance.

The `os_str_non_utf8_round_trip` test already carries `#[cfg(unix)]` and
stays green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant