Skip to content

Commit b5db818

Browse files
committed
feat(NODE-7314): export byteUtils & add missing methods
1 parent a23e788 commit b5db818

File tree

6 files changed

+181
-3
lines changed

6 files changed

+181
-3
lines changed

src/bson.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ export {
4949
MaxKey,
5050
BSONRegExp,
5151
Decimal128,
52-
NumberUtils
52+
NumberUtils,
53+
ByteUtils
5354
};
5455
export { BSONValue, bsonType, type BSONTypeTag } from './bson_value';
5556
export { BSONError, BSONVersionError, BSONRuntimeError, BSONOffsetError } from './error';

src/utils/byte_utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ export type ByteUtils = {
1515
allocate: (size: number) => Uint8Array;
1616
/** Create empty space of size, use pooled memory when available */
1717
allocateUnsafe: (size: number) => Uint8Array;
18+
/** Compare 2 Uint8Arrays lexicographically */
19+
compare: (buffer1: Uint8Array, buffer2: Uint8Array) => -1 | 0 | 1;
20+
/** Concatenating all the Uint8Arrays in new Uint8Array. */
21+
concat: (list: Uint8Array[]) => Uint8Array;
1822
/** Check if two Uint8Arrays are deep equal */
1923
equals: (a: Uint8Array, b: Uint8Array) => boolean;
2024
/** Check if two Uint8Arrays are deep equal */

src/utils/node_byte_utils.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ type NodeJsBuffer = ArrayBufferView &
1010
toString: (this: Uint8Array, encoding: NodeJsEncoding, start?: number, end?: number) => string;
1111
equals: (this: Uint8Array, other: Uint8Array) => boolean;
1212
swap32: (this: NodeJsBuffer) => NodeJsBuffer;
13+
compare: (this: Uint8Array, other: Uint8Array) => -1 | 0 | 1;
1314
};
1415
type NodeJsBufferConstructor = Omit<Uint8ArrayConstructor, 'from'> & {
1516
alloc: (size: number) => NodeJsBuffer;
@@ -21,6 +22,7 @@ type NodeJsBufferConstructor = Omit<Uint8ArrayConstructor, 'from'> & {
2122
from(base64: string, encoding: NodeJsEncoding): NodeJsBuffer;
2223
byteLength(input: string, encoding: 'utf8'): number;
2324
isBuffer(value: unknown): value is NodeJsBuffer;
25+
concat(list: Uint8Array[]): NodeJsBuffer;
2426
};
2527

2628
// This can be nullish, but we gate the nodejs functions on being exported whether or not this exists
@@ -88,6 +90,14 @@ export const nodeJsByteUtils = {
8890
return Buffer.allocUnsafe(size);
8991
},
9092

93+
compare(a: Uint8Array, b: Uint8Array) {
94+
return nodeJsByteUtils.toLocalBufferType(a).compare(b);
95+
},
96+
97+
concat(list: Uint8Array[]): NodeJsBuffer {
98+
return Buffer.concat(list);
99+
},
100+
91101
equals(a: Uint8Array, b: Uint8Array): boolean {
92102
return nodeJsByteUtils.toLocalBufferType(a).equals(b);
93103
},

src/utils/web_byte_utils.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,42 @@ export const webByteUtils = {
114114
return webByteUtils.allocate(size);
115115
},
116116

117+
compare(a: Uint8Array, b: Uint8Array): -1 | 0 | 1 {
118+
if (a === b) return 0;
119+
120+
const len = Math.min(a.length, b.length);
121+
122+
for (let i = 0; i < len; i++) {
123+
if (a[i] < b[i]) return -1;
124+
if (a[i] > b[i]) return 1;
125+
}
126+
127+
if (a.length < b.length) return -1;
128+
if (a.length > b.length) return 1;
129+
130+
return 0;
131+
},
132+
133+
concat(list: Uint8Array[]): Uint8Array {
134+
if (list.length === 0) return webByteUtils.allocate(0);
135+
if (list.length === 1) return list[0];
136+
137+
let totalLength = 0;
138+
for (const arr of list) {
139+
totalLength += arr.length;
140+
}
141+
142+
const result = webByteUtils.allocate(totalLength);
143+
let offset = 0;
144+
145+
for (const arr of list) {
146+
result.set(arr, offset);
147+
offset += arr.length;
148+
}
149+
150+
return result;
151+
},
152+
117153
equals(a: Uint8Array, b: Uint8Array): boolean {
118154
if (a.byteLength !== b.byteLength) {
119155
return false;

test/node/byte_utils.test.ts

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,130 @@ const swap32Tests: ByteUtilTest<'swap32'>[] = [
517517
}
518518
}
519519
];
520+
const compareTests: ByteUtilTest<'compare'>[] = [
521+
{
522+
name: 'returns 0 for two equal arrays (same content)',
523+
inputs: [new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 3])],
524+
expectation({ output }) {
525+
expect(output).to.equal(0);
526+
}
527+
},
528+
{
529+
name: 'returns 0 when comparing the same buffer by reference',
530+
inputs: (() => {
531+
const buf = new Uint8Array([5, 6, 7]);
532+
return [buf, buf];
533+
})(),
534+
expectation({ output }) {
535+
expect(output).to.equal(0);
536+
}
537+
},
538+
{
539+
name: 'array a is lexicographically less than array b (first differing byte)',
540+
inputs: [new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 4])],
541+
expectation({ output }) {
542+
expect(output).to.equal(-1);
543+
}
544+
},
545+
{
546+
name: 'array a is lexicographically greater than array b (first differing byte)',
547+
inputs: [new Uint8Array([1, 2, 4]), new Uint8Array([1, 2, 3])],
548+
expectation({ output }) {
549+
expect(output).to.equal(1);
550+
}
551+
},
552+
{
553+
name: 'a is a strict prefix of b (a shorter, same starting bytes) -> a < b',
554+
inputs: [new Uint8Array([1, 2]), new Uint8Array([1, 2, 3])],
555+
expectation({ output }) {
556+
expect(output).to.equal(-1);
557+
}
558+
},
559+
{
560+
name: 'b is a strict prefix of a (b shorter, same starting bytes) -> a > b',
561+
inputs: [new Uint8Array([1, 2, 3]), new Uint8Array([1, 2])],
562+
expectation({ output }) {
563+
expect(output).to.equal(1);
564+
}
565+
},
566+
{
567+
name: 'handles empty arrays',
568+
inputs: [new Uint8Array([]), new Uint8Array([])],
569+
expectation({ output }) {
570+
expect(output).to.equal(0);
571+
}
572+
}
573+
];
574+
const concatTests: ByteUtilTest<'concat'>[] = [
575+
{
576+
name: 'concatenates two non-empty arrays',
577+
inputs: [[new Uint8Array([1, 2]), new Uint8Array([3, 4])]],
578+
expectation({ output, error }) {
579+
expect(error).to.be.null;
580+
expect(output).to.not.be.undefined;
581+
expect([...(output ?? [])]).to.deep.equal([1, 2, 3, 4]);
582+
}
583+
},
584+
{
585+
name: 'concatenates multiple arrays in order',
586+
inputs: [[new Uint8Array([1]), new Uint8Array([2, 3]), new Uint8Array([4, 5, 6])]],
587+
expectation({ output, error }) {
588+
expect(error).to.be.null;
589+
expect(output).to.not.be.undefined;
590+
expect([...(output ?? [])]).to.deep.equal([1, 2, 3, 4, 5, 6]);
591+
}
592+
},
593+
{
594+
name: 'returns an empty Uint8Array when given an empty list',
595+
inputs: [[]],
596+
expectation({ output, error }) {
597+
expect(error).to.be.null;
598+
expect(output).to.not.be.undefined;
599+
expect(output).to.have.property('byteLength', 0);
600+
expect([...(output ?? [])]).to.deep.equal([]);
601+
}
602+
},
603+
{
604+
name: 'returns the same contents when given a single array',
605+
inputs: [[new Uint8Array([7, 8, 9])]],
606+
expectation({ output, error }) {
607+
expect(error).to.be.null;
608+
expect(output).to.not.be.undefined;
609+
expect([...(output ?? [])]).to.deep.equal([7, 8, 9]);
610+
}
611+
},
612+
{
613+
name: 'handles concatenation with empty arrays inside the list',
614+
inputs: [
615+
[new Uint8Array([]), new Uint8Array([1, 2, 3]), new Uint8Array([]), new Uint8Array([4])]
616+
],
617+
expectation({ output, error }) {
618+
expect(error).to.be.null;
619+
expect(output).to.not.be.undefined;
620+
expect([...(output ?? [])]).to.deep.equal([1, 2, 3, 4]);
621+
}
622+
},
623+
{
624+
name: 'result has correct total byteLength',
625+
inputs: [[new Uint8Array([1, 2]), new Uint8Array([3]), new Uint8Array([4, 5, 6])]],
626+
expectation({ output, error }) {
627+
expect(error).to.be.null;
628+
expect(output).to.not.be.undefined;
629+
// 2 + 1 + 3 = 6
630+
expect(output).to.have.property('byteLength', 6);
631+
expect([...(output ?? [])]).to.deep.equal([1, 2, 3, 4, 5, 6]);
632+
}
633+
},
634+
{
635+
name: 'concatenates arrays with overlapping contents correctly',
636+
inputs: [[new Uint8Array([0, 0, 1]), new Uint8Array([1, 0, 0])]],
637+
expectation({ output, error }) {
638+
expect(error).to.be.null;
639+
expect(output).to.not.be.undefined;
640+
expect([...(output ?? [])]).to.deep.equal([0, 0, 1, 1, 0, 0]);
641+
}
642+
}
643+
];
520644

521645
const utils = new Map([
522646
['nodeJsByteUtils', nodeJsByteUtils],
@@ -538,7 +662,9 @@ const table = new Map<keyof ByteUtils, ByteUtilTest<keyof ByteUtils>[]>([
538662
['toUTF8', toUTF8Tests],
539663
['utf8ByteLength', utf8ByteLengthTests],
540664
['randomBytes', randomBytesTests],
541-
['swap32', swap32Tests]
665+
['swap32', swap32Tests],
666+
['compare', compareTests],
667+
['concat', concatTests]
542668
]);
543669

544670
describe('ByteUtils', () => {

test/node/exports.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ const EXPECTED_EXPORTS = [
3939
'deserializeStream',
4040
'BSON',
4141
'bsonType',
42-
'NumberUtils'
42+
'NumberUtils',
43+
'ByteUtils'
4344
];
4445

4546
const EXPECTED_EJSON_EXPORTS = ['parse', 'stringify', 'serialize', 'deserialize'];

0 commit comments

Comments
 (0)