Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix escaping #103

Merged
merged 2 commits into from
Jul 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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