From 6df33b1ba4499df780a5b1c66beaaab83a6cbecc Mon Sep 17 00:00:00 2001 From: Philip Chimento Date: Thu, 3 Oct 2024 11:56:42 -0700 Subject: [PATCH] Tighten validation of offset string and month code in property bags This is a follow-up request from Anba, to the normative change in #2925. It moves syntactic validation of month codes and offset strings into PrepareCalendarFields. This allows implementations to store month codes and offset strings as integers in their equivalents of Calendar Fields Records, instead of allocated strings. Closes: #2962 --- polyfill/lib/ecmascript.mjs | 32 +++++++++++++++----- spec/abstractops.html | 60 +++++++++++++++++++++++++++++++++---- spec/calendar.html | 6 ++-- spec/zoneddatetime.html | 4 +-- 4 files changed, 85 insertions(+), 17 deletions(-) diff --git a/polyfill/lib/ecmascript.mjs b/polyfill/lib/ecmascript.mjs index a950109b6..2060fbc78 100644 --- a/polyfill/lib/ecmascript.mjs +++ b/polyfill/lib/ecmascript.mjs @@ -67,6 +67,7 @@ import { SetPrototypeHas, StringFromCharCode, StringPrototypeCharCodeAt, + StringPrototypeIndexOf, StringPrototypeMatch, StringPrototypeReplace, StringPrototypeSlice, @@ -234,11 +235,28 @@ export function RequireString(value) { return value; } -// This function is an enum in the spec, but it's helpful to make it a -// function in the polyfill. -function ToPrimitiveAndRequireString(value) { +function ToSyntacticallyValidMonthCode(value) { value = ToPrimitive(value, StringCtor); - return RequireString(value); + RequireString(value); + if ( + value.length < 3 || + value.length > 4 || + value[0] !== 'M' || + Call(StringPrototypeIndexOf, '0123456789', [value[1]]) === -1 || + Call(StringPrototypeIndexOf, '0123456789', [value[2]]) === -1 || + (value[1] + value[2] === '00' && value[3] !== 'L') || + (value[3] !== 'L' && value[3] !== undefined) + ) { + throw new RangeError(`bad month code ${value}; must match M01-M99 or M00L-M99L`); + } + return value; +} + +function ToOffsetString(value) { + value = ToPrimitive(value, StringCtor); + RequireString(value); + ParseDateTimeUTCOffset(value); + return value; } const CALENDAR_FIELD_KEYS = [ @@ -263,7 +281,7 @@ const BUILTIN_CASTS = new MapCtor([ ['eraYear', ToIntegerWithTruncation], ['year', ToIntegerWithTruncation], ['month', ToPositiveIntegerWithTruncation], - ['monthCode', ToPrimitiveAndRequireString], + ['monthCode', ToSyntacticallyValidMonthCode], ['day', ToPositiveIntegerWithTruncation], ['hour', ToIntegerWithTruncation], ['minute', ToIntegerWithTruncation], @@ -271,7 +289,7 @@ const BUILTIN_CASTS = new MapCtor([ ['millisecond', ToIntegerWithTruncation], ['microsecond', ToIntegerWithTruncation], ['nanosecond', ToIntegerWithTruncation], - ['offset', ToPrimitiveAndRequireString], + ['offset', ToOffsetString], ['timeZone', ToTemporalTimeZoneIdentifier] ]); @@ -2149,7 +2167,7 @@ export function IsOffsetTimeZoneIdentifier(string) { export function ParseDateTimeUTCOffset(string) { const match = Call(RegExpPrototypeExec, OFFSET_WITH_PARTS, [string]); if (!match) { - throw new RangeErrorCtor(`invalid time zone offset: ${string}`); + throw new RangeErrorCtor(`invalid time zone offset: ${string}; must match ±HH:MM[:SS.SSSSSSSSS]`); } const sign = match[1] === '-' ? -1 : +1; const hours = +match[2]; diff --git a/spec/abstractops.html b/spec/abstractops.html index c24aa27d9..38af95959 100644 --- a/spec/abstractops.html +++ b/spec/abstractops.html @@ -632,7 +632,7 @@

1. Let _plainDate_ be ? CreateTemporalDate(_isoDate_, _calendar_). 1. Return the Record { [[PlainRelativeTo]]: _plainDate_, [[ZonedRelativeTo]]: *undefined* }. 1. If _offsetBehaviour_ is ~option~, then - 1. Let _offsetNs_ be ? ParseDateTimeUTCOffset(_offsetString_). + 1. Let _offsetNs_ be ! ParseDateTimeUTCOffset(_offsetString_). 1. Else, 1. Let _offsetNs_ be 0. 1. Let _epochNanoseconds_ be ? InterpretISODateTimeOffset(_isoDate_, _time_, _offsetBehaviour_, _offsetNs_, _timeZone_, ~compatible~, ~reject~, _matchBehaviour_). @@ -1764,6 +1764,56 @@

+ +

+ ToMonthCode ( + _argument_: an ECMAScript language value, + ): either a normal completion containing a String or a throw completion +

