Initial Commit
This commit is contained in:
195
packages/foundation/data/bytes/src/uint8/uint8.ts
Normal file
195
packages/foundation/data/bytes/src/uint8/uint8.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user