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:
yzxcj797
2026-08-22 07:28:21 +08:00
parent 9d69d9c7e7
commit af9d9badb4
+39 -3
View File
@@ -1105,9 +1105,15 @@ impl CalendarEvent {
}
// Standard UTC form: YYYYMMDDTHHMMSSZ, 16 chars, trailing 'Z'.
// Floating-time (no 'Z') and TZID-anchored forms aren't yet
// supported — future work when we tackle VTIMEZONE properly.
if value.len() < 15 || !value.ends_with('Z') {
// Floating-time (no 'Z', RFC 5545 §3.3.5) is what calendar apps emit
// for events without a timezone — DAVx5 sends it from Fossify
// 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!(
"Invalid datetime format: expected YYYYMMDDTHHMMSSZ, got {:?}",
value
@@ -1355,6 +1361,24 @@ SUMMARY:Weekly all-day — rescheduled\r
RECURRENCE-ID;VALUE=DATE:20260112\r
END:VEVENT\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 {
@@ -1369,6 +1393,18 @@ END:VCALENDAR\r
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]
fn all_day_event_parses_and_flags_as_all_day() {
// Regression: DTSTART;VALUE=DATE:20260201 used to fail