+
+
description
+
+ It converts _argument_ to a String, or throws a *TypeError* if that is not possible. + It also requires that the String is a syntactically valid month code, or throws a *RangeError* if it is not. + The month code is not guaranteed to be correct in the context of any particular calendar; for example, some calendars do not have leap months. +
+
+ + 1. Let _monthCode_ be ? ToPrimitive(_argument_, ~string~). + 1. If _monthCode_ is not a String, throw a *TypeError* exception. + 1. If the length of _monthCode_ is not 3 or 4, throw a *RangeError* exception. + 1. If the first code unit of _monthCode_ is not 0x004D (LATIN CAPITAL LETTER M), throw a *RangeError* exception. + 1. If the second code unit of _monthCode_ is not in the inclusive interval from 0x0030 (DIGIT ZERO) to 0x0039 (DIGIT NINE), throw a *RangeError* exception. + 1. If the third code unit of _monthCode_ is not in the inclusive interval from 0x0030 (DIGIT ZERO) to 0x0039 (DIGIT NINE), throw a *RangeError* exception. + 1. If the length of _monthCode_ is 4 and the fourth code unit of _monthCode_ is not 0x004C (LATIN CAPITAL LETTER L), throw a *RangeError* exception. + 1. Let _monthCodeDigits_ be the substring of monthCode from 1 to 3. + 1. Let _monthCodeInteger_ be ℝ(StringToNumber(_monthCodeDigits_)). + 1. If _monthCodeInteger_ is 0 and the length of _monthCode_ is not 4, throw a *RangeError* exception. + 1. Return _monthCode_. + +
+ + +

+ ToOffsetString ( + _argument_: an ECMAScript language value, + ): either a normal completion containing a String or a throw completion +

+
+
description
+
+ It converts _argument_ to a String, or throws a *TypeError* if that is not possible. + It also requires that the String is parseable as a UTC offset string, or throws a *RangeError* if it is not. +
+
+ + 1. Let _offset_ be ? ToPrimitive(_argument_, ~string~). + 1. If _offset_ is not a String, throw a *TypeError* exception. + 1. Perform ? ParseDateTimeUTCOffset(_offset_). + 1. Return _offset_. + +
+

ISODateToFields ( @@ -1832,11 +1882,11 @@

1. Set _value_ to ? ToString(_value_). 1. Else if _Conversion_ is ~to-temporal-time-zone-identifier~, then 1. Set _value_ to ? ToTemporalTimeZoneIdentifier(_value_). + 1. Else if _Conversion_ is ~to-month-code~, then + 1. Set _value_ to ? ToMonthCode(_value_). 1. Else, - 1. Assert: _Conversion_ is ~to-primitive-and-require-string~. - 1. NOTE: Non-primitive values are supported here for consistency with other fields, but such values must coerce to Strings. - 1. Set _value_ to ? ToPrimitive(_value_, ~string~). - 1. If _value_ is not a String, throw a *TypeError* exception. + 1. Assert: _Conversion_ is ~to-offset-string~. + 1. Set _value_ to ? ToOffsetString(_value_). 1. Set _result_'s field whose name is given in the Field Name column of the same row to _value_. 1. Else if _requiredFieldNames_ is a List, then 1. If _requiredFieldNames_ contains _key_, then diff --git a/spec/calendar.html b/spec/calendar.html index d2fff2241..58883d24c 100644 --- a/spec/calendar.html +++ b/spec/calendar.html @@ -258,7 +258,7 @@

Calendar Fields Records

~unset~ *"monthCode"* ~month-code~ - ~to-primitive-and-require-string~ + ~to-month-code~ The month code of the month. @@ -346,9 +346,9 @@

Calendar Fields Records

~unset~ *"offset"* ~offset~ - ~to-primitive-and-require-string~ + ~to-offset-string~ - A string of the form `±HH:MM` that can be parsed by ParseDateTimeUTCOffset. + A string of the form `±HH:MM[:SS.SSSSSSSSS]` that can be parsed by ParseDateTimeUTCOffset. diff --git a/spec/zoneddatetime.html b/spec/zoneddatetime.html index c04415609..f1c6cc151 100644 --- a/spec/zoneddatetime.html +++ b/spec/zoneddatetime.html @@ -530,7 +530,7 @@

Temporal.ZonedDateTime.prototype.with ( _temporalZonedDateTimeLike_ [ , _opt 1. Let _offset_ be ? GetTemporalOffsetOption(_resolvedOptions_, ~prefer~). 1. Let _overflow_ be ? GetTemporalOverflowOption(_resolvedOptions_). 1. Let _dateTimeResult_ be ? InterpretTemporalDateTimeFields(_calendar_, _fields_, _overflow_). - 1. Let _newOffsetNanoseconds_ be ? ParseDateTimeUTCOffset(_fields_.[[OffsetString]]). + 1. Let _newOffsetNanoseconds_ be ! ParseDateTimeUTCOffset(_fields_.[[OffsetString]]). 1. Let _epochNanoseconds_ be ? InterpretISODateTimeOffset(_dateTimeResult_.[[ISODate]], _dateTimeResult_.[[Time]], ~option~, _newOffsetNanoseconds_, _timeZone_, _disambiguation_, _offset_, ~match-exactly~). 1. Return ! CreateTemporalZonedDateTime(_epochNanoseconds_, _timeZone_, _calendar_). @@ -999,7 +999,7 @@

1. Let _time_ be _result_.[[Time]]. 1. Let _offsetNanoseconds_ be 0. 1. If _offsetBehaviour_ is ~option~, then - 1. Set _offsetNanoseconds_ to ? ParseDateTimeUTCOffset(_offsetString_). + 1. Set _offsetNanoseconds_ to ! ParseDateTimeUTCOffset(_offsetString_). 1. Let _epochNanoseconds_ be ? InterpretISODateTimeOffset(_isoDate_, _time_, _offsetBehaviour_, _offsetNanoseconds_, _timeZone_, _disambiguation_, _offsetOption_, _matchBehaviour_). 1. Return ! CreateTemporalZonedDateTime(_epochNanoseconds_, _timeZone_, _calendar_).