made using Leaflet

Everyone is wrong about iCalendar

There are many implementations of iCalendar on crates.io, which is great! Unfortunately, they are all wrong. This is not great.

Background

iCalendar is an open file format for sharing calendar data, originally defined in 1998 by RFC 2445. This document was obsoleted by RFC 5545 in 2009, which has become the baseline definition. Other RFCs—particularly RFCs 7986, 9073, 9074, and 9253—have extended RFC 5545 over the years.

The most important division of data in an .ics file is a component, which is essentially a record with a name and zero or more subcomponents. An .ics file is just a list of components with the VCALENDAR name, and almost all the data in the file is stored as a subcomponent of a VCALENDAR. Each component contains a sequence of properties, which are triples consisting of a name, a list of property parameters, and a value. The value format of a property is dictated by its name.

Let's pick an example. The following snippet is the ABNF definition for the event component as it appears in RFC 5545 § 3.6.1.

eventc     = "BEGIN" ":" "VEVENT" CRLF
             eventprop *alarmc
             "END" ":" "VEVENT" CRLF

eventprop  = *(
           ;
           ; The following are REQUIRED,
           ; but MUST NOT occur more than once.
           ;
           dtstamp / uid /
           ;
           ; The following is REQUIRED if the component
           ; appears in an iCalendar object that doesn't
           ; specify the "METHOD" property; otherwise, it
           ; is OPTIONAL; in any case, it MUST NOT occur
           ; more than once.
           ;
           dtstart /
           ;
           ; The following are OPTIONAL,
           ; but MUST NOT occur more than once.
           ;
           class / created / description / geo /
           last-mod / location / organizer / priority /
           seq / status / summary / transp /
           url / recurid /
           ;
           ; The following is OPTIONAL,
           ; but SHOULD NOT occur more than once.
           ;
           rrule /
           ;
           ; Either 'dtend' or 'duration' MAY appear in
           ; a 'eventprop', but 'dtend' and 'duration'
           ; MUST NOT occur in the same 'eventprop'.
           ;
           dtend / duration /
           ;
           ; The following are OPTIONAL,
           ; and MAY occur more than once.
           ;
           attach / attendee / categories / comment /
           contact / exdate / rstatus / related /
           resources / rdate / x-prop / iana-prop
           ;
           )

What the grammar outlines here is that an event is delimited by BEGIN:VEVENT<CR><LF> and END:VEVENT<CR><LF>, and between those delimiters it expects a sequence of properties followed by zero or more VALARM subcomponents. While RFC 5545 never explicitly provides a grammar for arbitrary components, this basic structure is used by every component.

For the most part, implementing iCalendar is basically just this. You look at the document, get a general feel for the architecture and the shape of the data, and then zoom in to particular areas as necessary. This is the easy part. But if and when you start trying to interpret minutiae, the text of the relevant RFCs can become hopelessly opaque; in several places they are underspecified and leave significant gaps to be filled by implementors.

I'm going to walk through some misinterpretations that I've seen in the wild, but I want to stress that these are easy mistakes to make; I've made most of them while working on my own implementation. I'll also touch on some edge cases where I think key aspects of iCalendar are ambiguous but no obviously correct interpretation exists.

Parameter Types

Because we're working in Rust, the easiest mistake in the world to make is to define your types as strictly as possible. I spent about a month working on my implementation before I realised this was a problem, and it boils down to the fact that most grammars in RFC 5545 have these little catch-alls in them: x-prop, iana-prop, other-param, and so on.

Take this example from RFC 5545 § 3.8.6.3.

trigger    = "TRIGGER" (trigrel / trigabs) CRLF

trigrel    = *(
           ;
           ; The following are OPTIONAL,
           ; but MUST NOT occur more than once.
           ;
           (";" "VALUE" "=" "DURATION") /
           (";" trigrelparam) /
           ;
           ; The following is OPTIONAL,
           ; and MAY occur more than once.
           ;
           (";" other-param)
           ;
           ) ":"  dur-value

trigabs    = *(
           ;
           ; The following is REQUIRED,
           ; but MUST NOT occur more than once.
           ;
           (";" "VALUE" "=" "DATE-TIME") /
           ;
           ; The following is OPTIONAL,
           ; and MAY occur more than once.
           ;
           (";" other-param)
           ;
           ) ":" date-time

If you're skimming and don't notice the other-param occurrences, you might produce a type like the following.

pub enum Trigger {
    Duration(Duration, Option<Related>),
    DateTime(CalendarDateTime),
}

The icalendar crate pitches itself as a nice statically-typed layer over iCalendar, and so it uses static types like these. And that's fine in the vast majority of cases except for when the other-param rule is used. I could write something like TRIGGER;X-PARAM=100:-PT15M, and while it would be odd it would also be perfectly legal. This type has no way to represent that parameter, so it obviously can't be a correct model for the TRIGGER property.

