Skip to content

Commit 0d49a63

Browse files
authored
feat(NODE-4855): add hex and base64 ctor methods to Binary and ObjectId (#569)
1 parent 5d2648e commit 0d49a63

File tree

7 files changed

+276
-28
lines changed

7 files changed

+276
-28
lines changed

src/binary.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,16 @@ export class Binary extends BSONValue {
258258
);
259259
}
260260

261+
/** Creates an Binary instance from a hex digit string */
262+
static createFromHexString(hex: string, subType?: number): Binary {
263+
return new Binary(ByteUtils.fromHex(hex), subType);
264+
}
265+
266+
/** Creates an Binary instance from a base64 string */
267+
static createFromBase64(base64: string, subType?: number): Binary {
268+
return new Binary(ByteUtils.fromBase64(base64), subType);
269+
}
270+
261271
/** @internal */
262272
static fromExtendedJSON(
263273
doc: BinaryExtendedLegacy | BinaryExtended | UUIDExtended,
@@ -292,7 +302,8 @@ export class Binary extends BSONValue {
292302
}
293303

294304
inspect(): string {
295-
return `new Binary(Buffer.from("${ByteUtils.toHex(this.buffer)}", "hex"), ${this.sub_type})`;
305+
const base64 = ByteUtils.toBase64(this.buffer.subarray(0, this.position));
306+
return `Binary.createFromBase64("${base64}", ${this.sub_type})`;
296307
}
297308
}
298309

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

