diff --git a/examples/custom_property_parsed.rs b/examples/custom_property_parsed.rs index cf4713c..c488616 100644 --- a/examples/custom_property_parsed.rs +++ b/examples/custom_property_parsed.rs @@ -5,7 +5,7 @@ fn main() -> Result<(), Box> { 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::() .unwrap(), ) diff --git a/examples/full_circle.rs b/examples/full_circle.rs index 4f81b8f..afd8774 100644 --- a/examples/full_circle.rs +++ b/examples/full_circle.rs @@ -6,8 +6,8 @@ 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)) @@ -15,6 +15,9 @@ fn main() { 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") @@ -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 } diff --git a/src/parser/parsed_string.rs b/src/parser/parsed_string.rs index 3ddcc1d..ade6dbf 100644 --- a/src/parser/parsed_string.rs +++ b/src/parser/parsed_string.rs @@ -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 for ParseString<'_> { fn eq(&self, rhs: &Self) -> bool { self.as_ref() == rhs.as_ref() diff --git a/src/parser/properties.rs b/src/parser/properties.rs index 48c01eb..d033c40 100644 --- a/src/parser/properties.rs +++ b/src/parser/properties.rs @@ -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"))] } ); @@ -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", @@ -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:mb@goerlitz.de\n", @@ -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![], }; @@ -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", diff --git a/src/properties.rs b/src/properties.rs index 6db3e82..757eb62 100644 --- a/src/properties.rs +++ b/src/properties.rs @@ -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(), } } @@ -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(), } } @@ -140,16 +140,38 @@ impl Property { } } + /// + 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(&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(()) } diff --git a/tests/full_circle.rs b/tests/full_circle.rs new file mode 100644 index 0000000..eda689b --- /dev/null +++ b/tests/full_circle.rs @@ -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),); +} diff --git a/tests/reserialize.rs b/tests/reserialize.rs index c3eb366..dca94d4 100644 --- a/tests/reserialize.rs +++ b/tests/reserialize.rs @@ -30,7 +30,7 @@ END:VTODO\r BEGIN:VALARM\r TRIGGER;RELATED=END:-P2D\r ACTION:EMAIL\r -ATTENDEE:mailto:john_doe@example.com\r +ATTENDEE:\"mailto:john_doe@example.com\"\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