diff --git a/packages/e2e-tests/test/e2e-bson.spec.ts b/packages/e2e-tests/test/e2e-bson.spec.ts index ca84f4657..7566c3eee 100644 --- a/packages/e2e-tests/test/e2e-bson.spec.ts +++ b/packages/e2e-tests/test/e2e-bson.spec.ts @@ -349,6 +349,13 @@ describe('BSON e2e', function () { expect(await shell.executeLine(value)).to.include(value); shell.assertNoErrors(); }); + it('BinData type 4 prints as UUID when valid', async function () { + const value = "BinData(4, 'ASNFZ4mrze8BI0VniavN7w==')"; + expect(await shell.executeLine(value)).to.include( + "UUID('01234567-89ab-cdef-0123-456789abcdef')" + ); + shell.assertNoErrors(); + }); it('BinData prints as MD5 when created by user as such', async function () { const value = "MD5('0123456789abcdef0123456789abcdef')"; expect(await shell.executeLine(value)).to.include(value); @@ -380,6 +387,45 @@ describe('BSON e2e', function () { ); shell.assertNoErrors(); }); + it('LegacyJavaUUID prints as Binary subtype 3 when created by user', async function () { + const value = 'LegacyJavaUUID()'; + const output = await shell.executeLine(value); + expect(output).to.match(/Binary\.createFromBase64\('.+', 3\)/); + shell.assertNoErrors(); + }); + it('LegacyCSharpUUID prints as Binary subtype 3 when created by user', async function () { + const value = 'LegacyCSharpUUID()'; + const output = await shell.executeLine(value); + expect(output).to.match(/Binary\.createFromBase64\('.+', 3\)/); + shell.assertNoErrors(); + }); + it('LegacyPythonUUID prints as Binary subtype 3 when created by user', async function () { + const value = 'LegacyPythonUUID()'; + const output = await shell.executeLine(value); + expect(output).to.match(/Binary\.createFromBase64\('.+', 3\)/); + shell.assertNoErrors(); + }); + it('BinData created as LegacyJavaUUID prints as Binary subtype 3', async function () { + const value = "LegacyJavaUUID('01234567-89ab-cdef-0123-456789abcdef')"; + expect(await shell.executeLine(value)).to.include( + "Binary.createFromBase64('782riWdFIwHvzauJZ0UjAQ==', 3)" + ); + shell.assertNoErrors(); + }); + it('BinData created as LegacyCSharpUUID prints as Binary subtype 3', async function () { + const value = "LegacyCSharpUUID('01234567-89ab-cdef-0123-456789abcdef')"; + expect(await shell.executeLine(value)).to.include( + "Binary.createFromBase64('Z0UjAauJ780BI0VniavN7w==', 3)" + ); + shell.assertNoErrors(); + }); + it('BinData created as LegacyPythonUUID prints as Binary subtype 3', async function () { + const value = "LegacyPythonUUID('01234567-89ab-cdef-0123-456789abcdef')"; + expect(await shell.executeLine(value)).to.include( + "Binary.createFromBase64('ASNFZ4mrze8BI0VniavN7w==', 3)" + ); + shell.assertNoErrors(); + }); }); describe('MaxKey/MinKey special handling', function () { it('inserts and retrieves MaxKey/MinKey regardless of whether they have been called as functions', async function () { diff --git a/packages/shell-bson/src/shell-bson.spec.ts b/packages/shell-bson/src/shell-bson.spec.ts index 74b30e0f9..cd8773810 100644 --- a/packages/shell-bson/src/shell-bson.spec.ts +++ b/packages/shell-bson/src/shell-bson.spec.ts @@ -560,6 +560,187 @@ describe('Shell BSON', function () { expect.fail('Expecting error, nothing thrown'); }); }); + describe('LegacyJavaUUID', function () { + it('creates a Binary with SUBTYPE_UUID_OLD', function () { + const uuid = shellBson.LegacyJavaUUID('0123456789abcdef0123456789abcdef'); + expect(uuid.sub_type).to.equal(3); // SUBTYPE_UUID_OLD + }); + + it('strips dashes from input', function () { + expect( + shellBson.LegacyJavaUUID('01234567-89ab-cdef-0123-456789abcdef').value() + ).to.deep.equal( + shellBson.LegacyJavaUUID('0123456789abcdef0123456789abcdef').value() + ); + }); + + it('generates a random UUID when no arguments are passed', function () { + const uuid = shellBson.LegacyJavaUUID(); + expect(uuid.sub_type).to.equal(3); + expect(uuid.value().length).to.equal(16); + }); + + it('has help and other metadata', function () { + const uuid = shellBson.LegacyJavaUUID('0123456789abcdef0123456789abcdef'); + expect(uuid?.help?.type).to.equal('Help'); + expect(uuid?.help?.().type).to.equal('Help'); + expect((uuid as any).serverVersions).to.deep.equal(ALL_SERVER_VERSIONS); + }); + + it('performs byte-swapping for Java compatibility', function () { + // Test that the byte order is swapped correctly for Java UUID format + const uuid = shellBson.LegacyJavaUUID('0123456789abcdef0123456789abcdef'); + // The MSB and LSB should be byte-swapped according to Java's UUID format + expect(uuid.toString('hex')).to.equal('efcdab8967452301efcdab8967452301'); + }); + + it('errors for wrong type of arg 1', function () { + try { + (shellBson.LegacyJavaUUID as any)(1); + } catch (e: any) { + return expect(e.message).to.contain('string, got number'); + } + expect.fail('Expecting error, nothing thrown'); + }); + }); + + describe('LegacyCSharpUUID', function () { + it('creates a Binary with SUBTYPE_UUID_OLD', function () { + const uuid = shellBson.LegacyCSharpUUID( + '0123456789abcdef0123456789abcdef' + ); + expect(uuid.sub_type).to.equal(3); // SUBTYPE_UUID_OLD + }); + + it('strips dashes from input', function () { + expect( + shellBson + .LegacyCSharpUUID('01234567-89ab-cdef-0123-456789abcdef') + .value() + ).to.deep.equal( + shellBson.LegacyCSharpUUID('0123456789abcdef0123456789abcdef').value() + ); + }); + + it('generates a random UUID when no arguments are passed', function () { + const uuid = shellBson.LegacyCSharpUUID(); + expect(uuid.sub_type).to.equal(3); + expect(uuid.value().length).to.equal(16); + }); + + it('has help and other metadata', function () { + const uuid = shellBson.LegacyCSharpUUID( + '0123456789abcdef0123456789abcdef' + ); + expect(uuid?.help?.type).to.equal('Help'); + expect(uuid?.help?.().type).to.equal('Help'); + expect((uuid as any).serverVersions).to.deep.equal(ALL_SERVER_VERSIONS); + }); + + it('performs byte-swapping for C# compatibility', function () { + // Test that the byte order is swapped correctly for C# UUID format + const uuid = shellBson.LegacyCSharpUUID( + '0123456789abcdef0123456789abcdef' + ); + // The first three groups should be byte-swapped according to C#'s GUID format + expect(uuid.toString('hex')).to.equal('67452301ab89efcd0123456789abcdef'); + }); + + it('errors for wrong type of arg 1', function () { + try { + (shellBson.LegacyCSharpUUID as any)(1); + } catch (e: any) { + return expect(e.message).to.contain('string, got number'); + } + expect.fail('Expecting error, nothing thrown'); + }); + }); + + describe('LegacyPythonUUID', function () { + it('creates a Binary with SUBTYPE_UUID_OLD', function () { + const uuid = shellBson.LegacyPythonUUID( + '0123456789abcdef0123456789abcdef' + ); + expect(uuid.sub_type).to.equal(3); // SUBTYPE_UUID_OLD + }); + + it('strips dashes from input', function () { + expect( + shellBson + .LegacyPythonUUID('01234567-89ab-cdef-0123-456789abcdef') + .value() + ).to.deep.equal( + shellBson.LegacyPythonUUID('0123456789abcdef0123456789abcdef').value() + ); + }); + + it('generates a random UUID when no arguments are passed', function () { + const uuid = shellBson.LegacyPythonUUID(); + expect(uuid.sub_type).to.equal(3); + expect(uuid.value().length).to.equal(16); + }); + + it('has help and other metadata', function () { + const uuid = shellBson.LegacyPythonUUID( + '0123456789abcdef0123456789abcdef' + ); + expect(uuid?.help?.type).to.equal('Help'); + expect(uuid?.help?.().type).to.equal('Help'); + expect((uuid as any).serverVersions).to.deep.equal(ALL_SERVER_VERSIONS); + }); + + it('uses standard byte order (no swapping)', function () { + // Python UUID uses standard network byte order (big-endian, no swapping) + const uuid = shellBson.LegacyPythonUUID( + '0123456789abcdef0123456789abcdef' + ); + expect(uuid.toString('hex')).to.equal('0123456789abcdef0123456789abcdef'); + }); + + it('errors for wrong type of arg 1', function () { + try { + (shellBson.LegacyPythonUUID as any)(1); + } catch (e: any) { + return expect(e.message).to.contain('string, got number'); + } + expect.fail('Expecting error, nothing thrown'); + }); + }); + + describe('Legacy UUID comparison', function () { + const testUuid = '0123456789abcdef0123456789abcdef'; + + it('each legacy UUID type produces different byte representations', function () { + const javaUuid = shellBson.LegacyJavaUUID(testUuid); + const csharpUuid = shellBson.LegacyCSharpUUID(testUuid); + const pythonUuid = shellBson.LegacyPythonUUID(testUuid); + + const javaBytes = javaUuid.toString('hex'); + const csharpBytes = csharpUuid.toString('hex'); + const pythonBytes = pythonUuid.toString('hex'); + + // They should all be different due to different byte-swapping + expect(javaBytes).to.not.equal(csharpBytes); + expect(javaBytes).to.not.equal(pythonBytes); + expect(csharpBytes).to.not.equal(pythonBytes); + + // Python should match the original (no swapping) + expect(pythonBytes).to.equal(testUuid); + }); + + it('all legacy UUID types use SUBTYPE_UUID_OLD', function () { + const javaUuid = shellBson.LegacyJavaUUID(testUuid); + const csharpUuid = shellBson.LegacyCSharpUUID(testUuid); + const pythonUuid = shellBson.LegacyPythonUUID(testUuid); + const uuid = shellBson.UUID(testUuid); + + expect(javaUuid.sub_type).to.equal(3); + expect(csharpUuid.sub_type).to.equal(3); + expect(pythonUuid.sub_type).to.equal(3); + expect(uuid.sub_type).to.equal(4); + }); + }); + describe('MD5', function () { let b: any; let h: any; diff --git a/packages/shell-bson/src/shell-bson.ts b/packages/shell-bson/src/shell-bson.ts index 354a91d6c..b4a162f9b 100644 --- a/packages/shell-bson/src/shell-bson.ts +++ b/packages/shell-bson/src/shell-bson.ts @@ -52,6 +52,9 @@ export interface ShellBsonBase { ) => BSONLib['Binary']['prototype']; HexData: (subtype: number, hexstr: string) => BSONLib['Binary']['prototype']; UUID: (hexstr?: string) => BSONLib['Binary']['prototype']; + LegacyJavaUUID: (hexstr?: string) => BSONLib['Binary']['prototype']; + LegacyCSharpUUID: (hexstr?: string) => BSONLib['Binary']['prototype']; + LegacyPythonUUID: (hexstr?: string) => BSONLib['Binary']['prototype']; MD5: (hexstr: string) => BSONLib['Binary']['prototype']; Decimal128: BSONLib['Decimal128']; BSONSymbol: BSONLib['BSONSymbol']; @@ -340,6 +343,92 @@ export function constructShellBson< }, { prototype: bson.Binary.prototype } ), + + LegacyJavaUUID: assignAll( + function LegacyJavaUUID(hexstr?: string): BinaryType { + if (hexstr === undefined) { + // Generate a new UUID and format it. + hexstr = new bson.UUID().toHexString(); + } + assertArgsDefinedType([hexstr], ['string'], 'LegacyJavaUUID'); + + let hex: string = String.prototype.replace.call( + hexstr, + /[{}-]/g, + () => '' + ); + let msb = String.prototype.substring.call(hex, 0, 16); + let lsb = String.prototype.substring.call(hex, 16, 32); + msb = + String.prototype.substring.call(msb, 14, 16) + + String.prototype.substring.call(msb, 12, 14) + + String.prototype.substring.call(msb, 10, 12) + + String.prototype.substring.call(msb, 8, 10) + + String.prototype.substring.call(msb, 6, 8) + + String.prototype.substring.call(msb, 4, 6) + + String.prototype.substring.call(msb, 2, 4) + + String.prototype.substring.call(msb, 0, 2); + lsb = + String.prototype.substring.call(lsb, 14, 16) + + String.prototype.substring.call(lsb, 12, 14) + + String.prototype.substring.call(lsb, 10, 12) + + String.prototype.substring.call(lsb, 8, 10) + + String.prototype.substring.call(lsb, 6, 8) + + String.prototype.substring.call(lsb, 4, 6) + + String.prototype.substring.call(lsb, 2, 4) + + String.prototype.substring.call(lsb, 0, 2); + hex = msb + lsb; + + const hexBuffer = Buffer.from(hex, 'hex'); + return new bson.Binary(hexBuffer, bson.Binary.SUBTYPE_UUID_OLD); + }, + { prototype: bson.Binary.prototype } + ), + LegacyCSharpUUID: assignAll( + function LegacyCSharpUUID(hexstr?: string): BinaryType { + if (hexstr === undefined) { + // Generate a new UUID and format it. + hexstr = new bson.UUID().toHexString(); + } + assertArgsDefinedType([hexstr], ['string'], 'LegacyCSharpUUID'); + + let hex: string = String.prototype.replace.call( + hexstr, + /[{}-]/g, + () => '' + ); + const a = + String.prototype.substring.call(hex, 6, 8) + + String.prototype.substring.call(hex, 4, 6) + + String.prototype.substring.call(hex, 2, 4) + + String.prototype.substring.call(hex, 0, 2); + const b = + String.prototype.substring.call(hex, 10, 12) + + String.prototype.substring.call(hex, 8, 10); + const c = + String.prototype.substring.call(hex, 14, 16) + + String.prototype.substring.call(hex, 12, 14); + const d = String.prototype.substring.call(hex, 16, 32); + hex = a + b + c + d; + + const hexBuffer = Buffer.from(hex, 'hex'); + return new bson.Binary(hexBuffer, bson.Binary.SUBTYPE_UUID_OLD); + }, + { prototype: bson.Binary.prototype } + ), + LegacyPythonUUID: assignAll( + function LegacyPythonUUID(hexstr?: string): BinaryType { + if (hexstr === undefined) { + hexstr = new bson.UUID().toString(); + } + assertArgsDefinedType([hexstr], ['string'], 'LegacyPythonUUID'); + // Strip any dashes, as they occur in the standard UUID formatting + // (e.g. 01234567-89ab-cdef-0123-456789abcdef). + const buffer = Buffer.from(hexstr.replace(/-/g, ''), 'hex'); + return new bson.Binary(buffer, bson.Binary.SUBTYPE_UUID_OLD); + }, + { prototype: bson.Binary.prototype } + ), MD5: assignAll( function MD5(hexstr: string): BinaryType { assertArgsDefinedType([hexstr], ['string'], 'MD5');