Added utils package for parsing helpers

This commit is contained in:
2026-03-12 18:00:52 +01:00
parent c33f6f781c
commit 3de4ec28b4
5 changed files with 417 additions and 0 deletions

163
src/utils.ts Normal file
View File

@@ -0,0 +1,163 @@
export type TypedArray = Int8Array | Uint8ClampedArray | Uint8Array | Uint16Array | Int16Array | Uint32Array | Int32Array;
export const U8 = (arr: ArrayBuffer | TypedArray): Uint8Array => {
if (arr instanceof ArrayBuffer) return new Uint8Array(arr);
return new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength);
}
export const I8 = (arr: ArrayBuffer | TypedArray): Int8Array => {
if (arr instanceof ArrayBuffer) return new Int8Array(arr);
return new Int8Array(arr.buffer, arr.byteOffset, arr.byteLength);
}
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));
}
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));
}
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));
}
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));
}
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));
}
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));
}
export const isBytes = (bytes: unknown): boolean => {
return bytes instanceof Uint8Array || (ArrayBuffer.isView(bytes) && bytes.constructor.name === "Uint8Array");
}
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}`);
}
}
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;
export const bytesToHex = (bytes: Uint8Array, length?: number): string => {
assertBytes(bytes, (typeof length !== 'undefined') ? length * 2 : undefined, "bytes");
let hex = '';
for (const byte of bytes) {
hex += hexes[byte];
}
return hex;
};
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;
}
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}`);
}
}
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;
};
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}`);
}
}
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}`);
}
}