Skip to content

Commit

Permalink
feat(NODE-4855): add hex and base64 ctor methods to Binary and Object…
Browse files Browse the repository at this point in the history
…Id (#569)
  • Loading branch information
nbbeeken committed Apr 4, 2023
1 parent 5d2648e commit 0d49a63
Show file tree
Hide file tree
Showing 7 changed files with 276 additions and 28 deletions.
20 changes: 18 additions & 2 deletions src/binary.ts
Expand Up @@ -258,6 +258,16 @@ export class Binary extends BSONValue {
);
}

/** Creates an Binary instance from a hex digit string */
static createFromHexString(hex: string, subType?: number): Binary {
return new Binary(ByteUtils.fromHex(hex), subType);
}

/** Creates an Binary instance from a base64 string */
static createFromBase64(base64: string, subType?: number): Binary {
return new Binary(ByteUtils.fromBase64(base64), subType);
}

/** @internal */
static fromExtendedJSON(
doc: BinaryExtendedLegacy | BinaryExtended | UUIDExtended,
Expand Down Expand Up @@ -292,7 +302,8 @@ export class Binary extends BSONValue {
}

inspect(): string {
return `new Binary(Buffer.from("${ByteUtils.toHex(this.buffer)}", "hex"), ${this.sub_type})`;
const base64 = ByteUtils.toBase64(this.buffer.subarray(0, this.position));
return `Binary.createFromBase64("${base64}", ${this.sub_type})`;
}
}

