fix(caldav): accept floating-time DTSTART/DTEND values
parse_ical_datetime rejected any datetime without the trailing 'Z', so events created without a timezone in calendar apps — which DAVx5 syncs as floating time per RFC 5545 3.3.5 form 2 — failed with 'Invalid DTSTART: Invalid datetime format: expected YYYYMMDDTHHMMSSZ' and HTTP 400, breaking the whole event upload. Accept the 15-char floating form and interpret the wall-clock time as UTC. TZID-anchored forms remain unsupported until VTIMEZONE handling lands. Fixes #682
This commit is contained in:
@@ -1105,9 +1105,15 @@ impl CalendarEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Standard UTC form: YYYYMMDDTHHMMSSZ, 16 chars, trailing 'Z'.
|
// Standard UTC form: YYYYMMDDTHHMMSSZ, 16 chars, trailing 'Z'.
|
||||||
// Floating-time (no 'Z') and TZID-anchored forms aren't yet
|
// Floating-time (no 'Z', RFC 5545 §3.3.5) is what calendar apps emit
|
||||||
// supported — future work when we tackle VTIMEZONE properly.
|
// for events without a timezone — DAVx5 sends it from Fossify
|
||||||
if value.len() < 15 || !value.ends_with('Z') {
|
// Calendar, and rejecting it failed the whole event sync with a 400
|
||||||
|
// (#682). Accept it and interpret the wall-clock time as UTC.
|
||||||
|
// TZID-anchored forms remain unsupported — future work when we
|
||||||
|
// tackle VTIMEZONE properly.
|
||||||
|
let has_utc_suffix = value.len() == 16 && value.ends_with('Z');
|
||||||
|
let is_floating = value.len() == 15;
|
||||||
|
if !has_utc_suffix && !is_floating {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Invalid datetime format: expected YYYYMMDDTHHMMSSZ, got {:?}",
|
"Invalid datetime format: expected YYYYMMDDTHHMMSSZ, got {:?}",
|
||||||
value
|
value
|
||||||
@@ -1355,6 +1361,24 @@ SUMMARY:Weekly all-day — rescheduled\r
|
|||||||
RECURRENCE-ID;VALUE=DATE:20260112\r
|
RECURRENCE-ID;VALUE=DATE:20260112\r
|
||||||
END:VEVENT\r
|
END:VEVENT\r
|
||||||
END:VCALENDAR\r
|
END:VCALENDAR\r
|
||||||
|
";
|
||||||
|
|
||||||
|
/// Floating-time VEVENT — DTSTART/DTEND without the UTC 'Z' suffix
|
||||||
|
/// (RFC 5545 §3.3.5 "form #2": local time, no timezone reference).
|
||||||
|
/// This is what DAVx5 syncs from calendar apps for events created
|
||||||
|
/// without a timezone (e.g. Fossify Calendar); rejecting it failed
|
||||||
|
/// the entire event upload with a 400 (#682).
|
||||||
|
const FLOATING_TIME_EVENT: &str = "BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
PRODID:-//OxiCloud test//EN
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:floating-1@oxicloud.test
|
||||||
|
DTSTAMP:20260101T100000Z
|
||||||
|
DTSTART:20260831T154000
|
||||||
|
DTEND:20260831T164000
|
||||||
|
SUMMARY:Floating time event
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
";
|
";
|
||||||
|
|
||||||
fn parse_ok(body: &str) -> CalendarEvent {
|
fn parse_ok(body: &str) -> CalendarEvent {
|
||||||
@@ -1369,6 +1393,18 @@ END:VCALENDAR\r
|
|||||||
assert!(!ev.all_day());
|
assert!(!ev.all_day());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn floating_time_event_parses_as_utc_wall_clock() {
|
||||||
|
// Regression (#682): '20260831T154000' used to be rejected with
|
||||||
|
// "Invalid datetime format: expected YYYYMMDDTHHMMSSZ" and the
|
||||||
|
// whole DAVx5 sync failed with HTTP 400.
|
||||||
|
let ev = parse_ok(FLOATING_TIME_EVENT);
|
||||||
|
assert_eq!(ev.summary(), "Floating time event");
|
||||||
|
assert!(!ev.all_day());
|
||||||
|
assert_eq!(ev.start_time().to_rfc3339(), "2026-08-31T15:40:00+00:00");
|
||||||
|
assert_eq!(ev.end_time().to_rfc3339(), "2026-08-31T16:40:00+00:00");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn all_day_event_parses_and_flags_as_all_day() {
|
fn all_day_event_parses_and_flags_as_all_day() {
|
||||||
// Regression: DTSTART;VALUE=DATE:20260201 used to fail
|
// Regression: DTSTART;VALUE=DATE:20260201 used to fail
|
||||||
|
|||||||
Reference in New Issue
Block a user