From 9fb62754e826984d9eccbb72c4e9c067ab5cb227 Mon Sep 17 00:00:00 2001 From: Damien Arrachequesne Date: Sun, 22 May 2022 09:15:10 +0200 Subject: [PATCH] feat: encode Date objects following the official timestamp extension Reference: https://github.com/msgpack/msgpack/blob/master/spec.md#timestamp-extension-type --- README.md | 3 +-- browser/decode.js | 23 +++++++++++++++++++++++ browser/encode.js | 32 ++++++++++++++++++++++++++------ lib/decode.js | 23 +++++++++++++++++++++++ lib/encode.js | 32 ++++++++++++++++++++++++++------ test/browser.js | 27 +++++++++++++++++++++++++++ test/test.js | 17 ++++++++++------- 7 files changed, 136 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index f9d4eff..7f0c47c 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,8 @@ A fast [Node.js](http://nodejs.org) implementation of the latest [MessagePack](h ## Notes -* This implementation is not backwards compatible with those that use the older spec. It is recommended that this library is only used in isolated systems. * `undefined` is encoded as `nil` -* `Date` objects are encoded as `fixext 8 [0, ms]`, e.g. `new Date('2000-06-13T00:00:00.000Z')` => `` +* `Date` objects are encoded following the [Timestamp extension](https://github.com/msgpack/msgpack/blob/master/spec.md#timestamp-extension-type), e.g. `new Date('2000-06-13T00:00:00.000Z')` => `` * `ArrayBuffer` are encoded as `ext 8/16/32 [0, data]`, e.g. `Uint8Array.of(1, 2, 3, 4)` => `` ## Install diff --git a/browser/decode.js b/browser/decode.js index 6bec7bf..f29d42a 100644 --- a/browser/decode.js +++ b/browser/decode.js @@ -139,6 +139,14 @@ Decoder.prototype._parse = function () { length = this._view.getUint8(this._offset); type = this._view.getInt8(this._offset + 1); this._offset += 2; + if (type === -1) { + // timestamp 96 + var ns = this._view.getUint32(this._offset); + hi = this._view.getInt32(this._offset + 4); + lo = this._view.getUint32(this._offset + 8); + this._offset += 12; + return new Date((hi * 0x100000000 + lo) * 1e3 + ns / 1e6); + } return [type, this._bin(length)]; case 0xc8: length = this._view.getUint16(this._offset); @@ -211,16 +219,31 @@ Decoder.prototype._parse = function () { case 0xd6: type = this._view.getInt8(this._offset); this._offset += 1; + if (type === -1) { + // timestamp 32 + value = this._view.getUint32(this._offset); + this._offset += 4; + return new Date(value * 1e3); + } return [type, this._bin(4)]; case 0xd7: type = this._view.getInt8(this._offset); this._offset += 1; if (type === 0x00) { + // custom date encoding (kept for backward-compatibility) hi = this._view.getInt32(this._offset) * Math.pow(2, 32); lo = this._view.getUint32(this._offset + 4); this._offset += 8; return new Date(hi + lo); } + if (type === -1) { + // timestamp 64 + hi = this._view.getUint32(this._offset); + lo = this._view.getUint32(this._offset + 4); + this._offset += 8; + var s = (hi & 0x3) * 0x100000000 + lo; + return new Date(s * 1e3 + (hi >>> 2) / 1e6); + } return [type, this._bin(8)]; case 0xd8: type = this._view.getInt8(this._offset); diff --git a/browser/encode.js b/browser/encode.js index 77caf97..c7853f0 100644 --- a/browser/encode.js +++ b/browser/encode.js @@ -1,5 +1,8 @@ 'use strict'; +var TIMESTAMP32_MAX_SEC = 0x100000000 - 1; // 32-bit unsigned int +var TIMESTAMP64_MAX_SEC = 0x400000000 - 1; // 34-bit unsigned int + function utf8Write(view, offset, str) { var c = 0; for (var i = 0, l = str.length; i < l; i++) { @@ -176,13 +179,30 @@ function _encode(bytes, defers, value) { return size; } - // fixext 8 / Date if (value instanceof Date) { - var time = value.getTime(); - hi = Math.floor(time / Math.pow(2, 32)); - lo = time >>> 0; - bytes.push(0xd7, 0, hi >> 24, hi >> 16, hi >> 8, hi, lo >> 24, lo >> 16, lo >> 8, lo); - return 10; + var ms = value.getTime(); + var s = Math.floor(ms / 1e3); + var ns = (ms - s * 1e3) * 1e6; + + if (s >= 0 && ns >= 0 && s <= TIMESTAMP64_MAX_SEC) { + if (ns === 0 && s <= TIMESTAMP32_MAX_SEC) { + // timestamp 32 + bytes.push(0xd6, 0xff, s >> 24, s >> 16, s >> 8, s); + return 6; + } else { + // timestamp 64 + hi = s / 0x100000000; + lo = s & 0xffffffff; + bytes.push(0xd7, 0xff, ns >> 22, ns >> 14, ns >> 6, hi, lo >> 24, lo >> 16, lo >> 8, lo); + return 10; + } + } else { + // timestamp 96 + hi = Math.floor(s / 0x100000000); + lo = s >>> 0; + bytes.push(0xc7, 0x0c, 0xff, ns >> 24, ns >> 16, ns >> 8, ns, hi >> 24, hi >> 16, hi >> 8, hi, lo >> 24, lo >> 16, lo >> 8, lo); + return 15; + } } if (value instanceof ArrayBuffer) { diff --git a/lib/decode.js b/lib/decode.js index c67dac5..3ff5e5a 100644 --- a/lib/decode.js +++ b/lib/decode.js @@ -119,6 +119,14 @@ Decoder.prototype.parse = function () { if (type === 0) { // ArrayBuffer return this.arraybuffer(length); } + if (type === -1) { + // timestamp 96 + const ns = this.buffer.readUInt32BE(this.offset); + hi = this.buffer.readInt32BE(this.offset + 4); + lo = this.buffer.readUInt32BE(this.offset + 8); + this.offset += 12; + return new Date((hi * 0x100000000 + lo) * 1e3 + ns / 1e6); + } return [type, this.bin(length)]; case 0xc8: length = this.buffer.readUInt16BE(this.offset); @@ -197,16 +205,31 @@ Decoder.prototype.parse = function () { case 0xd6: type = this.buffer.readInt8(this.offset); this.offset += 1; + if (type === -1) { + // timestamp 32 + value = this.buffer.readUInt32BE(this.offset); + this.offset += 4; + return new Date(value * 1e3); + } return [type, this.bin(4)]; case 0xd7: type = this.buffer.readInt8(this.offset); this.offset += 1; if (type === 0x00) { + // custom date encoding (kept for backward-compatibility) hi = this.buffer.readInt32BE(this.offset) * Math.pow(2, 32); lo = this.buffer.readUInt32BE(this.offset + 4); this.offset += 8; return new Date(hi + lo); } + if (type === -1) { + // timestamp 64 + hi = this.buffer.readUInt32BE(this.offset); + lo = this.buffer.readUInt32BE(this.offset + 4); + this.offset += 8; + const s = (hi & 0x3) * 0x100000000 + lo; + return new Date(s * 1e3 + (hi >>> 2) / 1e6); + } return [type, this.bin(8)]; case 0xd8: type = this.buffer.readInt8(this.offset); diff --git a/lib/encode.js b/lib/encode.js index a6b54e1..2faf233 100644 --- a/lib/encode.js +++ b/lib/encode.js @@ -1,6 +1,8 @@ 'use strict'; const MICRO_OPT_LEN = 32; +const TIMESTAMP32_MAX_SEC = 0x100000000 - 1; // 32-bit unsigned int +const TIMESTAMP64_MAX_SEC = 0x400000000 - 1; // 34-bit unsigned int // Faster for short strings than buffer.write function utf8Write(arr, offset, str) { @@ -189,12 +191,30 @@ function _encode(bytes, defers, value) { return size; } - if (value instanceof Date) { // fixext 8 / Date - const time = value.getTime(); - hi = Math.floor(time / Math.pow(2, 32)); - lo = time >>> 0; - bytes.push(0xd7, 0, hi >> 24, hi >> 16, hi >> 8, hi, lo >> 24, lo >> 16, lo >> 8, lo); - return 10; + if (value instanceof Date) { + const ms = value.getTime(); + const s = Math.floor(ms / 1e3); + const ns = (ms - s * 1e3) * 1e6; + + if (s >= 0 && ns >= 0 && s <= TIMESTAMP64_MAX_SEC) { + if (ns === 0 && s <= TIMESTAMP32_MAX_SEC) { + // timestamp 32 + bytes.push(0xd6, 0xff, s >> 24, s >> 16, s >> 8, s); + return 6; + } else { + // timestamp 64 + hi = s / 0x100000000; + lo = s & 0xffffffff; + bytes.push(0xd7, 0xff, ns >> 22, ns >> 14, ns >> 6, hi, lo >> 24, lo >> 16, lo >> 8, lo); + return 10; + } + } else { + // timestamp 96 + hi = Math.floor(s / 0x100000000); + lo = s >>> 0; + bytes.push(0xc7, 0x0c, 0xff, ns >> 24, ns >> 16, ns >> 8, ns, hi >> 24, hi >> 16, hi >> 8, hi, lo >> 24, lo >> 16, lo >> 8, lo); + return 15; + } } if (value instanceof Buffer) { diff --git a/test/browser.js b/test/browser.js index 2e6ced2..64c5281 100644 --- a/test/browser.js +++ b/test/browser.js @@ -22,6 +22,24 @@ function map(length) { return result; } +function checkDecode(value, hex) { + const decodedValue = notepack.decode(Buffer.from(hex, 'hex')); + expect(decodedValue).to.deep.equal(value, 'decode failed'); +} + +function checkEncode(value, hex) { + const encodedHex = Buffer.from(notepack.encode(value)).toString('hex'); + expect(encodedHex).to.equal(hex, 'encode failed'); +} + +function check(value, hex) { + checkEncode(value, hex); + checkDecode(value, hex); + + // And full circle for fun + expect(notepack.decode(notepack.encode(value))).to.deep.equal(value); +} + describe('notepack (browser build)', function() { it('ArrayBuffer view', function() { expect(notepack.decode(Uint8Array.from([ 0x93, 1, 2, 3 ]))).to.deep.equal([ 1, 2, 3 ]); @@ -62,6 +80,15 @@ describe('notepack (browser build)', function() { expect(notepack.decode(notepack.encode('🌐'))).to.equal('🌐'); }); + it('timestamp ext', function () { + check(new Date(0), 'd6ff00000000'); + check(new Date('1956-06-17T00:00:00.000Z'), 'c70cff00000000ffffffffe6876500'); + check(new Date('1970-01-01T00:00:00.000Z'), 'd6ff00000000'); + check(new Date('2000-06-13T00:00:00.000Z'), 'd6ff39457980'); + check(new Date('2005-12-31T23:59:59.999Z'), 'd7ffee2e1f0043b71b7f'); + check(new Date('2140-01-01T13:14:15.678Z'), 'd7ffa1a5d6013fc2faa7'); + }); + it('all formats', function () { this.timeout(20000); const expected = { diff --git a/test/test.js b/test/test.js index 03c5566..54c251c 100644 --- a/test/test.js +++ b/test/test.js @@ -221,13 +221,7 @@ describe('notepack', function () { checkDecode([127, Buffer.from('abcd')], 'd6' + '7f' + '61626364'); }); - it('fixext 8 / Date', function () { - check(new Date(0), 'd7000000000000000000'); - check(new Date('1956-06-17T00:00:00.000Z'), 'd700ffffff9c80e28800'); - check(new Date('1970-01-01T00:00:00.000Z'), 'd7000000000000000000'); - check(new Date('2000-06-13T00:00:00.000Z'), 'd700000000dfb7629c00'); - check(new Date('2005-12-31T23:59:59.999Z'), 'd7000000010883436bff'); - check(new Date('2140-01-01T13:14:15.678Z'), 'd700000004e111a31efe'); + it('fixext 8', function () { checkDecode([127, Buffer.from('abcd'.repeat(2))], 'd7' + '7f' + '61626364'.repeat(2)); }); @@ -235,6 +229,15 @@ describe('notepack', function () { checkDecode([-128, Buffer.from('abcd'.repeat(4))], 'd8' + '80' + '61626364'.repeat(4)); }); + it('timestamp ext', function () { + check(new Date(0), 'd6ff00000000'); + check(new Date('1956-06-17T00:00:00.000Z'), 'c70cff00000000ffffffffe6876500'); + check(new Date('1970-01-01T00:00:00.000Z'), 'd6ff00000000'); + check(new Date('2000-06-13T00:00:00.000Z'), 'd6ff39457980'); + check(new Date('2005-12-31T23:59:59.999Z'), 'd7ffee2e1f0043b71b7f'); + check(new Date('2140-01-01T13:14:15.678Z'), 'd7ffa1a5d6013fc2faa7'); + }); + it('str 8', function () { check('α', 'a2ceb1'); check('亜', 'a3e4ba9c');