359 lines
15 KiB
TypeScript
359 lines
15 KiB
TypeScript
// TypedArray is a union type of all the standard typed array types in JavaScript. This allows us to write
|
|
// functions that can accept any typed array or an ArrayBuffer and convert it to the appropriate view.
|
|
export type TypedArray = Int8Array | Uint8ClampedArray | Uint8Array | Uint16Array | Int16Array | Uint32Array | Int32Array;
|
|
|
|
/**
|
|
* Converts the given ArrayBuffer or TypedArray to a Uint8Array.
|
|
*
|
|
* @param arr The ArrayBuffer or TypedArray to convert.
|
|
* @returns A Uint8Array representing the same data.
|
|
*/
|
|
export const U8 = (arr: ArrayBuffer | TypedArray): Uint8Array => {
|
|
if (arr instanceof ArrayBuffer) return new Uint8Array(arr);
|
|
return new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength);
|
|
}
|
|
|
|
/**
|
|
* Converts the given ArrayBuffer or TypedArray to an Int8Array.
|
|
*
|
|
* @param arr The ArrayBuffer or TypedArray to convert.
|
|
* @returns An Int8Array representing the same data.
|
|
*/
|
|
export const I8 = (arr: ArrayBuffer | TypedArray): Int8Array => {
|
|
if (arr instanceof ArrayBuffer) return new Int8Array(arr);
|
|
return new Int8Array(arr.buffer, arr.byteOffset, arr.byteLength);
|
|
}
|
|
|
|
/**
|
|
* Converts the given ArrayBuffer or TypedArray to a Uint16Array. If a TypedArray is provided,
|
|
* the byte length is adjusted to account for the 2 bytes per value in a Uint16Array.
|
|
*
|
|
* @param arr The ArrayBuffer or TypedArray to convert.
|
|
* @returns A Uint16Array representing the same data.
|
|
*/
|
|
export const U16 = (arr: ArrayBuffer | TypedArray): Uint16Array => {
|
|
if (arr instanceof ArrayBuffer) return new Uint16Array(arr);
|
|
return new Uint16Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 2));
|
|
}
|
|
|
|
/**
|
|
* Converts the given ArrayBuffer or TypedArray to an Int16Array. If a TypedArray is provided,
|
|
* the byte length is adjusted to account for the 2 bytes per value in an Int16Array.
|
|
*
|
|
* @param arr The ArrayBuffer or TypedArray to convert.
|
|
* @returns An Int16Array representing the same data.
|
|
*/
|
|
export const I16 = (arr: ArrayBuffer | TypedArray): Int16Array => {
|
|
if (arr instanceof ArrayBuffer) return new Int16Array(arr);
|
|
return new Int16Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 2));
|
|
}
|
|
|
|
/**
|
|
* Converts the given ArrayBuffer or TypedArray to a Uint32Array. If a TypedArray is provided,
|
|
* the byte length is adjusted to account for the 4 bytes per value in a Uint32Array.
|
|
*
|
|
* @param arr The ArrayBuffer or TypedArray to convert.
|
|
* @returns A Uint32Array representing the same data.
|
|
*/
|
|
export const U32 = (arr: ArrayBuffer | TypedArray): Uint32Array => {
|
|
if (arr instanceof ArrayBuffer) return new Uint32Array(arr);
|
|
return new Uint32Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 4));
|
|
}
|
|
|
|
/**
|
|
* Converts the given ArrayBuffer or TypedArray to an Int32Array. If a TypedArray is provided,
|
|
* the byte length is adjusted to account for the 4 bytes per value in an Int32Array.
|
|
*
|
|
* @param arr The ArrayBuffer or TypedArray to convert.
|
|
* @returns An Int32Array representing the same data.
|
|
*/
|
|
export const I32 = (arr: ArrayBuffer | TypedArray): Int32Array => {
|
|
if (arr instanceof ArrayBuffer) return new Int32Array(arr);
|
|
return new Int32Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 4));
|
|
}
|
|
|
|
/**
|
|
* Converts the given ArrayBuffer or TypedArray to a Float32Array. If a TypedArray is provided,
|
|
* the byte length is adjusted to account for the 4 bytes per value in a Float32Array.
|
|
*
|
|
* @param arr The ArrayBuffer or TypedArray to convert.
|
|
* @returns A Float32Array representing the same data.
|
|
*/
|
|
export const F32 = (arr: ArrayBuffer | TypedArray): Float32Array => {
|
|
if (arr instanceof ArrayBuffer) return new Float32Array(arr);
|
|
return new Float32Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 4));
|
|
}
|
|
|
|
/**
|
|
* Converts the given ArrayBuffer or TypedArray to a Float64Array. If a TypedArray is provided,
|
|
* the byte length is adjusted to account for the 8 bytes per value in a Float64Array.
|
|
*
|
|
* @param arr The ArrayBuffer or TypedArray to convert.
|
|
* @returns A Float64Array representing the same data.
|
|
*/
|
|
export const F64 = (arr: ArrayBuffer | TypedArray): Float64Array => {
|
|
if (arr instanceof ArrayBuffer) return new Float64Array(arr);
|
|
return new Float64Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 8));
|
|
}
|
|
|
|
/**
|
|
* Tests whether the provided value is a Uint8Array or a view of an ArrayBuffer that is a
|
|
* Uint8Array. This is used to validate that a given value can be treated as raw bytes for
|
|
* encoding/decoding operations.
|
|
*
|
|
* @param bytes The value to test.
|
|
* @returns True if the value is a Uint8Array or a view of an ArrayBuffer that is a Uint8Array, false otherwise.
|
|
*/
|
|
export const isBytes = (bytes: unknown): boolean => {
|
|
return bytes instanceof Uint8Array || (ArrayBuffer.isView(bytes) && bytes.constructor.name === "Uint8Array");
|
|
}
|
|
|
|
/**
|
|
* Asserts that the provided value is a Uint8Array of the specified length (if provided). Throws a
|
|
* TypeError if the assertion fails.
|
|
*
|
|
* @param bytes The value to check.
|
|
* @param length The expected length of the Uint8Array.
|
|
* @param name The name of the variable being checked (for error messages).
|
|
*/
|
|
export const assertBytes = (bytes: Uint8Array, length?: number, name: string = ""): void => {
|
|
const valid = isBytes(bytes);
|
|
const sized = (typeof length !== 'undefined') ? (bytes.byteLength === length) : true;
|
|
if (!valid || !sized) {
|
|
const expected = typeof length !== 'undefined' ? `Uint8Array of length ${length}` : 'Uint8Array';
|
|
const actual = valid ? `Uint8Array of length ${bytes.byteLength}` : typeof bytes;
|
|
throw new TypeError(`Expected ${name} to be ${expected}, got ${actual}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test for equality of two Uint8Arrays. Returns true if the arrays are equal, false otherwise.
|
|
*
|
|
* Note: This function is not designed to be resistant to timing attacks. For security-sensitive
|
|
* comparisons (e.g., cryptographic keys, hashes), use `constantTimeEqualBytes` instead.
|
|
*
|
|
* @param a The first Uint8Array to compare.
|
|
* @param b The second Uint8Array to compare.
|
|
* @returns True if the arrays are equal, false otherwise.
|
|
*/
|
|
export const equalBytes = (a: Uint8Array, b: Uint8Array): boolean => {
|
|
if (a.byteLength !== b.byteLength) return false;
|
|
for (let i = 0; i < a.byteLength; i++) {
|
|
if (a[i] !== b[i]) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Performs a constant-time comparison of two Uint8Arrays to prevent timing attacks. Returns true
|
|
* if the arrays are equal, false otherwise.
|
|
*
|
|
* This function should be used for security-sensitive comparisons (e.g., cryptographic keys,
|
|
* hashes) to mitigate timing attacks. For general use where performance is a concern and security
|
|
* is not an issue, `equalBytes` may be more efficient.
|
|
*
|
|
* NB: This function is not truly constant-time in JavaScript due to the nature of the language and
|
|
* runtime, but it is designed to minimize timing differences based on the content of the arrays.
|
|
*
|
|
* @param a The first Uint8Array to compare.
|
|
* @param b The second Uint8Array to compare.
|
|
* @returns True if the arrays are equal, false otherwise.
|
|
*/
|
|
export const constantTimeEqualBytes = (a: Uint8Array, b: Uint8Array): boolean => {
|
|
if (a.byteLength !== b.byteLength) return false;
|
|
let result = 0;
|
|
for (let i = 0; i < a.byteLength; i++) {
|
|
result |= a[i] ^ b[i];
|
|
}
|
|
return result === 0;
|
|
}
|
|
|
|
const hasHexMethods = (() =>
|
|
// @ts-ignore testing for builtins
|
|
'toHex' in Uint8Array.from([]) && typeof Uint8Array.from([])['toHex'] === 'function' &&
|
|
'fromHex' in Uint8Array && typeof Uint8Array.fromHex === 'function')();
|
|
|
|
// Array where index 0xf0 (240) is mapped to string 'f0'
|
|
const hexes = Array.from({ length: 256 }, (_, i) =>
|
|
i.toString(16).padStart(2, '0')
|
|
);
|
|
|
|
export type HexEncoding = 'hex';
|
|
export type Base64Encoding = 'base64';
|
|
export type Base64RawEncoding = 'base64raw';
|
|
export type Base64URLEncoding = 'base64url';
|
|
export type Base64RawURLEncoding = 'base64urlraw';
|
|
export type Encoding = HexEncoding | Base64Encoding | Base64RawEncoding | Base64URLEncoding | Base64RawURLEncoding;
|
|
|
|
/**
|
|
* Converts a Uint8Array of bytes to a hexadecimal string. If the environment supports built-in
|
|
* hex methods on Uint8Array, those will be used for better performance. Otherwise, a manual
|
|
* conversion is performed.
|
|
*
|
|
* If a length is provided, the function will assert that the input bytes have the expected
|
|
* length (in bytes). The length parameter is optional and can be used to enforce that the
|
|
* input bytes match an expected size for encoding/decoding operations.
|
|
*
|
|
* @param bytes The Uint8Array of bytes to convert to a hexadecimal string.
|
|
* @param length Optional expected length of the input bytes (in bytes).
|
|
* @returns The hexadecimal string representation of the input bytes.
|
|
*/
|
|
export const bytesToHex = (bytes: Uint8Array, length?: number): string => {
|
|
assertBytes(bytes, (typeof length !== 'undefined') ? length * 2 : undefined, "bytes");
|
|
if (hasHexMethods) {
|
|
// @ts-ignore using built-in hex methods if available
|
|
return bytes.toHex();
|
|
}
|
|
let hex = '';
|
|
for (const byte of bytes) {
|
|
hex += hexes[byte];
|
|
}
|
|
return hex;
|
|
};
|
|
|
|
/**
|
|
* Converts a hexadecimal string to a Uint8Array of bytes. If the environment supports built-in
|
|
* hex methods on Uint8Array, those will be used for better performance. Otherwise, a manual
|
|
* conversion is performed.
|
|
*
|
|
* @param hex The hexadecimal string to convert to a Uint8Array of bytes.
|
|
* @returns The Uint8Array of bytes represented by the hexadecimal string.
|
|
*/
|
|
export const hexToBytes = (hex: string): Uint8Array => {
|
|
if (typeof hex !== 'string' || hex.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(hex)) {
|
|
throw new TypeError(`Expected hex string of even length, got ${hex}`);
|
|
}
|
|
if (hasHexMethods) {
|
|
// @ts-ignore using built-in hex methods if available
|
|
return Uint8Array.fromHex(hex);
|
|
}
|
|
|
|
const bytes = new Uint8Array(hex.length / 2);
|
|
for (let i = 0; i < hex.length; i += 2) {
|
|
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
/**
|
|
* Converts a Uint8Array of bytes to a base64 string using the specified encoding variant. The function
|
|
* asserts that the input is a valid Uint8Array and optionally checks for an expected length (in bytes).
|
|
*
|
|
* The `encoding` parameter specifies the base64 variant to use:
|
|
* - 'base64': Standard base64 encoding with padding.
|
|
* - 'base64raw': Standard base64 encoding without padding.
|
|
* - 'base64url': URL-safe base64 encoding with padding (replaces '+' with '-' and '/' with '_').
|
|
* - 'base64urlraw': URL-safe base64 encoding without padding.
|
|
*
|
|
* @param bytes The Uint8Array of bytes to convert to a base64 string.
|
|
* @param encoding The base64 encoding variant to use.
|
|
* @returns The base64 string representation of the input bytes.
|
|
*/
|
|
export const bytesToBase64 = (bytes: Uint8Array, encoding: Base64Encoding | Base64RawEncoding | Base64URLEncoding | Base64RawURLEncoding): string => {
|
|
assertBytes(bytes, (typeof length !== 'undefined') ? length * 2 : undefined, "bytes");
|
|
const binary = String.fromCharCode(...bytes);
|
|
switch (encoding) {
|
|
case 'base64':
|
|
return btoa(binary);
|
|
case 'base64raw':
|
|
return btoa(binary).replace(/=+$/, '');
|
|
case 'base64url':
|
|
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_');
|
|
case 'base64urlraw':
|
|
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
default:
|
|
throw new TypeError(`Unsupported encoding: ${encoding}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Converts a base64 string to a Uint8Array of bytes. The function accepts both standard and URL-safe
|
|
* base64 variants, and normalizes the input by replacing URL-safe characters and adding padding if necessary.
|
|
*
|
|
* The function asserts that the input is a valid base64 string after normalization. It then decodes the
|
|
* base64 string to binary and converts it to a Uint8Array of bytes.
|
|
*
|
|
* @param b64 The base64 string to convert to a Uint8Array of bytes.
|
|
* @returns The Uint8Array of bytes represented by the base64 string.
|
|
*/
|
|
export const base64ToBytes = (b64: string): Uint8Array => {
|
|
if (typeof b64 !== "string") {
|
|
throw new TypeError(`Expected base64 string, got ${b64}`);
|
|
}
|
|
// Accept URL-safe base64 by replacing '-' with '+' and '_' with '/'
|
|
let normalized = b64.replace(/-/g, "+").replace(/_/g, "/");
|
|
// Pad with '=' to make length a multiple of 4
|
|
if (normalized.length % 4 !== 0) {
|
|
normalized += "=".repeat(4 - (normalized.length % 4));
|
|
}
|
|
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(normalized)) {
|
|
throw new TypeError(`Expected base64 string, got ${b64}`);
|
|
}
|
|
const binary = atob(normalized);
|
|
const bytes = new Uint8Array(binary.length);
|
|
for (let i = 0; i < binary.length; i++) {
|
|
bytes[i] = binary.charCodeAt(i);
|
|
}
|
|
return bytes;
|
|
};
|
|
|
|
/**
|
|
* Decodes a string encoded in the specified encoding variant to a Uint8Array of bytes. The function
|
|
* supports the following encoding variants:
|
|
* - 'hex': Hexadecimal encoding (e.g., "deadbeef").
|
|
* - 'base64': Standard base64 encoding with padding.
|
|
* - 'base64raw': Standard base64 encoding without padding.
|
|
* - 'base64url': URL-safe base64 encoding with padding (replaces '+' with '-' and '/' with '_').
|
|
* - 'base64urlraw': URL-safe base64 encoding without padding.
|
|
*
|
|
* The function asserts that the input string is valid for the specified encoding and then decodes
|
|
* it to a Uint8Array of bytes. If the encoding is not supported, a TypeError is thrown.
|
|
*
|
|
* @param encoded The encoded string to decode.
|
|
* @param encoding The encoding variant of the input string.
|
|
* @returns The Uint8Array of bytes represented by the encoded string.
|
|
*/
|
|
export const decodeBytes = (encoded: string, encoding: Encoding): Uint8Array => {
|
|
switch (encoding) {
|
|
case 'hex':
|
|
return hexToBytes(encoded);
|
|
case 'base64':
|
|
case 'base64raw':
|
|
case 'base64url':
|
|
case 'base64urlraw':
|
|
return base64ToBytes(encoded);
|
|
default:
|
|
throw new TypeError(`Unsupported encoding: ${encoding}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Encodes a Uint8Array of bytes to a string using the specified encoding variant. The function supports
|
|
* the following encoding variants:
|
|
* - 'hex': Hexadecimal encoding (e.g., "deadbeef").
|
|
* - 'base64': Standard base64 encoding with padding.
|
|
* - 'base64raw': Standard base64 encoding without padding.
|
|
* - 'base64url': URL-safe base64 encoding with padding (replaces '+' with '-' and '/' with '_').
|
|
* - 'base64urlraw': URL-safe base64 encoding without padding.
|
|
*
|
|
* The function asserts that the input is a valid Uint8Array of bytes and then encodes it to a string
|
|
* using the specified encoding variant. If the encoding is not supported, a TypeError is thrown.
|
|
*
|
|
* @param bytes The Uint8Array of bytes to encode.
|
|
* @param encoding The encoding variant to use.
|
|
* @returns The encoded string.
|
|
*/
|
|
export const encodeBytes = (bytes: Uint8Array, encoding: Encoding): string => {
|
|
switch (encoding) {
|
|
case 'hex':
|
|
return bytesToHex(bytes);
|
|
case 'base64':
|
|
case 'base64raw':
|
|
case 'base64url':
|
|
case 'base64urlraw':
|
|
return bytesToBase64(bytes, encoding);
|
|
default:
|
|
throw new TypeError(`Unsupported encoding: ${encoding}`);
|
|
}
|
|
}
|