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

3.1 KiB

calcard, vendored

The published calcard 0.3.14, taken from crates.io (.crate sha256 c601473ec15a875626bce73db1a1f0fc81e9c949ca25f1463b714947c0a70a1f, the checksum the server's Cargo.lock had for it) under Apache-2.0 OR MIT. INBUXA takes it under MIT. The root Cargo.toml patches it in with [patch.crates-io].

Left out: the top-level resources/ (26 MB of iCalendar, vCard, JSCalendar and JSContact samples) and .github/, which the server never builds, and Cargo.lock, Cargo.toml.orig, .cargo_vcs_info.json and .cargo-ok. The crate's own tests that read resources/ (expand_rrule, parse_ical, parse_vcard, convert_jscalendar, convert_jscontact and remove_component_ids) can't run here; the crate's other tests run from this directory with:

cargo test --offline --target-dir ../../target/calcard-tests -- \
  --skip expand_rrule --skip parse_ical --skip parse_vcard \
  --skip convert_jscalendar --skip convert_jscontact \
  --skip remove_component_ids

Changes

Every changed file carries a Coffey Labs notice in its header.

  • .gitignore: VENDORED.md is tracked (upstream ignores every *.md but the changelog and README).
  • src/datecalc/mod.rs, src/datecalc/rrule_iter.rs, src/datecalc/counter_date.rs, src/icalendar/dates.rs: one expand_dates call has a single work budget for all its recurrence rules (MAX_EXPANSION_WORK, 1,000,000 units, about 50 ms in a release build). Upstream only stops a rule after 100,000 passes that find nothing, counted afresh for every instance and every rule, so a calendar of a few thousand rules that never match (FREQ=MONTHLY;BYMONTH=2;BYMONTHDAY=31), or one sparse rule, took seconds to minutes. A pass over a period costs one unit, a pass that finds nothing one per 16 days it scanned, each candidate a pass looks at without producing it one, and each time skipped while looking for the next BYHOUR/BYMINUTE/BYSECOND match one (DateTimeIter::increment_counted counts those). When the budget runs out, expansion stops: the instances found so far are kept, as when the instance limit is reached, later rules aren't expanded, and the result carries a CalendarErrorType::ExpansionBudgetExhausted error for the component it stopped at. Rules are no longer started once the instance limit is reached (they couldn't add anything). The budget_tests in dates.rs cover it, and rule_expansion_shares_one_budget in crates/groupware fails to build if the server stops using this copy.

Updating

When the server's Cargo.lock moves calcard to a new version, Cargo stops using this copy and only warns that the patch went unused, so check Cargo.lock has no calcard entry with a source line. Re-vendor the new version from ~/.cargo/registry/src/*/calcard-*, dropping the top-level resources/ and .github/, Cargo.lock, Cargo.toml.orig, .cargo_vcs_info.json and .cargo-ok (rsync --exclude /resources --exclude /.github ...), repeat the changes above, and update the version here and in the root Cargo.toml comment.