use alloc::{borrow::Cow, string::String, vec::Vec};
use crate::tree::codec::mode::Escaper;
pub(crate) fn escape_with(bytes: &[u8], escaper: Escaper) -> Cow<'_, [u8]> {
match escaper {
Escaper::Modern => escape_modern(bytes),
Escaper::V1_0 => escape_v21(bytes),
}
}
pub(crate) fn escape_param(value: &str, escaper: Escaper) -> Cow<'_, str> {
let value = match escaper.has_param_encoding() {
true => escape_carets(value),
false => Cow::Borrowed(value),
};
if !escaper.has_param_quoting() || !value.contains([',', ';', ':']) {
return value;
}
let mut out = String::with_capacity(value.len() + 2);
out.push('"');
out.push_str(&value);
out.push('"');
Cow::Owned(out)
}
fn escape_modern(bytes: &[u8]) -> Cow<'_, [u8]> {
if !bytes
.iter()
.any(|b| matches!(b, b'\\' | b',' | b';' | b'\n'))
{
return Cow::Borrowed(bytes);
}
let mut out = Vec::with_capacity(bytes.len());
for &b in bytes {
match b {
b'\\' => out.extend_from_slice(b"\\\\"),
b',' => out.extend_from_slice(b"\\,"),
b';' => out.extend_from_slice(b"\\;"),
b'\n' => out.extend_from_slice(b"\\n"),
other => out.push(other),
}
}
Cow::Owned(out)
}
fn escape_v21(bytes: &[u8]) -> Cow<'_, [u8]> {
if !bytes.iter().any(|b| matches!(b, b';' | b'\n')) {
return Cow::Borrowed(bytes);
}
let mut out = Vec::with_capacity(bytes.len() + 2);
for &b in bytes {
match b {
b';' => out.extend_from_slice(b"\\;"),
b'\n' => out.extend_from_slice(b"\\n"),
other => out.push(other),
}
}
Cow::Owned(out)
}
fn escape_carets(value: &str) -> Cow<'_, str> {
if !value.contains(['\n', '^', '"']) {
return Cow::Borrowed(value);
}
let mut out = String::with_capacity(value.len());
for c in value.chars() {
match c {
'\n' => out.push_str("^n"),
'^' => out.push_str("^^"),
'"' => out.push_str("^'"),
other => out.push(other),
}
}
Cow::Owned(out)
}
#[cfg(test)]
mod tests {
use alloc::borrow::Cow;
use crate::tree::codec::{
escape::{escape_param, escape_with},
mode::Escaper,
};
#[test]
fn escapes_separators_and_newlines_and_borrows_when_clean() {
assert_eq!(
escape_with(b"a,b;c\nd", Escaper::Modern).as_ref(),
br"a\,b\;c\nd".as_slice(),
);
assert!(matches!(
escape_with(b"plain", Escaper::Modern),
Cow::Borrowed(b"plain")
));
assert_eq!(
escape_with(b"a,b;c\nd", Escaper::V1_0).as_ref(),
br"a,b\;c\nd".as_slice(),
);
}
#[test]
fn encodes_the_rfc_6868_parameter_sequences_and_borrows_when_clean() {
assert_eq!(escape_param("a\nb^c\"d", Escaper::Modern), "a^nb^^c^'d");
assert!(matches!(
escape_param("plain", Escaper::Modern),
Cow::Borrowed("plain")
));
assert_eq!(escape_param(r"C:\temp", Escaper::Modern), r#""C:\temp""#);
}
#[test]
fn quotes_a_parameter_value_only_where_a_delimiter_needs_it() {
assert_eq!(
escape_param("cid:part1.0001@example.org", Escaper::Modern),
"\"cid:part1.0001@example.org\"",
);
assert_eq!(
escape_param("America/New_York", Escaper::Modern),
"America/New_York",
);
assert!(matches!(
escape_param("CHAIR", Escaper::Modern),
Cow::Borrowed("CHAIR")
));
}
#[test]
fn encodes_a_double_quote_rather_than_reading_it_as_a_delimiter() {
assert_eq!(
escape_param("say \"hi\", then go", Escaper::Modern),
"\"say ^'hi^', then go\"",
);
}
#[test]
fn never_quotes_a_vcalendar_1_0_parameter_value() {
assert!(matches!(
escape_param("a,b", Escaper::V1_0),
Cow::Borrowed("a,b")
));
}
#[test]
fn writes_a_vcalendar_1_0_parameter_unencoded() {
assert!(matches!(
escape_param("a^b", Escaper::V1_0),
Cow::Borrowed("a^b")
));
}
}