411 lines
9.8 KiB
JavaScript
411 lines
9.8 KiB
JavaScript
/**
|
||
* 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);
|
||
}
|