eska_presentation/
encoding.rs1use std::{ffi::OsStr, fmt::Write as _};
4
5#[must_use]
7pub fn json_git_text(value: &[u8]) -> (String, &'static str) {
8 std::str::from_utf8(value).map_or_else(
9 |_| (percent_encode_bytes(value), "percent"),
10 |value| (value.to_owned(), "utf-8"),
11 )
12}
13
14#[must_use]
16pub fn json_path(path: &OsStr) -> (String, &'static str) {
17 path.to_str()
18 .map_or_else(|| encoded_path(path), |value| (value.to_owned(), "utf-8"))
19}
20
21#[cfg(unix)]
22fn encoded_path(path: &OsStr) -> (String, &'static str) {
24 use std::os::unix::ffi::OsStrExt;
25
26 (percent_encode_bytes(path.as_bytes()), "percent")
27}
28
29#[cfg(windows)]
30fn encoded_path(path: &OsStr) -> (String, &'static str) {
32 use std::os::windows::ffi::OsStrExt;
33
34 let mut encoded = String::new();
35 for unit in path.encode_wide() {
36 write!(encoded, "%{unit:04X}").expect("writing to String cannot fail");
37 }
38 (encoded, "utf-16-percent")
39}
40
41fn percent_encode_bytes(value: &[u8]) -> String {
43 let mut encoded = String::with_capacity(value.len() * 3);
44 for byte in value {
45 write!(encoded, "%{byte:02X}").expect("writing to String cannot fail");
46 }
47 encoded
48}
49
50#[cfg(test)]
51mod tests {
52 use super::{json_git_text, json_path};
53 use std::ffi::OsStr;
54
55 #[test]
57 fn unicode_text_and_paths_are_preserved_verbatim() {
58 for value in ["", "src/Заказы 100%.bsl", "emoji-😀", "line\nnext"] {
59 let expected = (value.to_owned(), "utf-8");
60 assert_eq!(json_git_text(value.as_bytes()), expected);
61 assert_eq!(json_path(OsStr::new(value)), expected);
62 }
63 }
64
65 #[test]
67 fn non_utf8_git_text_has_a_reversible_encoding() {
68 assert_eq!(
69 json_git_text(b"raw-%\xFF"),
70 ("%72%61%77%2D%25%FF".to_owned(), "percent")
71 );
72 let bytes: Vec<_> = (0..=u8::MAX).collect();
73 let (encoded, encoding) = json_git_text(&bytes);
74 assert_eq!(encoding, "percent");
75 let decoded: Vec<_> = encoded
76 .split('%')
77 .skip(1)
78 .map(|byte| u8::from_str_radix(byte, 16).expect("encoded byte"))
79 .collect();
80 assert_eq!(decoded, bytes);
81 }
82
83 #[cfg(unix)]
85 #[test]
86 fn non_utf8_unix_path_preserves_native_bytes() {
87 use std::os::unix::ffi::OsStrExt;
88
89 let bytes = b"src/%\xFF.bsl";
90 assert_eq!(json_path(OsStr::from_bytes(bytes)), json_git_text(bytes));
91 }
92
93 #[cfg(windows)]
95 #[test]
96 fn windows_path_preserves_unpaired_surrogates() {
97 use std::{ffi::OsString, os::windows::ffi::OsStringExt};
98
99 let path = OsString::from_wide(&[0x0043, 0x003A, 0x005C, 0xD800, 0x0025]);
100 assert_eq!(
101 json_path(&path),
102 ("%0043%003A%005C%D800%0025".to_owned(), "utf-16-percent")
103 );
104 }
105}