diff --git a/src/binary.ts b/src/binary.ts index f95de920..525093ae 100644 --- a/src/binary.ts +++ b/src/binary.ts @@ -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, @@ -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})`; } } @@ -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. * diff --git a/src/objectid.ts b/src/objectid.ts index a0358208..7e719898 100644 --- a/src/objectid.ts +++ b/src/objectid.ts @@ -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 * diff --git a/test/node/binary.test.ts b/test/node/binary.test.ts new file mode 100644 index 00000000..5fdaa107 --- /dev/null +++ b/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); + }); + }); + }); +}); diff --git a/test/node/bson_test.js b/test/node/bson_test.js index eea885d9..f0850468 100644 --- a/test/node/bson_test.js +++ b/test/node/bson_test.js @@ -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)'); }); /** diff --git a/test/node/bson_type_classes.test.ts b/test/node/bson_type_classes.test.ts index 8d69d5b5..3d7c3497 100644 --- a/test/node/bson_type_classes.test.ts +++ b/test/node/bson_type_classes.test.ts @@ -17,6 +17,7 @@ import { UUID, BSONValue } from '../register-bson'; +import * as vm from 'node:vm'; const BSONTypeClasses = [ Binary, @@ -36,7 +37,7 @@ const BSONTypeClasses = [ ]; const BSONTypeClassCtors = new Map 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')], @@ -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); + }); + } + }); }); diff --git a/test/node/object_id_tests.js b/test/node/object_id.test.ts similarity index 92% rename from test/node/object_id_tests.js rename to test/node/object_id.test.ts index 8bbf4420..45b6f640 100644 --- a/test/node/object_id_tests.js +++ b/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()', () => { @@ -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/); + }); + }); + }); }); diff --git a/test/node/uuid_tests.js b/test/node/uuid.test.ts similarity index 79% rename from test/node/uuid_tests.js rename to test/node/uuid.test.ts index f5088c4a..bf94494e 100644 --- a/test/node/uuid_tests.js +++ b/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'; @@ -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/); + }); + }); + }); });