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::V1_0 => unescape_v21(bytes),
}
}
pub(crate) fn unescape_param(text: &str, escaper: Escaper) -> Cow<'_, str> {
let text = match escaper.has_param_quoting() {
true => text
.strip_prefix('"')
.and_then(|inner| inner.strip_suffix('"'))
.unwrap_or(text),
false => text,
};
if !escaper.has_param_encoding() || !text.contains('^') {
return Cow::Borrowed(text);
}
let mut out = String::with_capacity(text.len());
let mut chars = text.chars().peekable();
while let Some(c) = chars.next() {
if c != '^' {
out.push(c);
continue;
}
match chars.peek() {
Some('n') => out.push('\n'),
Some('^') => out.push('^'),
Some('\'') => out.push('"'),
_ => {
out.push('^');
continue;
}
}
chars.next();
}
Cow::Owned(out)
}
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_param, unescape_with};
#[test]
fn unescapes_value_escapes_and_borrows_when_clean() {
use crate::tree::codec::mode::Escaper;
assert_eq!(unescape_with(br"a\,b\;c\nd", Escaper::Modern), "a,b;c\nd");
assert!(matches!(
unescape_with(b"plain", Escaper::Modern),
Cow::Borrowed("plain")
));
}
#[test]
fn unescapes_the_rfc_6868_parameter_sequences() {
use crate::tree::codec::mode::Escaper;
assert_eq!(unescape_param("a^nb^^c^'d", Escaper::Modern), "a\nb^c\"d");
assert!(matches!(
unescape_param("plain", Escaper::Modern),
Cow::Borrowed("plain")
));
}
#[test]
fn keeps_an_unknown_caret_sequence_and_a_backslash() {
use crate::tree::codec::mode::Escaper;
assert_eq!(unescape_param("a^xb^Nc^", Escaper::Modern), "a^xb^Nc^");
assert_eq!(
unescape_param(r"C:\temp\note", Escaper::Modern),
r"C:\temp\note",
);
}
#[test]
fn strips_the_parameter_value_delimiters() {
use crate::tree::codec::mode::Escaper;
assert!(matches!(
unescape_param("\"cid:part1.0001.org\"", Escaper::Modern),
Cow::Borrowed("cid:part1.0001.org")
));
assert_eq!(unescape_param("\"a^'b\"", Escaper::Modern), "a\"b");
}
#[test]
fn keeps_a_quote_that_delimits_nothing() {
use crate::tree::codec::mode::Escaper;
assert!(matches!(
unescape_param("\"CHAIR", Escaper::Modern),
Cow::Borrowed("\"CHAIR")
));
assert!(matches!(
unescape_param("\"a,b\"", Escaper::V1_0),
Cow::Borrowed("\"a,b\"")
));
}
#[test]
fn leaves_a_vcalendar_1_0_parameter_caret_alone() {
use crate::tree::codec::mode::Escaper;
assert!(matches!(
unescape_param("a^nb", Escaper::V1_0),
Cow::Borrowed("a^nb")
));
}
#[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::V1_0), "a;b\\nc\\");
}
}