From 8e6fc3d4de25539871cf34b16f7c6cc72fc98cf6 Mon Sep 17 00:00:00 2001 From: "David I. Lehn" Date: Fri, 5 Sep 2025 07:34:19 -0400 Subject: [PATCH 1/4] Add dateTime range tests. --- tests/roundtrip.spec.js | 47 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/roundtrip.spec.js b/tests/roundtrip.spec.js index de8a369..8ed6d66 100644 --- a/tests/roundtrip.spec.js +++ b/tests/roundtrip.spec.js @@ -854,6 +854,53 @@ describe('cborld round trip', () => { expect(decodedDocument).to.eql(jsonldDocument); }); + it('should handle wide dateTime range', async () => { + const CONTEXT = { + '@context': { + date: { + '@id': 'ex:date', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime' + }, + } + }; + const jsonldDocument = { + '@context': CONTEXT['@context'], + date: [ + '1000-01-01T00:00:00Z', + '1960-01-01T00:00:00Z', + '1960-01-01T23:59:59Z', + '1969-12-31T23:59:59Z', + '1970-01-01T00:00:00Z', + '1970-01-01T00:00:01Z', + '1970-12-31T23:59:59Z', + '2000-01-01T00:00:00Z', + '3000-01-01T00:00:00Z' + // TODO: increase possible xsd:dateTime value coverage + ] + }; + + const documentLoader = url => { + throw new Error(`Refused to load URL "${url}".`); + }; + + const typeTable = new Map(TYPE_TABLE); + + const cborldBytes = await encode({ + jsonldDocument, + format: 'cbor-ld-1.0', + registryEntryId: 1, + documentLoader, + typeTableLoader: () => typeTable + }); + + const decodedDocument = await decode({ + cborldBytes, + documentLoader, + typeTableLoader: () => typeTable + }); + expect(decodedDocument).to.eql(jsonldDocument); + }); + it('should revert remote type-scope', async () => { const CONTEXT_URL = 'urn:foo'; const CONTEXT = { From 0487c9d8c46e6f96fa112075846a375848b7f9d6 Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Fri, 5 Sep 2025 10:39:23 -0400 Subject: [PATCH 2/4] Handle dates from before the epoch. --- CHANGELOG.md | 5 +++++ lib/codecs/ValueEncoder.js | 5 +++-- lib/codecs/XsdDateEncoder.js | 5 +++-- lib/codecs/XsdDateTimeEncoder.js | 11 ++++++++--- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e794b5..ac67542 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # @digitalbazaar/cborld ChangeLog +## 8.0.2 - 2025-mm-dd + +### Fixed +- Properly encode/decode dates from before the epoch. + ## 8.0.1 - 2025-05-21 ### Fixed diff --git a/lib/codecs/ValueEncoder.js b/lib/codecs/ValueEncoder.js index 642ca83..86c79ce 100644 --- a/lib/codecs/ValueEncoder.js +++ b/lib/codecs/ValueEncoder.js @@ -1,5 +1,5 @@ /*! - * Copyright (c) 2024 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2024-2025 Digital Bazaar, Inc. All rights reserved. */ import { bytesFromInt, @@ -37,7 +37,8 @@ export class ValueEncoder extends CborldEncoder { const bytes = toBytes({intValue}); return new Token(Type.bytes, bytes); } - return new Token(Type.uint, intValue); + const tokenType = intValue >= 0 ? Type.uint : Type.negint; + return new Token(tokenType, intValue); } static createEncoder({value, converter, termInfo, termType} = {}) { diff --git a/lib/codecs/XsdDateEncoder.js b/lib/codecs/XsdDateEncoder.js index cca0d4c..ecada4b 100644 --- a/lib/codecs/XsdDateEncoder.js +++ b/lib/codecs/XsdDateEncoder.js @@ -1,5 +1,5 @@ /*! - * Copyright (c) 2021-2024 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2021-2025 Digital Bazaar, Inc. All rights reserved. */ import {Token, Type} from 'cborg'; import {CborldEncoder} from './CborldEncoder.js'; @@ -21,7 +21,8 @@ export class XsdDateEncoder extends CborldEncoder { // compression would be lossy, do not compress return new Token(Type.string, value); } - return new Token(Type.uint, secondsSinceEpoch); + const tokenType = secondsSinceEpoch >= 0 ? Type.uint : Type.negint; + return new Token(tokenType, secondsSinceEpoch); } static createEncoder({value} = {}) { diff --git a/lib/codecs/XsdDateTimeEncoder.js b/lib/codecs/XsdDateTimeEncoder.js index 37e539b..459c0e7 100644 --- a/lib/codecs/XsdDateTimeEncoder.js +++ b/lib/codecs/XsdDateTimeEncoder.js @@ -1,5 +1,5 @@ /*! - * Copyright (c) 2021-2024 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2021-2025 Digital Bazaar, Inc. All rights reserved. */ import {Token, Type} from 'cborg'; import {CborldEncoder} from './CborldEncoder.js'; @@ -14,7 +14,8 @@ export class XsdDateTimeEncoder extends CborldEncoder { encode() { const {value, parsed} = this; const secondsSinceEpoch = Math.floor(parsed / 1000); - const secondsToken = new Token(Type.uint, secondsSinceEpoch); + const tokenType = _getNumberTokenType(secondsSinceEpoch); + const secondsToken = new Token(tokenType, secondsSinceEpoch); const millisecondIndex = value.indexOf('.'); if(millisecondIndex === -1) { const expectedDate = new Date( @@ -38,7 +39,7 @@ export class XsdDateTimeEncoder extends CborldEncoder { // compress with subsecond precision const entries = [ secondsToken, - new Token(Type.uint, milliseconds) + new Token(_getNumberTokenType(milliseconds), milliseconds) ]; return [new Token(Type.array, entries.length), entries]; } @@ -56,3 +57,7 @@ export class XsdDateTimeEncoder extends CborldEncoder { return new XsdDateTimeEncoder({value, parsed}); } } + +function _getNumberTokenType(number) { + return number >= 0 ? Type.uint : Type.negint; +} From 3e428c27352ee8815d9f91cfa455313f6bf95316 Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Fri, 5 Sep 2025 10:47:39 -0400 Subject: [PATCH 3/4] Add `xsd:date` roundtrip range tests; increase date ranges in tests. --- tests/roundtrip.spec.js | 55 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/tests/roundtrip.spec.js b/tests/roundtrip.spec.js index 8ed6d66..f39c3a5 100644 --- a/tests/roundtrip.spec.js +++ b/tests/roundtrip.spec.js @@ -866,6 +866,8 @@ describe('cborld round trip', () => { const jsonldDocument = { '@context': CONTEXT['@context'], date: [ + '-1000-01-01T00:00:00Z', + '0-01-01T00:00:00Z', '1000-01-01T00:00:00Z', '1960-01-01T00:00:00Z', '1960-01-01T23:59:59Z', @@ -874,7 +876,8 @@ describe('cborld round trip', () => { '1970-01-01T00:00:01Z', '1970-12-31T23:59:59Z', '2000-01-01T00:00:00Z', - '3000-01-01T00:00:00Z' + '3000-01-01T00:00:00Z', + '10000-01-01T00:00:00Z' // TODO: increase possible xsd:dateTime value coverage ] }; @@ -901,6 +904,56 @@ describe('cborld round trip', () => { expect(decodedDocument).to.eql(jsonldDocument); }); + it('should handle wide date range', async () => { + const CONTEXT = { + '@context': { + date: { + '@id': 'ex:date', + '@type': 'http://www.w3.org/2001/XMLSchema#date' + }, + } + }; + const jsonldDocument = { + '@context': CONTEXT['@context'], + date: [ + '-1000-01-01', + '0-01-01', + '1000-01-01', + '1960-01-01', + '1960-01-01', + '1969-12-31', + '1970-01-01', + '1970-01-01', + '1970-12-31', + '2000-01-01', + '3000-01-01', + '10000-01-01' + // TODO: increase possible xsd:date value coverage + ] + }; + + const documentLoader = url => { + throw new Error(`Refused to load URL "${url}".`); + }; + + const typeTable = new Map(TYPE_TABLE); + + const cborldBytes = await encode({ + jsonldDocument, + format: 'cbor-ld-1.0', + registryEntryId: 1, + documentLoader, + typeTableLoader: () => typeTable + }); + + const decodedDocument = await decode({ + cborldBytes, + documentLoader, + typeTableLoader: () => typeTable + }); + expect(decodedDocument).to.eql(jsonldDocument); + }); + it('should revert remote type-scope', async () => { const CONTEXT_URL = 'urn:foo'; const CONTEXT = { From 49781207c32a1c1bfd25e5f28d34cb3b48b89c48 Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Fri, 5 Sep 2025 11:04:23 -0400 Subject: [PATCH 4/4] Simplify millisecond processing (ms can't be negative). --- lib/codecs/XsdDateTimeEncoder.js | 8 ++------ tests/roundtrip.spec.js | 2 ++ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/codecs/XsdDateTimeEncoder.js b/lib/codecs/XsdDateTimeEncoder.js index 459c0e7..a317204 100644 --- a/lib/codecs/XsdDateTimeEncoder.js +++ b/lib/codecs/XsdDateTimeEncoder.js @@ -14,7 +14,7 @@ export class XsdDateTimeEncoder extends CborldEncoder { encode() { const {value, parsed} = this; const secondsSinceEpoch = Math.floor(parsed / 1000); - const tokenType = _getNumberTokenType(secondsSinceEpoch); + const tokenType = secondsSinceEpoch >= 0 ? Type.uint : Type.negint; const secondsToken = new Token(tokenType, secondsSinceEpoch); const millisecondIndex = value.indexOf('.'); if(millisecondIndex === -1) { @@ -39,7 +39,7 @@ export class XsdDateTimeEncoder extends CborldEncoder { // compress with subsecond precision const entries = [ secondsToken, - new Token(_getNumberTokenType(milliseconds), milliseconds) + new Token(Type.uint, milliseconds) ]; return [new Token(Type.array, entries.length), entries]; } @@ -57,7 +57,3 @@ export class XsdDateTimeEncoder extends CborldEncoder { return new XsdDateTimeEncoder({value, parsed}); } } - -function _getNumberTokenType(number) { - return number >= 0 ? Type.uint : Type.negint; -} diff --git a/tests/roundtrip.spec.js b/tests/roundtrip.spec.js index f39c3a5..d0e27a1 100644 --- a/tests/roundtrip.spec.js +++ b/tests/roundtrip.spec.js @@ -869,6 +869,7 @@ describe('cborld round trip', () => { '-1000-01-01T00:00:00Z', '0-01-01T00:00:00Z', '1000-01-01T00:00:00Z', + '1000-01-01T00:00:00.123Z', '1960-01-01T00:00:00Z', '1960-01-01T23:59:59Z', '1969-12-31T23:59:59Z', @@ -877,6 +878,7 @@ describe('cborld round trip', () => { '1970-12-31T23:59:59Z', '2000-01-01T00:00:00Z', '3000-01-01T00:00:00Z', + '3000-01-01T00:00:00.123Z', '10000-01-01T00:00:00Z' // TODO: increase possible xsd:dateTime value coverage ]