From 671c1d1e047abdfce2724689a32509120818ab88 Mon Sep 17 00:00:00 2001 From: Michael Sun Date: Fri, 1 Dec 2023 15:24:12 -0800 Subject: [PATCH] chore: add range error validation for date types --- src/data-type.ts | 2 +- src/data-types/date.ts | 22 +++++- src/data-types/datetime.ts | 22 +++++- src/data-types/datetime2.ts | 22 +++++- src/data-types/datetimeoffset.ts | 22 +++++- src/data-types/smalldatetime.ts | 20 +++++- test/unit/data-type.js | 115 +++++++++++++++++++++++++++++++ 7 files changed, 214 insertions(+), 11 deletions(-) diff --git a/src/data-type.ts b/src/data-type.ts index 77d81eed9..ae0e4447b 100644 --- a/src/data-type.ts +++ b/src/data-type.ts @@ -80,7 +80,7 @@ export interface DataType { generateTypeInfo(parameter: ParameterData, options: InternalConnectionOptions): Buffer; generateParameterLength(parameter: ParameterData, options: InternalConnectionOptions): Buffer; generateParameterData(parameter: ParameterData, options: InternalConnectionOptions): Generator; - validate(value: any, collation: Collation | undefined): any; // TODO: Refactor 'any' and replace with more specific type. + validate(value: any, collation: Collation | undefined, options?: InternalConnectionOptions): any; // TODO: Refactor 'any' and replace with more specific type. hasTableName?: boolean; diff --git a/src/data-types/date.ts b/src/data-types/date.ts index 8e8e1b760..b5433541b 100644 --- a/src/data-types/date.ts +++ b/src/data-types/date.ts @@ -1,5 +1,8 @@ import { type DataType } from '../data-type'; import { ChronoUnit, LocalDate } from '@js-joda/core'; +import { type InternalConnectionOptions } from '../connection'; + +import { Collation } from '../collation'; // globalDate is to be used for JavaScript's global 'Date' object to avoid name clashing with the 'Date' constant below const globalDate = global.Date; @@ -7,6 +10,9 @@ const EPOCH_DATE = LocalDate.ofYearDay(1, 1); const NULL_LENGTH = Buffer.from([0x00]); const DATA_LENGTH = Buffer.from([0x03]); +const MIN_DATE = new globalDate('January 1, 0001'); +const MAX_DATE = new globalDate('December 31, 9999'); + const Date: DataType = { id: 0x28, type: 'DATEN', @@ -35,7 +41,7 @@ const Date: DataType = { const value = parameter.value as any; // Temporary solution. Remove 'any' later. - let date; + let date: LocalDate; if (options.useUTC) { date = LocalDate.of(value.getUTCFullYear(), value.getUTCMonth() + 1, value.getUTCDate()); } else { @@ -49,7 +55,7 @@ const Date: DataType = { }, // TODO: value is technically of type 'unknown'. - validate: function(value): null | Date { + validate: function(value: any, collation: Collation | undefined, options: InternalConnectionOptions): null | Date { if (value == null) { return null; } @@ -58,6 +64,18 @@ const Date: DataType = { value = new globalDate(globalDate.parse(value)); } + value = value as Date; + + // TODO: check date range: January 1, 0001, through December 31, 9999 + // : time range: 00:00:00 through 23:59:59.997 + if (options && options.useUTC) { + value = new globalDate(value.toUTCString()); + } + + if (value < MIN_DATE || value > MAX_DATE) { + throw new TypeError('Out of range.'); + } + if (isNaN(value)) { throw new TypeError('Invalid date.'); } diff --git a/src/data-types/datetime.ts b/src/data-types/datetime.ts index 02f62ef15..3058f3a63 100644 --- a/src/data-types/datetime.ts +++ b/src/data-types/datetime.ts @@ -1,11 +1,17 @@ import { type DataType } from '../data-type'; import DateTimeN from './datetimen'; import { ChronoUnit, LocalDate } from '@js-joda/core'; +import { type InternalConnectionOptions } from '../connection'; + +import { Collation } from '../collation'; const EPOCH_DATE = LocalDate.ofYearDay(1900, 1); const NULL_LENGTH = Buffer.from([0x00]); const DATA_LENGTH = Buffer.from([0x08]); +const MIN_DATE = new Date('January 1, 1753'); +const MAX_DATE = new Date('December 31, 9999'); + const DateTime: DataType = { id: 0x3D, type: 'DATETIME', @@ -34,7 +40,7 @@ const DateTime: DataType = { const value = parameter.value as any; // Temporary solution. Remove 'any' later. - let date; + let date: LocalDate; if (options.useUTC) { date = LocalDate.of(value.getUTCFullYear(), value.getUTCMonth() + 1, value.getUTCDate()); } else { @@ -72,7 +78,7 @@ const DateTime: DataType = { }, // TODO: type 'any' needs to be revisited. - validate: function(value): null | number { + validate: function(value: any, collation: Collation | undefined, options: InternalConnectionOptions): null | number { if (value == null) { return null; } @@ -81,6 +87,18 @@ const DateTime: DataType = { value = new Date(Date.parse(value)); } + value = value as Date; + + // TODO: check date range: January 1, 1753, through December 31, 9999 + // : time range: 00:00:00 through 23:59:59.997 + if (options && options.useUTC) { + value = new Date(value.toUTCString()); + } + + if (value < MIN_DATE || value > MAX_DATE) { + throw new TypeError('Out of range.'); + } + if (isNaN(value)) { throw new TypeError('Invalid date.'); } diff --git a/src/data-types/datetime2.ts b/src/data-types/datetime2.ts index a31151107..ab7dfa821 100644 --- a/src/data-types/datetime2.ts +++ b/src/data-types/datetime2.ts @@ -1,10 +1,16 @@ import { type DataType } from '../data-type'; import { ChronoUnit, LocalDate } from '@js-joda/core'; import WritableTrackingBuffer from '../tracking-buffer/writable-tracking-buffer'; +import { type InternalConnectionOptions } from '../connection'; + +import { Collation } from '../collation'; const EPOCH_DATE = LocalDate.ofYearDay(1, 1); const NULL_LENGTH = Buffer.from([0x00]); +const MIN_DATE = new Date('January 1, 0001'); +const MAX_DATE = new Date('December 31, 9999'); + const DateTime2: DataType & { resolveScale: NonNullable } = { id: 0x2A, type: 'DATETIME2N', @@ -64,7 +70,7 @@ const DateTime2: DataType & { resolveScale: NonNullable MAX_DATE) { + throw new TypeError('Out of range.'); + } + if (isNaN(value)) { throw new TypeError('Invalid date.'); } diff --git a/src/data-types/datetimeoffset.ts b/src/data-types/datetimeoffset.ts index b936fcc18..aa7fa888a 100644 --- a/src/data-types/datetimeoffset.ts +++ b/src/data-types/datetimeoffset.ts @@ -1,10 +1,16 @@ import { type DataType } from '../data-type'; import { ChronoUnit, LocalDate } from '@js-joda/core'; import WritableTrackingBuffer from '../tracking-buffer/writable-tracking-buffer'; +import { type InternalConnectionOptions } from '../connection'; + +import { Collation } from '../collation'; const EPOCH_DATE = LocalDate.ofYearDay(1, 1); const NULL_LENGTH = Buffer.from([0x00]); +const MIN_DATE = new Date('January 1, 0001'); +const MAX_DATE = new Date('December 31, 9999'); + const DateTimeOffset: DataType & { resolveScale: NonNullable } = { id: 0x2B, type: 'DATETIMEOFFSETN', @@ -62,7 +68,7 @@ const DateTimeOffset: DataType & { resolveScale: NonNullable MAX_DATE) { + throw new TypeError('Out of range.'); + } + if (isNaN(value)) { throw new TypeError('Invalid date.'); } diff --git a/src/data-types/smalldatetime.ts b/src/data-types/smalldatetime.ts index b17d0d834..86db10d03 100644 --- a/src/data-types/smalldatetime.ts +++ b/src/data-types/smalldatetime.ts @@ -1,9 +1,15 @@ import { type DataType } from '../data-type'; import DateTimeN from './datetimen'; +import { type InternalConnectionOptions } from '../connection'; + +import { Collation } from '../collation'; const EPOCH_DATE = new Date(1900, 0, 1); const UTC_EPOCH_DATE = new Date(Date.UTC(1900, 0, 1)); +const MIN_DATE = new Date(1900, 1, 1); +const MAX_DATE = new Date(2079, 5, 6, 23, 59, 59, 0); + const DATA_LENGTH = Buffer.from([0x04]); const NULL_LENGTH = Buffer.from([0x00]); @@ -35,7 +41,7 @@ const SmallDateTime: DataType = { const buffer = Buffer.alloc(4); - let days, dstDiff, minutes; + let days: number, dstDiff: number, minutes: number; if (options.useUTC) { days = Math.floor((parameter.value.getTime() - UTC_EPOCH_DATE.getTime()) / (1000 * 60 * 60 * 24)); minutes = (parameter.value.getUTCHours() * 60) + parameter.value.getUTCMinutes(); @@ -51,7 +57,7 @@ const SmallDateTime: DataType = { yield buffer; }, - validate: function(value): null | Date { + validate: function(value, collation: Collation | undefined, options: InternalConnectionOptions): null | Date { if (value == null) { return null; } @@ -60,6 +66,16 @@ const SmallDateTime: DataType = { value = new Date(Date.parse(value)); } + value = value as Date; + + if (options && options.useUTC) { + value = new Date(value.toUTCString()); + } + + if (value < MIN_DATE || value > MAX_DATE) { + throw new TypeError('Out of range.'); + } + if (isNaN(value)) { throw new TypeError('Invalid date.'); } diff --git a/test/unit/data-type.js b/test/unit/data-type.js index 6f51391bd..0186f9e32 100644 --- a/test/unit/data-type.js +++ b/test/unit/data-type.js @@ -211,6 +211,18 @@ describe('Date', function() { assert.deepEqual(result, expected); }); }); + + describe('.validate', function() { + it('returns a TypeError for dates that are out of range', function() { + assert.throws(() => { + TYPES.Date.validate(new Date('Dec 31 2000')); + }, TypeError, 'Out of range.'); + + assert.throws(() => { + TYPES.Date.validate(new Date('Jan 1, 10000')); + }, TypeError, 'Out of range.'); + }); + }); }); describe('DateTime', function() { @@ -246,6 +258,18 @@ describe('DateTime', function() { assert.deepEqual(result, expected); }); }); + + describe('.validate', function() { + it('returns a TypeError for dates that are out of range', function() { + assert.throws(() => { + TYPES.DateTime.validate(new Date('Dec 1, 1752')); + }, TypeError, 'Out of range.'); + + assert.throws(() => { + TYPES.DateTime.validate('Jan 1, 10000'); + }, TypeError, 'Out of range.'); + }); + }); }); describe('DateTime2', function() { @@ -293,6 +317,17 @@ describe('DateTime2', function() { assert.deepEqual(buffer, expected); }); }); + describe('.validate', function() { + it('returns a TypeError for dates that are out of range', function() { + assert.throws(() => { + TYPES.DateTime2.validate(new Date('Dec 31, 2000')); + }, TypeError, 'Out of range.'); + + assert.throws(() => { + TYPES.DateTime2.validate(new Date('Jan 1, 10000')); + }, TypeError, 'Out of range.'); + }); + }); }); describe('DateTimeOffset', function() { @@ -346,6 +381,18 @@ describe('DateTimeOffset', function() { assert.deepEqual(buffer, expected); }); }); + + describe('.validate', function() { + it('returns a TypeError for dates that are out of range', function() { + assert.throws(() => { + TYPES.DateTimeOffset.validate(new Date('Dec 31, 2000')); + }, TypeError, 'Out of range.'); + + assert.throws(() => { + TYPES.DateTimeOffset.validate(new Date('Jan 1, 10000')); + }, TypeError, 'Out of range.'); + }); + }); }); describe('Decimal', function() { @@ -449,6 +496,34 @@ describe('Decimal', function() { assert.deepEqual(result4, expected4); }); }); + + describe('.validate', function() { + it('returns a TypeError for decimals if the passed in value is unacceptable', function() { + assert.throws(() => { + TYPES.Decimal.validate('ABC'); + }, TypeError, 'Invalid number.'); + assert.throws(() => { + TYPES.Decimal.validate('e123'); + }, TypeError, 'Invalid number.'); + }); + + it('returns a the "Infinity" literal the decimals is outside the double-precision 64-bit IEEE 754-2019 format range', function() { + assert.equal(TYPES.Decimal.validate(1.7976931348623159e+308), Infinity); + assert.equal(TYPES.Decimal.validate(-1.7976931348623159e+308), -Infinity); + assert.equal(TYPES.Decimal.validate('Infinity'), Infinity); + assert.equal(TYPES.Decimal.validate('-Infinity'), -Infinity); + }); + + it('Corect pasing the decimals with special cases', function() { + assert.equal(TYPES.Decimal.validate('123.3.3'), 123.3); + assert.equal(TYPES.Decimal.validate('1-23'), 1); + assert.equal(TYPES.Decimal.validate('1+23'), 1); + assert.equal(TYPES.Decimal.validate('1e23e4'), 1e23); + assert.equal(TYPES.Decimal.validate(' 123'), 123); + assert.equal(TYPES.Decimal.validate('1-e5'), 1); + assert.equal(TYPES.Decimal.validate('1e2e3'), 100); + }); + }); }); describe('Float', function() { @@ -492,6 +567,34 @@ describe('Float', function() { assert.deepEqual(result, expected); }); }); + + describe('.validate', function() { + it('returns a TypeError for decimals if the passed in value is unacceptable', function() { + assert.throws(() => { + TYPES.Float.validate('ABC'); + }, TypeError, 'Invalid number.'); + assert.throws(() => { + TYPES.Float.validate('e123'); + }, TypeError, 'Invalid number.'); + }); + + it('returns a the "Infinity" literal the decimals is outside the double-precision 64-bit IEEE 754-2019 format range', function() { + assert.equal(TYPES.Float.validate(1.7976931348623159e+308), Infinity); + assert.equal(TYPES.Float.validate(-1.7976931348623159e+308), -Infinity); + assert.equal(TYPES.Float.validate('Infinity'), Infinity); + assert.equal(TYPES.Float.validate('-Infinity'), -Infinity); + }); + + it('Corect pasing the decimals with special cases', function() { + assert.equal(TYPES.Float.validate('123.3.3'), 123.3); + assert.equal(TYPES.Float.validate('1-23'), 1); + assert.equal(TYPES.Float.validate('1+23'), 1); + assert.equal(TYPES.Float.validate('1e23e4'), 1e23); + assert.equal(TYPES.Float.validate(' 123'), 123); + assert.equal(TYPES.Float.validate('1-e5'), 1); + assert.equal(TYPES.Float.validate('1e2e3'), 100); + }); + }); }); describe('Image', function() { @@ -919,6 +1022,18 @@ describe('SmallDateTime', function() { assert.deepEqual(result, expected); }); }); + + describe('.validate', function() { + it('returns a TypeError for dates that are out of range', function() { + assert.throws(() => { + TYPES.SmallDateTime.validate(new Date('Dec 31, 1889')); + }, TypeError, 'Out of range.'); + + assert.throws(() => { + TYPES.SmallDateTime.validate(new Date('June 7, 2079')); + }, TypeError, 'Out of range.'); + }); + }); }); describe('SmallInt', function() {