diff --git a/src/bson.ts b/src/bson.ts index 46678360..111ec408 100644 --- a/src/bson.ts +++ b/src/bson.ts @@ -49,7 +49,8 @@ export { MaxKey, BSONRegExp, Decimal128, - NumberUtils + NumberUtils, + ByteUtils }; export { BSONValue, bsonType, type BSONTypeTag } from './bson_value'; export { BSONError, BSONVersionError, BSONRuntimeError, BSONOffsetError } from './error'; diff --git a/src/utils/byte_utils.ts b/src/utils/byte_utils.ts index 05e30515..15b77daa 100644 --- a/src/utils/byte_utils.ts +++ b/src/utils/byte_utils.ts @@ -15,6 +15,10 @@ export type ByteUtils = { allocate: (size: number) => Uint8Array; /** Create empty space of size, use pooled memory when available */ allocateUnsafe: (size: number) => Uint8Array; + /** Compare 2 Uint8Arrays lexicographically */ + compare: (buffer1: Uint8Array, buffer2: Uint8Array) => -1 | 0 | 1; + /** Concatenating all the Uint8Arrays in new Uint8Array. */ + concat: (list: Uint8Array[]) => Uint8Array; /** Check if two Uint8Arrays are deep equal */ equals: (a: Uint8Array, b: Uint8Array) => boolean; /** Check if two Uint8Arrays are deep equal */ @@ -58,6 +62,7 @@ const hasGlobalBuffer = typeof Buffer === 'function' && Buffer.prototype?._isBuf * The type annotation is important here, it asserts that each of the platform specific * utils implementations are compatible with the common one. * - * @internal + * @public + * @experimental */ export const ByteUtils: ByteUtils = hasGlobalBuffer ? nodeJsByteUtils : webByteUtils; diff --git a/src/utils/node_byte_utils.ts b/src/utils/node_byte_utils.ts index 8f2dc7df..4a58c552 100644 --- a/src/utils/node_byte_utils.ts +++ b/src/utils/node_byte_utils.ts @@ -10,6 +10,7 @@ type NodeJsBuffer = ArrayBufferView & toString: (this: Uint8Array, encoding: NodeJsEncoding, start?: number, end?: number) => string; equals: (this: Uint8Array, other: Uint8Array) => boolean; swap32: (this: NodeJsBuffer) => NodeJsBuffer; + compare: (this: Uint8Array, other: Uint8Array) => -1 | 0 | 1; }; type NodeJsBufferConstructor = Omit & { alloc: (size: number) => NodeJsBuffer; @@ -21,6 +22,7 @@ type NodeJsBufferConstructor = Omit & { from(base64: string, encoding: NodeJsEncoding): NodeJsBuffer; byteLength(input: string, encoding: 'utf8'): number; isBuffer(value: unknown): value is NodeJsBuffer; + concat(list: Uint8Array[]): NodeJsBuffer; }; // This can be nullish, but we gate the nodejs functions on being exported whether or not this exists @@ -51,7 +53,10 @@ const nodejsRandomBytes = (() => { } })(); -/** @internal */ +/** + * @public + * @experimental + */ export const nodeJsByteUtils = { toLocalBufferType(potentialBuffer: Uint8Array | NodeJsBuffer | ArrayBuffer): NodeJsBuffer { if (Buffer.isBuffer(potentialBuffer)) { @@ -88,6 +93,14 @@ export const nodeJsByteUtils = { return Buffer.allocUnsafe(size); }, + compare(a: Uint8Array, b: Uint8Array) { + return nodeJsByteUtils.toLocalBufferType(a).compare(b); + }, + + concat(list: Uint8Array[]): NodeJsBuffer { + return Buffer.concat(list); + }, + equals(a: Uint8Array, b: Uint8Array): boolean { return nodeJsByteUtils.toLocalBufferType(a).equals(b); }, diff --git a/src/utils/web_byte_utils.ts b/src/utils/web_byte_utils.ts index 336d37ed..e2592239 100644 --- a/src/utils/web_byte_utils.ts +++ b/src/utils/web_byte_utils.ts @@ -69,7 +69,10 @@ const webRandomBytes: (byteLength: number) => Uint8Array = (() => { const HEX_DIGIT = /(\d|[a-f])/i; -/** @internal */ +/** + * @public + * @experimental + */ export const webByteUtils = { toLocalBufferType( potentialUint8array: Uint8Array | ArrayBufferViewWithTag | ArrayBuffer @@ -114,6 +117,42 @@ export const webByteUtils = { return webByteUtils.allocate(size); }, + compare(a: Uint8Array, b: Uint8Array): -1 | 0 | 1 { + if (a === b) return 0; + + const len = Math.min(a.length, b.length); + + for (let i = 0; i < len; i++) { + if (a[i] < b[i]) return -1; + if (a[i] > b[i]) return 1; + } + + if (a.length < b.length) return -1; + if (a.length > b.length) return 1; + + return 0; + }, + + concat(list: Uint8Array[]): Uint8Array { + if (list.length === 0) return webByteUtils.allocate(0); + if (list.length === 1) return list[0]; + + let totalLength = 0; + for (const arr of list) { + totalLength += arr.length; + } + + const result = webByteUtils.allocate(totalLength); + let offset = 0; + + for (const arr of list) { + result.set(arr, offset); + offset += arr.length; + } + + return result; + }, + equals(a: Uint8Array, b: Uint8Array): boolean { if (a.byteLength !== b.byteLength) { return false; diff --git a/test/node/byte_utils.test.ts b/test/node/byte_utils.test.ts index d0c34d21..53f5518a 100644 --- a/test/node/byte_utils.test.ts +++ b/test/node/byte_utils.test.ts @@ -517,6 +517,123 @@ const swap32Tests: ByteUtilTest<'swap32'>[] = [ } } ]; +const compareTests: ByteUtilTest<'compare'>[] = [ + { + name: 'returns 0 for two equal arrays (same content)', + inputs: [new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 3])], + expectation({ output }) { + expect(output).to.equal(0); + } + }, + { + name: 'returns 0 when comparing the same buffer by reference', + inputs: (() => { + const buf = new Uint8Array([5, 6, 7]); + return [buf, buf]; + })(), + expectation({ output }) { + expect(output).to.equal(0); + } + }, + { + name: 'array a is lexicographically less than array b (first differing byte)', + inputs: [new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 4])], + expectation({ output }) { + expect(output).to.equal(-1); + } + }, + { + name: 'array a is lexicographically greater than array b (first differing byte)', + inputs: [new Uint8Array([1, 2, 4]), new Uint8Array([1, 2, 3])], + expectation({ output }) { + expect(output).to.equal(1); + } + }, + { + name: 'a is a strict prefix of b (a shorter, same starting bytes) -> a < b', + inputs: [new Uint8Array([1, 2]), new Uint8Array([1, 2, 3])], + expectation({ output }) { + expect(output).to.equal(-1); + } + }, + { + name: 'b is a strict prefix of a (b shorter, same starting bytes) -> a > b', + inputs: [new Uint8Array([1, 2, 3]), new Uint8Array([1, 2])], + expectation({ output }) { + expect(output).to.equal(1); + } + }, + { + name: 'handles empty arrays', + inputs: [new Uint8Array([]), new Uint8Array([])], + expectation({ output }) { + expect(output).to.equal(0); + } + } +]; +const concatTests: ByteUtilTest<'concat'>[] = [ + { + name: 'concatenates two non-empty arrays', + inputs: [[new Uint8Array([1, 2]), new Uint8Array([3, 4])]], + expectation({ output, error }) { + expect(error).to.be.null; + expect(output).to.deep.equal(Buffer.from([1, 2, 3, 4])); + } + }, + { + name: 'concatenates multiple arrays in order', + inputs: [[new Uint8Array([1]), new Uint8Array([2, 3]), new Uint8Array([4, 5, 6])]], + expectation({ output, error }) { + expect(error).to.be.null; + expect(output).to.deep.equal(Buffer.from([1, 2, 3, 4, 5, 6])); + } + }, + { + name: 'returns an empty Uint8Array when given an empty list', + inputs: [[]], + expectation({ output, error }) { + expect(error).to.be.null; + expect(output).to.have.property('byteLength', 0); + expect(output).to.deep.equal(Buffer.from([])); + } + }, + { + name: 'returns the same contents when given a single array', + inputs: [[new Uint8Array([7, 8, 9])]], + expectation({ output, error }) { + expect(error).to.be.null; + expect(output).to.deep.equal(Buffer.from([7, 8, 9])); + } + }, + { + name: 'handles concatenation with empty arrays inside the list', + inputs: [ + [new Uint8Array([]), new Uint8Array([1, 2, 3]), new Uint8Array([]), new Uint8Array([4])] + ], + expectation({ output, error }) { + expect(error).to.be.null; + expect(output).to.deep.equal(Buffer.from([1, 2, 3, 4])); + } + }, + { + name: 'result has correct total byteLength', + inputs: [[new Uint8Array([1, 2]), new Uint8Array([3]), new Uint8Array([4, 5, 6])]], + expectation({ output, error }) { + expect(error).to.be.null; + // 2 + 1 + 3 = 6 + expect(output).to.have.property('byteLength', 6); + expect(output).to.deep.equal(Buffer.from([1, 2, 3, 4, 5, 6])); + } + }, + { + name: 'concatenates arrays with overlapping contents correctly', + inputs: [[new Uint8Array([0, 0, 1]), new Uint8Array([1, 0, 0])]], + expectation({ output, error }) { + expect(error).to.be.null; + expect(output).to.deep.equal(Buffer.from([0, 0, 1, 1, 0, 0])); + } + } +]; const utils = new Map([ ['nodeJsByteUtils', nodeJsByteUtils], @@ -538,7 +655,9 @@ const table = new Map[]>([ ['toUTF8', toUTF8Tests], ['utf8ByteLength', utf8ByteLengthTests], ['randomBytes', randomBytesTests], - ['swap32', swap32Tests] + ['swap32', swap32Tests], + ['compare', compareTests], + ['concat', concatTests] ]); describe('ByteUtils', () => { diff --git a/test/node/exports.test.ts b/test/node/exports.test.ts index ba2da484..f262f859 100644 --- a/test/node/exports.test.ts +++ b/test/node/exports.test.ts @@ -39,7 +39,8 @@ const EXPECTED_EXPORTS = [ 'deserializeStream', 'BSON', 'bsonType', - 'NumberUtils' + 'NumberUtils', + 'ByteUtils' ]; const EXPECTED_EJSON_EXPORTS = ['parse', 'stringify', 'serialize', 'deserialize'];