In practice the only solution is to represent property parameters in a table; since almost every parameter is optional you don't even need to define specific table types for each property. And since every property must have a parameter table, you can factor out the enum and get something like the following:

pub struct Property<V> {
    pub parameters: ParameterTable,
    pub value: V,
}

pub enum Trigger {
    Duration(Property<Duration>),
    DateTime(Property<DateTime>),
}

Component APIs

If you define a type called something like Event, and say "this type is a VEVENT component," then your API is obliged to enforce that fact. Most crates fail to reinforce even the most basic invariants, and when they provide mutable access it is trivial to break invariants.

Low-hanging fruit first: the ics crate provides an Event::new method that just accepts two impl Into<Cow<'_, str>> values for the UID and DTSTAMP properties, so the following code creates an invalid Event:

use ics::Event;

// emojis are obviously invalid here
let invalid_event = Event::new("⚠️", "⚠️");

Even if you had a valid Event, it would still be possible to invalidate it by taking a property which cannot occur more than once and using Event::push to add it twice:

use ics::{Event, properties::Class};

let mut valid_event = Event::new(
    "d4092ed9-1667-4518-a7c0-bcfaac4f1fc6", 
    "20181021T190000",
);

// the CLASS property can occur at most once on VEVENT
valid_event.push(Class::public());
valid_event.push(Class::private());

You can pull basically the same trick with icalendar, but it's even easier: Event::new takes no parameters here, so it must automatically be invalid because it doesn't contain the mandatory UID and DTSTAMP properties.

use icalendar::Event;

// all events in icalendar begin in an invalid form
let invalid_event = Event::new();

But icalendar also introduces a new class of problem: it provides unrestricted mutable access to Event by way of its Component trait, which means that you can trivially invalidate an Event by removing one of the mandatory properties.

use icalendar::{Component, Event, Property};

// suppose we manually construct a valid event
let mut event = Event::new();
event.append_property(Property::new(
    "UID",
    "d4092ed9-1667-4518-a7c0-bcfaac4f1fc6",
));
event.append_property(Property::new(
    "DTSTAMP",
    "20181021T190000",
));

// then we can just trivially invalidate it again
event.remove_property("UID");

Durations and Time Semantics

This is a subtle point, so I want to emphasise it here. Calendaring systems are not about communicating time precisely because that's just not how we use them. When we say some event will last for a day, we don't literally mean it will take 86,400,000 milliseconds, we mean it will take a day. A day is a single semantic unit whose duration varies, and so it is a useful calendaring unit and a terrible timekeeping unit. To use the relevant PL jargon, calendaring systems have a denotational semantics that cannot be accurately captured by ISO 8601 implementations.

Durations are the best example for this. In RFC 5545, a duration is a signed period of time measured either in weeks or in days, hours, minutes, and seconds. Quoting RFC 5545 § 3.3.6 directly, "the format can represent nominal durations (weeks and days) and actual durations (hours, minutes, and seconds)" and "the duration of a week or day depends on its position in the calendar." So to be absolutely clear: you cannot represent durations using an actual measurement in terms of seconds or milliseconds because you cannot assign a single number to the length of the day or the week.

When icalendar, ickle, and web_ical use chrono::Duration to represent durations, they are strictly incorrect; chrono measures durations as a signed number of milliseconds. As a consequence they cannot correctly handle recurrence rules and will fail to render .ics files accurately.

Quickfire Round: icalendar

A few days ago, in some frustration, I posted about how I felt essentially every Rust implementation of iCalendar was incorrect.

oliver's avatar
oliver
@eikopf.com

the degree to which every single implementation of icalendar on crates.io is wrong amazes me

Oct 28, 2025, 9:13 AM
2

This was the original impetus for this post, largely written as a joke, but in the course of some related threads I also posted my thoughts about icalendar in particular.

oliver's avatar
oliver
@eikopf.com

icalendar is ~fine (if incorrect), but the API is extremely clunky, the docs are terrible, and it allocates for no reason all over the place

crates.io: Rust Package Registry

crates.io: Rust Package Registry


https://crates.io/crates/icalendar
Oct 28, 2025, 9:20 AM
4

Was this a little mean? Probably, although I was mostly just giving my thoughts about what crates people should use. Anyway, the important part is a reply I received about a day later.

𝗵𝗼𝗼𝗱𝗶𝗲's avatar
𝗵𝗼𝗼𝗱𝗶𝗲
@hoodie.de

I'm not happy with the allocations either. it's a "scratch-my-own-itch" library that I continued because I had squatted the name. There is a friendly header at the top saying: "You want to help make this more mature? Please talk to me, Pull Requests and suggestions are very welcome."

Oct 29, 2025, 9:15 PM
2