Expand Down Expand Up @@ -464,11 +475,16 @@ export class UUID extends Binary {
* Creates an UUID from a hex string representation of an UUID.
* @param hexString - 32 or 36 character hex string (dashes excluded/included).
*/
static createFromHexString(hexString: string): UUID {
static override createFromHexString(hexString: string): UUID {
const buffer = uuidHexStringToBuffer(hexString);
return new UUID(buffer);
}

/** Creates an UUID from a base64 string representation of an UUID. */
static override createFromBase64(base64: string): UUID {
return new UUID(ByteUtils.fromBase64(base64));
}

/**
* Converts to a string representation of this Id.
*
Expand Down
16 changes: 11 additions & 5 deletions src/objectid.ts
Expand Up @@ -264,16 +264,22 @@ export class ObjectId extends BSONValue {
* @param hexString - create a ObjectId from a passed in 24 character hexstring.
*/
static createFromHexString(hexString: string): ObjectId {
// Throw an error if it's not a valid setup
if (typeof hexString === 'undefined' || (hexString != null && hexString.length !== 24)) {
throw new BSONError(
'Argument passed in must be a single String of 12 bytes or a string of 24 hex characters'
);
if (hexString?.length !== 24) {
throw new BSONError('hex string must be 24 characters');
}

return new ObjectId(ByteUtils.fromHex(hexString));
}

/** Creates an ObjectId instance from a base64 string */
static createFromBase64(base64: string): ObjectId {
if (base64?.length !== 16) {
throw new BSONError('base64 string must be 16 characters');
}

return new ObjectId(ByteUtils.fromBase64(base64));
}

/**
* Checks if a value is a valid bson ObjectId
*
Expand Down
144 changes: 144 additions & 0 deletions test/node/binary.test.ts
@@ -0,0 +1,144 @@
import { expect } from 'chai';
import * as vm from 'node:vm';
import { Binary, BSON } from '../register-bson';

describe('class Binary', () => {
context('constructor()', () => {
it('creates an 256 byte Binary with subtype 0 by default', () => {
const binary = new Binary();
expect(binary).to.have.property('buffer');
expect(binary).to.have.property('position', 0);
expect(binary).to.have.property('sub_type', 0);
expect(binary).to.have.nested.property('buffer.byteLength', 256);
const emptyZeroedArray = new Uint8Array(256);
emptyZeroedArray.fill(0x00);
expect(binary.buffer).to.deep.equal(emptyZeroedArray);
});
});

context('createFromHexString()', () => {
context('when called with a hex sequence', () => {
it('returns a Binary instance with the decoded bytes', () => {
const bytes = Buffer.from('abc', 'utf8');
const binary = Binary.createFromHexString(bytes.toString('hex'));
expect(binary).to.have.deep.property('buffer', bytes);
expect(binary).to.have.property('sub_type', 0);
});

it('returns a Binary instance with the decoded bytes and subtype', () => {
const bytes = Buffer.from('abc', 'utf8');
const binary = Binary.createFromHexString(bytes.toString('hex'), 0x23);
expect(binary).to.have.deep.property('buffer', bytes);
expect(binary).to.have.property('sub_type', 0x23);
});
});

context('when called with an empty string', () => {
it('creates an empty binary', () => {
const binary = Binary.createFromHexString('');
expect(binary).to.have.deep.property('buffer', new Uint8Array(0));
expect(binary).to.have.property('sub_type', 0);
});

it('creates an empty binary with subtype', () => {
const binary = Binary.createFromHexString('', 0x42);
expect(binary).to.have.deep.property('buffer', new Uint8Array(0));
expect(binary).to.have.property('sub_type', 0x42);
});
});
});

context('createFromBase64()', () => {
context('when called with a base64 sequence', () => {
it('returns a Binary instance with the decoded bytes', () => {
const bytes = Buffer.from('abc', 'utf8');
const binary = Binary.createFromBase64(bytes.toString('base64'));
expect(binary).to.have.deep.property('buffer', bytes);
expect(binary).to.have.property('sub_type', 0);
});

it('returns a Binary instance with the decoded bytes and subtype', () => {
const bytes = Buffer.from('abc', 'utf8');
const binary = Binary.createFromBase64(bytes.toString('base64'), 0x23);
expect(binary).to.have.deep.property('buffer', bytes);
expect(binary).to.have.property('sub_type', 0x23);
});
});

context('when called with an empty string', () => {
it('creates an empty binary', () => {
const binary = Binary.createFromBase64('');
expect(binary).to.have.deep.property('buffer', new Uint8Array(0));
expect(binary).to.have.property('sub_type', 0);
});

it('creates an empty binary with subtype', () => {
const binary = Binary.createFromBase64('', 0x42);
expect(binary).to.have.deep.property('buffer', new Uint8Array(0));
expect(binary).to.have.property('sub_type', 0x42);
});
});
});

context('inspect()', () => {
it('when value is default returns "Binary.createFromBase64("", 0)"', () => {
expect(new Binary().inspect()).to.equal('Binary.createFromBase64("", 0)');
});

it('when value is empty returns "Binary.createFromBase64("", 0)"', () => {
expect(new Binary(new Uint8Array(0)).inspect()).to.equal('Binary.createFromBase64("", 0)');
});

it('when value is default with a subtype returns "Binary.createFromBase64("", 35)"', () => {
expect(new Binary(null, 0x23).inspect()).to.equal('Binary.createFromBase64("", 35)');
});

it('when value is empty with a subtype returns "Binary.createFromBase64("", 35)"', () => {
expect(new Binary(new Uint8Array(0), 0x23).inspect()).to.equal(
'Binary.createFromBase64("", 35)'
);
});

it('when value has utf8 "abcdef" encoded returns "Binary.createFromBase64("YWJjZGVm", 0)"', () => {
expect(new Binary(Buffer.from('abcdef', 'utf8')).inspect()).to.equal(
'Binary.createFromBase64("YWJjZGVm", 0)'
);
});

context('when result is executed', () => {
it('has a position of zero when constructed with default space', () => {
const bsonValue = new Binary();
const ctx = { ...BSON, module: { exports: { result: null } } };
vm.runInNewContext(`module.exports.result = ${bsonValue.inspect()}`, ctx);
expect(ctx.module.exports.result).to.have.property('position', 0);
expect(ctx.module.exports.result).to.have.property('sub_type', 0);

// While the default Binary has 256 bytes the newly constructed one will have 0
// both will have a position of zero so when serialized to BSON they are the equivalent.
expect(ctx.module.exports.result).to.have.nested.property('buffer.byteLength', 0);
expect(bsonValue).to.have.nested.property('buffer.byteLength', 256);
});

it('is deep equal with a Binary that has no data', () => {
const bsonValue = new Binary(new Uint8Array(0));
const ctx = { ...BSON, module: { exports: { result: null } } };
vm.runInNewContext(`module.exports.result = ${bsonValue.inspect()}`, ctx);
expect(ctx.module.exports.result).to.deep.equal(bsonValue);
});

it('is deep equal with a Binary that has a subtype but no data', () => {
const bsonValue = new Binary(new Uint8Array(0), 0x23);
const ctx = { ...BSON, module: { exports: { result: null } } };
vm.runInNewContext(`module.exports.result = ${bsonValue.inspect()}`, ctx);
expect(ctx.module.exports.result).to.deep.equal(bsonValue);
});

it('is deep equal with a Binary that has data', () => {
const bsonValue = new Binary(Buffer.from('abc', 'utf8'));
const ctx = { ...BSON, module: { exports: { result: null } } };
vm.runInNewContext(`module.exports.result = ${bsonValue.inspect()}`, ctx);
expect(ctx.module.exports.result).to.deep.equal(bsonValue);
});
});
});
});
4 changes: 1 addition & 3 deletions test/node/bson_test.js
Expand Up @@ -1829,9 +1829,7 @@ describe('BSON', function () {
*/
it('Binary', function () {
const binary = new Binary(Buffer.from('0123456789abcdef0123456789abcdef', 'hex'), 4);
expect(inspect(binary)).to.equal(
'new Binary(Buffer.from("0123456789abcdef0123456789abcdef", "hex"), 4)'
);
expect(inspect(binary)).to.equal('Binary.createFromBase64("ASNFZ4mrze8BI0VniavN7w==", 4)');
});

/**
Expand Down
19 changes: 18 additions & 1 deletion test/node/bson_type_classes.test.ts
Expand Up @@ -17,6 +17,7 @@ import {
UUID,
BSONValue
} from '../register-bson';
import * as vm from 'node:vm';

const BSONTypeClasses = [
Binary,
Expand All @@ -36,7 +37,7 @@ const BSONTypeClasses = [
];

const BSONTypeClassCtors = new Map<string, () => BSONValue>([
['Binary', () => new Binary()],
['Binary', () => new Binary(new Uint8Array(0), 0)],
['Code', () => new Code('function () {}')],
['DBRef', () => new DBRef('name', new ObjectId('00'.repeat(12)))],
['Decimal128', () => new Decimal128('1.23')],
Expand Down Expand Up @@ -97,4 +98,20 @@ describe('BSON Type classes common interfaces', () => {
.that.is.a('function'));
});
}

context(`when inspect() is called`, () => {
for (const [name, factory] of BSONTypeClassCtors) {
it(`${name} returns string that is runnable and has deep equality`, () => {
const bsonValue = factory();
// All BSON types should only need exactly their constructor available on the global
const ctx = { [name]: bsonValue.constructor, module: { exports: { result: null } } };
if (name === 'DBRef') {
// DBRef is the only type that requires another BSON type
ctx.ObjectId = ObjectId;
}
vm.runInNewContext(`module.exports.result = ${bsonValue.inspect()}`, ctx);
expect(ctx.module.exports.result).to.deep.equal(bsonValue);
});
}
});
});
48 changes: 39 additions & 9 deletions test/node/object_id_tests.js → test/node/object_id.test.ts
@@ -1,12 +1,10 @@
'use strict';

const Buffer = require('buffer').Buffer;
const { BSON, BSONError, EJSON, ObjectId } = require('../register-bson');
const util = require('util');
const { expect } = require('chai');
const { bufferFromHexArray } = require('./tools/utils');
const getSymbolFrom = require('./tools/utils').getSymbolFrom;
const isBufferOrUint8Array = require('./tools/utils').isBufferOrUint8Array;
import { Buffer } from 'buffer';
import { BSON, BSONError, EJSON, ObjectId } from '../register-bson';
import * as util from 'util';
import { expect } from 'chai';
import { bufferFromHexArray } from './tools/utils';
import { getSymbolFrom } from './tools/utils';
import { isBufferOrUint8Array } from './tools/utils';

describe('ObjectId', function () {
describe('static createFromTime()', () => {
Expand Down Expand Up @@ -477,4 +475,36 @@ describe('ObjectId', function () {
// class method equality
expect(Buffer.prototype.equals.call(inBuffer, outBuffer.id)).to.be.true;
});

context('createFromHexString()', () => {
context('when called with a hex sequence', () => {
it('returns a ObjectId instance with the decoded bytes', () => {
const bytes = Buffer.from('0'.repeat(24), 'hex');
const binary = ObjectId.createFromHexString(bytes.toString('hex'));
expect(binary).to.have.deep.property('id', bytes);
});
});

context('when called with an incorrect length string', () => {
it('throws an error indicating the expected length of 24', () => {
expect(() => ObjectId.createFromHexString('')).to.throw(/24/);
});
});
});

context('createFromBase64()', () => {
context('when called with a base64 sequence', () => {
it('returns a ObjectId instance with the decoded bytes', () => {
const bytes = Buffer.from('A'.repeat(16), 'base64');
const binary = ObjectId.createFromBase64(bytes.toString('base64'));
expect(binary).to.have.deep.property('id', bytes);
});
});

context('when called with an incorrect length string', () => {
it('throws an error indicating the expected length of 16', () => {
expect(() => ObjectId.createFromBase64('')).to.throw(/16/);
});
});
});
});
53 changes: 45 additions & 8 deletions test/node/uuid_tests.js → test/node/uuid.test.ts
@@ -1,12 +1,10 @@
'use strict';

const { Buffer } = require('buffer');
const { Binary, UUID } = require('../register-bson');
const { inspect } = require('util');
const { validate: uuidStringValidate, version: uuidStringVersion } = require('uuid');
const { BSON, BSONError } = require('../register-bson');
import { Binary, UUID } from '../register-bson';
import { inspect } from 'util';
import { validate as uuidStringValidate, version as uuidStringVersion } from 'uuid';
import { BSON, BSONError } from '../register-bson';
const BSON_DATA_BINARY = BSON.BSONType.binData;
const { BSON_BINARY_SUBTYPE_UUID_NEW } = require('../../src/constants');
import { BSON_BINARY_SUBTYPE_UUID_NEW } from '../../src/constants';
import { expect } from 'chai';

// Test values
const UPPERCASE_DASH_SEPARATED_UUID_STRING = 'AAAAAAAA-AAAA-4AAA-AAAA-AAAAAAAAAAAA';
Expand Down Expand Up @@ -202,4 +200,43 @@ describe('UUID', () => {
expect(deserializedUUID).to.deep.equal(expectedResult);
});
});

context('createFromHexString()', () => {
context('when called with a hex sequence', () => {
it('returns a UUID instance with the decoded bytes', () => {
const bytes = Buffer.from(UPPERCASE_VALUES_ONLY_UUID_STRING, 'hex');

const uuidDashed = UUID.createFromHexString(UPPERCASE_DASH_SEPARATED_UUID_STRING);
expect(uuidDashed).to.have.deep.property('buffer', bytes);
expect(uuidDashed).to.be.instanceOf(UUID);

const uuidNoDashed = UUID.createFromHexString(UPPERCASE_VALUES_ONLY_UUID_STRING);
expect(uuidNoDashed).to.have.deep.property('buffer', bytes);
expect(uuidNoDashed).to.be.instanceOf(UUID);
});
});

context('when called with an incorrect length string', () => {
it('throws an error indicating the expected length of 32 or 36 characters', () => {
expect(() => UUID.createFromHexString('')).to.throw(/32 or 36 character/);
});
});
});

context('createFromBase64()', () => {
context('when called with a base64 sequence', () => {
it('returns a UUID instance with the decoded bytes', () => {
const bytes = Buffer.from(UPPERCASE_VALUES_ONLY_UUID_STRING, 'hex');
const uuid = UUID.createFromBase64(bytes.toString('base64'));
expect(uuid).to.have.deep.property('buffer', bytes);
expect(uuid).to.be.instanceOf(UUID);
});
});

context('when called with an incorrect length string', () => {
it('throws an error indicating the expected length of 16 byte Buffer', () => {
expect(() => UUID.createFromBase64('')).to.throw(/16 byte Buffer/);
});
});
});
});

0 comments on commit 0d49a63

Please sign in to comment.