483+
/** Creates an UUID from a base64 string representation of an UUID. */
484+
static override createFromBase64(base64: string): UUID {
485+
return new UUID(ByteUtils.fromBase64(base64));
486+
}
487+
472488
/**
473489
* Converts to a string representation of this Id.
474490
*

src/objectid.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -264,16 +264,22 @@ export class ObjectId extends BSONValue {
264264
* @param hexString - create a ObjectId from a passed in 24 character hexstring.
265265
*/
266266
static createFromHexString(hexString: string): ObjectId {
267-
// Throw an error if it's not a valid setup
268-
if (typeof hexString === 'undefined' || (hexString != null && hexString.length !== 24)) {
269-
throw new BSONError(
270-
'Argument passed in must be a single String of 12 bytes or a string of 24 hex characters'
271-
);
267+
if (hexString?.length !== 24) {
268+
throw new BSONError('hex string must be 24 characters');
272269
}
273270

274271
return new ObjectId(ByteUtils.fromHex(hexString));
275272
}
276273

274+
/** Creates an ObjectId instance from a base64 string */
275+
static createFromBase64(base64: string): ObjectId {
276+
if (base64?.length !== 16) {
277+
throw new BSONError('base64 string must be 16 characters');
278+
}
279+
280+
return new ObjectId(ByteUtils.fromBase64(base64));
281+
}
282+
277283
/**
278284
* Checks if a value is a valid bson ObjectId
279285
*

test/node/binary.test.ts

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { expect } from 'chai';
2+
import * as vm from 'node:vm';
3+
import { Binary, BSON } from '../register-bson';
4+
5+
describe('class Binary', () => {
6+
context('constructor()', () => {
7+
it('creates an 256 byte Binary with subtype 0 by default', () => {
8+
const binary = new Binary();
9+
expect(binary).to.have.property('buffer');
10+
expect(binary).to.have.property('position', 0);
11+
expect(binary).to.have.property('sub_type', 0);
12+
expect(binary).to.have.nested.property('buffer.byteLength', 256);
13+
const emptyZeroedArray = new Uint8Array(256);
14+
emptyZeroedArray.fill(0x00);
15+
expect(binary.buffer).to.deep.equal(emptyZeroedArray);
16+
});
17+
});
18+
19+
context('createFromHexString()', () => {
20+
context('when called with a hex sequence', () => {
21+
it('returns a Binary instance with the decoded bytes', () => {
22+
const bytes = Buffer.from('abc', 'utf8');
23+
const binary = Binary.createFromHexString(bytes.toString('hex'));
24+
expect(binary).to.have.deep.property('buffer', bytes);
25+
expect(binary).to.have.property('sub_type', 0);
26+
});
27+
28+
it('returns a Binary instance with the decoded bytes and subtype', () => {
29+
const bytes = Buffer.from('abc', 'utf8');
30+
const binary = Binary.createFromHexString(bytes.toString('hex'), 0x23);
31+
expect(binary).to.have.deep.property('buffer', bytes);
32+
expect(binary).to.have.property('sub_type', 0x23);
33+
});
34+
});
35+
36+
context('when called with an empty string', () => {
37+
it('creates an empty binary', () => {
38+
const binary = Binary.createFromHexString('');
39+
expect(binary).to.have.deep.property('buffer', new Uint8Array(0));
40+
expect(binary).to.have.property('sub_type', 0);
41+
});
42+
43+
it('creates an empty binary with subtype', () => {
44+
const binary = Binary.createFromHexString('', 0x42);
45+
expect(binary).to.have.deep.property('buffer', new Uint8Array(0));
46+
expect(binary).to.have.property('sub_type', 0x42);
47+
});
48+
});
49+
});
50+
51+
context('createFromBase64()', () => {
52+
context('when called with a base64 sequence', () => {
53+
it('returns a Binary instance with the decoded bytes', () => {
54+
const bytes = Buffer.from('abc', 'utf8');
55+
const binary = Binary.createFromBase64(bytes.toString('base64'));
56+
expect(binary).to.have.deep.property('buffer', bytes);
57+
expect(binary).to.have.property('sub_type', 0);
58+
});
59+
60+
it('returns a Binary instance with the decoded bytes and subtype', () => {
61+
const bytes = Buffer.from('abc', 'utf8');
62+
const binary = Binary.createFromBase64(bytes.toString('base64'), 0x23);
63+
expect(binary).to.have.deep.property('buffer', bytes);
64+
expect(binary).to.have.property('sub_type', 0x23);
65+
});
66+
});
67+
68+
context('when called with an empty string', () => {
69+
it('creates an empty binary', () => {
70+
const binary = Binary.createFromBase64('');
71+
expect(binary).to.have.deep.property('buffer', new Uint8Array(0));
72+
expect(binary).to.have.property('sub_type', 0);
73+
});
74+
75+
it('creates an empty binary with subtype', () => {
76+
const binary = Binary.createFromBase64('', 0x42);
77+
expect(binary).to.have.deep.property('buffer', new Uint8Array(0));
78+
expect(binary).to.have.property('sub_type', 0x42);
79+
});
80+
});
81+
});
82+
83+
context('inspect()', () => {
84+
it('when value is default returns "Binary.createFromBase64("", 0)"', () => {
85+
expect(new Binary().inspect()).to.equal('Binary.createFromBase64("", 0)');
86+
});
87+
88+
it('when value is empty returns "Binary.createFromBase64("", 0)"', () => {
89+
expect(new Binary(new Uint8Array(0)).inspect()).to.equal('Binary.createFromBase64("", 0)');
90+
});
91+
92+
it('when value is default with a subtype returns "Binary.createFromBase64("", 35)"', () => {
93+
expect(new Binary(null, 0x23).inspect()).to.equal('Binary.createFromBase64("", 35)');
94+
});
95+
96+
it('when value is empty with a subtype returns "Binary.createFromBase64("", 35)"', () => {
97+
expect(new Binary(new Uint8Array(0), 0x23).inspect()).to.equal(
98+
'Binary.createFromBase64("", 35)'
99+
);
100+
});
101+
102+
it('when value has utf8 "abcdef" encoded returns "Binary.createFromBase64("YWJjZGVm", 0)"', () => {
103+
expect(new Binary(Buffer.from('abcdef', 'utf8')).inspect()).to.equal(
104+
'Binary.createFromBase64("YWJjZGVm", 0)'
105+
);
106+
});
107+
108+
context('when result is executed', () => {
109+
it('has a position of zero when constructed with default space', () => {
110+
const bsonValue = new Binary();
111+
const ctx = { ...BSON, module: { exports: { result: null } } };
112+
vm.runInNewContext(`module.exports.result = ${bsonValue.inspect()}`, ctx);
113+
expect(ctx.module.exports.result).to.have.property('position', 0);
114+
expect(ctx.module.exports.result).to.have.property('sub_type', 0);
115+
116+
// While the default Binary has 256 bytes the newly constructed one will have 0
117+
// both will have a position of zero so when serialized to BSON they are the equivalent.
118+
expect(ctx.module.exports.result).to.have.nested.property('buffer.byteLength', 0);
119+
expect(bsonValue).to.have.nested.property('buffer.byteLength', 256);
120+
});
121+
122+
it('is deep equal with a Binary that has no data', () => {
123+
const bsonValue = new Binary(new Uint8Array(0));
124+
const ctx = { ...BSON, module: { exports: { result: null } } };
125+
vm.runInNewContext(`module.exports.result = ${bsonValue.inspect()}`, ctx);
126+
expect(ctx.module.exports.result).to.deep.equal(bsonValue);
127+
});
128+
129+
it('is deep equal with a Binary that has a subtype but no data', () => {
130+
const bsonValue = new Binary(new Uint8Array(0), 0x23);
131+
const ctx = { ...BSON, module: { exports: { result: null } } };
132+
vm.runInNewContext(`module.exports.result = ${bsonValue.inspect()}`, ctx);
133+
expect(ctx.module.exports.result).to.deep.equal(bsonValue);
134+
});
135+
136+
it('is deep equal with a Binary that has data', () => {
137+
const bsonValue = new Binary(Buffer.from('abc', 'utf8'));
138+
const ctx = { ...BSON, module: { exports: { result: null } } };
139+
vm.runInNewContext(`module.exports.result = ${bsonValue.inspect()}`, ctx);
140+
expect(ctx.module.exports.result).to.deep.equal(bsonValue);
141+
});
142+
});
143+
});
144+
});

test/node/bson_test.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1829,9 +1829,7 @@ describe('BSON', function () {
18291829
*/
18301830
it('Binary', function () {
18311831
const binary = new Binary(Buffer.from('0123456789abcdef0123456789abcdef', 'hex'), 4);
1832-
expect(inspect(binary)).to.equal(
1833-
'new Binary(Buffer.from("0123456789abcdef0123456789abcdef", "hex"), 4)'
1834-
);
1832+
expect(inspect(binary)).to.equal('Binary.createFromBase64("ASNFZ4mrze8BI0VniavN7w==", 4)');
18351833
});
18361834

18371835
/**

test/node/bson_type_classes.test.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
UUID,
1818
BSONValue
1919
} from '../register-bson';
20+
import * as vm from 'node:vm';
2021

2122
const BSONTypeClasses = [
2223
Binary,
@@ -36,7 +37,7 @@ const BSONTypeClasses = [
3637
];
3738

3839
const BSONTypeClassCtors = new Map<string, () => BSONValue>([
39-
['Binary', () => new Binary()],
40+
['Binary', () => new Binary(new Uint8Array(0), 0)],
4041
['Code', () => new Code('function () {}')],
4142
['DBRef', () => new DBRef('name', new ObjectId('00'.repeat(12)))],
4243
['Decimal128', () => new Decimal128('1.23')],
@@ -97,4 +98,20 @@ describe('BSON Type classes common interfaces', () => {
9798
.that.is.a('function'));
9899
});
99100
}
101+
102+
context(`when inspect() is called`, () => {
103+
for (const [name, factory] of BSONTypeClassCtors) {
104+
it(`${name} returns string that is runnable and has deep equality`, () => {
105+
const bsonValue = factory();
106+
// All BSON types should only need exactly their constructor available on the global
107+
const ctx = { [name]: bsonValue.constructor, module: { exports: { result: null } } };
108+
if (name === 'DBRef') {
109+
// DBRef is the only type that requires another BSON type
110+
ctx.ObjectId = ObjectId;
111+
}
112+
vm.runInNewContext(`module.exports.result = ${bsonValue.inspect()}`, ctx);
113+
expect(ctx.module.exports.result).to.deep.equal(bsonValue);
114+
});
115+
}
116+
});
100117
});

test/node/object_id_tests.js renamed to test/node/object_id.test.ts

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
'use strict';
2-
3-
const Buffer = require('buffer').Buffer;
4-
const { BSON, BSONError, EJSON, ObjectId } = require('../register-bson');
5-
const util = require('util');
6-
const { expect } = require('chai');
7-
const { bufferFromHexArray } = require('./tools/utils');
8-
const getSymbolFrom = require('./tools/utils').getSymbolFrom;
9-
const isBufferOrUint8Array = require('./tools/utils').isBufferOrUint8Array;
1+
import { Buffer } from 'buffer';
2+
import { BSON, BSONError, EJSON, ObjectId } from '../register-bson';
3+
import * as util from 'util';
4+
import { expect } from 'chai';
5+
import { bufferFromHexArray } from './tools/utils';
6+
import { getSymbolFrom } from './tools/utils';
7+
import { isBufferOrUint8Array } from './tools/utils';
108

119
describe('ObjectId', function () {
1210
describe('static createFromTime()', () => {
@@ -477,4 +475,36 @@ describe('ObjectId', function () {
477475
// class method equality
478476
expect(Buffer.prototype.equals.call(inBuffer, outBuffer.id)).to.be.true;
479477
});
478+
479+
context('createFromHexString()', () => {
480+
context('when called with a hex sequence', () => {
481+
it('returns a ObjectId instance with the decoded bytes', () => {
482+
const bytes = Buffer.from('0'.repeat(24), 'hex');
483+
const binary = ObjectId.createFromHexString(bytes.toString('hex'));
484+
expect(binary).to.have.deep.property('id', bytes);
485+
});
486+
});
487+
488+
context('when called with an incorrect length string', () => {
489+
it('throws an error indicating the expected length of 24', () => {
490+
expect(() => ObjectId.createFromHexString('')).to.throw(/24/);
491+
});
492+
});
493+
});
494+
495+
context('createFromBase64()', () => {
496+
context('when called with a base64 sequence', () => {
497+
it('returns a ObjectId instance with the decoded bytes', () => {
498+
const bytes = Buffer.from('A'.repeat(16), 'base64');
499+
const binary = ObjectId.createFromBase64(bytes.toString('base64'));
500+
expect(binary).to.have.deep.property('id', bytes);
501+
});
502+
});
503+
504+
context('when called with an incorrect length string', () => {
505+
it('throws an error indicating the expected length of 16', () => {
506+
expect(() => ObjectId.createFromBase64('')).to.throw(/16/);
507+
});
508+
});
509+
});
480510
});

test/node/uuid_tests.js renamed to test/node/uuid.test.ts

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
'use strict';
2-
3-
const { Buffer } = require('buffer');
4-
const { Binary, UUID } = require('../register-bson');
5-
const { inspect } = require('util');
6-
const { validate: uuidStringValidate, version: uuidStringVersion } = require('uuid');
7-
const { BSON, BSONError } = require('../register-bson');
1+
import { Binary, UUID } from '../register-bson';
2+
import { inspect } from 'util';
3+
import { validate as uuidStringValidate, version as uuidStringVersion } from 'uuid';
4+
import { BSON, BSONError } from '../register-bson';
85
const BSON_DATA_BINARY = BSON.BSONType.binData;
9-
const { BSON_BINARY_SUBTYPE_UUID_NEW } = require('../../src/constants');
6+
import { BSON_BINARY_SUBTYPE_UUID_NEW } from '../../src/constants';
7+
import { expect } from 'chai';
108

119
// Test values
1210
const UPPERCASE_DASH_SEPARATED_UUID_STRING = 'AAAAAAAA-AAAA-4AAA-AAAA-AAAAAAAAAAAA';
@@ -202,4 +200,43 @@ describe('UUID', () => {
202200
expect(deserializedUUID).to.deep.equal(expectedResult);
203201
});
204202
});
203+
204+
context('createFromHexString()', () => {
205+
context('when called with a hex sequence', () => {
206+
it('returns a UUID instance with the decoded bytes', () => {
207+
const bytes = Buffer.from(UPPERCASE_VALUES_ONLY_UUID_STRING, 'hex');
208+
209+
const uuidDashed = UUID.createFromHexString(UPPERCASE_DASH_SEPARATED_UUID_STRING);
210+
expect(uuidDashed).to.have.deep.property('buffer', bytes);
211+
expect(uuidDashed).to.be.instanceOf(UUID);
212+
213+
const uuidNoDashed = UUID.createFromHexString(UPPERCASE_VALUES_ONLY_UUID_STRING);
214+
expect(uuidNoDashed).to.have.deep.property('buffer', bytes);
215+
expect(uuidNoDashed).to.be.instanceOf(UUID);
216+
});
217+
});
218+
219+
context('when called with an incorrect length string', () => {
220+
it('throws an error indicating the expected length of 32 or 36 characters', () => {
221+
expect(() => UUID.createFromHexString('')).to.throw(/32 or 36 character/);
222+
});
223+
});
224+
});
225+
226+
context('createFromBase64()', () => {
227+
context('when called with a base64 sequence', () => {
228+
it('returns a UUID instance with the decoded bytes', () => {
229+
const bytes = Buffer.from(UPPERCASE_VALUES_ONLY_UUID_STRING, 'hex');
230+
const uuid = UUID.createFromBase64(bytes.toString('base64'));
231+
expect(uuid).to.have.deep.property('buffer', bytes);
232+
expect(uuid).to.be.instanceOf(UUID);
233+
});
234+
});
235+
236+
context('when called with an incorrect length string', () => {
237+
it('throws an error indicating the expected length of 16 byte Buffer', () => {
238+
expect(() => UUID.createFromBase64('')).to.throw(/16 byte Buffer/);
239+
});
240+
});
241+
});
205242
});

0 commit comments

Comments
 (0)