We chatted a little, and ultimately I suggested that I'd include some more details about icalendar in this post so I could actually provide some help instead of just posting. What follows here are issues I've found in icalendar that I think are worth addressing for the sake of correctness (not including topics I've already covered).

Property Multiplicity

All properties have a multiplicity within their enclosing components, i.e. they define how many times they can occur. For example, the UID property must occur exactly once on VEVENT, whereas it may occur at most once on VCALENDAR.

To properly set up the API for a component, you need to encode the multiplicities of every property in the type system. If a property is mandatory, it must be provided to initialize the component; if a property is optional it may only be provided at most once.

Importantly, this means you cannot have a single Component trait that shares all these property methods across the various component types; you need to be able to alter the signatures of the relevant methods for each particular component. Note that sometimes it is not enough to know the component type, you must also know the value of some special property: on a VALARM component the multiplicity of its properties depends on the value of the ACTION property.

Time Semantics and chrono

I would strongly discourage the use of chrono in icalendar, since it has a different semantic model of time. It's very reasonable to provide some utilities that interface with chrono, perhaps gated by a feature, but it should not be used in the parser or data model.

We have already discussed how this applies to durations, but a similar problem occurs with dates: it is not clear that NaiveDate (hence the proleptic Gregorian calendar) will always accurately reflect a valid iCalendar date, nor is it clear that the reverse is true.

Extension Values

Types like Class, EventStatus, TodoStatus, and ValueType need to provide support for extension values that begin with X- and are explicitly marked out as permissible by RFC 5545. Similar support should probably also be provided for unknown IANA registered values, though this depends on how willing you are to potentially support future RFCs or those outside of a strictly defined set.

Other Remarks

I won't dwell too much on the architecture of icalendar, since I'm mostly interested in correctness here. My primary concerns would be about the Component trait, which seems too strict, and about how much data is being stored in raw String values rather than as strongly typed values.

I would recommend first settling on a design and architecture, and then focusing on a full implementation of RFC 5545 with forward compatibility in mind. And if that's the direction the project will take, then I'd be happy to contribute!

What About calcard?

One crate I haven't mentioned so far is calcard, and you could very well say that it breaks my entire argument. It is quite professional, with a lot of thought clearly given to minimising allocations and optimising the parser; more importantly it appears to be correct in a way that the other crates I've discussed haven't been.

And like, sort of? Basically every element of the crate can be reasonably supported by the relevant RFCs, especially with respect to its stated goal: "to parse non-conformant iCal/vCard/JSCalendar/JSContact objects as long as these do not deviate too much from the standard."

But I cannot help but feel that it fails to be correct. Take a look at the following example:

use calcard::icalendar::ICalendar;

let input = "BEGIN:VCALENDAR\nVERSION:2.0\nEND:VCALENDAR\n";
let cal = ICalendar::parse(input).unwrap();
let mut out = String::new();
cal.write_to(&mut out).unwrap();
println!("{out}");

// prints:
// BEGIN:VCALENDAR
// VERSION:2.0
// END:VCALENDAR

This succeeds, both in parsing and in writing, and yet the result it produces is obviously invalid (VCALENDAR components must contain the PRODID property). To me this must be a form of incorrectness, or else the ICalendar type does not actually represent an iCalendar object.

In truth it's hard to evaluate calcard with respect to its guarantees because it has so little documentation. If the crate explicitly said that an ICalendar represents an iCalendar object then I could unambiguously describe it as incorrect, but instead I have to infer intent and guarantees through type names and a few scant comments. Without documented guarantees and invariants, I would feel very uncomfortable using calcard for anything non-trivial.

Remarks and Recommendations

In the immediate short term, I'd recommend people either use calcard, ical, or a custom implementation. You could technically use libical with bindgen, but that seems painful.

In the medium term, I'm working on a crate called calico that should hopefully provide a correct, well-documented alternative to the current options. It's currently in an unusable state and undergoing a major refactor, but I'll hopefully be able to cut a usable release relatively soon.

In the longer term, it's apparent to me that iCalendar is just a fundamentally flawed standard; just look at how obscenely difficult it is to implement correctly. And other people agree! RFC 8984, published in 2021, defines a JSON-based alternative called JSCalendar with a distinct semantic model that avoids the inherent issues of iCalendar. Currently the only crate I'm aware of that supports it is calcard, but I intend to add JSCalendar support to calico once the iCalendar implementation is reasonably stable.


oliver's avatar
oliver
@eikopf.com

Everyone is wrong about iCalendar

Everyone is wrong about iCalendar

There are many implementations of iCalendar on crates.io, which is great! Unfortunately, they are all wrong. This is not great.


https://leaflet.pub/759dc65b-edbd-4da2-93e3-fde91812fee3
oliver's avatar
oliver
@eikopf.com
the degree to which every single implementation of icalendar on crates.io is wrong amazes me
Nov 2, 2025, 2:15 PM
0
made using Leaflet