Skip to content

Commit

Permalink
Merge pull request #103 from hoodie/feature/escaping
Browse files Browse the repository at this point in the history
fix escaping
  • Loading branch information
hoodie authored Jul 7, 2024
2 parents 38d992c + c0ffeee commit 30c79fb
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 18 deletions.
2 changes: 1 addition & 1 deletion examples/custom_property_parsed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let event = Event::new()
.summary("test event")
.append_property(
"TEST;IMPORTANCE=very;DUE=tomorrow:FOOBAR\n"
r#"TEST;IMPORTANCE=very;DUE=tomorrow:FOOBAR;COMPLEX=\n"#
.parse::<Property>()
.unwrap(),
)
Expand Down
13 changes: 8 additions & 5 deletions examples/full_circle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@ use icalendar::*;

fn main() {
let event = Event::new()
.summary("test event")
.description("here I have something really important to do")
.summary(";IMPORTANCE=very;test event")
.description("here; I have something; really important to do")
.starts(Utc::now())
.class(Class::Confidential)
.ends(Utc::now() + Duration::days(1))
.append_property(
Property::new("TEST", "FOOBAR")
.add_parameter("IMPORTANCE", "very")
.add_parameter("DUE", "tomorrow")
.add_parameter("COMPLEX", r#"this is code; I think"#)
.add_parameter("keyval", "color:red")
.add_parameter("CODE", "this is code; so I quote")
.done(),
)
.uid("my.own.id")
Expand All @@ -34,10 +37,10 @@ fn main() {

println!("{}", &ical); // print what we built
println!("{}", from_parsed); // print what parsed
println!("{:#?}", built_calendar); // inner representation of what we built
println!("{:#?}", from_parsed); // inner representation of what we built and then parsed
println!("built calendar:\n{:#?}", built_calendar); // inner representation of what we built
println!("from parsed:\n{:#?}", from_parsed); // inner representation of what we built and then parsed
println!(
"{:#?}",
"read_calenar:\n{:#?}",
parser::read_calendar(&parser::unfold(&ical)).unwrap()
); // inner presentation of the parser's data structure
}
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
22 changes: 16 additions & 6 deletions src/parser/properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,12 +166,22 @@ fn test_property() {
);

// 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,9 @@ 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(ParseString::from)
.map(ParseString::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
109 changes: 109 additions & 0 deletions tests/full_circle.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#![cfg(feature = "parser")]
use std::str::FromStr;

use chrono::*;
use icalendar::*;
// use pretty_assertions::assert_eq;

fn get_summary(calendar: &Calendar) -> &str {
calendar.components[0]
.as_event()
.unwrap()
.get_summary()
.unwrap()
}
fn get_description(calendar: &Calendar) -> &str {
calendar.components[0]
.as_event()
.unwrap()
.get_description()
.unwrap()
}

fn init_test_event() -> (&'static str, &'static str, Calendar) {
let summary = ";IMPORTANCE=very;test event";
let description = "this, contains: many escapeworthy->\n<-;characters";

let event = Event::new()
.summary(summary)
.description(description)
.starts(Utc::now())
.class(Class::Confidential)
.ends(Utc::now() + Duration::days(1))
.append_property(
Property::new("TEST", "FOOBAR")
.add_parameter("IMPORTANCE", "very")
.add_parameter("COMPLEX", r#"this is code; I think"#)
.add_parameter("keyval", "color:red")
.done(),
)
.uid("my.own.id")
.done();

let todo = Todo::new().summary("Buy some: milk").done();

let mut built_calendar = Calendar::new();
built_calendar.push(event);
built_calendar.push(todo);
(summary, description, built_calendar)
}

#[test]
#[ignore = "equality difficult"]
fn serializes_correctly() {
let (_, _, built_calendar) = init_test_event();
println!("built calendar:\n{:#?}", built_calendar); // inner representation of what we built

let serialized = built_calendar.to_string();
println!("serialized: {}", &serialized); // print what we built

let from_parsed = Calendar::from_str(&serialized).unwrap();
println!("parsed again:\n{:#?}", from_parsed); // inner representation of what we built and then parsed

assert_eq!(built_calendar, from_parsed)
}

#[test]
fn escape_late() {
let (summary, description, built_calendar) = init_test_event();
println!("built calendar:\n{:#?}", built_calendar); // inner representation of what we built

let serialized = built_calendar.to_string();
println!("serialized: {}", &serialized); // print what we built

let from_parsed = Calendar::from_str(&serialized).unwrap();
println!("parsed again:\n{:#?}", from_parsed); // inner representation of what we built and then parsed

// these should not be escaped
assert_eq!(get_summary(&built_calendar), summary);
assert_eq!(get_description(&built_calendar), description);
}

#[test]
fn unescape_text() {
let (summary, description, built_calendar) = init_test_event();
println!("built calendar:\n{:#?}", built_calendar); // inner representation of what we built

let serialized = built_calendar.to_string();
println!("serialized:\n {}", &serialized); // print what we built

let from_parsed = Calendar::from_str(&serialized).unwrap();
println!("parsed again:\n{:#?}", from_parsed); // inner representation of what we built and then parsed

assert_eq!(get_summary(&from_parsed), summary);
assert_eq!(get_description(&from_parsed), description);
}

#[test]
fn reparse_equivalence() {
let (_summary, _description, built_calendar) = init_test_event();
println!("built calendar:\n{:#?}", built_calendar); // inner representation of what we built

let serialized = built_calendar.to_string();
println!("serialized: {}", &serialized); // print what we built

let from_parsed = Calendar::from_str(&serialized).unwrap();
println!("parsed again:\n{:#?}", from_parsed); // inner representation of what we built and then parsed

assert_eq!(get_summary(&built_calendar), get_summary(&from_parsed),);
}
2 changes: 1 addition & 1 deletion tests/reserialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ END:VTODO\r
BEGIN:VALARM\r
TRIGGER;RELATED=END:-P2D\r
ACTION:EMAIL\r
ATTENDEE:mailto:[email protected]\r
ATTENDEE:\"mailto:[email protected]\"\r
SUMMARY:*** REMINDER: SEND AGENDA FOR WEEKLY STAFF MEETING ***\r
DESCRIPTION:A draft agenda needs to be sent out to the attendees to the wee\r
kly managers meeting (MGR-LIST). Attached is a pointer the document templat\r
Expand Down

0 comments on commit 30c79fb

Please sign in to comment.