diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index d01a9fc15d0..c4379896bff 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -32,7 +32,7 @@ This release also includes changes from prior 3.7.x releases. * Bumped Hadoop to 3.4.3 (and Kerby to 2.0.3) to enable `hadoop-gremlin` to build and run on Java 25. * Add missing `Configuring` interface to `GraphStepPlaceholder` and `VertexStepPlaceholder` * Fixed bug in `group()` value traversal where keys were retained with stale barrier state instead of being filtered when steps following a `Barrier` in the second `by()` produced no output (e.g. `by(values("age").fold().unfold())` or `by(__.out().fold().count(local).is(P.gt(0)))` for vertices with no out-edges). -* Corrected numerous inaccuracies in the reference documentation, including wrong default values (connection pool sizes, buffer sizes, ports, timeouts), stale serializer class names, removed options documented as available, and broken code examples across the JVM, Python, `.NET`, Go, and JavaScript drivers. +* Fixed bug in `gremlin-javascript` GraphBinary and GraphSON deserialization where `OffsetDateTime` values outside the JavaScript `Date` range were silently returned as invalid `Date` objects instead of failing deserialization. * Added a `propertyMap()` helper to view an element's properties as a map keyed by property key, on the `Element` structure API in `gremlin-core` (inherited by `Vertex`, `Edge`, and `VertexProperty`) and on `Vertex`, `Edge`, and `VertexProperty` in `gremlin-javascript`, `gremlin-python`, `gremlin-dotnet`, and `gremlin-go`. * Fixed a bug in `gremlin-go` where a `VertexProperty` deserialized from GraphBinary did not have its `Key` field populated, causing `Vertex.PropertyMap()` to group properties under an empty key. diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/structure/io/binary/internals/OffsetDateTimeSerializer.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/structure/io/binary/internals/OffsetDateTimeSerializer.js index 30045ffd0ff..0a30855d293 100644 --- a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/structure/io/binary/internals/OffsetDateTimeSerializer.js +++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/structure/io/binary/internals/OffsetDateTimeSerializer.js @@ -143,6 +143,15 @@ module.exports = class OffsetDateTimeSerializer { // use UTC time calculated with offset above const v = new Date(Date.UTC(year, month, date, h, m, s, ms)); + // The GraphBinary DateTime/OffsetDateTime wire format can carry values (e.g. extreme years near + // +/-999999999) that fall outside the range representable by a JavaScript Date. In those cases + // Date.UTC(...) returns NaN and new Date(NaN) yields an invalid Date without throwing. Reject such + // values here so unsupported boundary date-times fail deserialization instead of silently producing + // an unusable Date instance. + if (Number.isNaN(v.getTime())) { + throw new Error('{value} is outside the range supported by JavaScript Date'); + } + return { v, len }; } catch (err) { throw this.ioc.utils.des_error({ serializer: this, args: arguments, cursor, err }); diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/structure/io/type-serializers.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/structure/io/type-serializers.js index 7f5a392fd6e..a3a683660cd 100644 --- a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/structure/io/type-serializers.js +++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/structure/io/type-serializers.js @@ -103,7 +103,15 @@ class OffsetDateTimeSerializer extends TypeSerializer { } deserialize(obj) { - return new Date(obj[valueKey]); + // An OffsetDateTime value can carry a date-time (e.g. extreme years) that falls outside the range + // representable by a JavaScript Date. In those cases new Date(...) yields an invalid Date without + // throwing. Reject such values so unsupported boundary date-times fail deserialization instead of + // silently producing an unusable Date instance. Mirrors the GraphBinary OffsetDateTime reader. + const value = new Date(obj[valueKey]); + if (Number.isNaN(value.getTime())) { + throw new Error('OffsetDateTime value is outside the range supported by JavaScript Date'); + } + return value; } canBeUsedFor(value) { diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/graphbinary/OffsetDateTimeSerializer-test.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/graphbinary/OffsetDateTimeSerializer-test.js new file mode 100644 index 00000000000..f854fad3512 --- /dev/null +++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/graphbinary/OffsetDateTimeSerializer-test.js @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +'use strict'; + +const utils = require('./utils'); +const assert = require('assert'); +const ioc = require('../../../lib/structure/io/binary/GraphBinary'); + +const { from, concat } = Buffer; + +const ID = 0x88; // OFFSETDATETIME + +describe('GraphBinary.OffsetDateTimeSerializer', () => { + + const type_code = from([ID]); + const value_flag = from([0x00]); + + const serializer = ioc.serializers[ID]; + + const cases = [ + { v: undefined, fq: 1, b: [ID, 0x01], av: null }, + { v: null, fq: 1, b: [ID, 0x01] }, + + // year=2022(0x07e6), month=5, day=1, ns=0x000042c277bd8e00, offset=0 + { v: new Date(1651436603000), + b: [0x00,0x00,0x07,0xe6, 0x05, 0x01, 0x00,0x00,0x42,0xc2,0x77,0xbd,0x8e,0x00, 0x00,0x00,0x00,0x00] }, + + { des: 1, err: /buffer is missing/, fq: 1, b: undefined }, + { des: 1, err: /buffer is empty/, fq: 1, b: [] }, + { des: 1, err: /unexpected {type_code}/, fq: 1, b: [ID - 1] }, + { des: 1, err: /{value_flag} is missing/, fq: 1, b: [ID] }, + { des: 1, err: /unexpected {value_flag}/, fq: 1, b: [ID, 0x02] }, + { des: 1, err: /unexpected {value} length/, fq: 1, b: [ID, 0x00] }, + + // Boundary values that fall outside the JavaScript Date range (year=999999999=0x3B9AC9FF) + // must be rejected rather than silently deserialized into an invalid Date. See TINKERPOP-3276. + { des: 1, err: /outside the range supported by JavaScript Date/, fq: 0, + b: [0x3B,0x9A,0xC9,0xFF, 0x01, 0x01, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00] }, + // year=-999999999=0xC4653601 + { des: 1, err: /outside the range supported by JavaScript Date/, fq: 0, + b: [0xC4,0x65,0x36,0x01, 0x01, 0x01, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00] }, + ]; + + describe('#serialize', () => + cases + .filter(({ des }) => !des) + .forEach(({ v, fq, b }, i) => it(utils.ser_title({ i, v }), () => { + b = from(b); + + if (fq !== undefined) { + assert.deepEqual(serializer.serialize(v, fq), b); + return; + } + + assert.deepEqual(serializer.serialize(v, true), concat([type_code, value_flag, b])); + assert.deepEqual(serializer.serialize(v, false), concat([b])); + })) + ); + + describe('#deserialize', () => + cases.forEach(({ v, fq, b, av, err }, i) => it(utils.des_title({ i, b }), () => { + if (Array.isArray(b)) + b = from(b); + + if (err !== undefined) { + if (fq !== undefined) + assert.throws(() => serializer.deserialize(b, fq), { message: err }); + else { + assert.throws(() => serializer.deserialize(concat([type_code, value_flag, b]), true), { message: err }); + assert.throws(() => serializer.deserialize(concat([b]), false), { message: err }); + } + return; + } + + if (av !== undefined) + v = av; + const len = b.length; + + if (fq !== undefined) { + assert.deepStrictEqual(serializer.deserialize(b, fq), { v, len }); + return; + } + + assert.deepStrictEqual(serializer.deserialize(concat([type_code, value_flag, b]), true), { v, len: len + 2 }); + assert.deepStrictEqual(serializer.deserialize(concat([b]), false), { v, len: len + 0 }); + })) + ); + + describe('#canBeUsedFor', () => + [ + { v: null, e: false }, + { v: undefined, e: false }, + { v: {}, e: false }, + { v: [], e: false }, + { v: new Date(), e: true }, + ].forEach(({ v, e }, i) => it(utils.cbuf_title({ i, v }), () => + assert.strictEqual(serializer.canBeUsedFor(v), e) + )) + ); + +}); diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/graphson-test.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/graphson-test.js index 77693067049..f968c352304 100644 --- a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/graphson-test.js +++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/graphson-test.js @@ -90,6 +90,22 @@ describe('GraphSONReader', function () { const result = reader.read(obj); assert.ok(result instanceof Date); }); + it('should parse OffsetDateTime', function() { + const obj = { "@type" : "gx:OffsetDateTime", "@value" : "2016-12-14T21:14:36.295Z" }; + const reader = new GraphSONReader(); + const result = reader.read(obj); + assert.ok(result instanceof Date); + assert.strictEqual(result.getTime(), 1481750076295); + }); + it('should reject OffsetDateTime outside the JavaScript Date range', function() { + const reader = new GraphSONReader(); + [ + { "@type" : "gx:OffsetDateTime", "@value" : "+999999-01-01T00:00:00Z" }, + { "@type" : "gx:OffsetDateTime", "@value" : "-999999-01-01T00:00:00Z" }, + ].forEach(function (obj) { + assert.throws(() => reader.read(obj), /outside the range supported by JavaScript Date/); + }); + }); it('should parse vertices from GraphSON', function () { const obj = { "@type":"g:Vertex","@value":{"id":{"@type":"g:Int32","@value":1},"label":"person",