Skip to content

Commit 445079a

Browse files
justingrantptomato
authored andcommitted
Throw RangeError if bad offset in property bags
Ensure that, per spec, a RangeError is thrown when an invalid offset is passed to ParseTimeZoneOffsetString. Note that validation of the offset in ISO strings is handled separately; this PR only fixes cases where an offset is parsed on its own, e.g. when property bag inputs are used in `ZonedDateTime#[with|from|equals|until|since|compare]` or as options in `Duration#[add|subtract|compare|round|total]`. The problem was also present in `TimeZone.from` and `ZonedDateTime#withTimeZone` but was caught downstream so didn't fail any previous tests. This commit also renames `ParseOffsetString` to `ParseTimeZoneOffsetString` which is the AO name in the spec. This commit also adds a new `ES.TestTimeZoneOffsetString` function which checks to see if the string matches the offset regex. This is used in cases where throwing on invalid offset strings is not desired.
1 parent 769dbc2 commit 445079a

File tree

5 files changed

+142
-15
lines changed

5 files changed

+142
-15
lines changed

polyfill/lib/ecmascript.mjs

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,7 @@ export const ES = ObjectAssign({}, ES2020, {
379379
let canonicalIdent = ES.GetCanonicalTimeZoneIdentifier(stringIdent);
380380
if (canonicalIdent) {
381381
canonicalIdent = canonicalIdent.toString();
382-
if (ES.ParseOffsetString(canonicalIdent) !== null) return { offset: canonicalIdent };
382+
if (ES.TestTimeZoneOffsetString(canonicalIdent)) return { offset: canonicalIdent };
383383
return { ianaName: canonicalIdent };
384384
}
385385
} catch {
@@ -447,7 +447,8 @@ export const ES = ObjectAssign({}, ES2020, {
447447
nanosecond
448448
);
449449
if (epochNs === null) throw new RangeError('DateTime outside of supported range');
450-
const offsetNs = z ? 0 : ES.ParseOffsetString(offset);
450+
if (!z && !offset) throw new RangeError('Temporal.Instant requires a time zone offset');
451+
const offsetNs = z ? 0 : ES.ParseTimeZoneOffsetString(offset);
451452
return epochNs.subtract(offsetNs);
452453
},
453454
RegulateISODateTime: (year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, overflow) => {
@@ -792,7 +793,7 @@ export const ES = ObjectAssign({}, ES2020, {
792793
if (timeZone) {
793794
timeZone = ES.ToTemporalTimeZone(timeZone);
794795
let offsetNs = 0;
795-
if (offsetBehaviour === 'option') offsetNs = ES.ParseOffsetString(ES.ToString(offset));
796+
if (offsetBehaviour === 'option') offsetNs = ES.ParseTimeZoneOffsetString(ES.ToString(offset));
796797
const epochNanoseconds = ES.InterpretISODateTimeOffset(
797798
year,
798799
month,
@@ -1373,7 +1374,7 @@ export const ES = ObjectAssign({}, ES2020, {
13731374
matchMinute = true; // ISO strings may specify offset with less precision
13741375
}
13751376
let offsetNs = 0;
1376-
if (offsetBehaviour === 'option') offsetNs = ES.ParseOffsetString(offset);
1377+
if (offsetBehaviour === 'option') offsetNs = ES.ParseTimeZoneOffsetString(offset);
13771378
const disambiguation = ES.ToTemporalDisambiguation(options);
13781379
const offsetOpt = ES.ToTemporalOffset(options, 'reject');
13791380
const epochNanoseconds = ES.InterpretISODateTimeOffset(
@@ -2177,9 +2178,14 @@ export const ES = ObjectAssign({}, ES2020, {
21772178
return result;
21782179
},
21792180

2180-
ParseOffsetString: (string) => {
2181+
TestTimeZoneOffsetString: (string) => {
2182+
return OFFSET.test(String(string));
2183+
},
2184+
ParseTimeZoneOffsetString: (string) => {
21812185
const match = OFFSET.exec(String(string));
2182-
if (!match) return null;
2186+
if (!match) {
2187+
throw new RangeError(`invalid time zone offset: ${string}`);
2188+
}
21832189
const sign = match[1] === '-' || match[1] === '\u2212' ? -1 : +1;
21842190
const hours = +match[2];
21852191
const minutes = +(match[3] || 0);
@@ -2188,8 +2194,10 @@ export const ES = ObjectAssign({}, ES2020, {
21882194
return sign * (((hours * 60 + minutes) * 60 + seconds) * 1e9 + nanoseconds);
21892195
},
21902196
GetCanonicalTimeZoneIdentifier: (timeZoneIdentifier) => {
2191-
const offsetNs = ES.ParseOffsetString(timeZoneIdentifier);
2192-
if (offsetNs !== null) return ES.FormatTimeZoneOffsetString(offsetNs);
2197+
if (ES.TestTimeZoneOffsetString(timeZoneIdentifier)) {
2198+
const offsetNs = ES.ParseTimeZoneOffsetString(timeZoneIdentifier);
2199+
return ES.FormatTimeZoneOffsetString(offsetNs);
2200+
}
21932201
const formatter = getIntlDateTimeFormatEnUsForTimeZone(String(timeZoneIdentifier));
21942202
return formatter.resolvedOptions().timeZone;
21952203
},

polyfill/lib/timezone.mjs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@ export class TimeZone {
4848
instant = ES.ToTemporalInstant(instant);
4949
const id = GetSlot(this, TIMEZONE_ID);
5050

51-
const offsetNs = ES.ParseOffsetString(id);
52-
if (offsetNs !== null) return offsetNs;
51+
if (ES.TestTimeZoneOffsetString(id)) {
52+
return ES.ParseTimeZoneOffsetString(id);
53+
}
5354

5455
return ES.GetIANATimeZoneOffsetNanoseconds(GetSlot(instant, EPOCHNANOSECONDS), id);
5556
}
@@ -76,8 +77,7 @@ export class TimeZone {
7677
const Instant = GetIntrinsic('%Temporal.Instant%');
7778
const id = GetSlot(this, TIMEZONE_ID);
7879

79-
const offsetNs = ES.ParseOffsetString(id);
80-
if (offsetNs !== null) {
80+
if (ES.TestTimeZoneOffsetString(id)) {
8181
const epochNs = ES.GetEpochFromISOParts(
8282
GetSlot(dateTime, ISO_YEAR),
8383
GetSlot(dateTime, ISO_MONTH),
@@ -90,6 +90,7 @@ export class TimeZone {
9090
GetSlot(dateTime, ISO_NANOSECOND)
9191
);
9292
if (epochNs === null) throw new RangeError('DateTime outside of supported range');
93+
const offsetNs = ES.ParseTimeZoneOffsetString(id);
9394
return [new Instant(epochNs.minus(offsetNs))];
9495
}
9596

@@ -113,7 +114,7 @@ export class TimeZone {
113114
const id = GetSlot(this, TIMEZONE_ID);
114115

115116
// Offset time zones or UTC have no transitions
116-
if (ES.ParseOffsetString(id) !== null || id === 'UTC') {
117+
if (ES.TestTimeZoneOffsetString(id) || id === 'UTC') {
117118
return null;
118119
}
119120

@@ -128,7 +129,7 @@ export class TimeZone {
128129
const id = GetSlot(this, TIMEZONE_ID);
129130

130131
// Offset time zones or UTC have no transitions
131-
if (ES.ParseOffsetString(id) !== null || id === 'UTC') {
132+
if (ES.TestTimeZoneOffsetString(id) || id === 'UTC') {
132133
return null;
133134
}
134135

polyfill/lib/zoneddatetime.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ export class ZonedDateTime {
223223
fields = ES.PrepareTemporalFields(fields, entries);
224224
let { year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } =
225225
ES.InterpretTemporalDateTimeFields(calendar, fields, options);
226-
const offsetNs = ES.ParseOffsetString(fields.offset);
226+
const offsetNs = ES.ParseTimeZoneOffsetString(fields.offset);
227227
const epochNanoseconds = ES.InterpretISODateTimeOffset(
228228
year,
229229
month,

polyfill/test/duration.mjs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,15 @@ describe('Duration', () => {
687687
throws(() => oneDay.add(hours24, { relativeTo: { year: 2019, month: 11 } }), TypeError);
688688
throws(() => oneDay.add(hours24, { relativeTo: { year: 2019, day: 3 } }), TypeError);
689689
});
690+
it('throws with invalid offset in relativeTo', () => {
691+
throws(
692+
() =>
693+
Temporal.Duration.from('P2D').add('P1M', {
694+
relativeTo: { year: 2021, month: 11, day: 26, offset: '+088:00', timeZone: 'Europe/London' }
695+
}),
696+
RangeError
697+
);
698+
});
690699
});
691700
describe('Duration.subtract()', () => {
692701
const duration = Duration.from({ days: 3, hours: 1, minutes: 10 });
@@ -930,6 +939,15 @@ describe('Duration', () => {
930939
equal(zero2.microseconds, 0);
931940
equal(zero2.nanoseconds, 0);
932941
});
942+
it('throws with invalid offset in relativeTo', () => {
943+
throws(
944+
() =>
945+
Temporal.Duration.from('P2D').subtract('P1M', {
946+
relativeTo: { year: 2021, month: 11, day: 26, offset: '+088:00', timeZone: 'Europe/London' }
947+
}),
948+
RangeError
949+
);
950+
});
933951
});
934952
describe('Duration.abs()', () => {
935953
it('makes a copy of a positive duration', () => {
@@ -1514,6 +1532,16 @@ describe('Duration', () => {
15141532
equal(`${yearAndHalf.round({ relativeTo: '2020-01-01', smallestUnit: 'years' })}`, 'P1Y');
15151533
equal(`${yearAndHalf.round({ relativeTo: '2020-07-01', smallestUnit: 'years' })}`, 'P2Y');
15161534
});
1535+
it('throws with invalid offset in relativeTo', () => {
1536+
throws(
1537+
() =>
1538+
Temporal.Duration.from('P1M280D').round({
1539+
smallestUnit: 'month',
1540+
relativeTo: { year: 2021, month: 11, day: 26, offset: '+088:00', timeZone: 'Europe/London' }
1541+
}),
1542+
RangeError
1543+
);
1544+
});
15171545
});
15181546
describe('Duration.total()', () => {
15191547
const d = new Duration(5, 5, 5, 5, 5, 5, 5, 5, 5, 5);
@@ -1853,6 +1881,16 @@ describe('Duration', () => {
18531881
equal(d.total({ unit: 'microsecond', relativeTo }), d.total({ unit: 'microseconds', relativeTo }));
18541882
equal(d.total({ unit: 'nanosecond', relativeTo }), d.total({ unit: 'nanoseconds', relativeTo }));
18551883
});
1884+
it('throws with invalid offset in relativeTo', () => {
1885+
throws(
1886+
() =>
1887+
Temporal.Duration.from('P1M280D').total({
1888+
unit: 'month',
1889+
relativeTo: { year: 2021, month: 11, day: 26, offset: '+088:00', timeZone: 'Europe/London' }
1890+
}),
1891+
RangeError
1892+
);
1893+
});
18561894
});
18571895
describe('Duration.compare', () => {
18581896
describe('time units only', () => {
@@ -1949,6 +1987,15 @@ describe('Duration', () => {
19491987
it('does not lose precision when totaling everything down to nanoseconds', () => {
19501988
notEqual(Duration.compare({ days: 200 }, { days: 200, nanoseconds: 1 }), 0);
19511989
});
1990+
it('throws with invalid offset in relativeTo', () => {
1991+
throws(() => {
1992+
const d1 = Temporal.Duration.from('P1M280D');
1993+
const d2 = Temporal.Duration.from('P1M281D');
1994+
Temporal.Duration.compare(d1, d2, {
1995+
relativeTo: { year: 2021, month: 11, day: 26, offset: '+088:00', timeZone: 'Europe/London' }
1996+
});
1997+
}, RangeError);
1998+
});
19521999
});
19532000
});
19542001

polyfill/test/zoneddatetime.mjs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,23 @@ describe('ZonedDateTime', () => {
589589
});
590590
equal(`${zdt}`, '1976-11-18T00:00:00-10:30[-10:30]');
591591
});
592+
it('throws with invalid offset', () => {
593+
const offsets = ['use', 'prefer', 'ignore', 'reject'];
594+
offsets.forEach((offset) => {
595+
throws(() => {
596+
Temporal.ZonedDateTime.from(
597+
{
598+
year: 2021,
599+
month: 11,
600+
day: 26,
601+
offset: '+099:00',
602+
timeZone: 'Europe/London'
603+
},
604+
{ offset }
605+
);
606+
}, RangeError);
607+
});
608+
});
592609
describe('Overflow option', () => {
593610
const bad = { year: 2019, month: 1, day: 32, timeZone: lagos };
594611
it('reject', () => throws(() => ZonedDateTime.from(bad, { overflow: 'reject' }), RangeError));
@@ -1025,6 +1042,14 @@ describe('ZonedDateTime', () => {
10251042
throws(() => zdt.with('12:00'), TypeError);
10261043
throws(() => zdt.with('invalid'), TypeError);
10271044
});
1045+
it('throws with invalid offset', () => {
1046+
const offsets = ['use', 'prefer', 'ignore', 'reject'];
1047+
offsets.forEach((offset) => {
1048+
throws(() => {
1049+
Temporal.ZonedDateTime.from('2022-11-26[Europe/London]').with({ offset: '+088:00' }, { offset });
1050+
}, RangeError);
1051+
});
1052+
});
10281053
});
10291054

10301055
describe('.withPlainTime manipulation', () => {
@@ -1617,6 +1642,18 @@ describe('ZonedDateTime', () => {
16171642
equal(`${dt1.until(dt2, { smallestUnit: 'years', roundingMode: 'halfExpand' })}`, 'P2Y');
16181643
equal(`${dt2.until(dt1, { smallestUnit: 'years', roundingMode: 'halfExpand' })}`, '-P1Y');
16191644
});
1645+
it('throws with invalid offset', () => {
1646+
throws(() => {
1647+
const zdt = ZonedDateTime.from('2019-01-01T00:00+00:00[UTC]');
1648+
zdt.until({
1649+
year: 2021,
1650+
month: 11,
1651+
day: 26,
1652+
offset: '+099:00',
1653+
timeZone: 'Europe/London'
1654+
});
1655+
}, RangeError);
1656+
});
16201657
});
16211658
describe('ZonedDateTime.since()', () => {
16221659
const zdt = ZonedDateTime.from('1976-11-18T15:23:30.123456789+01:00[Europe/Vienna]');
@@ -1948,6 +1985,18 @@ describe('ZonedDateTime', () => {
19481985
equal(`${dt2.since(dt1, { smallestUnit: 'years', roundingMode: 'halfExpand' })}`, 'P1Y');
19491986
equal(`${dt1.since(dt2, { smallestUnit: 'years', roundingMode: 'halfExpand' })}`, '-P2Y');
19501987
});
1988+
it('throws with invalid offset', () => {
1989+
throws(() => {
1990+
const zdt = ZonedDateTime.from('2019-01-01T00:00+00:00[UTC]');
1991+
zdt.since({
1992+
year: 2021,
1993+
month: 11,
1994+
day: 26,
1995+
offset: '+099:00',
1996+
timeZone: 'Europe/London'
1997+
});
1998+
}, RangeError);
1999+
});
19512000
});
19522001

19532002
describe('ZonedDateTime.round()', () => {
@@ -2188,6 +2237,17 @@ describe('ZonedDateTime', () => {
21882237
TypeError
21892238
);
21902239
});
2240+
it('throws with invalid offset', () => {
2241+
throws(() => {
2242+
zdt.equals({
2243+
year: 2021,
2244+
month: 11,
2245+
day: 26,
2246+
offset: '+099:00',
2247+
timeZone: 'Europe/London'
2248+
});
2249+
}, RangeError);
2250+
});
21912251
});
21922252
describe('ZonedDateTime.toString()', () => {
21932253
const zdt1 = ZonedDateTime.from('1976-11-18T15:23+01:00[Europe/Vienna]');
@@ -2895,6 +2955,17 @@ describe('ZonedDateTime', () => {
28952955
equal(ZonedDateTime.compare(clockBefore, clockAfter), 1);
28962956
equal(Temporal.PlainDateTime.compare(clockBefore.toPlainDateTime(), clockAfter.toPlainDateTime()), -1);
28972957
});
2958+
it('throws with invalid offset', () => {
2959+
throws(() => {
2960+
Temporal.ZonedDateTime.compare(zdt1, {
2961+
year: 2021,
2962+
month: 11,
2963+
day: 26,
2964+
offset: '+099:00',
2965+
timeZone: 'Europe/London'
2966+
});
2967+
}, RangeError);
2968+
});
28982969
});
28992970
});
29002971

0 commit comments

Comments
 (0)