Skip to content

Commit

Permalink
Tighten validation of offset string and month code in property bags
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ptomato committed Oct 7, 2024
1 parent ed2d49a commit 8048bfe
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 17 deletions.
32 changes: 25 additions & 7 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import {
SetPrototypeHas,
StringFromCharCode,
StringPrototypeCharCodeAt,
StringPrototypeIndexOf,
StringPrototypeMatch,
StringPrototypeReplace,
StringPrototypeSlice,
Expand Down Expand Up @@ -240,11 +241,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 = [
Expand All @@ -269,15 +287,15 @@ const BUILTIN_CASTS = new MapCtor([
['eraYear', ToIntegerWithTruncation],
['year', ToIntegerWithTruncation],
['month', ToPositiveIntegerWithTruncation],
['monthCode', ToPrimitiveAndRequireString],
['monthCode', ToSyntacticallyValidMonthCode],
['day', ToPositiveIntegerWithTruncation],
['hour', ToIntegerWithTruncation],
['minute', ToIntegerWithTruncation],
['second', ToIntegerWithTruncation],
['millisecond', ToIntegerWithTruncation],
['microsecond', ToIntegerWithTruncation],
['nanosecond', ToIntegerWithTruncation],
['offset', ToPrimitiveAndRequireString],
['offset', ToOffsetString],
['timeZone', ToTemporalTimeZoneIdentifier]
]);

Expand Down Expand Up @@ -2377,7 +2395,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];
Expand Down
60 changes: 55 additions & 5 deletions spec/abstractops.html
Original file line number Diff line number Diff line change
Expand Up @@ -628,7 +628,7 @@ <h1>
1. Let _plainDate_ be ? CreateTemporalDate(_result_.[[Year]], _result_.[[Month]], _result_.[[Day]], _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(_result_.[[Year]], _result_.[[Month]], _result_.[[Day]], _result_.[[Time]], _offsetBehaviour_, _offsetNs_, _timeZone_, ~compatible~, ~reject~, _matchBehaviour_).
Expand Down Expand Up @@ -1761,6 +1761,56 @@ <h1>
</emu-alg>
</emu-clause>

<emu-clause id="sec-temporal-tomonthcode" type="abstract operation">
<h1>
ToMonthCode (
_argument_: an ECMAScript language value,
): either a normal completion containing a String or a throw completion
</h1>
<dl class="header">
<dt>description</dt>
<dd>
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.
</dd>
</dl>
<emu-alg>
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_.
</emu-alg>
</emu-clause>

<emu-clause id="sec-temporal-tooffsetstring" type="abstract operation">
<h1>
ToOffsetString (
_argument_: an ECMAScript language value,
): either a normal completion containing a String or a throw completion
</h1>
<dl class="header">
<dt>description</dt>
<dd>
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.
</dd>
</dl>
<emu-alg>
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_.
</emu-alg>
</emu-clause>

<emu-clause id="sec-temporal-temporalobjecttofields" type="abstract operation">
<h1>
TemporalObjectToFields (
Expand Down Expand Up @@ -1852,11 +1902,11 @@ <h1>
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
Expand Down
6 changes: 3 additions & 3 deletions spec/calendar.html
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ <h1>Calendar Fields Records</h1>
<td>~unset~</td>
<td>*"monthCode"*</td>
<td>~month-code~</td>
<td>~to-primitive-and-require-string~</td>
<td>~to-month-code~</td>
<td>
The month code of the month.
</td>
Expand Down Expand Up @@ -346,9 +346,9 @@ <h1>Calendar Fields Records</h1>
<td>~unset~</td>
<td>*"offset"*</td>
<td>~offset~</td>
<td>~to-primitive-and-require-string~</td>
<td>~to-offset-string~</td>
<td>
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.
</td>
</tr>
<tr>
Expand Down
4 changes: 2 additions & 2 deletions spec/zoneddatetime.html
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,7 @@ <h1>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_.[[Year]], _dateTimeResult_.[[Month]], _dateTimeResult_.[[Day]], _dateTimeResult_.[[Time]], ~option~, _newOffsetNanoseconds_, _timeZone_, _disambiguation_, _offset_, ~match-exactly~).
1. Return ! CreateTemporalZonedDateTime(_epochNanoseconds_, _timeZone_, _calendar_).
</emu-alg>
Expand Down Expand Up @@ -1018,7 +1018,7 @@ <h1>
1. Perform ? GetTemporalOverflowOption(_resolvedOptions_).
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(_result_.[[Year]], _result_.[[Month]], _result_.[[Day]], _result_.[[Time]], _offsetBehaviour_, _offsetNanoseconds_, _timeZone_, _disambiguation_, _offsetOption_, _matchBehaviour_).
1. Return ! CreateTemporalZonedDateTime(_epochNanoseconds_, _timeZone_, _calendar_).
</emu-alg>
Expand Down

0 comments on commit 8048bfe

Please sign in to comment.