use alloc::{borrow::Cow, string::String, vec::Vec};
use crate::tree::codec::mode::VcardEscaper;
pub(crate) fn escape_with(bytes: &[u8], escaper: VcardEscaper) -> Cow<'_, [u8]> {
match escaper {
VcardEscaper::V3_0 | VcardEscaper::V4_0 => escape_modern(bytes),
VcardEscaper::V2_1 => escape_v21(bytes),
}
}
pub(crate) fn escape_param(value: &str, escaper: VcardEscaper) -> Cow<'_, str> {
if !escaper.has_param_encoding() {
return Cow::Borrowed(value);
}
let Some(inner) = value.strip_prefix('"').and_then(|v| v.strip_suffix('"')) else {
return escape_carets(value);
};
match escape_carets(inner) {
Cow::Borrowed(_) => Cow::Borrowed(value),
Cow::Owned(inner) => {
let mut out = String::with_capacity(inner.len() + 2);
out.push('"');
out.push_str(&inner);
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(br"\;"),
b'\n' => out.extend_from_slice(br"\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::VcardEscaper,
};
#[test]
fn escapes_separators_and_newlines_and_borrows_when_clean() {
assert_eq!(
escape_with(b"a,b;c\nd", VcardEscaper::V4_0).as_ref(),
br"a\,b\;c\nd".as_slice(),
);
assert!(matches!(
escape_with(b"plain", VcardEscaper::V4_0),
Cow::Borrowed(b"plain")
));
assert_eq!(
escape_with(b"a,b;c", VcardEscaper::V2_1).as_ref(),
br"a,b\;c".as_slice(),
);
}
#[test]
fn a_line_break_is_escaped_in_every_version() {
assert_eq!(
escape_with(b"a\nb", VcardEscaper::V4_0).as_ref(),
br"a\nb".as_slice(),
);
assert_eq!(
escape_with(b"a\nb", VcardEscaper::V2_1).as_ref(),
br"a\nb".as_slice(),
);
}
#[test]
fn a_literal_backslash_doubles_and_resolves_back() {
use crate::tree::codec::unescape::unescape_with;
assert_eq!(
escape_with(br"C:\path", VcardEscaper::V4_0).as_ref(),
br"C:\\path".as_slice(),
);
assert_eq!(unescape_with(br"C:\\path", VcardEscaper::V4_0), r"C:\path",);
assert_eq!(
escape_with(br"trailing\", VcardEscaper::V4_0).as_ref(),
br"trailing\\".as_slice(),
);
assert_eq!(
unescape_with(br"dangling\", VcardEscaper::V4_0),
r"dangling\"
);
}
#[test]
fn encodes_the_rfc_6868_parameter_sequences_and_borrows_when_clean() {
assert_eq!(escape_param("a\nb^c\"d", VcardEscaper::V4_0), "a^nb^^c^'d",);
assert!(matches!(
escape_param("plain", VcardEscaper::V4_0),
Cow::Borrowed("plain")
));
assert!(matches!(
escape_param(r"C:\temp", VcardEscaper::V4_0),
Cow::Borrowed(r"C:\temp")
));
}
#[test]
fn keeps_a_quoted_parameter_value_wrapped() {
assert!(matches!(
escape_param("\"geo:37.386,-122.083\"", VcardEscaper::V4_0),
Cow::Borrowed("\"geo:37.386,-122.083\"")
));
assert_eq!(escape_param("\"a^b\"", VcardEscaper::V4_0), "\"a^^b\"");
}
#[test]
fn writes_a_pre_4_0_parameter_unencoded() {
assert!(matches!(
escape_param("a^b", VcardEscaper::V3_0),
Cow::Borrowed("a^b")
));
assert!(matches!(
escape_param("a^b", VcardEscaper::V2_1),
Cow::Borrowed("a^b")
));
}
}