Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
hoodie committed Jul 5, 2024
1 parent 38d992c commit c9f8c44
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 25 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
}
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
//! [`chrono::DateTime<Utc>`](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,
Expand Down
23 changes: 23 additions & 0 deletions src/parser/parsed_string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self> for ParseString<'_> {
fn eq(&self, rhs: &Self) -> bool {
self.as_ref() == rhs.as_ref()
Expand Down
31 changes: 21 additions & 10 deletions src/parser/properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -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"))]
}
Expand All @@ -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]
Expand Down Expand Up @@ -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![],
};

Expand Down Expand Up @@ -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(
Expand Down
36 changes: 29 additions & 7 deletions src/properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}
}
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#"\"#, 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
110 changes: 110 additions & 0 deletions tests/full_circle.rs
Original file line number Diff line number Diff line change
@@ -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),);
}
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 c9f8c44

Please sign in to comment.