From 4d868f4069e59ba8154f5f9294a413ac48b7332e Mon Sep 17 00:00:00 2001 From: Manish Goregaokar Date: Thu, 19 Mar 2026 08:47:02 -0700 Subject: [PATCH] Check offset value before discarding --- src/builtins/core/plain_date.rs | 9 +++++-- src/builtins/core/plain_time.rs | 9 +++++++ src/builtins/core/plain_year_month.rs | 5 ++++ src/parsers.rs | 38 +++++++++++++++++++++------ 4 files changed, 51 insertions(+), 10 deletions(-) diff --git a/src/builtins/core/plain_date.rs b/src/builtins/core/plain_date.rs index 9647b1a0d..16db34926 100644 --- a/src/builtins/core/plain_date.rs +++ b/src/builtins/core/plain_date.rs @@ -982,7 +982,7 @@ mod tests { // test262/test/built-ins/Temporal/Calendar/prototype/month/argument-string-invalid.js #[test] fn invalid_strings() { - const INVALID_STRINGS: [&str; 35] = [ + const INVALID_STRINGS: &[&str] = &[ // invalid ISO strings: "", "invalid iso8601", @@ -1023,9 +1023,14 @@ mod tests { // valid, but outside the supported range: "-999999-01-01", "+999999-01-01", + // built-ins/Temporal/PlainDate/from/argument-string-too-many-decimals + "1970-01-01T00:00:00.1234567891", + "1970-01-01T00:00:00.1234567890", + "1970-01-01T00+00:00:00.1234567891", + "1970-01-01T00+00:00:00.1234567890", ]; for s in INVALID_STRINGS { - assert!(PlainDate::from_str(s).is_err()) + assert!(PlainDate::from_str(s).is_err(), "{} should not parse", s) } } diff --git a/src/builtins/core/plain_time.rs b/src/builtins/core/plain_time.rs index 68c27c8d6..017dffa00 100644 --- a/src/builtins/core/plain_time.rs +++ b/src/builtins/core/plain_time.rs @@ -852,6 +852,15 @@ mod tests { "2019-10-01T09:00:00Z[UTC]", "09:00:00Z[UTC]", "09:00:00Z", + // built-ins/Temporal/PlainTime/from/argument-string-too-many-decimals + "1970-01-01T00:00:00.1234567891", + "1970-01-01T00:00:00.1234567890", + "1970-01-01T00+00:00:00.1234567891", + "1970-01-01T00+00:00:00.1234567890", + "00:00:00.1234567891", + "00:00:00.1234567890", + "00+00:00:00.1234567891", + "00+00:00:00.1234567890", ]; for invalid_str in invalid_cases { let err = PlainTime::from_str(invalid_str); diff --git a/src/builtins/core/plain_year_month.rs b/src/builtins/core/plain_year_month.rs index d1ae5b344..98af6e8b5 100644 --- a/src/builtins/core/plain_year_month.rs +++ b/src/builtins/core/plain_year_month.rs @@ -987,6 +987,11 @@ mod tests { "+275760-10-01", "+275760-10-01T00:00", "1976-11[u-ca=hebrew]", + // built-ins/Temporal/PlainYearMonth/from/argument-string-too-many-decimals + "1970-01-01T00:00:00.1234567891", + "1970-01-01T00:00:00.1234567890", + "1970-01-01T00+00:00:00.1234567891", + "1970-01-01T00+00:00:00.1234567890", ]; for invalid_case in invalid_strings { diff --git a/src/parsers.rs b/src/parsers.rs index 20a3d6c79..f8eb981e6 100644 --- a/src/parsers.rs +++ b/src/parsers.rs @@ -1,6 +1,7 @@ //! This module implements Temporal Date/Time parsing functionality. use crate::{ + error::ErrorMessage, iso::{IsoDate, IsoTime}, options::{DisplayCalendar, DisplayOffset, DisplayTimeZone}, Sign, TemporalError, TemporalResult, @@ -8,7 +9,10 @@ use crate::{ use ixdtf::{ encoding::Utf8, parsers::IxdtfParser, - records::{Annotation, DateRecord, IxdtfParseRecord, TimeRecord, UtcOffsetRecordOrZ}, + records::{ + Annotation, DateRecord, FullPrecisionOffset, IxdtfParseRecord, TimeRecord, UtcOffsetRecord, + UtcOffsetRecordOrZ, + }, }; use writeable::{impl_display_with_writeable, LengthHint, Writeable}; @@ -694,6 +698,21 @@ fn parse_ixdtf(source: &[u8], variant: ParseVariant) -> TemporalResult parser.parse_time_with_annotation_handler(handler), }?; + // The ixdtf crate allows arbitrary precision for offsets, but Temporal only allows 9 + if let Some(UtcOffsetRecordOrZ::Offset(UtcOffsetRecord::FullPrecisionOffset( + FullPrecisionOffset { + fraction: Some(fraction), + .. + }, + ))) = record.offset + { + if fraction.to_nanoseconds().is_none() { + return Err( + TemporalError::range().with_enum(ErrorMessage::FractionalTimeMoreThanNineDigits) + ); + } + } + record.calendar = first_calendar.map(|v| v.value); // Note: this method only handles the specific AnnotatedFoo nonterminals; @@ -806,14 +825,17 @@ pub(crate) fn parse_instant(source: &[u8]) -> TemporalResult) -> TemporalResult> { +fn check_offset_no_z( + record: IxdtfParseRecord<'_, Utf8>, +) -> TemporalResult> { if record.offset == Some(UtcOffsetRecordOrZ::Z) { return Err(TemporalError::range() .with_message("UTC designator is not valid for plain date/time parsing.")); } + Ok(record) } @@ -823,13 +845,13 @@ pub(crate) fn parse_year_month(source: &[u8]) -> TemporalResult check_offset(dt), + Ok(dt) => check_offset_no_z(dt), // Format and return the error from parsing YearMonth. _ => Err(e), } @@ -839,13 +861,13 @@ pub(crate) fn parse_year_month(source: &[u8]) -> TemporalResult TemporalResult> { let md_record = parse_ixdtf(source, ParseVariant::MonthDay); let Err(e) = md_record else { - return md_record.and_then(check_offset); + return md_record.and_then(check_offset_no_z); }; let dt_parse = parse_date_time(source); match dt_parse { - Ok(dt) => check_offset(dt), + Ok(dt) => check_offset_no_z(dt), // Format and return the error from parsing MonthDay. _ => Err(e), } @@ -854,7 +876,7 @@ pub(crate) fn parse_month_day(source: &[u8]) -> TemporalResult) -> TemporalResult { // Handle [~Zoned] - let record = check_offset(record)?; + let record = check_offset_no_z(record)?; // Handle [+TimeRequired] let Some(time) = record.time else { return Err(TemporalError::range()