diff --git a/src/binary.ts b/src/binary.ts index f424a547..032a1e02 100644 --- a/src/binary.ts +++ b/src/binary.ts @@ -1,9 +1,10 @@ import { Buffer } from 'buffer'; import { ensureBuffer } from './ensure_buffer'; -import { uuidHexStringToBuffer } from './uuid_utils'; -import { UUID, UUIDExtended } from './uuid'; +import { bufferToUuidHexString, uuidHexStringToBuffer, uuidValidateString } from './uuid_utils'; +import { isUint8Array, randomBytes } from './parser/utils'; import type { EJSONOptions } from './extended_json'; import { BSONError, BSONTypeError } from './error'; +import { BSON_BINARY_SUBTYPE_UUID_NEW } from './constants'; /** @public */ export type BinarySequence = Uint8Array | Buffer | number[]; @@ -292,3 +293,189 @@ export class Binary { } Object.defineProperty(Binary.prototype, '_bsontype', { value: 'Binary' }); + +/** @public */ +export type UUIDExtended = { + $uuid: string; +}; +const UUID_BYTE_LENGTH = 16; + +/** + * A class representation of the BSON UUID type. + * @public + */ +export class UUID extends Binary { + static cacheHexString: boolean; + + /** UUID hexString cache @internal */ + private __id?: string; + + /** + * Create an UUID type + * + * @param input - Can be a 32 or 36 character hex string (dashes excluded/included) or a 16 byte binary Buffer. + */ + constructor(input?: string | Buffer | UUID) { + let bytes; + let hexStr; + if (input == null) { + bytes = UUID.generate(); + } else if (input instanceof UUID) { + bytes = Buffer.from(input.buffer); + hexStr = input.__id; + } else if (ArrayBuffer.isView(input) && input.byteLength === UUID_BYTE_LENGTH) { + bytes = ensureBuffer(input); + } else if (typeof input === 'string') { + bytes = uuidHexStringToBuffer(input); + } else { + throw new BSONTypeError( + 'Argument passed in UUID constructor must be a UUID, a 16 byte Buffer or a 32/36 character hex string (dashes excluded/included, format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).' + ); + } + super(bytes, BSON_BINARY_SUBTYPE_UUID_NEW); + this.__id = hexStr; + } + + /** + * The UUID bytes + * @readonly + */ + get id(): Buffer { + return this.buffer; + } + + set id(value: Buffer) { + this.buffer = value; + + if (UUID.cacheHexString) { + this.__id = bufferToUuidHexString(value); + } + } + + /** + * Returns the UUID id as a 32 or 36 character hex string representation, excluding/including dashes (defaults to 36 character dash separated) + * @param includeDashes - should the string exclude dash-separators. + * */ + toHexString(includeDashes = true): string { + if (UUID.cacheHexString && this.__id) { + return this.__id; + } + + const uuidHexString = bufferToUuidHexString(this.id, includeDashes); + + if (UUID.cacheHexString) { + this.__id = uuidHexString; + } + + return uuidHexString; + } + + /** + * Converts the id into a 36 character (dashes included) hex string, unless a encoding is specified. + */ + toString(encoding?: string): string { + return encoding ? this.id.toString(encoding) : this.toHexString(); + } + + /** + * Converts the id into its JSON string representation. + * A 36 character (dashes included) hex string in the format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + */ + toJSON(): string { + return this.toHexString(); + } + + /** + * Compares the equality of this UUID with `otherID`. + * + * @param otherId - UUID instance to compare against. + */ + equals(otherId: string | Buffer | UUID): boolean { + if (!otherId) { + return false; + } + + if (otherId instanceof UUID) { + return otherId.id.equals(this.id); + } + + try { + return new UUID(otherId).id.equals(this.id); + } catch { + return false; + } + } + + /** + * Creates a Binary instance from the current UUID. + */ + toBinary(): Binary { + return new Binary(this.id, Binary.SUBTYPE_UUID); + } + + /** + * Generates a populated buffer containing a v4 uuid + */ + static generate(): Buffer { + const bytes = randomBytes(UUID_BYTE_LENGTH); + + // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` + // Kindly borrowed from https://github.com/uuidjs/uuid/blob/master/src/v4.js + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + + return Buffer.from(bytes); + } + + /** + * Checks if a value is a valid bson UUID + * @param input - UUID, string or Buffer to validate. + */ + static isValid(input: string | Buffer | UUID): boolean { + if (!input) { + return false; + } + + if (input instanceof UUID) { + return true; + } + + if (typeof input === 'string') { + return uuidValidateString(input); + } + + if (isUint8Array(input)) { + // check for length & uuid version (https://tools.ietf.org/html/rfc4122#section-4.1.3) + if (input.length !== UUID_BYTE_LENGTH) { + return false; + } + + return (input[6] & 0xf0) === 0x40 && (input[8] & 0x80) === 0x80; + } + + return false; + } + + /** + * 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 { + const buffer = uuidHexStringToBuffer(hexString); + return new UUID(buffer); + } + + /** + * Converts to a string representation of this Id. + * + * @returns return the 36 character hex string representation. + * @internal + */ + [Symbol.for('nodejs.util.inspect.custom')](): string { + return this.inspect(); + } + + inspect(): string { + return `new UUID("${this.toHexString()}")`; + } +} diff --git a/src/bson.ts b/src/bson.ts index bff2b031..d32cfe83 100644 --- a/src/bson.ts +++ b/src/bson.ts @@ -1,5 +1,5 @@ import { Buffer } from 'buffer'; -import { Binary } from './binary'; +import { Binary, UUID } from './binary'; import { Code } from './code'; import { DBRef } from './db_ref'; import { Decimal128 } from './decimal128'; @@ -20,8 +20,7 @@ import { serializeInto as internalSerialize, SerializeOptions } from './parser/s import { BSONRegExp } from './regexp'; import { BSONSymbol } from './symbol'; import { Timestamp } from './timestamp'; -import { UUID } from './uuid'; -export type { BinaryExtended, BinaryExtendedLegacy, BinarySequence } from './binary'; +export type { UUIDExtended, BinaryExtended, BinaryExtendedLegacy, BinarySequence } from './binary'; export type { CodeExtended } from './code'; export { BSON_BINARY_SUBTYPE_BYTE_ARRAY, @@ -73,7 +72,6 @@ export type { BSONRegExpExtended, BSONRegExpExtendedLegacy } from './regexp'; export type { BSONSymbolExtended } from './symbol'; export type { LongWithoutOverrides, TimestampExtended, TimestampOverrides } from './timestamp'; export { LongWithoutOverridesClass } from './timestamp'; -export type { UUIDExtended } from './uuid'; export type { SerializeOptions, DeserializeOptions }; export { Code, diff --git a/src/parser/serializer.ts b/src/parser/serializer.ts index f20ddc36..e76402aa 100644 --- a/src/parser/serializer.ts +++ b/src/parser/serializer.ts @@ -837,8 +837,6 @@ export function serializeInto( ); } else if (value['_bsontype'] === 'Binary') { index = serializeBinary(buffer, key, value, index, true); - } else if (value['_bsontype'] === 'UUID') { - index = serializeBinary(buffer, key, value.toBinary(), index); } else if (value['_bsontype'] === 'Symbol') { index = serializeSymbol(buffer, key, value, index, true); } else if (value['_bsontype'] === 'DBRef') { @@ -940,8 +938,6 @@ export function serializeInto( index = serializeFunction(buffer, key, value, index, checkKeys, depth, serializeFunctions); } else if (value['_bsontype'] === 'Binary') { index = serializeBinary(buffer, key, value, index); - } else if (value['_bsontype'] === 'UUID') { - index = serializeBinary(buffer, key, value.toBinary(), index); } else if (value['_bsontype'] === 'Symbol') { index = serializeSymbol(buffer, key, value, index); } else if (value['_bsontype'] === 'DBRef') { @@ -1047,8 +1043,6 @@ export function serializeInto( index = serializeFunction(buffer, key, value, index, checkKeys, depth, serializeFunctions); } else if (value['_bsontype'] === 'Binary') { index = serializeBinary(buffer, key, value, index); - } else if (value['_bsontype'] === 'UUID') { - index = serializeBinary(buffer, key, value.toBinary(), index); } else if (value['_bsontype'] === 'Symbol') { index = serializeSymbol(buffer, key, value, index); } else if (value['_bsontype'] === 'DBRef') { diff --git a/src/uuid.ts b/src/uuid.ts deleted file mode 100644 index 625e57de..00000000 --- a/src/uuid.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { Buffer } from 'buffer'; -import { ensureBuffer } from './ensure_buffer'; -import { Binary } from './binary'; -import { bufferToUuidHexString, uuidHexStringToBuffer, uuidValidateString } from './uuid_utils'; -import { isUint8Array, randomBytes } from './parser/utils'; -import { BSONTypeError } from './error'; - -/** @public */ -export type UUIDExtended = { - $uuid: string; -}; - -const BYTE_LENGTH = 16; - -const kId = Symbol('id'); - -/** - * A class representation of the BSON UUID type. - * @public - */ -export class UUID { - // This property is not meant for direct serialization, but simply an indication that this type originates from this package. - _bsontype!: 'UUID'; - - static cacheHexString: boolean; - - /** UUID Bytes @internal */ - private [kId]!: Buffer; - /** UUID hexString cache @internal */ - private __id?: string; - - /** - * Create an UUID type - * - * @param input - Can be a 32 or 36 character hex string (dashes excluded/included) or a 16 byte binary Buffer. - */ - constructor(input?: string | Buffer | UUID) { - if (typeof input === 'undefined') { - // The most common use case (blank id, new UUID() instance) - this.id = UUID.generate(); - } else if (input instanceof UUID) { - this[kId] = Buffer.from(input.id); - this.__id = input.__id; - } else if (ArrayBuffer.isView(input) && input.byteLength === BYTE_LENGTH) { - this.id = ensureBuffer(input); - } else if (typeof input === 'string') { - this.id = uuidHexStringToBuffer(input); - } else { - throw new BSONTypeError( - 'Argument passed in UUID constructor must be a UUID, a 16 byte Buffer or a 32/36 character hex string (dashes excluded/included, format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).' - ); - } - } - - /** - * The UUID bytes - * @readonly - */ - get id(): Buffer { - return this[kId]; - } - - set id(value: Buffer) { - this[kId] = value; - - if (UUID.cacheHexString) { - this.__id = bufferToUuidHexString(value); - } - } - - /** - * Generate a 16 byte uuid v4 buffer used in UUIDs - */ - - /** - * Returns the UUID id as a 32 or 36 character hex string representation, excluding/including dashes (defaults to 36 character dash separated) - * @param includeDashes - should the string exclude dash-separators. - * */ - toHexString(includeDashes = true): string { - if (UUID.cacheHexString && this.__id) { - return this.__id; - } - - const uuidHexString = bufferToUuidHexString(this.id, includeDashes); - - if (UUID.cacheHexString) { - this.__id = uuidHexString; - } - - return uuidHexString; - } - - /** - * Converts the id into a 36 character (dashes included) hex string, unless a encoding is specified. - */ - toString(encoding?: string): string { - return encoding ? this.id.toString(encoding) : this.toHexString(); - } - - /** - * Converts the id into its JSON string representation. - * A 36 character (dashes included) hex string in the format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - */ - toJSON(): string { - return this.toHexString(); - } - - /** - * Compares the equality of this UUID with `otherID`. - * - * @param otherId - UUID instance to compare against. - */ - equals(otherId: string | Buffer | UUID): boolean { - if (!otherId) { - return false; - } - - if (otherId instanceof UUID) { - return otherId.id.equals(this.id); - } - - try { - return new UUID(otherId).id.equals(this.id); - } catch { - return false; - } - } - - /** - * Creates a Binary instance from the current UUID. - */ - toBinary(): Binary { - return new Binary(this.id, Binary.SUBTYPE_UUID); - } - - /** - * Generates a populated buffer containing a v4 uuid - */ - static generate(): Buffer { - const bytes = randomBytes(BYTE_LENGTH); - - // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` - // Kindly borrowed from https://github.com/uuidjs/uuid/blob/master/src/v4.js - bytes[6] = (bytes[6] & 0x0f) | 0x40; - bytes[8] = (bytes[8] & 0x3f) | 0x80; - - return Buffer.from(bytes); - } - - /** - * Checks if a value is a valid bson UUID - * @param input - UUID, string or Buffer to validate. - */ - static isValid(input: string | Buffer | UUID): boolean { - if (!input) { - return false; - } - - if (input instanceof UUID) { - return true; - } - - if (typeof input === 'string') { - return uuidValidateString(input); - } - - if (isUint8Array(input)) { - // check for length & uuid version (https://tools.ietf.org/html/rfc4122#section-4.1.3) - if (input.length !== BYTE_LENGTH) { - return false; - } - - try { - // get this byte as hex: xxxxxxxx-xxxx-XXxx-xxxx-xxxxxxxxxxxx - // check first part as uuid version: xxxxxxxx-xxxx-Xxxx-xxxx-xxxxxxxxxxxx - return parseInt(input[6].toString(16)[0], 10) === Binary.SUBTYPE_UUID; - } catch { - return false; - } - } - - return false; - } - - /** - * 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 { - const buffer = uuidHexStringToBuffer(hexString); - return new UUID(buffer); - } - - /** - * Converts to a string representation of this Id. - * - * @returns return the 36 character hex string representation. - * @internal - */ - [Symbol.for('nodejs.util.inspect.custom')](): string { - return this.inspect(); - } - - inspect(): string { - return `new UUID("${this.toHexString()}")`; - } -} - -Object.defineProperty(UUID.prototype, '_bsontype', { value: 'UUID' }); diff --git a/test/node/extended_json_tests.js b/test/node/extended_json_tests.js index e8f97457..37632bf6 100644 --- a/test/node/extended_json_tests.js +++ b/test/node/extended_json_tests.js @@ -6,6 +6,7 @@ const vm = require('vm'); // BSON types const Binary = BSON.Binary; +const UUID = BSON.UUID; const Code = BSON.Code; const DBRef = BSON.DBRef; const Decimal128 = BSON.Decimal128; @@ -739,4 +740,38 @@ Converting circular structure to EJSON: }); }); }); + + describe('UUID stringify', () => { + const uuid = new UUID(); + const stringifiedPlainUUID = EJSON.stringify({ u: uuid }); + it('should return same values for UUID.toBinary() and UUID', () => { + const stringifiedToBinary = EJSON.stringify({ u: uuid.toBinary() }); + expect(stringifiedToBinary).to.deep.equal(stringifiedPlainUUID); + }); + it('should serialize to correct subType', () => { + const stringifiedUUIDtoObject = JSON.parse(stringifiedPlainUUID); + const stringifiedBinaryNewUUIDSubType = '04'; + expect(stringifiedUUIDtoObject.u.$binary.subType).to.equal(stringifiedBinaryNewUUIDSubType); + }); + }); + + describe('UUID parse', () => { + const uuid = new UUID(); + const stringifiedPlainUUID = EJSON.stringify({ u: uuid }); + it('should return same values for UUID.toBinary() and UUID', () => { + const stringifiedToBinary = EJSON.stringify({ u: uuid.toBinary() }); + const parsedToBinary = EJSON.parse(stringifiedToBinary); + const parsedPlainUUID = EJSON.parse(stringifiedPlainUUID); + expect(parsedToBinary).to.deep.equal(parsedPlainUUID); + }); + it('should parse both input formats the same way', () => { + const parsedUndashedInput = EJSON.parse( + `{"u":{"$binary":{"base64":"vDzrMPEAQOGkA8wGUNSOxw==","subType":"04"}}}` + ); + const parsedDashedInput = EJSON.parse( + `{"u":{"$uuid":"bc3ceb30-f100-40e1-a403-cc0650d48ec7"}}` + ); + expect(parsedUndashedInput).to.deep.equal(parsedDashedInput); + }); + }); }); diff --git a/test/node/type_identifier_tests.js b/test/node/type_identifier_tests.js index cda29de1..2dde9e12 100644 --- a/test/node/type_identifier_tests.js +++ b/test/node/type_identifier_tests.js @@ -64,7 +64,7 @@ describe('_bsontype identifier', () => { it('should be equal to BSONRegExp for BSONRegExp', () => { expect(BSONRegExp.prototype._bsontype).to.equal('BSONRegExp'); }); - it('should be equal to UUID for UUID', () => { - expect(UUID.prototype._bsontype).to.equal('UUID'); + it('should be equal to Binary for UUID', () => { + expect(UUID.prototype._bsontype).to.equal('Binary'); }); }); diff --git a/test/node/uuid_tests.js b/test/node/uuid_tests.js index f48360d8..f1458542 100644 --- a/test/node/uuid_tests.js +++ b/test/node/uuid_tests.js @@ -166,6 +166,13 @@ describe('UUID', () => { }); describe('serialize', () => { + it('should serialize BSON.UUID() input the same as BSON.UUID().toBinary()', () => { + const exampleUUID = new BSON.UUID(); + const toBinarySerialization = BSON.serialize({ uuid: exampleUUID.toBinary() }); + const plainUUIDSerialization = BSON.serialize({ uuid: exampleUUID }); + expect(plainUUIDSerialization).to.deep.equal(toBinarySerialization); + }); + it('should have a valid UUID _bsontype with Object input without error', () => { const output = BSON.serialize({ uuid: new BSON.UUID() }); expect(output[4]).to.equal(BSON_DATA_BINARY); @@ -183,12 +190,5 @@ describe('UUID', () => { expect(output[11]).to.equal(BSON_DATA_BINARY); expect(output[18]).to.equal(BSON_BINARY_SUBTYPE_UUID_NEW); }); - - it('should serialize BSON.UUID() input the same as BSON.UUID().toBinary()', () => { - const exampleUUID = new BSON.UUID(); - const toBinarySerialization = BSON.serialize({ uuid: exampleUUID.toBinary() }); - const plainUUIDSerialization = BSON.serialize({ uuid: exampleUUID }); - expect(plainUUIDSerialization).to.deep.equal(toBinarySerialization); - }); }); }); diff --git a/test/types/bson.test-d.ts b/test/types/bson.test-d.ts index d92196e4..0a04870a 100644 --- a/test/types/bson.test-d.ts +++ b/test/types/bson.test-d.ts @@ -73,4 +73,4 @@ expectType<'Long'>(Long.prototype._bsontype) expectType<'MaxKey'>(MaxKey.prototype._bsontype) expectType<'MinKey'>(MinKey.prototype._bsontype) expectType<'BSONRegExp'>(BSONRegExp.prototype._bsontype) -expectType<'UUID'>(UUID.prototype._bsontype) +expectType<'Binary'>(UUID.prototype._bsontype)