use std::{ffi::OsStr, fmt::Write as _};
#[must_use]
pub fn json_git_text(value: &[u8]) -> (String, &'static str) {
std::str::from_utf8(value).map_or_else(
|_| (percent_encode_bytes(value), "percent"),
|value| (value.to_owned(), "utf-8"),
)
}
#[must_use]
pub fn json_path(path: &OsStr) -> (String, &'static str) {
path.to_str()
.map_or_else(|| encoded_path(path), |value| (value.to_owned(), "utf-8"))
}
#[cfg(unix)]
fn encoded_path(path: &OsStr) -> (String, &'static str) {
use std::os::unix::ffi::OsStrExt;
(percent_encode_bytes(path.as_bytes()), "percent")
}
#[cfg(windows)]
fn encoded_path(path: &OsStr) -> (String, &'static str) {
use std::os::windows::ffi::OsStrExt;
let mut encoded = String::new();
for unit in path.encode_wide() {
write!(encoded, "%{unit:04X}").expect("writing to String cannot fail");
}
(encoded, "utf-16-percent")
}
fn percent_encode_bytes(value: &[u8]) -> String {
let mut encoded = String::with_capacity(value.len() * 3);
for byte in value {
write!(encoded, "%{byte:02X}").expect("writing to String cannot fail");
}
encoded
}
#[cfg(test)]
mod tests {
use super::{json_git_text, json_path};
use std::ffi::OsStr;
#[test]
fn unicode_text_and_paths_are_preserved_verbatim() {
for value in ["", "src/Заказы 100%.bsl", "emoji-😀", "line\nnext"] {
let expected = (value.to_owned(), "utf-8");
assert_eq!(json_git_text(value.as_bytes()), expected);
assert_eq!(json_path(OsStr::new(value)), expected);
}
}
#[test]
fn non_utf8_git_text_has_a_reversible_encoding() {
assert_eq!(
json_git_text(b"raw-%\xFF"),
("%72%61%77%2D%25%FF".to_owned(), "percent")
);
let bytes: Vec<_> = (0..=u8::MAX).collect();
let (encoded, encoding) = json_git_text(&bytes);
assert_eq!(encoding, "percent");
let decoded: Vec<_> = encoded
.split('%')
.skip(1)
.map(|byte| u8::from_str_radix(byte, 16).expect("encoded byte"))
.collect();
assert_eq!(decoded, bytes);
}
#[cfg(unix)]
#[test]
fn non_utf8_unix_path_preserves_native_bytes() {
use std::os::unix::ffi::OsStrExt;
let bytes = b"src/%\xFF.bsl";
assert_eq!(json_path(OsStr::from_bytes(bytes)), json_git_text(bytes));
}
#[cfg(windows)]
#[test]
fn windows_path_preserves_unpaired_surrogates() {
use std::{ffi::OsString, os::windows::ffi::OsStringExt};
let path = OsString::from_wide(&[0x0043, 0x003A, 0x005C, 0xD800, 0x0025]);
assert_eq!(
json_path(&path),
("%0043%003A%005C%D800%0025".to_owned(), "utf-16-percent")
);
}
}