From cfcee420e86214a109a921241253e3034c1f1a7b Mon Sep 17 00:00:00 2001 From: lutovich Date: Thu, 21 Jun 2018 17:24:00 +0200 Subject: [PATCH] Value range checks for temporal types Added checks for all temporal types to assert that values used to create them are in expected numeric ranges. This should disallow negative months, days more than 31 and things like that. --- src/v1/internal/temporal-util.js | 110 +++++++++++++++++++++++++ src/v1/temporal-types.js | 50 ++++++------ test/internal/temporal-util.test.js | 122 ++++++++++++++++++++++++++++ test/v1/temporal-types.test.js | 69 +++++++++++++++- 4 files changed, 325 insertions(+), 26 deletions(-) diff --git a/src/v1/internal/temporal-util.js b/src/v1/internal/temporal-util.js index 291878ae2..f94a642e6 100644 --- a/src/v1/internal/temporal-util.js +++ b/src/v1/internal/temporal-util.js @@ -19,6 +19,8 @@ import {int, isInt} from '../integer'; import {Date, LocalDateTime, LocalTime} from '../temporal-types'; +import {assertNumberOrInteger} from './util'; +import {newError} from '../error'; /* Code in this util should be compatible with code in the database that uses JSR-310 java.time APIs. @@ -31,6 +33,36 @@ import {Date, LocalDateTime, LocalTime} from '../temporal-types'; conversion functions. */ +class ValueRange { + + constructor(min, max) { + this._minNumber = min; + this._maxNumber = max; + this._minInteger = int(min); + this._maxInteger = int(max); + } + + contains(value) { + if (isInt(value)) { + return value.greaterThanOrEqual(this._minInteger) && value.lessThanOrEqual(this._maxInteger); + } else { + return value >= this._minNumber && value <= this._maxNumber; + } + } + + toString() { + return `[${this._minNumber}, ${this._maxNumber}]`; + } +} + +const YEAR_RANGE = new ValueRange(-999999999, 999999999); +const MONTH_OF_YEAR_RANGE = new ValueRange(1, 12); +const DAY_OF_MONTH_RANGE = new ValueRange(1, 31); +const HOUR_OF_DAY_RANGE = new ValueRange(0, 23); +const MINUTE_OF_HOUR_RANGE = new ValueRange(0, 59); +const SECOND_OF_MINUTE_RANGE = new ValueRange(0, 59); +const NANOSECOND_OF_SECOND_RANGE = new ValueRange(0, 999999999); + const MINUTES_PER_HOUR = 60; const SECONDS_PER_MINUTE = 60; const SECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR; @@ -286,6 +318,84 @@ export function timeZoneOffsetInSeconds(standardDate) { return standardDate.getTimezoneOffset() * SECONDS_PER_MINUTE; } +/** + * Assert that the year value is valid. + * @param {Integer|number} year the value to check. + * @return {Integer|number} the value of the year if it is valid. Exception is thrown otherwise. + */ +export function assertValidYear(year) { + return assertValidTemporalValue(year, YEAR_RANGE, 'Year'); +} + +/** + * Assert that the month value is valid. + * @param {Integer|number} month the value to check. + * @return {Integer|number} the value of the month if it is valid. Exception is thrown otherwise. + */ +export function assertValidMonth(month) { + return assertValidTemporalValue(month, MONTH_OF_YEAR_RANGE, 'Month'); +} + +/** + * Assert that the day value is valid. + * @param {Integer|number} day the value to check. + * @return {Integer|number} the value of the day if it is valid. Exception is thrown otherwise. + */ +export function assertValidDay(day) { + return assertValidTemporalValue(day, DAY_OF_MONTH_RANGE, 'Day'); +} + +/** + * Assert that the hour value is valid. + * @param {Integer|number} hour the value to check. + * @return {Integer|number} the value of the hour if it is valid. Exception is thrown otherwise. + */ +export function assertValidHour(hour) { + return assertValidTemporalValue(hour, HOUR_OF_DAY_RANGE, 'Hour'); +} + +/** + * Assert that the minute value is valid. + * @param {Integer|number} minute the value to check. + * @return {Integer|number} the value of the minute if it is valid. Exception is thrown otherwise. + */ +export function assertValidMinute(minute) { + return assertValidTemporalValue(minute, MINUTE_OF_HOUR_RANGE, 'Minute'); +} + +/** + * Assert that the second value is valid. + * @param {Integer|number} second the value to check. + * @return {Integer|number} the value of the second if it is valid. Exception is thrown otherwise. + */ +export function assertValidSecond(second) { + return assertValidTemporalValue(second, SECOND_OF_MINUTE_RANGE, 'Second'); +} + +/** + * Assert that the nanosecond value is valid. + * @param {Integer|number} nanosecond the value to check. + * @return {Integer|number} the value of the nanosecond if it is valid. Exception is thrown otherwise. + */ +export function assertValidNanosecond(nanosecond) { + return assertValidTemporalValue(nanosecond, NANOSECOND_OF_SECOND_RANGE, 'Nanosecond'); +} + +/** + * Check if the given value is of expected type and is in the expected range. + * @param {Integer|number} value the value to check. + * @param {ValueRange} range the range. + * @param {string} name the name of the value. + * @return {Integer|number} the value if valid. Exception is thrown otherwise. + */ +function assertValidTemporalValue(value, range, name) { + assertNumberOrInteger(value, name); + if (!range.contains(value)) { + throw newError(`${name} is expected to be in range ${range} but was: ${value}`); + } + return value; +} + /** * Converts given local time into a single integer representing this same time in seconds of the day. Nanoseconds are skipped. * @param {Integer|number|string} hour the hour of the local time. diff --git a/src/v1/temporal-types.js b/src/v1/temporal-types.js index 7f77deb15..641ca8797 100644 --- a/src/v1/temporal-types.js +++ b/src/v1/temporal-types.js @@ -87,10 +87,10 @@ export class LocalTime { * @param {Integer|number} nanosecond the nanosecond for the new local time. */ constructor(hour, minute, second, nanosecond) { - this.hour = assertNumberOrInteger(hour, 'Hour'); - this.minute = assertNumberOrInteger(minute, 'Minute'); - this.second = assertNumberOrInteger(second, 'Second'); - this.nanosecond = assertNumberOrInteger(nanosecond, 'Nanosecond'); + this.hour = util.assertValidHour(hour); + this.minute = util.assertValidMinute(minute); + this.second = util.assertValidSecond(second); + this.nanosecond = util.assertValidNanosecond(nanosecond); Object.freeze(this); } @@ -142,10 +142,10 @@ export class Time { * @param {Integer|number} timeZoneOffsetSeconds the time zone offset in seconds. */ constructor(hour, minute, second, nanosecond, timeZoneOffsetSeconds) { - this.hour = assertNumberOrInteger(hour, 'Hour'); - this.minute = assertNumberOrInteger(minute, 'Minute'); - this.second = assertNumberOrInteger(second, 'Second'); - this.nanosecond = assertNumberOrInteger(nanosecond, 'Nanosecond'); + this.hour = util.assertValidHour(hour); + this.minute = util.assertValidMinute(minute); + this.second = util.assertValidSecond(second); + this.nanosecond = util.assertValidNanosecond(nanosecond); this.timeZoneOffsetSeconds = assertNumberOrInteger(timeZoneOffsetSeconds, 'Time zone offset in seconds'); Object.freeze(this); } @@ -197,9 +197,9 @@ export class Date { * @param {Integer|number} day the day for the new local date. */ constructor(year, month, day) { - this.year = assertNumberOrInteger(year, 'Year'); - this.month = assertNumberOrInteger(month, 'Month'); - this.day = assertNumberOrInteger(day, 'Day'); + this.year = util.assertValidYear(year); + this.month = util.assertValidMonth(month); + this.day = util.assertValidDay(day); Object.freeze(this); } @@ -251,13 +251,13 @@ export class LocalDateTime { * @param {Integer|number} nanosecond the nanosecond for the new local time. */ constructor(year, month, day, hour, minute, second, nanosecond) { - this.year = assertNumberOrInteger(year, 'Year'); - this.month = assertNumberOrInteger(month, 'Month'); - this.day = assertNumberOrInteger(day, 'Day'); - this.hour = assertNumberOrInteger(hour, 'Hour'); - this.minute = assertNumberOrInteger(minute, 'Minute'); - this.second = assertNumberOrInteger(second, 'Second'); - this.nanosecond = assertNumberOrInteger(nanosecond, 'Nanosecond'); + this.year = util.assertValidYear(year); + this.month = util.assertValidMonth(month); + this.day = util.assertValidDay(day); + this.hour = util.assertValidHour(hour); + this.minute = util.assertValidMinute(minute); + this.second = util.assertValidSecond(second); + this.nanosecond = util.assertValidNanosecond(nanosecond); Object.freeze(this); } @@ -316,13 +316,13 @@ export class DateTime { * @param {string|null} timeZoneId the time zone id for the new date-time. Either this argument or timeZoneOffsetSeconds should be defined. */ constructor(year, month, day, hour, minute, second, nanosecond, timeZoneOffsetSeconds, timeZoneId) { - this.year = assertNumberOrInteger(year, 'Year'); - this.month = assertNumberOrInteger(month, 'Month'); - this.day = assertNumberOrInteger(day, 'Day'); - this.hour = assertNumberOrInteger(hour, 'Hour'); - this.minute = assertNumberOrInteger(minute, 'Minute'); - this.second = assertNumberOrInteger(second, 'Second'); - this.nanosecond = assertNumberOrInteger(nanosecond, 'Nanosecond'); + this.year = util.assertValidYear(year); + this.month = util.assertValidMonth(month); + this.day = util.assertValidDay(day); + this.hour = util.assertValidHour(hour); + this.minute = util.assertValidMinute(minute); + this.second = util.assertValidSecond(second); + this.nanosecond = util.assertValidNanosecond(nanosecond); const [offset, id] = verifyTimeZoneArguments(timeZoneOffsetSeconds, timeZoneId); this.timeZoneOffsetSeconds = offset; diff --git a/test/internal/temporal-util.test.js b/test/internal/temporal-util.test.js index f0d05c387..ee3610a4b 100644 --- a/test/internal/temporal-util.test.js +++ b/test/internal/temporal-util.test.js @@ -195,6 +195,128 @@ describe('temporal-util', () => { expect(util.timeZoneOffsetInSeconds(fakeStandardDateWithOffset(101))).toEqual(6060); }); + it('should verify year', () => { + expect(util.assertValidYear(-1)).toEqual(-1); + expect(util.assertValidYear(-2010)).toEqual(-2010); + expect(util.assertValidYear(int(-42))).toEqual(int(-42)); + expect(util.assertValidYear(int(-2019))).toEqual(int(-2019)); + + expect(util.assertValidYear(0)).toEqual(0); + expect(util.assertValidYear(1)).toEqual(1); + expect(util.assertValidYear(int(2015))).toEqual(int(2015)); + expect(util.assertValidYear(int(99999))).toEqual(int(99999)); + + expect(() => util.assertValidYear(1000000000)).toThrow(); + expect(() => util.assertValidYear(1999999999)).toThrow(); + expect(() => util.assertValidYear(int(2000000000))).toThrow(); + expect(() => util.assertValidYear(int(3999999999))).toThrow(); + + expect(() => util.assertValidYear(-1000000001)).toThrow(); + expect(() => util.assertValidYear(-1888888888)).toThrow(); + expect(() => util.assertValidYear(int(-2000000001))).toThrow(); + expect(() => util.assertValidYear(int(-3888888888))).toThrow(); + }); + + it('should verify month', () => { + for (let i = 1; i <= 12; i++) { + expect(util.assertValidMonth(i)).toEqual(i); + expect(util.assertValidMonth(int(i))).toEqual(int(i)); + } + + expect(() => util.assertValidMonth(0)).toThrow(); + expect(() => util.assertValidMonth(int(0))).toThrow(); + expect(() => util.assertValidMonth(-1)).toThrow(); + expect(() => util.assertValidMonth(int(-1))).toThrow(); + expect(() => util.assertValidMonth(-42)).toThrow(); + expect(() => util.assertValidMonth(int(-42))).toThrow(); + expect(() => util.assertValidMonth(13)).toThrow(); + expect(() => util.assertValidMonth(int(13))).toThrow(); + expect(() => util.assertValidMonth(42)).toThrow(); + expect(() => util.assertValidMonth(int(42))).toThrow(); + }); + + it('should verify day', () => { + for (let i = 1; i <= 31; i++) { + expect(util.assertValidDay(i)).toEqual(i); + expect(util.assertValidDay(int(i))).toEqual(int(i)); + } + + expect(() => util.assertValidDay(0)).toThrow(); + expect(() => util.assertValidDay(int(0))).toThrow(); + expect(() => util.assertValidDay(-1)).toThrow(); + expect(() => util.assertValidDay(int(-1))).toThrow(); + expect(() => util.assertValidDay(-42)).toThrow(); + expect(() => util.assertValidDay(int(-42))).toThrow(); + expect(() => util.assertValidDay(42)).toThrow(); + expect(() => util.assertValidDay(int(42))).toThrow(); + }); + + it('should verify hour', () => { + for (let i = 0; i <= 23; i++) { + expect(util.assertValidHour(i)).toEqual(i); + expect(util.assertValidHour(int(i))).toEqual(int(i)); + } + + expect(() => util.assertValidHour(-1)).toThrow(); + expect(() => util.assertValidHour(int(-1))).toThrow(); + expect(() => util.assertValidHour(-42)).toThrow(); + expect(() => util.assertValidHour(int(-42))).toThrow(); + expect(() => util.assertValidHour(24)).toThrow(); + expect(() => util.assertValidHour(int(24))).toThrow(); + expect(() => util.assertValidHour(42)).toThrow(); + expect(() => util.assertValidHour(int(42))).toThrow(); + }); + + it('should verify minute', () => { + for (let i = 0; i <= 59; i++) { + expect(util.assertValidMinute(i)).toEqual(i); + expect(util.assertValidMinute(int(i))).toEqual(int(i)); + } + + expect(() => util.assertValidMinute(-1)).toThrow(); + expect(() => util.assertValidMinute(int(-1))).toThrow(); + expect(() => util.assertValidMinute(-42)).toThrow(); + expect(() => util.assertValidMinute(int(-42))).toThrow(); + expect(() => util.assertValidMinute(60)).toThrow(); + expect(() => util.assertValidMinute(int(60))).toThrow(); + expect(() => util.assertValidMinute(91023)).toThrow(); + expect(() => util.assertValidMinute(int(1234))).toThrow(); + }); + + it('should verify second', () => { + for (let i = 0; i <= 59; i++) { + expect(util.assertValidSecond(i)).toEqual(i); + expect(util.assertValidSecond(int(i))).toEqual(int(i)); + } + + expect(() => util.assertValidSecond(-1)).toThrow(); + expect(() => util.assertValidSecond(int(-1))).toThrow(); + expect(() => util.assertValidSecond(-42)).toThrow(); + expect(() => util.assertValidSecond(int(-42))).toThrow(); + expect(() => util.assertValidSecond(60)).toThrow(); + expect(() => util.assertValidSecond(int(60))).toThrow(); + expect(() => util.assertValidSecond(123)).toThrow(); + expect(() => util.assertValidSecond(int(321))).toThrow(); + }); + + it('should verify nanosecond', () => { + expect(util.assertValidNanosecond(0)).toEqual(0); + expect(util.assertValidNanosecond(1)).toEqual(1); + expect(util.assertValidNanosecond(42)).toEqual(42); + expect(util.assertValidNanosecond(999)).toEqual(999); + expect(util.assertValidNanosecond(123456789)).toEqual(123456789); + expect(util.assertValidNanosecond(999999999)).toEqual(999999999); + + expect(() => util.assertValidNanosecond(-1)).toThrow(); + expect(() => util.assertValidNanosecond(int(-1))).toThrow(); + expect(() => util.assertValidNanosecond(-42)).toThrow(); + expect(() => util.assertValidNanosecond(int(-42))).toThrow(); + expect(() => util.assertValidNanosecond(1000000000)).toThrow(); + expect(() => util.assertValidNanosecond(int(1000000000))).toThrow(); + expect(() => util.assertValidNanosecond(1999999999)).toThrow(); + expect(() => util.assertValidNanosecond(int(1222222222))).toThrow(); + }); + }); function date(year, month, day) { diff --git a/test/v1/temporal-types.test.js b/test/v1/temporal-types.test.js index eea25e741..f008b5624 100644 --- a/test/v1/temporal-types.test.js +++ b/test/v1/temporal-types.test.js @@ -463,7 +463,7 @@ describe('temporal-types', () => { it('should convert LocalDateTime to ISO string', () => { expect(localDateTime(1992, 11, 8, 9, 42, 17, 22).toString()).toEqual('1992-11-08T09:42:17.000000022'); expect(localDateTime(-10, 7, 15, 8, 15, 33, 500).toString()).toEqual('-0010-07-15T08:15:33.000000500'); - expect(localDateTime(0, 0, 0, 0, 0, 0, 1).toString()).toEqual('0000-00-00T00:00:00.000000001'); + expect(localDateTime(0, 1, 1, 0, 0, 0, 1).toString()).toEqual('0000-01-01T00:00:00.000000001'); }); it('should convert DateTime with time zone offset to ISO string', () => { @@ -847,6 +847,73 @@ describe('temporal-types', () => { testSendReceiveTemporalValue(neo4jDateTime, done); }); + it('should fail to create LocalTime with out of range values', () => { + expect(() => localTime(999, 1, 1, 1)).toThrow(); + expect(() => localTime(1, 999, 1, 1)).toThrow(); + expect(() => localTime(1, 1, 999, 1)).toThrow(); + expect(() => localTime(1, 1, 1, -999)).toThrow(); + expect(() => localTime(1, 1, 1, 1000000000)).toThrow(); + }); + + it('should fail to create Time with out of range values', () => { + expect(() => time(999, 1, 1, 1, 1)).toThrow(); + expect(() => time(1, 999, 1, 1, 1)).toThrow(); + expect(() => time(1, 1, 999, 1, 1)).toThrow(); + expect(() => time(1, 1, 1, -999, 1)).toThrow(); + expect(() => time(1, 1, 1, 1000000000, 1)).toThrow(); + }); + + it('should fail to create Date with out of range values', () => { + expect(() => date(1000000000, 1, 1)).toThrow(); + expect(() => date(1, 0, 1)).toThrow(); + expect(() => date(1, 13, 1)).toThrow(); + expect(() => date(1, 1, 0)).toThrow(); + expect(() => date(1, 1, -1)).toThrow(); + expect(() => date(1, 1, 33)).toThrow(); + }); + + it('should fail to create LocalDateTime with out of range values', () => { + expect(() => localDateTime(1000000000, 1, 1, 1, 1, 1, 1)).toThrow(); + expect(() => localDateTime(1, 0, 1, 1, 1, 1, 1)).toThrow(); + expect(() => localDateTime(1, 13, 1, 1, 1, 1, 1)).toThrow(); + expect(() => localDateTime(1, -1, 1, 1, 1, 1, 1)).toThrow(); + expect(() => localDateTime(1, 1, 0, 1, 1, 1, 1)).toThrow(); + expect(() => localDateTime(1, 1, -1, 1, 1, 1, 1)).toThrow(); + expect(() => localDateTime(1, 1, 33, 1, 1, 1, 1)).toThrow(); + expect(() => localDateTime(1, 1, 1, -1, 1, 1, 1)).toThrow(); + expect(() => localDateTime(1, 1, 1, 24, 1, 1, 1)).toThrow(); + expect(() => localDateTime(1, 1, 1, 42, 1, 1, 1)).toThrow(); + expect(() => localDateTime(1, 1, 1, 1, -1, 1, 1)).toThrow(); + expect(() => localDateTime(1, 1, 1, 1, 60, 1, 1)).toThrow(); + expect(() => localDateTime(1, 1, 1, 1, 999, 1, 1)).toThrow(); + expect(() => localDateTime(1, 1, 1, 1, 1, -1, 1)).toThrow(); + expect(() => localDateTime(1, 1, 1, 1, 1, 60, 1)).toThrow(); + expect(() => localDateTime(1, 1, 1, 1, 1, 99, 1)).toThrow(); + expect(() => localDateTime(1, 1, 1, 1, 1, 1, -1)).toThrow(); + expect(() => localDateTime(1, 1, 1, 1, 1, 1, 1000000000)).toThrow(); + }); + + it('should fail to create DateTime with out of range values', () => { + expect(() => dateTimeWithZoneOffset(1000000000, 1, 1, 1, 1, 1, 1, 0)).toThrow(); + expect(() => dateTimeWithZoneOffset(1, 0, 1, 1, 1, 1, 1, 0)).toThrow(); + expect(() => dateTimeWithZoneOffset(1, 13, 1, 1, 1, 1, 1, 0)).toThrow(); + expect(() => dateTimeWithZoneOffset(1, -1, 1, 1, 1, 1, 1, 0)).toThrow(); + expect(() => dateTimeWithZoneOffset(1, 1, 0, 1, 1, 1, 1, 0)).toThrow(); + expect(() => dateTimeWithZoneOffset(1, 1, -1, 1, 1, 1, 1, 0)).toThrow(); + expect(() => dateTimeWithZoneOffset(1, 1, 33, 1, 1, 1, 1, 0)).toThrow(); + expect(() => dateTimeWithZoneOffset(1, 1, 1, -1, 1, 1, 1, 0)).toThrow(); + expect(() => dateTimeWithZoneOffset(1, 1, 1, 24, 1, 1, 1, 0)).toThrow(); + expect(() => dateTimeWithZoneOffset(1, 1, 1, 42, 1, 1, 1, 0)).toThrow(); + expect(() => dateTimeWithZoneOffset(1, 1, 1, 1, -1, 1, 1, 0)).toThrow(); + expect(() => dateTimeWithZoneOffset(1, 1, 1, 1, 60, 1, 1, 0)).toThrow(); + expect(() => dateTimeWithZoneOffset(1, 1, 1, 1, 999, 1, 1, 0)).toThrow(); + expect(() => dateTimeWithZoneOffset(1, 1, 1, 1, 1, -1, 1, 0)).toThrow(); + expect(() => dateTimeWithZoneOffset(1, 1, 1, 1, 1, 60, 1, 0)).toThrow(); + expect(() => dateTimeWithZoneOffset(1, 1, 1, 1, 1, 99, 1, 0)).toThrow(); + expect(() => dateTimeWithZoneOffset(1, 1, 1, 1, 1, 1, -1, 0)).toThrow(); + expect(() => dateTimeWithZoneOffset(1, 1, 1, 1, 1, 1, 1000000000, 0)).toThrow(); + }); + function testSendAndReceiveRandomTemporalValues(valueGenerator, done) { const asyncFunction = (index, callback) => { const next = () => callback();