Skip to content

Commit

Permalink
feat: encode Date objects following the official timestamp extension
Browse files Browse the repository at this point in the history
  • Loading branch information
darrachequesne committed May 22, 2022
1 parent 21c6592 commit 9fb6275
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 21 deletions.
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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')` => `<Buffer d7 00 00 00 00 df b7 62 9c 00>`
* `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')` => `<Buffer d6 ff 39 45 79 80>`
* `ArrayBuffer` are encoded as `ext 8/16/32 [0, data]`, e.g. `Uint8Array.of(1, 2, 3, 4)` => `<Buffer c7 04 00 01 02 03 04>`

## Install
Expand Down
23 changes: 23 additions & 0 deletions browser/decode.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
32 changes: 26 additions & 6 deletions browser/encode.js
Original file line number Diff line number Diff line change
@@ -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++) {
Expand Down Expand Up @@ -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) {
Expand Down
23 changes: 23 additions & 0 deletions lib/decode.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
32 changes: 26 additions & 6 deletions lib/encode.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
27 changes: 27 additions & 0 deletions test/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]);
Expand Down Expand Up @@ -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 = {
Expand Down
17 changes: 10 additions & 7 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -221,20 +221,23 @@ 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));
});

it('fixext 16', 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');
Expand Down

0 comments on commit 9fb6275

Please sign in to comment.