Skip to content

Support ISO-8601 expanded years in XML dates (fix panic on dates outside 0000-9999) - #195

Open
gaoflow wants to merge 1 commit into
ebarnard:masterfrom
gaoflow:fix-xml-date-expanded-years
Open

Support ISO-8601 expanded years in XML dates (fix panic on dates outside 0000-9999)#195
gaoflow wants to merge 1 commit into
ebarnard:masterfrom
gaoflow:fix-xml-date-expanded-years

Conversation

@gaoflow

@gaoflow gaoflow commented Jul 21, 2026

Copy link
Copy Markdown

Date::to_xml_format panics on any date whose year falls outside 0000-9999.
Binary plists store dates as an f64 offset from the 2001 epoch, so such dates are
valid and round-trip fine through Apple's tools; the common "read a binary plist,
re-serialize as XML" path then aborts the process on a file plutil considers OK.

Repro (year 11476, plutil -lint reports OK):

let value = Value::from_reader(far_future_bplist)?; // reads fine
value.to_writer_xml(&mut out)?;                      // panics: "overflow adding duration to date"

There are two panic sites, both in the date <-> string layer:

  • year >= 10000 (or < -9999): SystemTime -> OffsetDateTime overflows inside time.
  • year 0000 down to -9999: time's RFC 3339 formatter rejects the year, and the
    .unwrap() on that Err panics.

They surface through every path that stringifies a Date: to_writer_xml /
to_file_xml, impl Debug for Date, and serde Serialize. The read side is the
mirror image: from_xml_format can't parse the expanded-year strings Apple writes
(11476-..., -29719-...), so a plist written by plutil can't be read back.

The fix keeps time's RFC 3339 for years 0000-9999 (output unchanged, including
sub-second precision) and handles the expanded range directly, byte-for-byte with
plutil:

  • 11476-08-15T05:20:00Z, 33658-09-27T01:46:40Z
  • -29719-04-05T22:13:20Z, -001-11-28T00:00:00Z (Apple pads the year %04d-style,
    so year -1 is -001, not -0001)

time can't produce this itself: even with large-dates its formatter emits
+11476 / -0001 rather than Apple's 11476 / -001, and its year range
(±999999) is narrower than the plist date domain (plutil prints year 285,200,616
for a large enough f64). So the expanded range uses a small proleptic-Gregorian
conversion. Out-of-range or malformed strings return InvalidXmlDate instead of
panicking.

New tests cover the 9999/10000 boundary, far past/future and negative years against
plutil's output, the read/write round-trip, the Debug path, and rejection of
invalid dates. cargo test --all-features passes.

This lines up with the Apple-compatibility goal in #118.

One thing left out on purpose: for in-range dates rust-plist still emits sub-second
precision (e.g. ...00.5Z) where plutil truncates to whole seconds (related to
#13). I kept in-range output unchanged to avoid touching round-trip behaviour, but
can fold that in if you'd like.

Dates outside 0000-9999 (valid in binary plists, which store an f64 offset
from the 2001 epoch) made to_xml_format panic, which took out to_writer_xml,
Debug, and serde Serialize. from_xml_format also could not read the
expanded-year strings Apple writes.

Keep RFC 3339 for years 0000-9999 and format/parse the wider range directly,
matching plutil byte for byte (e.g. 11476-08-15T05:20:00Z, -29719-...,
-001-... for year -1). Invalid or unrepresentable strings return
InvalidXmlDate instead of panicking.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant