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/lib.rs b/src/lib.rs index 17bcee1..a3f823f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,7 +55,7 @@ //! [`chrono::DateTime`](chrono::DateTime) are provided for ergonomics, the latter also restoring API //! compatibility in case of UTC date-times. -#![allow(deprecated)] +#![allow(deprecated, unused_imports)] #![warn( missing_docs, missing_copy_implementations, diff --git a/src/parser/parsed_string.rs b/src/parser/parsed_string.rs index 3ddcc1d..6929079 100644 --- a/src/parser/parsed_string.rs +++ b/src/parser/parsed_string.rs @@ -12,17 +12,40 @@ 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 + .replace(r#"\\"#, r#"\"#) + .replace(r#"\,"#, ",") + .replace(r#"\;"#, ";") + .replace(r#"\:"#, ":") + .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..d0e60a3 100644 --- a/src/parser/properties.rs +++ b/src/parser/properties.rs @@ -12,11 +12,11 @@ use super::{ }; use nom::{ branch::alt, - bytes::complete::{tag, take_until, take_while}, - character::complete::{line_ending, multispace0}, + bytes::complete::{escaped, tag, take_until, take_while}, + character::complete::{alphanumeric1, line_ending, multispace0, one_of}, combinator::{cut, opt}, error::{context, convert_error, ContextError, ParseError, VerboseError}, - sequence::{preceded, separated_pair, tuple}, + sequence::{preceded, separated_pair, terminated, tuple}, Finish, IResult, Parser, }; @@ -157,9 +157,9 @@ 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"))] } @@ -168,13 +168,23 @@ fn test_property() { // TODO: newlines followed by spaces must be ignored assert_parser!( property, - "KEY3;foo=bar:VALUE\\n newline separated\n", + "KEY4;foo=bar:VALUE\\n newline separated\n", Property { - name: "KEY3".into(), - val: "VALUE\\n newline separated".into(), + name: "KEY4".into(), + val: "VALUE\n newline separated".into(), params: vec![Parameter::new_ref("foo", Some("bar"))] } ); + + // assert_parser!( + // property, + // "KEY3;foo=bar:VALUE\\nnewline separated\n", + // Property { + // name: "KEY3".into(), + // val: "VALUE\nnewline separated".into(), + // params: vec![Parameter::new_ref("foo", Some("bar"))] + // } + // ); } #[test] @@ -221,7 +231,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![], }; @@ -308,8 +318,9 @@ pub fn property<'a, E: ParseError<&'a str> + ContextError<&'a str>>( take_until("\n"), // this is for single line prop parsing, just so I can leave off the '\n' take_while(|_| true), + // TODO: try unescaping later )) - .map(ParseString::from), + .map(|x| ParseString::from(x).unescape_text()), ), // val TODO: replace this with something simpler! ), context( diff --git a/src/properties.rs b/src/properties.rs index 6db3e82..e8f0560 100644 --- a/src/properties.rs +++ b/src/properties.rs @@ -51,18 +51,18 @@ impl From<(&str, &str)> for Property { impl Property { /// Guess what this does :D pub fn new(key: &str, val: &str) -> Self { - Property { + dbg!(Property { key: key.to_owned(), - val: val.replace('\n', "\\n"), + val: val.to_owned(), params: HashMap::new(), - } + }) } /// if you already have `String`s I'll gladly take 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#"\"#, 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..1a8de92 --- /dev/null +++ b/tests/full_circle.rs @@ -0,0 +1,110 @@ +#![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 = "reason"] +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 + + todo!(); + // 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..f25cdf3 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