From 2a54053b7e2bda99e812ce7bb928db43c41ba3e0 Mon Sep 17 00:00:00 2001 From: Dan Aprahamian Date: Tue, 19 Jun 2018 16:57:01 -0400 Subject: [PATCH] feat(UintArray): Adds support for Uint8Arrays All API calls can now handle taking in a Uint8Array instead of a buffer. Consumers will still need to globally provide a polyfill for Buffer, as it is still used internally. --- README.md | 4 +- lib/bson/bson.js | 13 +++-- lib/bson/ensure_buffer.js | 20 ++++++++ test/node/bson_test.js | 89 ++++++++++++++++++++++----------- test/node/ensure_buffer_test.js | 61 ++++++++++++++++++++++ 5 files changed, 152 insertions(+), 35 deletions(-) create mode 100644 lib/bson/ensure_buffer.js create mode 100644 test/node/ensure_buffer_test.js diff --git a/README.md b/README.md index d5c238bf..43646c1b 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ The BSON `serializeWithBufferAndIndex` method takes an object, a target buffer i * `BSON.serializeWithBufferAndIndex(object, buffer, options)` * @param {Object} object the JavaScript object to serialize. - * @param {Buffer} buffer the Buffer you pre-allocated to store the serialized BSON object. + * @param {Buffer|Uint8Array} buffer the Buffer you pre-allocated to store the serialized BSON object. * @param {Boolean} [options.checkKeys=false] the serializer will check if keys are valid. * @param {Boolean} [options.serializeFunctions=false] serialize the JavaScript functions. * @param {Boolean} [options.ignoreUndefined=true] ignore undefined fields. @@ -128,7 +128,7 @@ The BSON `deserialize` method takes a Node.js Buffer and an optional options obj The BSON `deserializeStream` method takes a Node.js Buffer, `startIndex` and allow more control over deserialization of a Buffer containing concatenated BSON documents. * `BSON.deserializeStream(buffer, startIndex, numberOfDocuments, documents, docStartIndex, options)` - * @param {Buffer} buffer the buffer containing the serialized set of BSON documents. + * @param {Buffer|Uint8Array} buffer the buffer containing the serialized set of BSON documents. * @param {Number} startIndex the start index in the data Buffer where the deserialization is to start. * @param {Number} numberOfDocuments number of documents to deserialize. * @param {Array} documents an array where to store the deserialized documents. diff --git a/lib/bson/bson.js b/lib/bson/bson.js index 16ecc90f..4e27011f 100644 --- a/lib/bson/bson.js +++ b/lib/bson/bson.js @@ -20,6 +20,8 @@ var deserialize = require('./parser/deserializer'), serializer = require('./parser/serializer'), calculateObjectSize = require('./parser/calculate_size'); +const ensureBuffer = require('./ensure_buffer'); + /** * @ignore * @api private @@ -82,7 +84,7 @@ BSON.prototype.serialize = function serialize(object, options) { * Serialize a Javascript object using a predefined Buffer and index into the buffer, useful when pre-allocating the space for serialization. * * @param {Object} object the Javascript object to serialize. - * @param {Buffer} buffer the Buffer you pre-allocated to store the serialized BSON object. + * @param {Buffer|Uint8Array} buffer the Buffer you pre-allocated to store the serialized BSON object. * @param {Boolean} [options.checkKeys] the serializer will check if keys are valid. * @param {Boolean} [options.serializeFunctions=false] serialize the javascript functions **(default:false)**. * @param {Boolean} [options.ignoreUndefined=true] ignore undefined fields **(default:true)**. @@ -110,6 +112,9 @@ BSON.prototype.serializeWithBufferAndIndex = function(object, finalBuffer, optio serializeFunctions, ignoreUndefined ); + + finalBuffer = ensureBuffer(finalBuffer); + buffer.copy(finalBuffer, startIndex, 0, serializationIndex); // Return the index @@ -119,7 +124,7 @@ BSON.prototype.serializeWithBufferAndIndex = function(object, finalBuffer, optio /** * Deserialize data as BSON. * - * @param {Buffer} buffer the buffer containing the serialized set of BSON documents. + * @param {Buffer|Uint8Array} buffer the buffer containing the serialized set of BSON documents. * @param {Object} [options.evalFunctions=false] evaluate functions in the BSON document scoped to the object deserialized. * @param {Object} [options.cacheFunctions=false] cache evaluated functions for reuse. * @param {Object} [options.cacheFunctionsCrc32=false] use a crc32 code for caching, otherwise use the string of the function. @@ -133,6 +138,7 @@ BSON.prototype.serializeWithBufferAndIndex = function(object, finalBuffer, optio * @api public */ BSON.prototype.deserialize = function(buffer, options) { + buffer = ensureBuffer(buffer); return deserialize(buffer, options); }; @@ -159,7 +165,7 @@ BSON.prototype.calculateObjectSize = function(object, options) { /** * Deserialize stream data as BSON documents. * - * @param {Buffer} data the buffer containing the serialized set of BSON documents. + * @param {Buffer|Uint8Array} data the buffer containing the serialized set of BSON documents. * @param {Number} startIndex the start index in the data Buffer where the deserialization is to start. * @param {Number} numberOfDocuments number of documents to deserialize. * @param {Array} documents an array where to store the deserialized documents. @@ -185,6 +191,7 @@ BSON.prototype.deserializeStream = function( options ) { options = Object.assign({ allowObjectSmallerThanBufferSize: true }, options); + data = ensureBuffer(data); var index = startIndex; // Loop over all documents for (var i = 0; i < numberOfDocuments; i++) { diff --git a/lib/bson/ensure_buffer.js b/lib/bson/ensure_buffer.js new file mode 100644 index 00000000..bae9f5e8 --- /dev/null +++ b/lib/bson/ensure_buffer.js @@ -0,0 +1,20 @@ +'use strict'; + +/** + * Makes sure that, if a Uint8Array is passed in, it is wrapped in a Buffer. + * + * @param {Buffer|Uint8Array} potentialBuffer The potential buffer + * @returns {Buffer} the input if potentialBuffer is a buffer, or a buffer that + * wraps a passed in Uint8Array + * @throws {TypeError} If anything other than a Buffer or Uint8Array is passed in + */ +module.exports = function ensureBuffer(potentialBuffer) { + if (potentialBuffer instanceof Buffer) { + return potentialBuffer; + } + if (potentialBuffer instanceof Uint8Array) { + return new Buffer(potentialBuffer.buffer); + } + + throw new TypeError('Must use either Buffer or Uint8Array'); +}; diff --git a/test/node/bson_test.js b/test/node/bson_test.js index 33538d27..de7432f3 100644 --- a/test/node/bson_test.js +++ b/test/node/bson_test.js @@ -86,6 +86,19 @@ var ISODate = function(string) { } else throw new Error('Invalid ISO 8601 date given.', __filename); }; +function runTestsOnBytesForBufferAndUint8Array(bytes, testFn) { + let serialized_data = ''; + // Convert to chars + for (let i = 0; i < bytes.length; i++) { + serialized_data = serialized_data + BinaryParser.fromByte(bytes[i]); + } + + const uint8Array = Uint8Array.from(bytes); + const buffer = new Buffer(serialized_data, 'binary'); + + [uint8Array, buffer].forEach(testFn); +} + describe('BSON', function() { /** * @ignore @@ -224,16 +237,19 @@ describe('BSON', function() { 0, 0 ]; - var serialized_data = ''; - // Convert to chars - for (var i = 0; i < bytes.length; i++) { - serialized_data = serialized_data + BinaryParser.fromByte(bytes[i]); - } - var object = createBSON().deserialize(new Buffer(serialized_data, 'binary')); - expect('a_1').to.equal(object.name); - expect(false).to.equal(object.unique); - expect(1).to.equal(object.key.a); + runTestsOnBytesForBufferAndUint8Array(bytes, data => { + let object = createBSON().deserialize(data); + expect('a_1').to.equal(object.name); + expect(false).to.equal(object.unique); + expect(1).to.equal(object.key.a); + + object = createBSON().deserialize(Uint8Array.from(bytes)); + expect('a_1').to.equal(object.name); + expect(false).to.equal(object.unique); + expect(1).to.equal(object.key.a); + }); + done(); }); @@ -525,29 +541,26 @@ describe('BSON', function() { 0, 0 ]; - var serialized_data = ''; - // Convert to chars - for (var i = 0; i < bytes.length; i++) { - serialized_data = serialized_data + BinaryParser.fromByte(bytes[i]); - } + runTestsOnBytesForBufferAndUint8Array(bytes, data => { + const object = createBSON().deserialize(data); + // Perform tests + expect('hello').to.equal(object.string); + expect([1, 2, 3]).to.deep.equal(object.array); + expect(1).to.equal(object.hash.a); + expect(2).to.equal(object.hash.b); + expect(object.date != null).to.be.ok; + expect(object.oid != null).to.be.ok; + expect(object.binary != null).to.be.ok; + expect(42).to.equal(object.int); + expect(33.3333).to.equal(object.float); + expect(object.regexp != null).to.be.ok; + expect(true).to.equal(object.boolean); + expect(object.where != null).to.be.ok; + expect(object.dbref != null).to.be.ok; + expect(object[null] == null).to.be.ok; + }); - var object = createBSON().deserialize(new Buffer(serialized_data, 'binary')); - // Perform tests - expect('hello').to.equal(object.string); - expect([1, 2, 3]).to.deep.equal(object.array); - expect(1).to.equal(object.hash.a); - expect(2).to.equal(object.hash.b); - expect(object.date != null).to.be.ok; - expect(object.oid != null).to.be.ok; - expect(object.binary != null).to.be.ok; - expect(42).to.equal(object.int); - expect(33.3333).to.equal(object.float); - expect(object.regexp != null).to.be.ok; - expect(true).to.equal(object.boolean); - expect(object.where != null).to.be.ok; - expect(object.dbref != null).to.be.ok; - expect(object[null] == null).to.be.ok; done(); }); @@ -2317,4 +2330,20 @@ describe('BSON', function() { expect(false).to.equal(id.equals(undefined)); done(); }); + + it('Should serialize the same values to a Buffer and a Uint8Array', function() { + const testData = { darmok: 'jalad' }; + + const dataLength = createBSON().serialize(testData).length; + const buffer = new Buffer(dataLength); + const uint8Array = new Uint8Array(dataLength); + + createBSON().serializeWithBufferAndIndex(testData, buffer); + createBSON().serializeWithBufferAndIndex(testData, uint8Array); + + const bufferRaw = Array.prototype.slice.call(buffer, 0); + const uint8ArrayRaw = Array.prototype.slice.call(uint8Array, 0); + + expect(bufferRaw).to.deep.equal(uint8ArrayRaw); + }); }); diff --git a/test/node/ensure_buffer_test.js b/test/node/ensure_buffer_test.js new file mode 100644 index 00000000..1495efc4 --- /dev/null +++ b/test/node/ensure_buffer_test.js @@ -0,0 +1,61 @@ +'use strict'; + +const ensureBuffer = require('../../lib/bson/ensure_buffer'); +const expect = require('chai').expect; + +describe('ensureBuffer tests', function() { + it('should be a function', function() { + expect(ensureBuffer).to.be.a('function'); + }); + + it('should return the exact same buffer if a buffer is passed in', function() { + const bufferIn = new Buffer(10); + let bufferOut; + + expect(function() { + bufferOut = ensureBuffer(bufferIn); + }).to.not.throw(Error); + + expect(bufferOut).to.equal(bufferIn); + }); + + it('should wrap a UInt8Array with a buffer', function() { + const arrayIn = Uint8Array.from([1, 2, 3]); + let bufferOut; + + expect(function() { + bufferOut = ensureBuffer(arrayIn); + }).to.not.throw(Error); + + expect(bufferOut).to.be.an.instanceOf(Buffer); + expect(bufferOut.buffer).to.equal(arrayIn.buffer); + }); + + [0, 12, -1, '', 'foo', null, undefined, ['list'], {}, /x/].forEach(function(item) { + it(`should throw if input is ${typeof item}: ${item}`, function() { + expect(function() { + ensureBuffer(item); + }).to.throw(TypeError); + }); + }); + + [ + /* eslint-disable */ + Int8Array, + Uint8ClampedArray, + Int16Array, + Uint16Array, + Int32Array, + Uint32Array, + Float32Array, + Float64Array + /* eslint-enable */ + ].forEach(function(TypedArray) { + it(`should throw if input is typed array ${TypedArray.name}`, function() { + const typedArray = new TypedArray(); + expect(function() { + ensureBuffer(typedArray); + }).to.throw(TypeError); + }); + }); +});