Skip to content

Commit ac6c406

Browse files
bigmontzrobsdedude
andauthored
Validate the ZoneId of the DateTime with ZoneId (#959)
The validation of the DateTime was only being done in the new patched protocol while unpacking the struct. This changes force any new DateTime with ZoneID to have a valid ZoneId. This changes also treats struct unpacking errors and defers the occurred to the moment the object is manipulate. For instance, a DateTime with invalid ZoneId returned in a Record will not break the records consumption until any code try to interacts with the broken DateTime. Co-authored-by: Robsdedude <[email protected]>
1 parent 11ec075 commit ac6c406

File tree

9 files changed

+100
-27
lines changed

9 files changed

+100
-27
lines changed

packages/bolt-connection/src/packstream/packstream-v1.js

+16-9
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,11 @@ import {
2828
Path,
2929
PathSegment,
3030
Relationship,
31-
UnboundRelationship
31+
UnboundRelationship,
32+
internal
3233
} from 'neo4j-driver-core'
3334

35+
const { util } = internal
3436
const { PROTOCOL_ERROR } = error
3537

3638
const TINY_STRING = 0x80
@@ -562,15 +564,20 @@ class Unpacker {
562564
}
563565

564566
_unpackStruct (marker, markerHigh, markerLow, buffer) {
565-
if (markerHigh === TINY_STRUCT) {
566-
return this._unpackStructWithSize(markerLow, buffer)
567-
} else if (marker === STRUCT_8) {
568-
return this._unpackStructWithSize(buffer.readUInt8(), buffer)
569-
} else if (marker === STRUCT_16) {
570-
return this._unpackStructWithSize(buffer.readUInt16(), buffer)
571-
} else {
572-
return null
567+
try {
568+
if (markerHigh === TINY_STRUCT) {
569+
return this._unpackStructWithSize(markerLow, buffer)
570+
} else if (marker === STRUCT_8) {
571+
return this._unpackStructWithSize(buffer.readUInt8(), buffer)
572+
} else if (marker === STRUCT_16) {
573+
return this._unpackStructWithSize(buffer.readUInt16(), buffer)
574+
} else {
575+
return null
576+
}
577+
} catch (error) {
578+
return util.createBrokenObject(error)
573579
}
580+
574581
}
575582

576583
_unpackStructWithSize (structSize, buffer) {

packages/bolt-connection/test/packstream/packstream-v2.test.js

+8-6
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,9 @@ describe('#unit PackStreamV2', () => {
315315
new Structure(0x69, [1, 2, 'America/Sao Paulo', 'Brasil'])
316316
]
317317
])('should not unpack with wrong size (%s)', (_, struct) => {
318-
expect(() => packAndUnpack(struct, { useUtc: true })).toThrowErrorMatchingSnapshot()
318+
const result = packAndUnpack(struct, { useUtc: true })
319+
// Errors are postponed for when the data is accessed.
320+
expect(() => result instanceof DateTime).toThrowErrorMatchingSnapshot()
319321
})
320322

321323
it.each([
@@ -352,7 +354,7 @@ describe('#unit PackStreamV2', () => {
352354
],
353355
[
354356
'DateTimeWithZoneId/0x66',
355-
new Structure(0x66, [1, 2, 'America/Sao Paulo'])
357+
new Structure(0x66, [1, 2, 'America/Sao_Paulo'])
356358
]
357359
])('should unpack deprecated temporal types as unknown structs (%s)', (_, struct) => {
358360
const unpacked = packAndUnpack(struct, { disableLosslessIntegers: true, useUtc: true})
@@ -368,7 +370,7 @@ describe('#unit PackStreamV2', () => {
368370
],
369371
[
370372
'DateTimeWithZoneId/0x69',
371-
new Structure(0x69, [1, 2, 'America/Sao Paulo'])
373+
new Structure(0x69, [1, 2, 'America/Sao_Paulo'])
372374
]
373375
])('should unpack utc temporal types as unknown structs (%s)', (_, struct) => {
374376
const unpacked = packAndUnpack(struct, { disableLosslessIntegers: true })
@@ -383,16 +385,16 @@ describe('#unit PackStreamV2', () => {
383385
],
384386
[
385387
'DateTimeWithZoneId',
386-
new Structure(0x66, [int(1), int(2), 'America/Sao Paulo']),
387-
new DateTime(1970, 1, 1, 0, 0, 1, 2, undefined, 'America/Sao Paulo')
388+
new Structure(0x66, [int(1), int(2), 'America/Sao_Paulo']),
389+
new DateTime(1970, 1, 1, 0, 0, 1, 2, undefined, 'America/Sao_Paulo')
388390
]
389391
])('should unpack temporal types without utc fix (%s)', (_, struct, object) => {
390392
const unpacked = packAndUnpack(struct, { disableLosslessIntegers: true })
391393
expect(unpacked).toEqual(object)
392394
})
393395

394396
it.each([
395-
['DateTimeWithZoneId', new DateTime(1, 1, 1, 1, 1, 1, 1, undefined, 'America/Sao Paulo')],
397+
['DateTimeWithZoneId', new DateTime(1, 1, 1, 1, 1, 1, 1, undefined, 'America/Sao_Paulo')],
396398
['DateTime', new DateTime(1, 1, 1, 1, 1, 1, 1, 1)]
397399
])('should pack temporal types (no utc) (%s)', (_, object) => {
398400
const unpacked = packAndUnpack(object, { disableLosslessIntegers: true })

packages/core/src/internal/temporal-util.ts

+10
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,16 @@ export function assertValidNanosecond (
407407
)
408408
}
409409

410+
export function assertValidZoneId (fieldName: string, zoneId: string) {
411+
try {
412+
Intl.DateTimeFormat(undefined, { timeZone: zoneId })
413+
} catch (e) {
414+
throw newError(
415+
`${fieldName} is expected to be a valid ZoneId but was: "${zoneId}"`
416+
)
417+
}
418+
}
419+
410420
/**
411421
* Check if the given value is of expected type and is in the expected range.
412422
* @param {Integer|number} value the value to check.

packages/core/src/internal/util.ts

+31-1
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,35 @@ function isString(str: any): str is string {
223223
return Object.prototype.toString.call(str) === '[object String]'
224224
}
225225

226+
/**
227+
* Creates a object which all method call will throw the given error
228+
*
229+
* @param {Error} error The error
230+
* @param {any} object The object. Default: {}
231+
* @returns {any} A broken object
232+
*/
233+
function createBrokenObject<T extends object> (error: Error, object: any = {}): T {
234+
const fail = () => {
235+
throw error
236+
}
237+
238+
return new Proxy(object, {
239+
get: fail,
240+
set: fail,
241+
apply: fail,
242+
construct: fail,
243+
defineProperty: fail,
244+
deleteProperty: fail,
245+
getOwnPropertyDescriptor: fail,
246+
getPrototypeOf: fail,
247+
has: fail,
248+
isExtensible: fail,
249+
ownKeys: fail,
250+
preventExtensions: fail,
251+
setPrototypeOf: fail,
252+
})
253+
}
254+
226255
export {
227256
isEmptyObjectOrNull,
228257
isObject,
@@ -234,5 +263,6 @@ export {
234263
assertValidDate,
235264
validateQueryAndParameters,
236265
ENCRYPTION_ON,
237-
ENCRYPTION_OFF
266+
ENCRYPTION_OFF,
267+
createBrokenObject
238268
}

packages/core/src/temporal-types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,7 @@ function verifyTimeZoneArguments(
722722

723723
if (idDefined) {
724724
assertString(timeZoneId, 'Time zone ID')
725+
util.assertValidZoneId('Time zone ID', timeZoneId)
725726
result[1] = timeZoneId
726727
}
727728

packages/neo4j-driver/test/temporal-types.test.js

+9-5
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ const MIN_YEAR = -MAX_YEAR
4444
const MAX_TIME_ZONE_OFFSET = 64800
4545
const MIN_TIME_ZONE_OFFSET = -MAX_TIME_ZONE_OFFSET
4646
const SECONDS_PER_MINUTE = 60
47-
const MIN_ZONE_ID = 'Etc/GMT+12'
48-
const MAX_ZONE_ID = 'Etc/GMT-14'
47+
const MIN_ZONE_ID = 'Pacific/Samoa'
48+
const MAX_ZONE_ID = 'Pacific/Kiritimati'
4949
const ZONE_IDS = ['Europe/Zaporozhye', 'Europe/London', 'UTC', 'Africa/Cairo']
5050

5151
describe('#integration temporal-types', () => {
@@ -644,9 +644,6 @@ describe('#integration temporal-types', () => {
644644
'Asia/Yangon'
645645
).toString()
646646
).toEqual('-30455-05-05T12:24:10.000000123[Asia/Yangon]')
647-
expect(
648-
dateTimeWithZoneId(248, 12, 30, 23, 59, 59, 3, 'CET').toString()
649-
).toEqual('0248-12-30T23:59:59.000000003[CET]')
650647
}, 60000)
651648

652649
it('should expose local time components in time', () => {
@@ -1401,6 +1398,13 @@ describe('#integration temporal-types', () => {
14011398
verifyTimeZoneOffset(neo4jDateTime5, -1 * 150 * 60, '-02:30')
14021399
}, 60000)
14031400

1401+
1402+
it('should not create DateTime with invalid ZoneId', () => {
1403+
expect(() => dateTimeWithZoneId(1999, 10, 1, 10, 15, 0, 0, 'Europe/Neo4j')).toThrowError(
1404+
'Time zone ID is expected to be a valid ZoneId but was: "Europe/Neo4j"'
1405+
)
1406+
})
1407+
14041408
function testSendAndReceiveRandomTemporalValues (valueGenerator) {
14051409
const asyncFunction = (index, callback) => {
14061410
testSendReceiveTemporalValue(valueGenerator())

packages/testkit-backend/src/request-handlers.js

+1
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ export function GetFeatures (_context, _params, wire) {
343343
'Feature:Bolt:4.3',
344344
'Feature:Bolt:4.4',
345345
'Feature:API:Result.List',
346+
'Detail:ResultStreamWorksAfterBrokenRecord',
346347
...SUPPORTED_TLS
347348
]
348349
})

packages/testkit-backend/src/skipped-tests/common.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import skip, { ifEquals, ifEndsWith } from './skip'
1+
import skip, { ifEquals, ifEndsWith, endsWith, ifStartsWith } from './skip'
22

33
const skippedTests = [
44
skip(
5-
'Driver does not return offset for old DateTime implementations',
5+
'Driver does not return offset for old DateTime implementations',
6+
ifStartsWith('stub.types.test_temporal_types.TestTemporalTypes').and(endsWith('test_zoned_date_time')),
67
ifEquals('neo4j.datatypes.test_temporal_types.TestDataTypes.test_nested_datetime'),
78
ifEquals('neo4j.datatypes.test_temporal_types.TestDataTypes.test_should_echo_all_timezone_ids'),
89
ifEquals('neo4j.datatypes.test_temporal_types.TestDataTypes.test_cypher_created_datetime')
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,38 @@
1+
2+
function asComposablePredicate (predicate) {
3+
return new Proxy(predicate, {
4+
get: (target, p) => {
5+
if (p === 'and') {
6+
return otherPredicate => asComposablePredicate(testName => target(testName) && otherPredicate(testName))
7+
} else if (p === 'or') {
8+
return otherPredicate => asComposablePredicate(testName => target(testName) || otherPredicate(testName))
9+
}
10+
return target[p]
11+
}
12+
})
13+
}
14+
115
export function ifEndsWith (suffix) {
2-
return testName => testName.endsWith(suffix)
16+
return asComposablePredicate(testName => testName.endsWith(suffix))
317
}
418

519
export function ifStartsWith (prefix) {
6-
return testName => testName.startsWith(prefix)
20+
return asComposablePredicate(testName => testName.startsWith(prefix))
721
}
822

923
export function ifEquals (expectedName) {
10-
return testName => testName === expectedName
24+
return asComposablePredicate(testName => testName === expectedName)
1125
}
1226

1327
export function or () {
14-
return testName => [...arguments].find(predicate => predicate(testName))
28+
return asComposablePredicate(testName => [...arguments].find(predicate => predicate(testName)))
1529
}
1630

1731
export function skip (reason, ...predicate) {
1832
return { reason, predicate: or(...predicate) }
1933
}
2034

35+
export const endsWith = ifEndsWith
36+
export const startsWith = ifStartsWith
37+
2138
export default skip

0 commit comments

Comments
 (0)