notepack.io
Version:
A fast Node.js implementation of the latest MessagePack spec
340 lines (306 loc) • 9.97 kB
JavaScript
;
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) {
let c = 0;
for (let i = 0, l = str.length; i < l; i++) {
c = str.charCodeAt(i);
if (c < 0x80) {
arr[offset++] = c;
} else if (c < 0x800) {
arr[offset++] = 0xc0 | (c >> 6);
arr[offset++] = 0x80 | (c & 0x3f);
} else if (c < 0xd800 || c >= 0xe000) {
arr[offset++] = 0xe0 | (c >> 12);
arr[offset++] = 0x80 | (c >> 6) & 0x3f;
arr[offset++] = 0x80 | (c & 0x3f);
} else {
i++;
c = 0x10000 + (((c & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff));
arr[offset++] = 0xf0 | (c >> 18);
arr[offset++] = 0x80 | (c >> 12) & 0x3f;
arr[offset++] = 0x80 | (c >> 6) & 0x3f;
arr[offset++] = 0x80 | (c & 0x3f);
}
}
}
// Faster for short strings than Buffer.byteLength
function utf8Length(str) {
let c = 0, length = 0;
for (let i = 0, l = str.length; i < l; i++) {
c = str.charCodeAt(i);
if (c < 0x80) {
length += 1;
} else if (c < 0x800) {
length += 2;
} else if (c < 0xd800 || c >= 0xe000) {
length += 3;
} else {
i++;
length += 4;
}
}
return length;
}
const cache = new Map();
const cacheMaxSize = process.env.NOTEPACK_ENCODE_CACHE_MAX_SIZE || 1024;
/*jshint latedef: nofunc */
function encodeKey(bytes, defers, key) {
if (cache.has(key)) {
const buffer = cache.get(key);
defers.push({ bin: buffer, length: buffer.length, offset: bytes.length });
return buffer.length;
}
if (cache.size > cacheMaxSize) {
return _encode(bytes, defers, key);
}
const keyBytes = [];
const size = _encode(keyBytes, [], key);
const keyBuffer = Buffer.allocUnsafe(size);
for (let i = 0, l = keyBytes.length; i < l; i++) {
keyBuffer[i] = keyBytes[i];
}
utf8Write(keyBuffer, keyBytes.length, key);
defers.push({ bin: keyBuffer, length: size, offset: bytes.length });
cache.set(key, keyBuffer);
return size;
}
function _encode(bytes, defers, value) {
let hi = 0, lo = 0, length = 0, size = 0;
switch (typeof value) {
case 'string':
if (value.length > MICRO_OPT_LEN) {
length = Buffer.byteLength(value);
} else {
length = utf8Length(value);
}
if (length < 0x20) { // fixstr
bytes.push(length | 0xa0);
size = 1;
} else if (length < 0x100) { // str 8
bytes.push(0xd9, length);
size = 2;
} else if (length < 0x10000) { // str 16
bytes.push(0xda, length >> 8, length);
size = 3;
} else if (length < 0x100000000) { // str 32
bytes.push(0xdb, length >> 24, length >> 16, length >> 8, length);
size = 5;
} else {
throw new Error('String too long');
}
defers.push({ str: value, length: length, offset: bytes.length });
return size + length;
case 'number':
// TODO: encode to float 32?
if (Math.floor(value) !== value || !isFinite(value)) { // float 64
bytes.push(0xcb);
defers.push({ float: value, length: 8, offset: bytes.length });
return 9;
}
if (value >= 0) {
if (value < 0x80) { // positive fixnum
bytes.push(value);
return 1;
}
if (value < 0x100) { // uint 8
bytes.push(0xcc, value);
return 2;
}
if (value < 0x10000) { // uint 16
bytes.push(0xcd, value >> 8, value);
return 3;
}
if (value < 0x100000000) { // uint 32
bytes.push(0xce, value >> 24, value >> 16, value >> 8, value);
return 5;
}
// uint 64
hi = (value / Math.pow(2, 32)) >> 0;
lo = value >>> 0;
bytes.push(0xcf, hi >> 24, hi >> 16, hi >> 8, hi, lo >> 24, lo >> 16, lo >> 8, lo);
return 9;
} else {
if (value >= -0x20) { // negative fixnum
bytes.push(value);
return 1;
}
if (value >= -0x80) { // int 8
bytes.push(0xd0, value);
return 2;
}
if (value >= -0x8000) { // int 16
bytes.push(0xd1, value >> 8, value);
return 3;
}
if (value >= -0x80000000) { // int 32
bytes.push(0xd2, value >> 24, value >> 16, value >> 8, value);
return 5;
}
// int 64
hi = Math.floor(value / Math.pow(2, 32));
lo = value >>> 0;
bytes.push(0xd3, hi >> 24, hi >> 16, hi >> 8, hi, lo >> 24, lo >> 16, lo >> 8, lo);
return 9;
}
break;
case 'object':
// nil
if (value === null) {
bytes.push(0xc0);
return 1;
}
if (Array.isArray(value)) {
length = value.length;
if (length < 0x10) { // fixarray
bytes.push(length | 0x90);
size = 1;
} else if (length < 0x10000) { // array 16
bytes.push(0xdc, length >> 8, length);
size = 3;
} else if (length < 0x100000000) { // array 32
bytes.push(0xdd, length >> 24, length >> 16, length >> 8, length);
size = 5;
} else {
throw new Error('Array too large');
}
for (let i = 0; i < length; i++) {
size += _encode(bytes, defers, value[i]);
}
return size;
}
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) {
length = value.length;
if (length < 0x100) { // bin 8
bytes.push(0xc4, length);
size = 2;
} else if (length < 0x10000) { // bin 16
bytes.push(0xc5, length >> 8, length);
size = 3;
} else if (length < 0x100000000) { // bin 32
bytes.push(0xc6, length >> 24, length >> 16, length >> 8, length);
size = 5;
} else {
throw new Error('Buffer too large');
}
defers.push({ bin: value, length: length, offset: bytes.length });
return size + length;
}
if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {
return _encode(bytes, defers, Buffer.from(value));
}
if (typeof value.toJSON === 'function') {
return _encode(bytes, defers, value.toJSON());
}
// note: faster than for...in + hasOwnProperty in most cases
const keys = [], allKeys = Object.keys(value);
let key = '';
for (let i = 0, l = allKeys.length; i < l; i++) {
key = allKeys[i];
if (value[key] !== undefined && typeof value[key] !== 'function') {
keys.push(key);
}
}
length = keys.length;
if (length < 0x10) { // fixmap
bytes.push(length | 0x80);
size = 1;
} else if (length < 0x10000) { // map 16
bytes.push(0xde, length >> 8, length);
size = 3;
} else if (length < 0x100000000) { // map 32
bytes.push(0xdf, length >> 24, length >> 16, length >> 8, length);
size = 5;
} else {
throw new Error('Object too large');
}
for (let i = 0; i < length; i++) {
key = keys[i];
size += encodeKey(bytes, defers, key);
size += _encode(bytes, defers, value[key]);
}
return size;
case 'boolean': // false/true
bytes.push(value ? 0xc3 : 0xc2);
return 1;
case 'undefined':
bytes.push(0xc0);
return 1;
default:
// custom types like BigInt (typeof value === 'bigint')
if (typeof value.toJSON === 'function') {
return _encode(bytes, defers, value.toJSON());
}
throw new Error('Could not encode');
}
}
function encode(value) {
const bytes = [], defers = [], size = _encode(bytes, defers, value);
const buf = Buffer.allocUnsafe(size);
let deferIndex = 0, deferWritten = 0, nextOffset = -1;
if (defers.length > 0) {
nextOffset = defers[0].offset;
}
let defer, deferLength = 0, offset = 0;
for (let i = 0, l = bytes.length; i < l; i++) {
buf[deferWritten + i] = bytes[i];
while (i + 1 === nextOffset) {
defer = defers[deferIndex];
deferLength = defer.length;
offset = deferWritten + nextOffset;
if (defer.bin) {
if (deferLength > MICRO_OPT_LEN) {
defer.bin.copy(buf, offset, 0, deferLength);
} else {
const bin = defer.bin;
for (let j = 0; j < deferLength; j++) {
buf[offset + j] = bin[j];
}
}
} else if (defer.str) {
if (deferLength > MICRO_OPT_LEN) {
buf.write(defer.str, offset, deferLength, 'utf8');
} else {
utf8Write(buf, offset, defer.str);
}
} else if (defer.float !== undefined) {
buf.writeDoubleBE(defer.float, offset);
}
deferIndex++;
deferWritten += deferLength;
if (defers[deferIndex]) {
nextOffset = defers[deferIndex].offset;
} else {
break;
}
}
}
return buf;
}
module.exports = encode;