mirror of
https://github.com/chatmail/core.git
synced 2026-05-20 15:26:30 +03:00
fix(vcard): Improve property value escaping (#7931)
Implements property value escaping according to RFC6350 section 3.4. <https://www.rfc-editor.org/rfc/rfc6350.html#section-3.4> Fixes: #7893
This commit is contained in:
committed by
GitHub
parent
c928015f20
commit
0622289420
@@ -36,6 +36,45 @@ impl VcardContact {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn escape(s: &str) -> String {
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc6350.html#section-3.4
|
||||||
|
s
|
||||||
|
// backslash must be first!
|
||||||
|
.replace(r"\", r"\\")
|
||||||
|
.replace(',', r"\,")
|
||||||
|
.replace(';', r"\;")
|
||||||
|
.replace('\n', r"\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unescape(s: &str) -> String {
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc6350.html#section-3.4
|
||||||
|
let mut out = String::new();
|
||||||
|
|
||||||
|
let mut chars = s.chars();
|
||||||
|
while let Some(c) = chars.next() {
|
||||||
|
if c == '\\' {
|
||||||
|
if let Some(next) = chars.next() {
|
||||||
|
match next {
|
||||||
|
'\\' | ',' | ';' => out.push(next),
|
||||||
|
'n' | 'N' => out.push('\n'),
|
||||||
|
_ => {
|
||||||
|
// Invalid escape sequence (keep unchanged)
|
||||||
|
out.push('\\');
|
||||||
|
out.push(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Invalid escape sequence (keep unchanged)
|
||||||
|
out.push('\\');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns a vCard containing given contacts.
|
/// Returns a vCard containing given contacts.
|
||||||
///
|
///
|
||||||
/// Calling [`parse_vcard()`] on the returned result is a reverse operation.
|
/// Calling [`parse_vcard()`] on the returned result is a reverse operation.
|
||||||
@@ -46,10 +85,6 @@ pub fn make_vcard(contacts: &[VcardContact]) -> String {
|
|||||||
Some(datetime.format("%Y%m%dT%H%M%SZ").to_string())
|
Some(datetime.format("%Y%m%dT%H%M%SZ").to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn escape(s: &str) -> String {
|
|
||||||
s.replace(',', "\\,")
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut res = "".to_string();
|
let mut res = "".to_string();
|
||||||
for c in contacts {
|
for c in contacts {
|
||||||
// Mustn't contain ',', but it's easier to escape than to error out.
|
// Mustn't contain ',', but it's easier to escape than to error out.
|
||||||
@@ -124,7 +159,7 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
|||||||
fn vcard_property<'a>(line: &'a str, property: &str) -> Option<(&'a str, String)> {
|
fn vcard_property<'a>(line: &'a str, property: &str) -> Option<(&'a str, String)> {
|
||||||
let (params, value) = vcard_property_raw(line, property)?;
|
let (params, value) = vcard_property_raw(line, property)?;
|
||||||
// Some fields can't contain commas, but unescape them everywhere for safety.
|
// Some fields can't contain commas, but unescape them everywhere for safety.
|
||||||
Some((params, value.replace("\\,", ",")))
|
Some((params, unescape(value)))
|
||||||
}
|
}
|
||||||
fn base64_key(line: &str) -> Option<&str> {
|
fn base64_key(line: &str) -> Option<&str> {
|
||||||
let (params, value) = vcard_property_raw(line, "key")?;
|
let (params, value) = vcard_property_raw(line, "key")?;
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ fn test_make_and_parse_vcard() {
|
|||||||
authname: "Alice Wonderland".to_string(),
|
authname: "Alice Wonderland".to_string(),
|
||||||
key: Some("[base64-data]".to_string()),
|
key: Some("[base64-data]".to_string()),
|
||||||
profile_image: Some("image in Base64".to_string()),
|
profile_image: Some("image in Base64".to_string()),
|
||||||
biography: Some("Hi, I'm Alice".to_string()),
|
biography: Some("Hi,\nI'm Alice; and this is a backslash: \\".to_string()),
|
||||||
timestamp: Ok(1713465762),
|
timestamp: Ok(1713465762),
|
||||||
},
|
},
|
||||||
VcardContact {
|
VcardContact {
|
||||||
@@ -110,7 +110,7 @@ fn test_make_and_parse_vcard() {
|
|||||||
FN:Alice Wonderland\r\n\
|
FN:Alice Wonderland\r\n\
|
||||||
KEY:data:application/pgp-keys;base64\\,[base64-data]\r\n\
|
KEY:data:application/pgp-keys;base64\\,[base64-data]\r\n\
|
||||||
PHOTO:data:image/jpeg;base64\\,image in Base64\r\n\
|
PHOTO:data:image/jpeg;base64\\,image in Base64\r\n\
|
||||||
NOTE:Hi\\, I'm Alice\r\n\
|
NOTE:Hi\\,\\nI'm Alice\\; and this is a backslash: \\\\\r\n\
|
||||||
REV:20240418T184242Z\r\n\
|
REV:20240418T184242Z\r\n\
|
||||||
END:VCARD\r\n",
|
END:VCARD\r\n",
|
||||||
"BEGIN:VCARD\r\n\
|
"BEGIN:VCARD\r\n\
|
||||||
@@ -276,3 +276,14 @@ END:VCARD",
|
|||||||
assert!(contacts[0].timestamp.is_err());
|
assert!(contacts[0].timestamp.is_err());
|
||||||
assert_eq!(contacts[0].profile_image.as_ref().unwrap(), "/9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Z");
|
assert_eq!(contacts[0].profile_image.as_ref().unwrap(), "/9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Z");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_vcard_value_escape_unescape() {
|
||||||
|
let original = "Text, with; chars and a \\ and a newline\nand a literal newline \\n";
|
||||||
|
let expected_escaped = r"Text\, with\; chars and a \\ and a newline\nand a literal newline \\n";
|
||||||
|
|
||||||
|
let escaped = escape(original);
|
||||||
|
assert_eq!(escaped, expected_escaped);
|
||||||
|
let unescaped = unescape(&escaped);
|
||||||
|
assert_eq!(original, unescaped);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1145,8 +1145,11 @@ async fn test_make_n_import_vcard() -> Result<()> {
|
|||||||
let alice = &tcm.alice().await;
|
let alice = &tcm.alice().await;
|
||||||
let bob = &tcm.bob().await;
|
let bob = &tcm.bob().await;
|
||||||
bob.set_config(Config::Displayname, Some("Bob")).await?;
|
bob.set_config(Config::Displayname, Some("Bob")).await?;
|
||||||
bob.set_config(Config::Selfstatus, Some("It's me, bob"))
|
bob.set_config(
|
||||||
.await?;
|
Config::Selfstatus,
|
||||||
|
Some("It's me,\nbob; and here's a backslash: \\"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
let avatar_path = bob.dir.path().join("avatar.png");
|
let avatar_path = bob.dir.path().join("avatar.png");
|
||||||
let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png");
|
let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png");
|
||||||
let avatar_base64 = base64::engine::general_purpose::STANDARD.encode(avatar_bytes);
|
let avatar_base64 = base64::engine::general_purpose::STANDARD.encode(avatar_bytes);
|
||||||
|
|||||||
Reference in New Issue
Block a user