You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							586 lines
						
					
					
						
							15 KiB
						
					
					
				
			
		
		
	
	
							586 lines
						
					
					
						
							15 KiB
						
					
					
				/* 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
 | 
						|
 |