import { BSONError } from '../error'; import { tryReadBasicLatin } from './latin'; import { parseUtf8 } from '../parse_utf8'; import { isUint8Array } from '../parser/utils'; type TextDecoder = { readonly encoding: string; readonly fatal: boolean; readonly ignoreBOM: boolean; decode(input?: Uint8Array): string; }; type TextDecoderConstructor = { new (label: 'utf8', options: { fatal: boolean; ignoreBOM?: boolean }): TextDecoder; }; type TextEncoder = { readonly encoding: string; encode(input?: string): Uint8Array; }; type TextEncoderConstructor = { new (): TextEncoder; }; // Web global declare const TextDecoder: TextDecoderConstructor; declare const TextEncoder: TextEncoderConstructor; declare const atob: (base64: string) => string; declare const btoa: (binary: string) => string; type ArrayBufferViewWithTag = ArrayBufferView & { [Symbol.toStringTag]?: string; }; function isReactNative() { const { navigator } = globalThis as { navigator?: { product?: string } }; return typeof navigator === 'object' && navigator.product === 'ReactNative'; } /** @internal */ export function webMathRandomBytes(byteLength: number) { if (byteLength < 0) { throw new RangeError(`The argument 'byteLength' is invalid. Received ${byteLength}`); } return webByteUtils.fromNumberArray( Array.from({ length: byteLength }, () => Math.floor(Math.random() * 256)) ); } /** @internal */ const webRandomBytes: (byteLength: number) => Uint8Array = (() => { const { crypto } = globalThis as { crypto?: { getRandomValues?: (space: Uint8Array) => Uint8Array }; }; if (crypto != null && typeof crypto.getRandomValues === 'function') { return (byteLength: number) => { // @ts-expect-error: crypto.getRandomValues cannot actually be null here // You cannot separate getRandomValues from crypto (need to have this === crypto) return crypto.getRandomValues(webByteUtils.allocate(byteLength)); }; } else { if (isReactNative()) { const { console } = globalThis as { console?: { warn?: (message: string) => void } }; console?.warn?.( 'BSON: For React Native please polyfill crypto.getRandomValues, e.g. using: https://www.npmjs.com/package/react-native-get-random-values.' ); } return webMathRandomBytes; } })(); const HEX_DIGIT = /(\d|[a-f])/i; /** * @public * @experimental */ export const webByteUtils = { isUint8Array: isUint8Array, toLocalBufferType( potentialUint8array: Uint8Array | ArrayBufferViewWithTag | ArrayBuffer ): Uint8Array { const stringTag = potentialUint8array?.[Symbol.toStringTag] ?? Object.prototype.toString.call(potentialUint8array); if (stringTag === 'Uint8Array') { return potentialUint8array as Uint8Array; } if (ArrayBuffer.isView(potentialUint8array)) { return new Uint8Array( potentialUint8array.buffer.slice( potentialUint8array.byteOffset, potentialUint8array.byteOffset + potentialUint8array.byteLength ) ); } if ( stringTag === 'ArrayBuffer' || stringTag === 'SharedArrayBuffer' || stringTag === '[object ArrayBuffer]' || stringTag === '[object SharedArrayBuffer]' ) { return new Uint8Array(potentialUint8array); } throw new BSONError(`Cannot make a Uint8Array from passed potentialBuffer.`); }, allocate(size: number): Uint8Array { if (typeof size !== 'number') { throw new TypeError(`The "size" argument must be of type number. Received ${String(size)}`); } return new Uint8Array(size); }, allocateUnsafe(size: number): Uint8Array { return webByteUtils.allocate(size); }, compare(uint8Array: Uint8Array, otherUint8Array: Uint8Array): -1 | 0 | 1 { if (uint8Array === otherUint8Array) return 0; const len = Math.min(uint8Array.length, otherUint8Array.length); for (let i = 0; i < len; i++) { if (uint8Array[i] < otherUint8Array[i]) return -1; if (uint8Array[i] > otherUint8Array[i]) return 1; } if (uint8Array.length < otherUint8Array.length) return -1; if (uint8Array.length > otherUint8Array.length) return 1; return 0; }, concat(uint8Arrays: Uint8Array[]): Uint8Array { if (uint8Arrays.length === 0) return webByteUtils.allocate(0); let totalLength = 0; for (const uint8Array of uint8Arrays) { totalLength += uint8Array.length; } const result = webByteUtils.allocate(totalLength); let offset = 0; for (const uint8Array of uint8Arrays) { result.set(uint8Array, offset); offset += uint8Array.length; } return result; }, copy( source: Uint8Array, target: Uint8Array, targetStart?: number, sourceStart?: number, sourceEnd?: number ): number { // validate and standardize passed-in sourceEnd if (sourceEnd !== undefined && sourceEnd < 0) { throw new RangeError( `The value of "sourceEnd" is out of range. It must be >= 0. Received ${sourceEnd}` ); } sourceEnd = sourceEnd ?? source.length; // validate and standardize passed-in sourceStart if (sourceStart !== undefined && (sourceStart < 0 || sourceStart > sourceEnd)) { throw new RangeError( `The value of "sourceStart" is out of range. It must be >= 0 and <= ${sourceEnd}. Received ${sourceStart}` ); } sourceStart = sourceStart ?? 0; // validate and standardize passed-in targetStart if (targetStart !== undefined && targetStart < 0) { throw new RangeError( `The value of "targetStart" is out of range. It must be >= 0. Received ${targetStart}` ); } targetStart = targetStart ?? 0; // figure out how many bytes we can copy const srcSlice = source.subarray(sourceStart, sourceEnd); const maxLen = Math.min(srcSlice.length, target.length - targetStart); if (maxLen <= 0) { return 0; } // perform the copy target.set(srcSlice.subarray(0, maxLen), targetStart); return maxLen; }, equals(uint8Array: Uint8Array, otherUint8Array: Uint8Array): boolean { if (uint8Array.byteLength !== otherUint8Array.byteLength) { return false; } for (let i = 0; i < uint8Array.byteLength; i++) { if (uint8Array[i] !== otherUint8Array[i]) { return false; } } return true; }, fromNumberArray(array: number[]): Uint8Array { return Uint8Array.from(array); }, fromBase64(base64: string): Uint8Array { return Uint8Array.from(atob(base64), c => c.charCodeAt(0)); }, fromUTF8(utf8: string): Uint8Array { return new TextEncoder().encode(utf8); }, toBase64(uint8array: Uint8Array): string { return btoa(webByteUtils.toISO88591(uint8array)); }, /** **Legacy** binary strings are an outdated method of data transfer. Do not add public API support for interpreting this format */ fromISO88591(codePoints: string): Uint8Array { return Uint8Array.from(codePoints, c => c.charCodeAt(0) & 0xff); }, /** **Legacy** binary strings are an outdated method of data transfer. Do not add public API support for interpreting this format */ toISO88591(uint8array: Uint8Array): string { return Array.from(Uint16Array.from(uint8array), b => String.fromCharCode(b)).join(''); }, fromHex(hex: string): Uint8Array { const evenLengthHex = hex.length % 2 === 0 ? hex : hex.slice(0, hex.length - 1); const buffer = []; for (let i = 0; i < evenLengthHex.length; i += 2) { const firstDigit = evenLengthHex[i]; const secondDigit = evenLengthHex[i + 1]; if (!HEX_DIGIT.test(firstDigit)) { break; } if (!HEX_DIGIT.test(secondDigit)) { break; } const hexDigit = Number.parseInt(`${firstDigit}${secondDigit}`, 16); buffer.push(hexDigit); } return Uint8Array.from(buffer); }, toHex(uint8array: Uint8Array): string { return Array.from(uint8array, byte => byte.toString(16).padStart(2, '0')).join(''); }, toUTF8(uint8array: Uint8Array, start: number, end: number, fatal: boolean): string { const basicLatin = end - start <= 20 ? tryReadBasicLatin(uint8array, start, end) : null; if (basicLatin != null) { return basicLatin; } return parseUtf8(uint8array, start, end, fatal); }, utf8ByteLength(input: string): number { return new TextEncoder().encode(input).byteLength; }, encodeUTF8Into(uint8array: Uint8Array, source: string, byteOffset: number): number { const bytes = new TextEncoder().encode(source); uint8array.set(bytes, byteOffset); return bytes.byteLength; }, randomBytes: webRandomBytes, swap32(buffer: Uint8Array): Uint8Array { if (buffer.length % 4 !== 0) { throw new RangeError('Buffer size must be a multiple of 32-bits'); } for (let i = 0; i < buffer.length; i += 4) { const byte0 = buffer[i]; const byte1 = buffer[i + 1]; const byte2 = buffer[i + 2]; const byte3 = buffer[i + 3]; buffer[i] = byte3; buffer[i + 1] = byte2; buffer[i + 2] = byte1; buffer[i + 3] = byte0; } return buffer; } };