/** * author: Sindre Sorhus * license: MIT * source: https://github.com/sindresorhus/uint8array-extras */ const objectToString = Object.prototype.toString; const uint8ArrayStringified = '[object Uint8Array]'; const arrayBufferStringified = '[object ArrayBuffer]'; function isType(value, typeConstructor, typeStringified) { if (!value) { return false; } if (value.constructor === typeConstructor) { return true; } return objectToString.call(value) === typeStringified; } export function isUint8Array(value) { return isType(value, Uint8Array, uint8ArrayStringified); } function isArrayBuffer(value) { return isType(value, ArrayBuffer, arrayBufferStringified); } function isUint8ArrayOrArrayBuffer(value) { return isUint8Array(value) || isArrayBuffer(value); } export function assertUint8Array(value) { if (!isUint8Array(value)) { throw new TypeError(`Expected \`Uint8Array\`, got \`${typeof value}\``); } } export function assertUint8ArrayOrArrayBuffer(value) { if (!isUint8ArrayOrArrayBuffer(value)) { throw new TypeError(`Expected \`Uint8Array\` or \`ArrayBuffer\`, got \`${typeof value}\``); } } export function toUint8Array(value) { if (value instanceof ArrayBuffer) { return new Uint8Array(value); } if (ArrayBuffer.isView(value)) { return new Uint8Array(value.buffer, value.byteOffset, value.byteLength); } throw new TypeError(`Unsupported value, got \`${typeof value}\`.`); } export function concatUint8Arrays(arrays, totalLength) { if (arrays.length === 0) { return new Uint8Array(0); } totalLength ??= arrays.reduce((accumulator, currentValue) => accumulator + currentValue.length, 0); const returnValue = new Uint8Array(totalLength); let offset = 0; for (const array of arrays) { assertUint8Array(array); returnValue.set(array, offset); offset += array.length; } return returnValue; } export function areUint8ArraysEqual(a, b) { assertUint8Array(a); assertUint8Array(b); if (a === b) { return true; } if (a.length !== b.length) { return false; } // eslint-disable-next-line unicorn/no-for-loop for (let index = 0; index < a.length; index++) { if (a[index] !== b[index]) { return false; } } return true; } export function compareUint8Arrays(a, b) { assertUint8Array(a); assertUint8Array(b); const length = Math.min(a.length, b.length); for (let index = 0; index < length; index++) { const diff = a[index] - b[index]; if (diff !== 0) { return Math.sign(diff); } } // At this point, all the compared elements are equal. // The shorter array should come first if the arrays are of different lengths. return Math.sign(a.length - b.length); } // const cachedDecoders = { // utf8: new globalThis.TextDecoder("utf8"), // }; // // !!!!!!! commented out because we dont have `TextDecoder` as dep anymore !!!!!!!! // // export function uint8ArrayToString(array, encoding = "utf8") { // assertUint8ArrayOrArrayBuffer(array); // cachedDecoders[encoding] ??= new globalThis.TextDecoder(encoding); // return cachedDecoders[encoding].decode(array); // } function assertString(value) { if (typeof value !== 'string') { throw new TypeError(`Expected \`string\`, got \`${typeof value}\``); } } const cachedEncoder = new globalThis.TextEncoder(); export function stringToUint8Array(string) { assertString(string); return cachedEncoder.encode(string); } function base64ToBase64Url(base64) { return base64.replaceAll('+', '-').replaceAll('/', '_').replace(/[=]+$/, ''); } function base64UrlToBase64(base64url) { return base64url.replaceAll('-', '+').replaceAll('_', '/'); } // Reference: https://phuoc.ng/collection/this-vs-that/concat-vs-push/ const MAX_BLOCK_SIZE = 65_535; export function uint8ArrayToBase64(array, { urlSafe = false } = {}) { assertUint8Array(array); let base64; if (array.length < MAX_BLOCK_SIZE) { // Required as `btoa` and `atob` don't properly support Unicode: https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem base64 = globalThis.btoa(String.fromCodePoint.apply(this, array)); } else { base64 = ''; for (const value of array) { base64 += String.fromCodePoint(value); } base64 = globalThis.btoa(base64); } return urlSafe ? base64ToBase64Url(base64) : base64; } export function base64ToUint8Array(base64String) { assertString(base64String); return Uint8Array.from(globalThis.atob(base64UrlToBase64(base64String)), x => x.codePointAt(0)); } export function stringToBase64(string, { urlSafe = false } = {}) { assertString(string); return uint8ArrayToBase64(stringToUint8Array(string), { urlSafe }); } // export function base64ToString(base64String) { // assertString(base64String); // return uint8ArrayToString(base64ToUint8Array(base64String)); // } const byteToHexLookupTable = Array.from({ length: 256 }, (_, index) => index.toString(16).padStart(2, '0')); export function uint8ArrayToHex(array) { assertUint8Array(array); // Concatenating a string is faster than using an array. let hexString = ''; // eslint-disable-next-line unicorn/no-for-loop -- Max performance is critical. for (let index = 0; index < array.length; index++) { hexString += byteToHexLookupTable[array[index]]; } return hexString; } const hexToDecimalLookupTable = { 0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, a: 10, b: 11, c: 12, d: 13, e: 14, f: 15, A: 10, B: 11, C: 12, D: 13, E: 14, F: 15, }; export function hexToUint8Array(hexString) { assertString(hexString); if (hexString.length % 2 !== 0) { throw new Error('Invalid Hex string length.'); } const resultLength = hexString.length / 2; const bytes = new Uint8Array(resultLength); for (let index = 0; index < resultLength; index++) { const highNibble = hexToDecimalLookupTable[hexString[index * 2]]; const lowNibble = hexToDecimalLookupTable[hexString[index * 2 + 1]]; if (highNibble === undefined || lowNibble === undefined) { throw new Error(`Invalid Hex character encountered at position ${index * 2}`); } bytes[index] = (highNibble << 4) | lowNibble; // eslint-disable-line no-bitwise } return bytes; } /** @param {DataView} view @returns {number} */ export function getUintBE(view) { const { byteLength } = view; if (byteLength === 6) { return view.getUint16(0) * 2 ** 32 + view.getUint32(2); } if (byteLength === 5) { return view.getUint8(0) * 2 ** 32 + view.getUint32(1); } if (byteLength === 4) { return view.getUint32(0); } if (byteLength === 3) { return view.getUint8(0) * 2 ** 16 + view.getUint16(1); } if (byteLength === 2) { return view.getUint16(0); } if (byteLength === 1) { return view.getUint8(0); } } /** @param {Uint8Array} array @param {Uint8Array} value @returns {number} */ export function indexOf(array, value) { const arrayLength = array.length; const valueLength = value.length; if (valueLength === 0) { return -1; } if (valueLength > arrayLength) { return -1; } const validOffsetLength = arrayLength - valueLength; for (let index = 0; index <= validOffsetLength; index++) { let isMatch = true; for (let index2 = 0; index2 < valueLength; index2++) { if (array[index + index2] !== value[index2]) { isMatch = false; break; } } if (isMatch) { return index; } } return -1; } /** @param {Uint8Array} array @param {Uint8Array} value @returns {boolean} */ export function includes(array, value) { return indexOf(array, value) !== -1; } // we can use this implementation when we will have TextDecoder in RN // const cachedDecoders = { // utf8: new globalThis.TextDecoder("utf8"), // }; // export function uint8ArrayToString(array, encoding = "utf8") { // assertUint8ArrayOrArrayBuffer(array); // cachedDecoders[encoding] ??= new globalThis.TextDecoder(encoding); // return cachedDecoders[encoding].decode(array); // } // meanwhile: /** * Convert a Uint8Array (or ArrayBuffer) of UTF-8 bytes into a JS string. * Only "utf8" is supported. For any other encoding you’ll need a polyfill. * * @param {Uint8Array|ArrayBuffer} input * @param {string} [encoding="utf8"] * @returns {string} */ export function uint8ArrayToString(input, encoding = 'utf8') { assertUint8ArrayOrArrayBuffer(input); // Reject anything other than UTF-8 if (!/utf-?8/i.test(encoding)) { throw new Error('Encoding "' + encoding + '" isn’t supported without a TextDecoder polyfill'); } // Normalise to Uint8Array const bytes = input instanceof Uint8Array ? input : new Uint8Array(input); return decodeUtf8(bytes); } /** * Minimal UTF-8 decoder * @param {Uint8Array} bytes * @returns {string} */ function decodeUtf8(bytes) { let i = 0; const l = bytes.length; const codeUnits = []; let result = ''; while (i < l) { const byte1 = bytes[i++]; // 1-byte (ASCII) if (byte1 < 0x80) { codeUnits.push(byte1); } // 2-byte else if (byte1 < 0xe0) { const byte2 = bytes[i++] & 0x3f; codeUnits.push(((byte1 & 0x1f) << 6) | byte2); } // 3-byte else if (byte1 < 0xf0) { const byte2 = bytes[i++] & 0x3f; const byte3 = bytes[i++] & 0x3f; codeUnits.push(((byte1 & 0x0f) << 12) | (byte2 << 6) | byte3); } // 4-byte (→ surrogate pair) else { const byte2 = bytes[i++] & 0x3f; const byte3 = bytes[i++] & 0x3f; const byte4 = bytes[i++] & 0x3f; let cp = ((byte1 & 0x07) << 18) | (byte2 << 12) | (byte3 << 6) | byte4; cp -= 0x10000; codeUnits.push(0xd800 + (cp >> 10), 0xdc00 + (cp & 0x3ff)); } // Flush periodically to avoid huge apply() calls if (codeUnits.length > 0x8000) { result += String.fromCharCode.apply(null, codeUnits); codeUnits.length = 0; } } return result + String.fromCharCode.apply(null, codeUnits); }