Never crash: a UTC offset of i16::MIN formats and validates without overflow (#2579)

DateTime deserializes with public fields, so its offset can arrive without passing
valid(). `i16::MIN.abs()` overflows: a panic in debug builds and a negative minute count
in release, which to_iso() then printed. valid() and to_iso() use unsigned_abs().
Item 4 of #2579.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
darthnut
2026-10-23 09:31:29 -07:00
co-authored by Claude Opus 5.5
parent 2e5c1dd41d
commit 8b3ce31b66
+11 -2
View File
@@ -31,7 +31,7 @@ impl DateTime {
// chrono rejects impossible days (leap years and all); the time is checked
// by hand so a leap second (`second == 60`) stays acceptable.
let day_ok = self.year > 0 && chrono::NaiveDate::from_ymd_opt(i32::from(self.year), u32::from(self.month), u32::from(self.day)).is_some();
let ok = day_ok && self.hour < 24 && self.minute < 60 && self.second < 61 && self.offset_minutes.is_none_or(|o| o.abs() <= 18 * 60);
let ok = day_ok && self.hour < 24 && self.minute < 60 && self.second < 61 && self.offset_minutes.is_none_or(|o| o.unsigned_abs() <= 18 * 60);
ok.then_some(self)
}
@@ -135,7 +135,8 @@ impl DateTime {
}
if let Some(o) = self.offset_minutes {
let sign = if o < 0 { '-' } else { '+' };
s += &format!("{sign}{:02}:{:02}", o.abs() / 60, o.abs() % 60);
let o = o.unsigned_abs();
s += &format!("{sign}{:02}:{:02}", o / 60, o % 60);
}
s
}
@@ -189,6 +190,14 @@ mod tests {
assert_eq!(DateTime::parse_offset("+99:00"), None);
}
#[test]
fn extreme_offsets_do_not_overflow() {
// `DateTime` deserializes with public fields, so an offset can arrive without passing `valid()`
let d = DateTime { offset_minutes: Some(i16::MIN), ..DateTime::parse_iso("2024-06-01T13:45:59").unwrap() };
assert_eq!(d.to_iso(), "2024-06-01T13:45:59-546:08");
assert_eq!(d.valid(), None);
}
#[test]
fn unicode_offsets_are_rejected_without_panicking() {
for offset in ["+1€", "-1€", "+€1", "-€1"] {