use alloc::{borrow::Cow, string::String, vec::Vec};
use crate::tree::codec::mode::Escaper;
pub(crate) fn unescape_with(bytes: &[u8], escaper: Escaper) -> Cow<'_, str> {
lossy(unescape_bytes(bytes, escaper))
}
pub(crate) fn unescape_bytes(bytes: &[u8], escaper: Escaper) -> Cow<'_, [u8]> {
match escaper {
Escaper::Modern => unescape_modern(bytes),
Escaper::V2_1 => unescape_v21(bytes),
}
}
pub(crate) fn unescape(text: &str) -> Cow<'_, str> {
lossy(unescape_modern(text.as_bytes()))
}
fn lossy(bytes: Cow<'_, [u8]>) -> Cow<'_, str> {
match bytes {
Cow::Borrowed(bytes) => String::from_utf8_lossy(bytes),
Cow::Owned(bytes) => Cow::Owned(String::from_utf8_lossy(&bytes).into_owned()),
}
}
fn unescape_modern(bytes: &[u8]) -> Cow<'_, [u8]> {
if !bytes.contains(&b'\\') {
return Cow::Borrowed(bytes);
}
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] != b'\\' {
out.push(bytes[i]);
i += 1;
continue;
}
match bytes.get(i + 1) {
Some(b'n' | b'N') => out.push(b'\n'),
Some(&other) => out.push(other),
None => out.push(b'\\'),
}
i += 2;
}
Cow::Owned(out)
}
fn unescape_v21(bytes: &[u8]) -> Cow<'_, [u8]> {
if !bytes.contains(&b'\\') {
return Cow::Borrowed(bytes);
}
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] != b'\\' {
out.push(bytes[i]);
i += 1;
continue;
}
match bytes.get(i + 1) {
Some(b';') => {
out.push(b';');
i += 2;
}
Some(&other) => {
out.push(b'\\');
out.push(other);
i += 2;
}
None => {
out.push(b'\\');
i += 1;
}
}
}
Cow::Owned(out)
}
#[cfg(test)]
mod tests {
use alloc::borrow::Cow;
use crate::tree::codec::unescape::unescape;
#[test]
fn unescapes_value_escapes_and_borrows_when_clean() {
assert_eq!(unescape(r"a\,b\;c\nd"), "a,b;c\nd");
assert!(matches!(unescape("plain"), Cow::Borrowed("plain")));
}
#[test]
fn unescapes_only_the_semicolon_in_v2_1() {
use crate::tree::codec::{mode::Escaper, unescape::unescape_with};
assert_eq!(unescape_with(br"a\;b\nc\", Escaper::V2_1), "a;b\\nc\\");
}
}