import { trace } from "@opentelemetry/api"; import { base64ToBytes, bytesToBase64, bytesToHex, bytesToUtf8, hexToBytes, utf8ToBytes, } from "../encoding/encoding.js"; import { BYTES_CODES, BytesError } from "../errors/bytes-error.js"; import type { BytesInput, InputFormat, OutputFormat } from "../types/types.js"; const tracer = trace.getTracer("@suaveplan/bytes"); /** Regex for even-length all-hex strings. */ const HEX_PATTERN = /^[0-9a-fA-F]+$/; /** * Concatenate any number of `Uint8Array` instances into a new `Uint8Array`. * * Returns an empty `Uint8Array` when called with no arguments. */ export function concatUint8Arrays(...arrays: Uint8Array[]): Uint8Array { const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0); const result = new Uint8Array(totalLength); let offset = 0; for (const arr of arrays) { result.set(arr, offset); offset += arr.length; } return result; } /** * Return a slice of `arr` from `start` (inclusive) to `end` (exclusive). * * Semantics mirror `Array.prototype.slice` — negative indices are not * supported; pass only non-negative integers. */ export function sliceUint8Array( arr: Uint8Array, start: number, end?: number ): Uint8Array { return arr.slice(start, end); } /** * Return `true` when `a` and `b` have the same length and identical byte * values at every position. * * This comparison is **not** constant-time — do not use it for * cryptographic MAC verification. Use `@suaveplan/crypto-utils` for * constant-time equality. */ export function uint8ArraysEqual(a: Uint8Array, b: Uint8Array): boolean { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) return false; } return true; } /** * Detect the most likely `InputFormat` of `input`. * * - `Uint8Array` → `"uint8array"` * - Even-length string composed entirely of hex characters → `"hex"` * - Everything else → `"utf8"` * * This heuristic is intentionally conservative: a string that looks like hex * (even length, all hex chars) is classified as hex. If the caller knows the * format, pass it explicitly to `inputToUint8Array`. */ export function detectInputFormat(input: BytesInput): InputFormat { if (input instanceof Uint8Array) { return "uint8array"; } if (input.length > 0 && input.length % 2 === 0 && HEX_PATTERN.test(input)) { return "hex"; } return "utf8"; } /** * Assert that `input`'s runtime type is compatible with `format`. * * - `"uint8array"` requires `input` to be a `Uint8Array`. * - `"hex"`, `"base64"`, `"utf8"` require `input` to be a `string`. * * Throws `BytesError(BYTES.UINT8.FORMAT_MISMATCH)` on mismatch. */ export function validateInputTypeFormat( input: BytesInput, format: InputFormat ): void { const isUint8 = input instanceof Uint8Array; if (format === "uint8array" && !isUint8) { throw new BytesError( BYTES_CODES["UINT8.FORMAT_MISMATCH"], `Format "uint8array" requires a Uint8Array input, but received a string.` ); } if (format !== "uint8array" && isUint8) { throw new BytesError( BYTES_CODES["UINT8.FORMAT_MISMATCH"], `Format "${format}" requires a string input, but received a Uint8Array.` ); } } /** * Convert `input` to a `Uint8Array`. * * When `format` is omitted and `input` is a string, `detectInputFormat` is * used to infer the encoding. * * Throws `BytesError(BYTES.UINT8.INVALID_INPUT)` when `input` is neither a * `string` nor a `Uint8Array`. */ export function inputToUint8Array( input: BytesInput, format?: InputFormat ): Uint8Array { if (input instanceof Uint8Array) { return input; } const resolvedFormat: InputFormat = format ?? detectInputFormat(input); switch (resolvedFormat) { case "hex": return hexToBytes(input); case "base64": return base64ToBytes(input); case "utf8": return utf8ToBytes(input); case "uint8array": throw new BytesError( BYTES_CODES["UINT8.FORMAT_MISMATCH"], `Format "uint8array" requires a Uint8Array, but received a string.` ); } } /** * Serialise `bytes` to the requested `OutputFormat`. * * - `"uint8array"` returns the same reference unchanged. * - `"hex"` returns a lowercase hex string. * - `"base64"` returns a standard base64 string. * - `"utf8"` returns a UTF-8 decoded string. */ export function formatOutput( bytes: Uint8Array, format: OutputFormat ): string | Uint8Array { switch (format) { case "uint8array": return bytes; case "hex": return bytesToHex(bytes); case "base64": return bytesToBase64(bytes); case "utf8": return bytesToUtf8(bytes); } } /** * Convert `input` from `inputFormat` (or auto-detected) to `outputFormat`. * * This is the primary façade for cross-format conversion: * * ```ts * convertDataFormat("48656c6c6f", "utf8", "hex") // → "Hello" * convertDataFormat("Hello", "hex", "utf8") // → "48656c6c6f" * convertDataFormat(bytes, "base64") // → "SGVsbG8=" * ``` * * When `inputFormat` is omitted and `input` is a string, `detectInputFormat` * is called automatically. */ export function convertDataFormat( input: BytesInput, outputFormat: OutputFormat, inputFormat?: InputFormat ): string | Uint8Array { return tracer.startActiveSpan("bytes.uint8.convertDataFormat", (span) => { try { const bytes = inputToUint8Array(input, inputFormat); return formatOutput(bytes, outputFormat); } finally { span.end(); } }); }