vcs_diff/pathbytes.rs
1//! Lossless raw-bytes → `OsString`/`PathBuf` bridge for filesystem paths taken
2//! from `git`/`jj` machine output.
3//!
4//! A filesystem path is *bytes*, not text: on Unix a filename can be any byte
5//! sequence except `/` and NUL, so it need not be valid UTF-8. Decoding such a
6//! path through [`String::from_utf8_lossy`] substitutes `U+FFFD` for the offending
7//! bytes, and the resulting `String` no longer names the same file — feeding it
8//! back to `add`/`commit_paths` then addresses a *different* path (or none at
9//! all). These helpers preserve the exact bytes so a path read from
10//! status/diff/conflict output round-trips into a mutating call unchanged.
11
12use std::ffi::OsString;
13use std::path::PathBuf;
14
15/// Build an [`OsString`] from raw filesystem-path `bytes`, losslessly on Unix.
16///
17/// - **Unix:** the bytes *are* the OS path encoding, wrapped verbatim via
18/// [`OsStringExt::from_vec`](std::os::unix::ffi::OsStringExt::from_vec), so a
19/// filename whose bytes are not valid UTF-8 survives byte-for-byte.
20/// - **Other platforms (Windows/WASI):** `git` and `jj` emit their `-z` / machine
21/// path output as UTF-8 there, so the bytes are decoded as UTF-8. A genuinely
22/// invalid sequence — which these tools do not produce on this path — falls back
23/// to the lossy replacement, preserving the pre-existing Windows
24/// `String`/`OsString` behaviour (Unicode names like `𝓁abc` still round-trip).
25pub fn os_from_bytes(bytes: &[u8]) -> OsString {
26 #[cfg(unix)]
27 {
28 use std::os::unix::ffi::OsStringExt;
29 OsString::from_vec(bytes.to_vec())
30 }
31 #[cfg(not(unix))]
32 {
33 OsString::from(String::from_utf8_lossy(bytes).into_owned())
34 }
35}
36
37/// [`os_from_bytes`] as a [`PathBuf`] — the path type the facade DTOs carry.
38pub fn path_from_bytes(bytes: &[u8]) -> PathBuf {
39 PathBuf::from(os_from_bytes(bytes))
40}
41
42#[cfg(test)]
43mod tests {
44 use super::*;
45
46 #[test]
47 fn ascii_and_utf8_round_trip_on_every_platform() {
48 assert_eq!(path_from_bytes(b"src/lib.rs"), PathBuf::from("src/lib.rs"));
49 // A multibyte UTF-8 name decodes to the same scalar on all platforms.
50 assert_eq!(
51 path_from_bytes("café.txt".as_bytes()),
52 PathBuf::from("café.txt")
53 );
54 }
55
56 // On Unix, a non-UTF-8 filename survives byte-for-byte (the load-bearing
57 // property this whole change exists for): the bytes go in and come back out
58 // of the `OsString` unchanged, never substituted with U+FFFD.
59 #[cfg(unix)]
60 #[test]
61 fn non_utf8_bytes_survive_on_unix() {
62 use std::os::unix::ffi::OsStrExt;
63 let raw = b"caf\xff.txt"; // 0xFF is never valid UTF-8
64 let os = os_from_bytes(raw);
65 assert_eq!(os.as_bytes(), raw, "the exact bytes must survive");
66 }
67}