A calendar's recurrence rules were each allowed 100,000 passes that find nothing before calcard gave up on them, counted afresh for every rule and after every instance found, and nothing bounded the total. Anyone who can put an object on a calendar (a CalDAV or JMAP upload, or an iMIP invitation the server processes) could send a few thousand components whose rules never match, such as FREQ=MONTHLY;BYMONTH=2;BYMONTHDAY=31, or rules that match rarely, and storing or processing that object cost tens of seconds of CPU. The per-period candidate check doesn't catch these: each period is small, there are just very many of them. Expansion now has one budget of 1,000,000 work units for all the rules of the object together (about 50 ms in a release build). Every pass over a period costs a unit, a pass that finds nothing a unit per 16 days it scanned, candidates looked at but not produced a unit each, and so does each time skipped while looking for the next BYHOUR/BYMINUTE/BYSECOND match. When it runs out, expansion stops the way it does at the instance limit: the object is kept and the instances found so far are indexed. The expansion also reports it, so the server logs a calendar rule expansion error naming it. Rules are no longer started once the instance limit is reached. Ordinary rules use a small part of the budget: 3,000 daily, weekly, monthly, yearly or every-leap-day instances expand as before. Test: in calcard's new budget tests, 40 rules that never match took 3,999,960 iterations on the vendored copy and a sparse daily rule 2,922,306 (both over the 1,000,000 asserted); both now stop within the budget and report it. 4,000 never-matching components (412 KB) took 13.7 s (daily), 31.7 s (yearly) and 41.8 s (monthly) in a release build, now 36 ms, 45 ms and 57 ms.
calcard
calcard is a Rust crate for parsing, generating, and converting calendaring and contact data across multiple formats. It supports iCalendar and vCard, making it easy to work with .ics and .vcf files. It also fully handles JSCalendar and JSContact, the emerging standards commonly used in JMAP-based systems. In addition to parsing and serializing these formats, calcard provides seamless conversion between iCalendar and JSCalendar, as well as between vCard and JSContact, enabling smooth interoperability between traditional and modern calendaring and contact protocols.
In general, this library abides by the Postel's law or Robustness Principle which states that an implementation must be conservative in its sending behavior and liberal in its receiving behavior. This means that calcard will make a best effort to parse non-conformant iCal/vCard/JSCalendar/JSContact objects as long as these do not deviate too much from the standard.
Features
Comprehensive features for working with calendaring and contact data, including:
- iCalendar support: Parsing and generating iCalendar data.
- vCard support: Parsing and generating vCard data.
- JSCalendar support: Parsing and generating JSCalendar data.
- JSContact support: Parsing and generating JSContact data.
- Seamless conversion between iCalendar and JSCalendar.
- Seamless conversion between vCard and JSContact.
- Recurrence rules expansion: Accurately computes and enumerates repeating events based on iCalendar and JSCalendar RRULEs.
- IANA timezone detection: Automatically resolves and handles custom and proprietary timezones.
Usage
Parsing an iCalendar and/or vCard stream
You can parse vCard and iCalendar files using the stream-based parser:
let input = "BEGIN:VCARD\nVERSION:3.0\nFN:John Doe\nEND:VCARD\n";
let mut parser = Parser::new(&input);
loop {
match parser.entry() {
Entry::VCard(vcard) => println!("Parsed VCard: {:?}", vcard),
Entry::ICalendar(ical) => println!("Parsed ICalendar: {:?}", ical),
Entry::InvalidLine(line) => eprintln!("Invalid line found: {}", line),
Entry::UnexpectedComponentEnd { expected, found } => {
eprintln!("Unexpected end: expected {:?}, found {:?}", expected, found);
},
Entry::UnterminatedComponent(component) => {
eprintln!("Unterminated component: {:?}", component);
},
Entry::Eof => {
break;
}
}
}
Parsing iCalendar
You can parse a single iCalendar using the ICalendar::parse method:
let input = "BEGIN:VCALENDAR\nVERSION:2.0\nEND:VCALENDAR\n";
let ical = ICalendar::parse(&input);
println!("Parsed ICalendar: {:?}", ical);
Parsing vCard
You can parse a single vCard using the VCard::parse method:
let input = "BEGIN:VCARD\nVERSION:3.0\nFN:John Doe\nEND:VCARD\n";
let vcard = VCard::parse(&input);
println!("Parsed VCard: {:?}", vcard);
Parsing JSCalendar
You can parse a JSCalendar JSON string using the JSCalendar::parse method:
let input = r#"{
"@type": "Group",
"uid": "bf0ac22b-4989-4caf-9ebd-54301b4ee51a",
"updated": "2020-01-15T18:00:00Z",
"title": "A simple group",
"entries": [{
"@type": "Event",
"uid": "a8df6573-0474-496d-8496-033ad45d7fea",
"updated": "2020-01-02T18:23:04Z",
"title": "Some event",
"start": "2020-01-15T13:00:00",
"timeZone": "America/New_York",
"duration": "PT1H"
},
{
"@type": "Task",
"uid": "2a358cee-6489-4f14-a57f-c104db4dc2f2",
"updated": "2020-01-09T14:32:01Z",
"title": "Do something"
}]
}"#;
let jscalendar = JSCalendar::<String, String>::parse(input).unwrap();
println!("Parsed JSCalendar: {}", jscalendar.to_string_pretty());
Parsing JSContact
You can parse a JSContact JSON string using the JSContact::parse method:
let input = r#"{
"@type": "Card",
"version": "1.0",
"uid": "22B2C7DF-9120-4969-8460-05956FE6B065",
"kind": "individual",
"name": {
"components": [
{ "kind": "given", "value": "John" },
{ "kind": "surname", "value": "Doe" }
],
"isOrdered": true
}
}"#;
let jscontact = JSContact::<String, String>::parse(input).unwrap();
println!("Parsed JSContact: {}", jscontact.to_string_pretty());
Converting to/from iCalendar and JSCalendar
To convert a JSCalendar to an iCalendar, use the JSCalendar::into_icalendar method:
let ical = JSCalendar::<String, String>::parse(input).unwrap().into_icalendar().unwrap();
To convert an iCalendar to a JSCalendar, use the ICalendar::into_jscalendar method:
let jscalendar = ICalendar::parse(&input).unwrap().into_jscalendar::<String, String>().unwrap();
Converting to/from vCard and JSContact
To convert a JSContact to a vCard, use the JSContact::into_vcard method:
let vcard = JSContact::<String, String>::parse(input).unwrap().into_vcard().unwrap();
To convert a vCard to a JSContact, use the VCard::into_jscontact method:
let jscontact = VCard::parse(&input).unwrap().into_jscontact::<String, String>().unwrap();
Generating iCalendar and vCard
To generate a vCard or iCalendar, simply call .to_string() on the parsed object:
let vcard_string = vcard.to_string();
println!("Generated vCard:\n{}", vcard_string);
let ical_string = ical.to_string();
println!("Generated ICalendar:\n{}", ical_string);
Note: Documentation for creating VCard and ICalendar objects is coming soon.
Generating JSCalendar and JSContact
To generate a JSCalendar or JSContact, simply call .to_string() on the parsed object or .to_string_pretty() for a more human-readable format:
let jscalendar_string = jscalendar.to_string();
println!("Generated JSCalendar:\n{}", jscalendar_string);
let jscontact_string = jscontact.to_string();
println!("Generated JSContact:\n{}", jscontact_string);
Note: Documentation for creating JSCalendar and JSContact objects is coming soon.
Testing and Fuzzing
To run the testsuite:
$ cargo test --all-features
or, to run the testsuite with MIRI:
$ cargo +nightly miri test --all-features
To fuzz the library with cargo-fuzz:
$ cargo +nightly fuzz run fuzz_all
$ cargo +nightly fuzz run fuzz_random
$ cargo +nightly fuzz run fuzz_structured
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Funding
Part of the development of this library was funded through the NGI Zero Core, a fund established by NLnet with financial support from the European Commission's programme, under the aegis of DG Communications Networks, Content and Technology under grant agreement No 101092990.
If you find this library useful you can help by becoming a sponsor. Thank you!
Copyright
Copyright (C) 2020, Stalwart Labs LLC