Added test to ensure that release notes are parsed properly Updated to latest dependencies to fix security vulnerability Updated GH Actions Workflow to run test before updating tag Updated README.md to reflect proper whatsnew documentation.
587 lines
15 KiB
JavaScript
587 lines
15 KiB
JavaScript
/* Copyright 2015-present Facebook, Inc.
|
|
* Licensed under the Apache License, Version 2.0 */
|
|
|
|
var EE = require('events').EventEmitter;
|
|
var util = require('util');
|
|
var os = require('os');
|
|
var assert = require('assert');
|
|
var Int64 = require('node-int64');
|
|
|
|
// BSER uses the local endianness to reduce byte swapping overheads
|
|
// (the protocol is expressly local IPC only). We need to tell node
|
|
// to use the native endianness when reading various native values.
|
|
var isBigEndian = os.endianness() == 'BE';
|
|
|
|
// Find the next power-of-2 >= size
|
|
function nextPow2(size) {
|
|
return Math.pow(2, Math.ceil(Math.log(size) / Math.LN2));
|
|
}
|
|
|
|
// Expandable buffer that we can provide a size hint for
|
|
function Accumulator(initsize) {
|
|
this.buf = Buffer.alloc(nextPow2(initsize || 8192));
|
|
this.readOffset = 0;
|
|
this.writeOffset = 0;
|
|
}
|
|
// For testing
|
|
exports.Accumulator = Accumulator
|
|
|
|
// How much we can write into this buffer without allocating
|
|
Accumulator.prototype.writeAvail = function() {
|
|
return this.buf.length - this.writeOffset;
|
|
}
|
|
|
|
// How much we can read
|
|
Accumulator.prototype.readAvail = function() {
|
|
return this.writeOffset - this.readOffset;
|
|
}
|
|
|
|
// Ensure that we have enough space for size bytes
|
|
Accumulator.prototype.reserve = function(size) {
|
|
if (size < this.writeAvail()) {
|
|
return;
|
|
}
|
|
|
|
// If we can make room by shunting down, do so
|
|
if (this.readOffset > 0) {
|
|
this.buf.copy(this.buf, 0, this.readOffset, this.writeOffset);
|
|
this.writeOffset -= this.readOffset;
|
|
this.readOffset = 0;
|
|
}
|
|
|
|
// If we made enough room, no need to allocate more
|
|
if (size < this.writeAvail()) {
|
|
return;
|
|
}
|
|
|
|
// Allocate a replacement and copy it in
|
|
var buf = Buffer.alloc(nextPow2(this.buf.length + size - this.writeAvail()));
|
|
this.buf.copy(buf);
|
|
this.buf = buf;
|
|
}
|
|
|
|
// Append buffer or string. Will resize as needed
|
|
Accumulator.prototype.append = function(buf) {
|
|
if (Buffer.isBuffer(buf)) {
|
|
this.reserve(buf.length);
|
|
buf.copy(this.buf, this.writeOffset, 0, buf.length);
|
|
this.writeOffset += buf.length;
|
|
} else {
|
|
var size = Buffer.byteLength(buf);
|
|
this.reserve(size);
|
|
this.buf.write(buf, this.writeOffset);
|
|
this.writeOffset += size;
|
|
}
|
|
}
|
|
|
|
Accumulator.prototype.assertReadableSize = function(size) {
|
|
if (this.readAvail() < size) {
|
|
throw new Error("wanted to read " + size +
|
|
" bytes but only have " + this.readAvail());
|
|
}
|
|
}
|
|
|
|
Accumulator.prototype.peekString = function(size) {
|
|
this.assertReadableSize(size);
|
|
return this.buf.toString('utf-8', this.readOffset, this.readOffset + size);
|
|
}
|
|
|
|
Accumulator.prototype.readString = function(size) {
|
|
var str = this.peekString(size);
|
|
this.readOffset += size;
|
|
return str;
|
|
}
|
|
|
|
Accumulator.prototype.peekInt = function(size) {
|
|
this.assertReadableSize(size);
|
|
switch (size) {
|
|
case 1:
|
|
return this.buf.readInt8(this.readOffset, size);
|
|
case 2:
|
|
return isBigEndian ?
|
|
this.buf.readInt16BE(this.readOffset, size) :
|
|
this.buf.readInt16LE(this.readOffset, size);
|
|
case 4:
|
|
return isBigEndian ?
|
|
this.buf.readInt32BE(this.readOffset, size) :
|
|
this.buf.readInt32LE(this.readOffset, size);
|
|
case 8:
|
|
var big = this.buf.slice(this.readOffset, this.readOffset + 8);
|
|
if (isBigEndian) {
|
|
// On a big endian system we can simply pass the buffer directly
|
|
return new Int64(big);
|
|
}
|
|
// Otherwise we need to byteswap
|
|
return new Int64(byteswap64(big));
|
|
default:
|
|
throw new Error("invalid integer size " + size);
|
|
}
|
|
}
|
|
|
|
Accumulator.prototype.readInt = function(bytes) {
|
|
var ival = this.peekInt(bytes);
|
|
if (ival instanceof Int64 && isFinite(ival.valueOf())) {
|
|
ival = ival.valueOf();
|
|
}
|
|
this.readOffset += bytes;
|
|
return ival;
|
|
}
|
|
|
|
Accumulator.prototype.peekDouble = function() {
|
|
this.assertReadableSize(8);
|
|
return isBigEndian ?
|
|
this.buf.readDoubleBE(this.readOffset) :
|
|
this.buf.readDoubleLE(this.readOffset);
|
|
}
|
|
|
|
Accumulator.prototype.readDouble = function() {
|
|
var dval = this.peekDouble();
|
|
this.readOffset += 8;
|
|
return dval;
|
|
}
|
|
|
|
Accumulator.prototype.readAdvance = function(size) {
|
|
if (size > 0) {
|
|
this.assertReadableSize(size);
|
|
} else if (size < 0 && this.readOffset + size < 0) {
|
|
throw new Error("advance with negative offset " + size +
|
|
" would seek off the start of the buffer");
|
|
}
|
|
this.readOffset += size;
|
|
}
|
|
|
|
Accumulator.prototype.writeByte = function(value) {
|
|
this.reserve(1);
|
|
this.buf.writeInt8(value, this.writeOffset);
|
|
++this.writeOffset;
|
|
}
|
|
|
|
Accumulator.prototype.writeInt = function(value, size) {
|
|
this.reserve(size);
|
|
switch (size) {
|
|
case 1:
|
|
this.buf.writeInt8(value, this.writeOffset);
|
|
break;
|
|
case 2:
|
|
if (isBigEndian) {
|
|
this.buf.writeInt16BE(value, this.writeOffset);
|
|
} else {
|
|
this.buf.writeInt16LE(value, this.writeOffset);
|
|
}
|
|
break;
|
|
case 4:
|
|
if (isBigEndian) {
|
|
this.buf.writeInt32BE(value, this.writeOffset);
|
|
} else {
|
|
this.buf.writeInt32LE(value, this.writeOffset);
|
|
}
|
|
break;
|
|
default:
|
|
throw new Error("unsupported integer size " + size);
|
|
}
|
|
this.writeOffset += size;
|
|
}
|
|
|
|
Accumulator.prototype.writeDouble = function(value) {
|
|
this.reserve(8);
|
|
if (isBigEndian) {
|
|
this.buf.writeDoubleBE(value, this.writeOffset);
|
|
} else {
|
|
this.buf.writeDoubleLE(value, this.writeOffset);
|
|
}
|
|
this.writeOffset += 8;
|
|
}
|
|
|
|
var BSER_ARRAY = 0x00;
|
|
var BSER_OBJECT = 0x01;
|
|
var BSER_STRING = 0x02;
|
|
var BSER_INT8 = 0x03;
|
|
var BSER_INT16 = 0x04;
|
|
var BSER_INT32 = 0x05;
|
|
var BSER_INT64 = 0x06;
|
|
var BSER_REAL = 0x07;
|
|
var BSER_TRUE = 0x08;
|
|
var BSER_FALSE = 0x09;
|
|
var BSER_NULL = 0x0a;
|
|
var BSER_TEMPLATE = 0x0b;
|
|
var BSER_SKIP = 0x0c;
|
|
|
|
var ST_NEED_PDU = 0; // Need to read and decode PDU length
|
|
var ST_FILL_PDU = 1; // Know the length, need to read whole content
|
|
|
|
var MAX_INT8 = 127;
|
|
var MAX_INT16 = 32767;
|
|
var MAX_INT32 = 2147483647;
|
|
|
|
function BunserBuf() {
|
|
EE.call(this);
|
|
this.buf = new Accumulator();
|
|
this.state = ST_NEED_PDU;
|
|
}
|
|
util.inherits(BunserBuf, EE);
|
|
exports.BunserBuf = BunserBuf;
|
|
|
|
BunserBuf.prototype.append = function(buf, synchronous) {
|
|
if (synchronous) {
|
|
this.buf.append(buf);
|
|
return this.process(synchronous);
|
|
}
|
|
|
|
try {
|
|
this.buf.append(buf);
|
|
} catch (err) {
|
|
this.emit('error', err);
|
|
return;
|
|
}
|
|
// Arrange to decode later. This allows the consuming
|
|
// application to make progress with other work in the
|
|
// case that we have a lot of subscription updates coming
|
|
// in from a large tree.
|
|
this.processLater();
|
|
}
|
|
|
|
BunserBuf.prototype.processLater = function() {
|
|
var self = this;
|
|
process.nextTick(function() {
|
|
try {
|
|
self.process(false);
|
|
} catch (err) {
|
|
self.emit('error', err);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Do something with the buffer to advance our state.
|
|
// If we're running synchronously we'll return either
|
|
// the value we've decoded or undefined if we don't
|
|
// yet have enought data.
|
|
// If we're running asynchronously, we'll emit the value
|
|
// when it becomes ready and schedule another invocation
|
|
// of process on the next tick if we still have data we
|
|
// can process.
|
|
BunserBuf.prototype.process = function(synchronous) {
|
|
if (this.state == ST_NEED_PDU) {
|
|
if (this.buf.readAvail() < 2) {
|
|
return;
|
|
}
|
|
// Validate BSER header
|
|
this.expectCode(0);
|
|
this.expectCode(1);
|
|
this.pduLen = this.decodeInt(true /* relaxed */);
|
|
if (this.pduLen === false) {
|
|
// Need more data, walk backwards
|
|
this.buf.readAdvance(-2);
|
|
return;
|
|
}
|
|
// Ensure that we have a big enough buffer to read the rest of the PDU
|
|
this.buf.reserve(this.pduLen);
|
|
this.state = ST_FILL_PDU;
|
|
}
|
|
|
|
if (this.state == ST_FILL_PDU) {
|
|
if (this.buf.readAvail() < this.pduLen) {
|
|
// Need more data
|
|
return;
|
|
}
|
|
|
|
// We have enough to decode it
|
|
var val = this.decodeAny();
|
|
if (synchronous) {
|
|
return val;
|
|
}
|
|
this.emit('value', val);
|
|
this.state = ST_NEED_PDU;
|
|
}
|
|
|
|
if (!synchronous && this.buf.readAvail() > 0) {
|
|
this.processLater();
|
|
}
|
|
}
|
|
|
|
BunserBuf.prototype.raise = function(reason) {
|
|
throw new Error(reason + ", in Buffer of length " +
|
|
this.buf.buf.length + " (" + this.buf.readAvail() +
|
|
" readable) at offset " + this.buf.readOffset + " buffer: " +
|
|
JSON.stringify(this.buf.buf.slice(
|
|
this.buf.readOffset, this.buf.readOffset + 32).toJSON()));
|
|
}
|
|
|
|
BunserBuf.prototype.expectCode = function(expected) {
|
|
var code = this.buf.readInt(1);
|
|
if (code != expected) {
|
|
this.raise("expected bser opcode " + expected + " but got " + code);
|
|
}
|
|
}
|
|
|
|
BunserBuf.prototype.decodeAny = function() {
|
|
var code = this.buf.peekInt(1);
|
|
switch (code) {
|
|
case BSER_INT8:
|
|
case BSER_INT16:
|
|
case BSER_INT32:
|
|
case BSER_INT64:
|
|
return this.decodeInt();
|
|
case BSER_REAL:
|
|
this.buf.readAdvance(1);
|
|
return this.buf.readDouble();
|
|
case BSER_TRUE:
|
|
this.buf.readAdvance(1);
|
|
return true;
|
|
case BSER_FALSE:
|
|
this.buf.readAdvance(1);
|
|
return false;
|
|
case BSER_NULL:
|
|
this.buf.readAdvance(1);
|
|
return null;
|
|
case BSER_STRING:
|
|
return this.decodeString();
|
|
case BSER_ARRAY:
|
|
return this.decodeArray();
|
|
case BSER_OBJECT:
|
|
return this.decodeObject();
|
|
case BSER_TEMPLATE:
|
|
return this.decodeTemplate();
|
|
default:
|
|
this.raise("unhandled bser opcode " + code);
|
|
}
|
|
}
|
|
|
|
BunserBuf.prototype.decodeArray = function() {
|
|
this.expectCode(BSER_ARRAY);
|
|
var nitems = this.decodeInt();
|
|
var arr = [];
|
|
for (var i = 0; i < nitems; ++i) {
|
|
arr.push(this.decodeAny());
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
BunserBuf.prototype.decodeObject = function() {
|
|
this.expectCode(BSER_OBJECT);
|
|
var nitems = this.decodeInt();
|
|
var res = {};
|
|
for (var i = 0; i < nitems; ++i) {
|
|
var key = this.decodeString();
|
|
var val = this.decodeAny();
|
|
res[key] = val;
|
|
}
|
|
return res;
|
|
}
|
|
|
|
BunserBuf.prototype.decodeTemplate = function() {
|
|
this.expectCode(BSER_TEMPLATE);
|
|
var keys = this.decodeArray();
|
|
var nitems = this.decodeInt();
|
|
var arr = [];
|
|
for (var i = 0; i < nitems; ++i) {
|
|
var obj = {};
|
|
for (var keyidx = 0; keyidx < keys.length; ++keyidx) {
|
|
if (this.buf.peekInt(1) == BSER_SKIP) {
|
|
this.buf.readAdvance(1);
|
|
continue;
|
|
}
|
|
var val = this.decodeAny();
|
|
obj[keys[keyidx]] = val;
|
|
}
|
|
arr.push(obj);
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
BunserBuf.prototype.decodeString = function() {
|
|
this.expectCode(BSER_STRING);
|
|
var len = this.decodeInt();
|
|
return this.buf.readString(len);
|
|
}
|
|
|
|
// This is unusual compared to the other decode functions in that
|
|
// we may not have enough data available to satisfy the read, and
|
|
// we don't want to throw. This is only true when we're reading
|
|
// the PDU length from the PDU header; we'll set relaxSizeAsserts
|
|
// in that case.
|
|
BunserBuf.prototype.decodeInt = function(relaxSizeAsserts) {
|
|
if (relaxSizeAsserts && (this.buf.readAvail() < 1)) {
|
|
return false;
|
|
} else {
|
|
this.buf.assertReadableSize(1);
|
|
}
|
|
var code = this.buf.peekInt(1);
|
|
var size = 0;
|
|
switch (code) {
|
|
case BSER_INT8:
|
|
size = 1;
|
|
break;
|
|
case BSER_INT16:
|
|
size = 2;
|
|
break;
|
|
case BSER_INT32:
|
|
size = 4;
|
|
break;
|
|
case BSER_INT64:
|
|
size = 8;
|
|
break;
|
|
default:
|
|
this.raise("invalid bser int encoding " + code);
|
|
}
|
|
|
|
if (relaxSizeAsserts && (this.buf.readAvail() < 1 + size)) {
|
|
return false;
|
|
}
|
|
this.buf.readAdvance(1);
|
|
return this.buf.readInt(size);
|
|
}
|
|
|
|
// synchronously BSER decode a string and return the value
|
|
function loadFromBuffer(input) {
|
|
var buf = new BunserBuf();
|
|
var result = buf.append(input, true);
|
|
if (buf.buf.readAvail()) {
|
|
throw Error(
|
|
'excess data found after input buffer, use BunserBuf instead');
|
|
}
|
|
if (typeof result === 'undefined') {
|
|
throw Error(
|
|
'no bser found in string and no error raised!?');
|
|
}
|
|
return result;
|
|
}
|
|
exports.loadFromBuffer = loadFromBuffer
|
|
|
|
// Byteswap an arbitrary buffer, flipping from one endian
|
|
// to the other, returning a new buffer with the resultant data
|
|
function byteswap64(buf) {
|
|
var swap = Buffer.alloc(buf.length);
|
|
for (var i = 0; i < buf.length; i++) {
|
|
swap[i] = buf[buf.length -1 - i];
|
|
}
|
|
return swap;
|
|
}
|
|
|
|
function dump_int64(buf, val) {
|
|
// Get the raw bytes. The Int64 buffer is big endian
|
|
var be = val.toBuffer();
|
|
|
|
if (isBigEndian) {
|
|
// We're a big endian system, so the buffer is exactly how we
|
|
// want it to be
|
|
buf.writeByte(BSER_INT64);
|
|
buf.append(be);
|
|
return;
|
|
}
|
|
// We need to byte swap to get the correct representation
|
|
var le = byteswap64(be);
|
|
buf.writeByte(BSER_INT64);
|
|
buf.append(le);
|
|
}
|
|
|
|
function dump_int(buf, val) {
|
|
var abs = Math.abs(val);
|
|
if (abs <= MAX_INT8) {
|
|
buf.writeByte(BSER_INT8);
|
|
buf.writeInt(val, 1);
|
|
} else if (abs <= MAX_INT16) {
|
|
buf.writeByte(BSER_INT16);
|
|
buf.writeInt(val, 2);
|
|
} else if (abs <= MAX_INT32) {
|
|
buf.writeByte(BSER_INT32);
|
|
buf.writeInt(val, 4);
|
|
} else {
|
|
dump_int64(buf, new Int64(val));
|
|
}
|
|
}
|
|
|
|
function dump_any(buf, val) {
|
|
switch (typeof(val)) {
|
|
case 'number':
|
|
// check if it is an integer or a float
|
|
if (isFinite(val) && Math.floor(val) === val) {
|
|
dump_int(buf, val);
|
|
} else {
|
|
buf.writeByte(BSER_REAL);
|
|
buf.writeDouble(val);
|
|
}
|
|
return;
|
|
case 'string':
|
|
buf.writeByte(BSER_STRING);
|
|
dump_int(buf, Buffer.byteLength(val));
|
|
buf.append(val);
|
|
return;
|
|
case 'boolean':
|
|
buf.writeByte(val ? BSER_TRUE : BSER_FALSE);
|
|
return;
|
|
case 'object':
|
|
if (val === null) {
|
|
buf.writeByte(BSER_NULL);
|
|
return;
|
|
}
|
|
if (val instanceof Int64) {
|
|
dump_int64(buf, val);
|
|
return;
|
|
}
|
|
if (Array.isArray(val)) {
|
|
buf.writeByte(BSER_ARRAY);
|
|
dump_int(buf, val.length);
|
|
for (var i = 0; i < val.length; ++i) {
|
|
dump_any(buf, val[i]);
|
|
}
|
|
return;
|
|
}
|
|
buf.writeByte(BSER_OBJECT);
|
|
var keys = Object.keys(val);
|
|
|
|
// First pass to compute number of defined keys
|
|
var num_keys = keys.length;
|
|
for (var i = 0; i < keys.length; ++i) {
|
|
var key = keys[i];
|
|
var v = val[key];
|
|
if (typeof(v) == 'undefined') {
|
|
num_keys--;
|
|
}
|
|
}
|
|
dump_int(buf, num_keys);
|
|
for (var i = 0; i < keys.length; ++i) {
|
|
var key = keys[i];
|
|
var v = val[key];
|
|
if (typeof(v) == 'undefined') {
|
|
// Don't include it
|
|
continue;
|
|
}
|
|
dump_any(buf, key);
|
|
try {
|
|
dump_any(buf, v);
|
|
} catch (e) {
|
|
throw new Error(
|
|
e.message + ' (while serializing object property with name `' +
|
|
key + "')");
|
|
}
|
|
}
|
|
return;
|
|
|
|
default:
|
|
throw new Error('cannot serialize type ' + typeof(val) + ' to BSER');
|
|
}
|
|
}
|
|
|
|
// BSER encode value and return a buffer of the contents
|
|
function dumpToBuffer(val) {
|
|
var buf = new Accumulator();
|
|
// Build out the header
|
|
buf.writeByte(0);
|
|
buf.writeByte(1);
|
|
// Reserve room for an int32 to hold our PDU length
|
|
buf.writeByte(BSER_INT32);
|
|
buf.writeInt(0, 4); // We'll come back and fill this in at the end
|
|
|
|
dump_any(buf, val);
|
|
|
|
// Compute PDU length
|
|
var off = buf.writeOffset;
|
|
var len = off - 7 /* the header length */;
|
|
buf.writeOffset = 3; // The length value to fill in
|
|
buf.writeInt(len, 4); // write the length in the space we reserved
|
|
buf.writeOffset = off;
|
|
|
|
return buf.buf.slice(0, off);
|
|
}
|
|
exports.dumpToBuffer = dumpToBuffer
|