#[must_use]
pub fn decode_escapes(input: &str) -> String {
let mut out = String::with_capacity(input.len());
let chars: Vec<char> = input.chars().collect();
let len = chars.len();
let mut i = 0;
while i < len {
if chars[i] == '\\' {
if i + 3 < len {
let d1 = chars[i + 1];
let d2 = chars[i + 2];
let d3 = chars[i + 3];
if ('0'..='7').contains(&d1)
&& ('0'..='7').contains(&d2)
&& ('0'..='7').contains(&d3)
{
let byte = ((d1 as u8 - b'0') as u16 * 64
+ (d2 as u8 - b'0') as u16 * 8
+ (d3 as u8 - b'0') as u16) as u8;
out.push(byte as char);
i += 4;
continue;
}
}
if i + 1 < len && chars[i + 1] == '\\' {
out.push('\\');
i += 2;
continue;
}
if i + 1 < len {
out.push('\\');
out.push(chars[i + 1]);
i += 2;
} else {
out.push('\\');
i += 1;
}
} else {
out.push(chars[i]);
i += 1;
}
}
out
}
#[must_use]
pub fn encode_escapes(input: &str) -> String {
let mut out = String::with_capacity(input.len());
for ch in input.chars() {
match ch {
' ' => out.push_str(r"\040"),
'\t' => out.push_str(r"\011"),
'\n' => out.push_str(r"\012"),
'\\' => out.push_str(r"\\"),
_ => out.push(ch),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode_space() {
assert_eq!(decode_escapes(r"\040"), " ");
}
#[test]
fn decode_tab() {
assert_eq!(decode_escapes(r"\011"), "\t");
}
#[test]
fn decode_newline() {
assert_eq!(decode_escapes(r"\012"), "\n");
}
#[test]
fn decode_backslash_literal() {
assert_eq!(decode_escapes(r"\\"), "\\");
}
#[test]
fn decode_backslash_octal_134() {
assert_eq!(decode_escapes(r"\134"), "\\");
}
#[test]
fn decode_mixed() {
assert_eq!(decode_escapes(r"foo\040bar\011baz"), "foo bar\tbaz");
}
#[test]
fn decode_plain_text_passthrough() {
assert_eq!(decode_escapes("hello world"), "hello world");
}
#[test]
fn decode_empty() {
assert_eq!(decode_escapes(""), "");
}
#[test]
fn decode_trailing_backslash() {
assert_eq!(decode_escapes(r"hello\"), r"hello\");
}
#[test]
fn decode_unknown_escape_preserved() {
assert_eq!(decode_escapes(r"\999"), r"\999");
assert_eq!(decode_escapes(r"\x"), r"\x");
}
#[test]
fn encode_space() {
assert_eq!(encode_escapes("hello world"), r"hello\040world");
}
#[test]
fn encode_tab() {
assert_eq!(encode_escapes("a\tb"), r"a\011b");
}
#[test]
fn encode_newline() {
assert_eq!(encode_escapes("a\nb"), r"a\012b");
}
#[test]
fn encode_backslash() {
assert_eq!(encode_escapes(r"a\b"), r"a\\b");
}
#[test]
fn encode_multiple_specials() {
assert_eq!(encode_escapes("a b\tc\nd\\e"), r"a\040b\011c\012d\\e");
}
#[test]
fn encode_plain_text_passthrough() {
assert_eq!(encode_escapes("hello"), "hello");
}
#[test]
fn roundtrip_decode_encode() {
let original = "/mnt/My Drive with spaces";
let encoded = encode_escapes(original);
let decoded = decode_escapes(&encoded);
assert_eq!(decoded, original);
}
#[test]
fn roundtrip_encode_decode() {
let encoded = r"UUID=3e6be9de\\8139\\11d1\\9106\\a43f08d823a6";
let decoded = decode_escapes(encoded);
let re_encoded = encode_escapes(&decoded);
assert_eq!(re_encoded, encoded);
}
#[test]
fn decode_escape_in_middle_of_field() {
assert_eq!(decode_escapes(r"foo\040bar"), "foo bar");
}
#[test]
fn decode_multiple_escapes_in_one_field() {
assert_eq!(decode_escapes(r"a\040b\040c"), "a b c");
}
#[test]
fn decode_all_five_escapes_in_order() {
assert_eq!(decode_escapes(r"\040\011\012\\\134"), " \t\n\\\\");
}
#[test]
fn decode_escaped_equal_sign() {
assert_eq!(decode_escapes(r"key\075value"), "key=value");
}
#[test]
fn decode_zero_byte() {
assert_eq!(decode_escapes(r"\000"), "\u{0000}");
}
#[test]
fn decode_all_octal_values() {
for byte in 0u8..=255u8 {
let octal = format!("\\{:03o}", byte);
let expected = char::from(byte).to_string();
let decoded = decode_escapes(&octal);
assert_eq!(decoded, expected, "mismatch for octal {:03o}", byte);
}
}
#[test]
fn decode_single_backslash_with_space_after() {
assert_eq!(decode_escapes(r"\ "), r"\ ");
}
#[test]
fn encode_only_targets_reserved_chars() {
let input = "abc123!@#$%^&*()_+-=[]{}|;:',.<>?/~`";
assert_eq!(encode_escapes(input), input);
}
#[test]
fn encode_then_decode_is_identity_for_any_string() {
let cases = [
"simple",
"with space",
"with\ttab",
"with\nnewline",
r"with\backslash",
"\u{0000}null",
"unicode: café 日本語",
];
for original in cases {
assert_eq!(
decode_escapes(&encode_escapes(original)),
original,
"roundtrip failed for: {original:?}"
);
}
}
#[test]
fn decode_preserves_utf8() {
assert_eq!(decode_escapes("café"), "café");
assert_eq!(decode_escapes("ñoño"), "ñoño");
}
#[test]
fn encode_preserves_utf8() {
assert_eq!(encode_escapes("café"), "café");
assert_eq!(encode_escapes("日本語"), "日本語");
}
}