Skip to content

Commit

Permalink
Throw RangeError if bad offset in property bags
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
justingrant committed Dec 15, 2021
1 parent 90ee179 commit c83a394
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 15 deletions.
24 changes: 16 additions & 8 deletions polyfill/lib/ecmascript.mjs
Expand Up @@ -379,7 +379,7 @@ export const ES = ObjectAssign({}, ES2020, {
let canonicalIdent = ES.GetCanonicalTimeZoneIdentifier(stringIdent);
if (canonicalIdent) {
canonicalIdent = canonicalIdent.toString();
if (ES.ParseOffsetString(canonicalIdent) !== null) return { offset: canonicalIdent };
if (ES.TestTimeZoneOffsetString(canonicalIdent)) return { offset: canonicalIdent };
return { ianaName: canonicalIdent };
}
} catch {
Expand Down Expand Up @@ -447,7 +447,8 @@ export const ES = ObjectAssign({}, ES2020, {
nanosecond
);
if (epochNs === null) throw new RangeError('DateTime outside of supported range');
const offsetNs = z ? 0 : ES.ParseOffsetString(offset);
if (!z && !offset) throw new RangeError('Temporal.Instant requires a time zone offset');
const offsetNs = z ? 0 : ES.ParseTimeZoneOffsetString(offset);
return epochNs.subtract(offsetNs);
},
RegulateISODateTime: (year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, overflow) => {
Expand Down Expand Up @@ -792,7 +793,7 @@ export const ES = ObjectAssign({}, ES2020, {
if (timeZone) {
timeZone = ES.ToTemporalTimeZone(timeZone);
let offsetNs = 0;
if (offsetBehaviour === 'option') offsetNs = ES.ParseOffsetString(ES.ToString(offset));
if (offsetBehaviour === 'option') offsetNs = ES.ParseTimeZoneOffsetString(ES.ToString(offset));
const epochNanoseconds = ES.InterpretISODateTimeOffset(
year,
month,
Expand Down Expand Up @@ -1373,7 +1374,7 @@ export const ES = ObjectAssign({}, ES2020, {
matchMinute = true; // ISO strings may specify offset with less precision
}
let offsetNs = 0;
if (offsetBehaviour === 'option') offsetNs = ES.ParseOffsetString(offset);
if (offsetBehaviour === 'option') offsetNs = ES.ParseTimeZoneOffsetString(offset);
const disambiguation = ES.ToTemporalDisambiguation(options);
const offsetOpt = ES.ToTemporalOffset(options, 'reject');
const epochNanoseconds = ES.InterpretISODateTimeOffset(
Expand Down Expand Up @@ -2177,9 +2178,14 @@ export const ES = ObjectAssign({}, ES2020, {
return result;
},

ParseOffsetString: (string) => {
TestTimeZoneOffsetString: (string) => {
return OFFSET.test(String(string));
},
ParseTimeZoneOffsetString: (string) => {
const match = OFFSET.exec(String(string));
if (!match) return null;
if (!match) {
throw new RangeError(`invalid time zone offset: ${string}`);
}
const sign = match[1] === '-' || match[1] === '\u2212' ? -1 : +1;
const hours = +match[2];
const minutes = +(match[3] || 0);
Expand All @@ -2188,8 +2194,10 @@ export const ES = ObjectAssign({}, ES2020, {
return sign * (((hours * 60 + minutes) * 60 + seconds) * 1e9 + nanoseconds);
},
GetCanonicalTimeZoneIdentifier: (timeZoneIdentifier) => {
const offsetNs = ES.ParseOffsetString(timeZoneIdentifier);
if (offsetNs !== null) return ES.FormatTimeZoneOffsetString(offsetNs);
if (ES.TestTimeZoneOffsetString(timeZoneIdentifier)) {
const offsetNs = ES.ParseTimeZoneOffsetString(timeZoneIdentifier);
return ES.FormatTimeZoneOffsetString(offsetNs);
}
const formatter = getIntlDateTimeFormatEnUsForTimeZone(String(timeZoneIdentifier));
return formatter.resolvedOptions().timeZone;
},
Expand Down
13 changes: 7 additions & 6 deletions polyfill/lib/timezone.mjs
Expand Up @@ -48,8 +48,9 @@ export class TimeZone {
instant = ES.ToTemporalInstant(instant);
const id = GetSlot(this, TIMEZONE_ID);

const offsetNs = ES.ParseOffsetString(id);
if (offsetNs !== null) return offsetNs;
if (ES.TestTimeZoneOffsetString(id)) {
return ES.ParseTimeZoneOffsetString(id);
}

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

const offsetNs = ES.ParseOffsetString(id);
if (offsetNs !== null) {
if (ES.TestTimeZoneOffsetString(id)) {
const epochNs = ES.GetEpochFromISOParts(
GetSlot(dateTime, ISO_YEAR),
GetSlot(dateTime, ISO_MONTH),
Expand All @@ -90,6 +90,7 @@ export class TimeZone {
GetSlot(dateTime, ISO_NANOSECOND)
);
if (epochNs === null) throw new RangeError('DateTime outside of supported range');
const offsetNs = ES.ParseTimeZoneOffsetString(id);
return [new Instant(epochNs.minus(offsetNs))];
}

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

// Offset time zones or UTC have no transitions
if (ES.ParseOffsetString(id) !== null || id === 'UTC') {
if (ES.TestTimeZoneOffsetString(id) || id === 'UTC') {
return null;
}

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

// Offset time zones or UTC have no transitions
if (ES.ParseOffsetString(id) !== null || id === 'UTC') {
if (ES.TestTimeZoneOffsetString(id) || id === 'UTC') {
return null;
}

Expand Down
2 changes: 1 addition & 1 deletion polyfill/lib/zoneddatetime.mjs
Expand Up @@ -223,7 +223,7 @@ export class ZonedDateTime {
fields = ES.PrepareTemporalFields(fields, entries);
let { year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } =
ES.InterpretTemporalDateTimeFields(calendar, fields, options);
const offsetNs = ES.ParseOffsetString(fields.offset);
const offsetNs = ES.ParseTimeZoneOffsetString(fields.offset);
const epochNanoseconds = ES.InterpretISODateTimeOffset(
year,
month,
Expand Down
47 changes: 47 additions & 0 deletions polyfill/test/duration.mjs
Expand Up @@ -685,6 +685,15 @@ describe('Duration', () => {
throws(() => oneDay.add(hours24, { relativeTo: { year: 2019, month: 11 } }), TypeError);
throws(() => oneDay.add(hours24, { relativeTo: { year: 2019, day: 3 } }), TypeError);
});
it('throws with invalid offset in relativeTo', () => {
throws(
() =>
Temporal.Duration.from('P2D').add('P1M', {
relativeTo: { year: 2021, month: 11, day: 26, offset: '+088:00', timeZone: 'Europe/London' }
}),
RangeError
);
});
});
describe('Duration.subtract()', () => {
const duration = Duration.from({ days: 3, hours: 1, minutes: 10 });
Expand Down Expand Up @@ -928,6 +937,15 @@ describe('Duration', () => {
equal(zero2.microseconds, 0);
equal(zero2.nanoseconds, 0);
});
it('throws with invalid offset in relativeTo', () => {
throws(
() =>
Temporal.Duration.from('P2D').subtract('P1M', {
relativeTo: { year: 2021, month: 11, day: 26, offset: '+088:00', timeZone: 'Europe/London' }
}),
RangeError
);
});
});
describe('Duration.abs()', () => {
it('makes a copy of a positive duration', () => {
Expand Down Expand Up @@ -1512,6 +1530,16 @@ describe('Duration', () => {
equal(`${yearAndHalf.round({ relativeTo: '2020-01-01', smallestUnit: 'years' })}`, 'P1Y');
equal(`${yearAndHalf.round({ relativeTo: '2020-07-01', smallestUnit: 'years' })}`, 'P2Y');
});
it('throws with invalid offset in relativeTo', () => {
throws(
() =>
Temporal.Duration.from('P1M280D').round({
smallestUnit: 'month',
relativeTo: { year: 2021, month: 11, day: 26, offset: '+088:00', timeZone: 'Europe/London' }
}),
RangeError
);
});
});
describe('Duration.total()', () => {
const d = new Duration(5, 5, 5, 5, 5, 5, 5, 5, 5, 5);
Expand Down Expand Up @@ -1851,6 +1879,16 @@ describe('Duration', () => {
equal(d.total({ unit: 'microsecond', relativeTo }), d.total({ unit: 'microseconds', relativeTo }));
equal(d.total({ unit: 'nanosecond', relativeTo }), d.total({ unit: 'nanoseconds', relativeTo }));
});
it('throws with invalid offset in relativeTo', () => {
throws(
() =>
Temporal.Duration.from('P1M280D').total({
unit: 'month',
relativeTo: { year: 2021, month: 11, day: 26, offset: '+088:00', timeZone: 'Europe/London' }
}),
RangeError
);
});
});
describe('Duration.compare', () => {
describe('time units only', () => {
Expand Down Expand Up @@ -1947,6 +1985,15 @@ describe('Duration', () => {
it('does not lose precision when totaling everything down to nanoseconds', () => {
notEqual(Duration.compare({ days: 200 }, { days: 200, nanoseconds: 1 }), 0);
});
it('throws with invalid offset in relativeTo', () => {
throws(() => {
const d1 = Temporal.Duration.from('P1M280D');
const d2 = Temporal.Duration.from('P1M281D');
Temporal.Duration.compare(d1, d2, {
relativeTo: { year: 2021, month: 11, day: 26, offset: '+088:00', timeZone: 'Europe/London' }
});
}, RangeError);
});
});
});

Expand Down
71 changes: 71 additions & 0 deletions polyfill/test/zoneddatetime.mjs
Expand Up @@ -589,6 +589,23 @@ describe('ZonedDateTime', () => {
});
equal(`${zdt}`, '1976-11-18T00:00:00-10:30[-10:30]');
});
it('throws with invalid offset', () => {
const offsets = ['use', 'prefer', 'ignore', 'reject'];
offsets.forEach((offset) => {
throws(() => {
Temporal.ZonedDateTime.from(
{
year: 2021,
month: 11,
day: 26,
offset: '+099:00',
timeZone: 'Europe/London'
},
{ offset }
);
}, RangeError);
});
});
describe('Overflow option', () => {
const bad = { year: 2019, month: 1, day: 32, timeZone: lagos };
it('reject', () => throws(() => ZonedDateTime.from(bad, { overflow: 'reject' }), RangeError));
Expand Down Expand Up @@ -1025,6 +1042,14 @@ describe('ZonedDateTime', () => {
throws(() => zdt.with('12:00'), TypeError);
throws(() => zdt.with('invalid'), TypeError);
});
it('throws with invalid offset', () => {
const offsets = ['use', 'prefer', 'ignore', 'reject'];
offsets.forEach((offset) => {
throws(() => {
Temporal.ZonedDateTime.from('2022-11-26[Europe/London]').with({ offset: '+088:00' }, { offset });
}, RangeError);
});
});
});

describe('.withPlainTime manipulation', () => {
Expand Down Expand Up @@ -1617,6 +1642,18 @@ describe('ZonedDateTime', () => {
equal(`${dt1.until(dt2, { smallestUnit: 'years', roundingMode: 'halfExpand' })}`, 'P2Y');
equal(`${dt2.until(dt1, { smallestUnit: 'years', roundingMode: 'halfExpand' })}`, '-P1Y');
});
it('throws with invalid offset', () => {
throws(() => {
const zdt = ZonedDateTime.from('2019-01-01T00:00+00:00[UTC]');
zdt.until({
year: 2021,
month: 11,
day: 26,
offset: '+099:00',
timeZone: 'Europe/London'
});
}, RangeError);
});
});
describe('ZonedDateTime.since()', () => {
const zdt = ZonedDateTime.from('1976-11-18T15:23:30.123456789+01:00[Europe/Vienna]');
Expand Down Expand Up @@ -1948,6 +1985,18 @@ describe('ZonedDateTime', () => {
equal(`${dt2.since(dt1, { smallestUnit: 'years', roundingMode: 'halfExpand' })}`, 'P1Y');
equal(`${dt1.since(dt2, { smallestUnit: 'years', roundingMode: 'halfExpand' })}`, '-P2Y');
});
it('throws with invalid offset', () => {
throws(() => {
const zdt = ZonedDateTime.from('2019-01-01T00:00+00:00[UTC]');
zdt.since({
year: 2021,
month: 11,
day: 26,
offset: '+099:00',
timeZone: 'Europe/London'
});
}, RangeError);
});
});

describe('ZonedDateTime.round()', () => {
Expand Down Expand Up @@ -2188,6 +2237,17 @@ describe('ZonedDateTime', () => {
TypeError
);
});
it('throws with invalid offset', () => {
throws(() => {
zdt.equals({
year: 2021,
month: 11,
day: 26,
offset: '+099:00',
timeZone: 'Europe/London'
});
}, RangeError);
});
});
describe('ZonedDateTime.toString()', () => {
const zdt1 = ZonedDateTime.from('1976-11-18T15:23+01:00[Europe/Vienna]');
Expand Down Expand Up @@ -2894,6 +2954,17 @@ describe('ZonedDateTime', () => {
equal(ZonedDateTime.compare(clockBefore, clockAfter), 1);
equal(Temporal.PlainDateTime.compare(clockBefore.toPlainDateTime(), clockAfter.toPlainDateTime()), -1);
});
it('throws with invalid offset', () => {
throws(() => {
Temporal.ZonedDateTime.compare(zdt1, {
year: 2021,
month: 11,
day: 26,
offset: '+099:00',
timeZone: 'Europe/London'
});
}, RangeError);
});
});
});

Expand Down

0 comments on commit c83a394

Please sign in to comment.