Added docstrings and functions for testing Uint8Array equality

This commit is contained in:
2026-03-12 18:15:56 +01:00
parent 01978cb867
commit a83c4ca4c0
2 changed files with 196 additions and 0 deletions

View File

@@ -785,6 +785,7 @@ export {
I32,
F32,
F64,
type TypedArray,
type Encoding,
type HexEncoding,
type Base64Encoding,

View File

@@ -1,49 +1,121 @@
// 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;
@@ -54,6 +126,48 @@ export const assertBytes = (bytes: Uint8Array, length?: number, name: string = "
}
}
/**
* 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' &&
@@ -71,8 +185,25 @@ 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];
@@ -80,6 +211,14 @@ export const bytesToHex = (bytes: Uint8Array, length?: number): string => {
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}`);
@@ -96,6 +235,20 @@ export const hexToBytes = (hex: string): Uint8Array => {
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);
@@ -113,6 +266,16 @@ export const bytesToBase64 = (bytes: Uint8Array, encoding: Base64Encoding | Base
}
}
/**
* 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}`);
@@ -134,6 +297,22 @@ export const base64ToBytes = (b64: string): Uint8Array => {
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':
@@ -148,6 +327,22 @@ export const decodeBytes = (encoded: string, encoding: Encoding): Uint8Array =>
}
}
/**
* 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':