use std::io;
use std::path::Path;
fn push_meta_byte(out: &mut String, b: u8) {
if b < 0x80 {
out.push(b as char);
} else {
out.push('\u{83}'); out.push(char::from(b ^ 32));
}
}
pub fn decode_script_bytes(bytes: &[u8]) -> String {
match std::str::from_utf8(bytes) {
Ok(s) => return s.to_string(),
Err(_) => {}
}
let mut out = String::with_capacity(bytes.len());
let mut rest = bytes;
loop {
match std::str::from_utf8(rest) {
Ok(s) => {
out.push_str(s);
return out;
}
Err(e) => {
let good = e.valid_up_to();
out.push_str(std::str::from_utf8(&rest[..good]).unwrap_or_default());
let bad = e.error_len().unwrap_or(rest.len() - good);
for &b in &rest[good..good + bad] {
push_meta_byte(&mut out, b);
}
rest = &rest[good + bad..];
}
}
}
}
pub fn regroup_meta_utf8(s: String) -> String {
if !s.contains('\u{83}') {
return s;
}
let mut out = String::with_capacity(s.len());
let mut bytes: Vec<u8> = Vec::new();
let mut it = s.chars().peekable();
while let Some(c) = it.next() {
if c == '\u{83}' {
if let Some(&n) = it.peek() {
if (0x80..=0xff).contains(&(n as u32)) {
it.next();
bytes.push((n as u32 as u8) ^ 32);
continue;
}
}
}
if !bytes.is_empty() {
out.push_str(&decode_script_bytes(&bytes));
bytes.clear();
}
out.push(c);
}
if !bytes.is_empty() {
out.push_str(&decode_script_bytes(&bytes));
}
out
}
pub fn read_script_file(path: impl AsRef<Path>) -> io::Result<String> {
Ok(decode_script_bytes(&std::fs::read(path)?))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ported::utils::unmetafy_str;
#[test]
fn ascii_and_utf8_decode_unchanged() {
for s in ["", "echo hi\n", "s=日本語\n", "e=🎉 é ü\n"] {
assert_eq!(decode_script_bytes(s.as_bytes()), s);
}
}
#[test]
fn invalid_bytes_round_trip_through_unmetafy() {
for raw in [
&b"echo caf\xe9\n"[..],
&b"\xff\xfe"[..],
&b"a\x80b\x81c\x82d"[..],
&b"tail\xe2\x80"[..],
"日本\u{0}語".as_bytes(),
] {
let decoded = decode_script_bytes(raw);
assert_eq!(unmetafy_str(&decoded), raw, "round trip for {raw:?}");
}
}
#[test]
fn one_bad_byte_does_not_lose_the_rest() {
let decoded = decode_script_bytes(b"echo before\necho caf\xe9\necho after\n");
assert!(decoded.starts_with("echo before\n"));
assert!(decoded.ends_with("echo after\n"));
assert!(!decoded.contains('\u{FFFD}'), "no lossy replacement");
}
}