Skip to main content

coven_foundation/files/
path_bytes.rs

1//! A path on this device as coven records it wherever it reopens the file,
2//! and the text that shows one (ยง17.3).
3//!
4//! A path is the operating system's own bytes, which needn't be UTF-8: Linux
5//! takes any bytes but `/` and NUL in a name, and Windows any UTF-16 units,
6//! unpaired surrogates included. Coven records those bytes exactly, so a file
7//! whose name isn't valid Unicode still opens. Text that shows a path is
8//! made from the same bytes, one way only: it never opens a file.
9
10use std::path::{Path, PathBuf};
11
12/// The path's bytes as coven records them. On Unix they are the path's own
13/// bytes. On Windows they are its UTF-16 units in WTF-8: UTF-8, with each
14/// unpaired surrogate written as the three bytes UTF-8 would give its code
15/// point. Rust's `OsStr::as_encoded_bytes` holds the same bytes today, but
16/// its encoding is unspecified and not to be stored, so coven writes them
17/// itself. A path that is valid Unicode is its UTF-8 on every platform.
18pub fn path_bytes(path: &Path) -> Vec<u8> {
19    #[cfg(unix)]
20    {
21        use std::os::unix::ffi::OsStrExt;
22        path.as_os_str().as_bytes().to_vec()
23    }
24    #[cfg(windows)]
25    {
26        use std::os::windows::ffi::OsStrExt;
27        wtf8(path.as_os_str().encode_wide())
28    }
29}
30
31/// The path `path_bytes` recorded. Bytes it never gives fail with
32/// `PathBytesError`: on Windows, bytes that aren't WTF-8, such as Latin-1
33/// from a Linux device. On Unix any bytes are a path.
34pub fn path_from_bytes(bytes: Vec<u8>) -> Result<PathBuf, PathBytesError> {
35    #[cfg(unix)]
36    {
37        use std::os::unix::ffi::OsStringExt;
38        Ok(std::ffi::OsString::from_vec(bytes).into())
39    }
40    #[cfg(windows)]
41    {
42        use std::os::windows::ffi::OsStringExt;
43        units(&bytes)
44            .map(|units| std::ffi::OsString::from_wide(&units).into())
45            .ok_or(PathBytesError)
46    }
47}
48
49/// Bytes that `path_bytes` never gives on this operating system, so no path
50/// here: on Windows, bytes that aren't WTF-8. Unix takes any bytes.
51#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
52#[error("the bytes are not a path recorded on this operating system")]
53pub struct PathBytesError;
54
55/// The text that shows `path`: `path_bytes_text` of its bytes.
56pub fn path_text(path: &Path) -> String {
57    path_bytes_text(&path_bytes(path))
58}
59
60/// The text that shows a path from its recorded bytes, as SQL's
61/// `coven_path_text` gives it: valid UTF-8 as it is, and each byte that
62/// isn't as `\x` and two uppercase hex digits. `Caf\xE9.flac` shows a Latin-1
63/// name. A backslash already in the name stays as it is, so a name spelled
64/// `Caf\xE9.flac` shows the same: the text can't be turned back into the
65/// path, and nothing opens a file by it.
66pub fn path_bytes_text(bytes: &[u8]) -> String {
67    use std::fmt::Write;
68    let mut text = String::with_capacity(bytes.len());
69    for chunk in bytes.utf8_chunks() {
70        text.push_str(chunk.valid());
71        for byte in chunk.invalid() {
72            write!(text, "\\x{byte:02X}").expect("writing to a String succeeds");
73        }
74    }
75    text
76}
77
78/// UTF-16 units in WTF-8.
79#[cfg(any(windows, test))]
80fn wtf8(units: impl Iterator<Item = u16>) -> Vec<u8> {
81    let mut bytes = Vec::new();
82    for unit in char::decode_utf16(units) {
83        match unit {
84            Ok(character) => {
85                let mut buffer = [0; 4];
86                bytes.extend_from_slice(character.encode_utf8(&mut buffer).as_bytes());
87            }
88            Err(unpaired) => {
89                let surrogate = unpaired.unpaired_surrogate();
90                bytes.extend([
91                    0xE0 | (surrogate >> 12) as u8,
92                    0x80 | ((surrogate >> 6) & 0x3F) as u8,
93                    0x80 | (surrogate & 0x3F) as u8,
94                ]);
95            }
96        }
97    }
98    bytes
99}
100
101/// The UTF-16 units `wtf8` wrote as `bytes`, or `None` for bytes it never
102/// writes: anything but UTF-8 and unpaired surrogates, and a lead surrogate
103/// followed by a trail one, which is one character.
104#[cfg(any(windows, test))]
105fn units(bytes: &[u8]) -> Option<Vec<u16>> {
106    let mut units = Vec::with_capacity(bytes.len());
107    let mut rest = bytes;
108    loop {
109        let (valid, after) = match std::str::from_utf8(rest) {
110            Ok(valid) => (valid, &[][..]),
111            Err(error) => {
112                let (valid, after) = rest.split_at(error.valid_up_to());
113                (std::str::from_utf8(valid).expect("the bytes up to the error are UTF-8"), after)
114            }
115        };
116        units.extend(valid.encode_utf16());
117        let [0xED, second @ 0xA0..=0xBF, third @ 0x80..=0xBF, after @ ..] = after else {
118            return after.is_empty().then_some(units);
119        };
120        let surrogate = 0xD000 | (u16::from(second & 0x3F) << 6) | u16::from(third & 0x3F);
121        let trail = (0xDC00..=0xDFFF).contains(&surrogate);
122        if trail && units.last().is_some_and(|unit| (0xD800..=0xDBFF).contains(unit)) {
123            return None;
124        }
125        units.push(surrogate);
126        rest = after;
127    }
128}
129
130#[cfg(test)]
131#[path = "path_bytes_tests.rs"]
132mod tests;