Skip to content

Commit

Permalink
fix: correctly escape and unescape text
Browse files Browse the repository at this point in the history
  • Loading branch information
hoodie committed Jul 7, 2024
1 parent c0ffee0 commit c0ffeee
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 13 deletions.
25 changes: 25 additions & 0 deletions src/parser/parsed_string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,42 @@ impl ParseString<'_> {
Cow::Owned(ref s) => ParseString(Cow::Owned(s.clone())),
}
}

pub fn into_owned(self) -> ParseString<'static> {
match self.0 {
Cow::Borrowed(s) => ParseString(Cow::Owned(s.to_owned())),
Cow::Owned(s) => ParseString(Cow::Owned(s)),
}
}

pub fn as_str(&self) -> &str {
self.0.as_ref()
}
}

impl<'a> ParseString<'a> {
pub fn unescape_text(self) -> ParseString<'a> {
if self.0.contains(r#"\\"#)
|| self.0.contains(r#"\,"#)
|| self.0.contains(r#"\;"#)
|| self.0.contains(r#"\:"#)
|| self.0.contains(r#"\N"#)
|| self.0.contains(r#"\n"#)
{
self.0
.replace(r#"\\"#, r#"\"#)
.replace(r#"\,"#, ",")
.replace(r#"\;"#, ";")
.replace(r#"\:"#, ":")
.replace(r#"\N"#, "\n")
.replace(r#"\n"#, "\n")
.into()
} else {
self
}
}
}

impl PartialEq<Self> for ParseString<'_> {
fn eq(&self, rhs: &Self) -> bool {
self.as_ref() == rhs.as_ref()
Expand Down
25 changes: 17 additions & 8 deletions src/parser/properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,21 +157,31 @@ fn test_property() {

assert_parser!(
property,
"KEY2;foo=bar:important:VALUE\n",
"KEY3;foo=bar:important:VALUE\n",
Property {
name: "KEY2".into(),
name: "KEY3".into(),
val: "important:VALUE".into(),
params: vec![Parameter::new_ref("foo", Some("bar"))]
}
);

// TODO: newlines followed by spaces must be ignored
assert_parser!(
property,
"KEY4;foo=bar:VALUE\\n newline separated\n",
Property {
name: "KEY4".into(),
val: "VALUE\n newline separated".into(),
params: vec![Parameter::new_ref("foo", Some("bar"))]
}
);

assert_parser!(
property,
"KEY3;foo=bar:VALUE\\n newline separated\n",
Property {
name: "KEY3".into(),
val: "VALUE\\n newline separated".into(),
val: "VALUE\n newline separated".into(),
params: vec![Parameter::new_ref("foo", Some("bar"))]
}
);
Expand All @@ -192,7 +202,6 @@ fn test_property_with_dash() {

#[test]
fn parse_properties_from_rfc() {
// TODO: newlines followed by spaces must be ignored
assert_parser!(
property,
"home.tel;type=fax,voice,msg:+49 3581 123456\n",
Expand All @@ -202,7 +211,7 @@ fn parse_properties_from_rfc() {
params: vec![Parameter::new_ref("type", Some("fax,voice,msg"),)]
}
);
// TODO: newlines followed by spaces must be ignored

assert_parser!(
property,
"email;internet:[email protected]\n",
Expand All @@ -221,7 +230,7 @@ fn parse_property_with_breaks() {

let expectation = Property {
name: "DESCRIPTION".into(),
val: "Hey, I'm gonna have a party\\n BYOB: Bring your own beer.\\n Hendrik\\n".into(),
val: "Hey, I'm gonna have a party\n BYOB: Bring your own beer.\n Hendrik\n".into(),
params: vec![],
};

Expand Down Expand Up @@ -309,8 +318,8 @@ pub fn property<'a, E: ParseError<&'a str> + ContextError<&'a str>>(
// this is for single line prop parsing, just so I can leave off the '\n'
take_while(|_| true),
))
.map(ParseString::from),
), // val TODO: replace this with something simpler!
.map(|x| ParseString::from(x).unescape_text()),
),
),
context(
"no-value property",
Expand Down
32 changes: 27 additions & 5 deletions src/properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ impl Property {
pub fn new(key: &str, val: &str) -> Self {
Property {
key: key.to_owned(),
val: val.replace('\n', "\\n"),
val: val.to_owned(),
params: HashMap::new(),
}
}
Expand All @@ -62,7 +62,7 @@ impl Property {
pub fn new_pre_alloc(key: String, val: String) -> Self {
Property {
key,
val: val.replace('\n', "\\n"),
val,
params: HashMap::new(),
}
}
Expand Down Expand Up @@ -140,16 +140,38 @@ impl Property {
}
}

/// <https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.11>
fn escape_text(input: &str) -> String {
input
.replace('\\', r#"\\"#)
.replace(',', r#"\,"#)
.replace(';', r#"\;"#)
.replace(':', r#"\:"#)
.replace('\n', r#"\N"#)
}

fn quote_if_contains_colon(input: &str) -> String {
if input.contains([':', ';']) {
let mut quoted = String::with_capacity(input.len() + 2);
quoted.push('"');
quoted.push_str(input);
quoted.push('"');
quoted
} else {
input.to_string()
}
}

/// Writes this Property to `out`
pub(crate) fn fmt_write<W: Write>(&self, out: &mut W) -> Result<(), fmt::Error> {
// A nice starting capacity for the majority of content lines
let mut line = String::with_capacity(150);

write!(line, "{}", self.key)?;
for Parameter { key, val: value } in self.params.values() {
write!(line, ";{}={}", key, value)?;
for Parameter { key, val } in self.params.values() {
write!(line, ";{}={}", key, Self::quote_if_contains_colon(val))?;
}
write!(line, ":{}", self.val)?;
write!(line, ":{}", Self::escape_text(&self.val))?;
write_crlf!(out, "{}", fold_line(&line))?;
Ok(())
}
Expand Down

0 comments on commit c0ffeee

Please sign in to comment.