Files
jcoffey-dev 0e532cddca Calendars: give each recurrence expansion one work budget
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.
2026-10-29 06:09:09 -07:00
..

calcard

crates.io build docs.rs crates.io

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

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 (C) 2020, Stalwart Labs LLC