12 KiB
Spec: @suaveplan/bytes
Capability
Low-level byte I/O for the Suaveplan monorepo. Provides lossless conversion between Uint8Array and the three ubiquitous text representations (hex, base64, UTF-8), ergonomic Uint8Array utilities (concat, slice, equality, format auto-detection, unified conversion façade), and UUID string ↔ 16-byte binary round-trip functions.
Requirements
Requirement: Hex encoding round-trips and rejects malformed input
The package SHALL export bytesToHex(bytes: Uint8Array): string producing lowercase hex (empty array → ""), and hexToBytes(hex: string): Uint8Array accepting case-insensitive hex. hexToBytes MUST throw BytesError with code BYTES.ENCODE.INVALID_HEX on odd-length input or non-hex characters. Round-tripping arbitrary Uint8Array through bytesToHex → hexToBytes MUST be lossless.
Scenario: Round-trip preserves arbitrary bytes
- WHEN any
Uint8Arrayis passed throughbytesToHexthenhexToBytes - THEN the resulting bytes equal the input byte-for-byte
Scenario: Empty input maps to empty output
- WHEN
bytesToHex(new Uint8Array(0))is called - THEN the result is
""
Scenario: Malformed hex is rejected with a structured error
- WHEN
hexToBytesis called with odd-length input or non-hex characters - THEN a
BytesErrorwith codeBYTES.ENCODE.INVALID_HEXis thrown
Verified by: src/encoding/encoding.test.ts::"round-trips arbitrary bytes" AND src/encoding/encoding.test.ts::"bytesToHex produces lowercase hex" AND src/encoding/encoding.test.ts::"bytesToHex handles empty array" AND src/encoding/encoding.test.ts::"hexToBytes handles empty string" AND src/encoding/encoding.test.ts::"hexToBytes rejects odd-length string" AND src/encoding/encoding.test.ts::"hexToBytes rejects non-hex chars" AND src/encoding/encoding.test.ts::"hexToBytes accepts uppercase hex letters" AND src/encoding/encoding.test.ts::"hexToBytes rejects uppercase letters that are not hex" AND src/encoding/encoding.test.ts::"hex round-trip for arbitrary bytes"
Requirement: Base64 encoding round-trips and rejects malformed input
The package SHALL export bytesToBase64(bytes: Uint8Array): string (standard base64 with = padding) and base64ToBytes(base64: string): Uint8Array. base64ToBytes MUST throw BytesError with code BYTES.ENCODE.INVALID_BASE64 on syntactically invalid or malformed input. Round-tripping arbitrary Uint8Array is lossless.
Scenario: Round-trip preserves arbitrary bytes
- WHEN any
Uint8Arrayis passed throughbytesToBase64thenbase64ToBytes - THEN the resulting bytes equal the input
Scenario: Malformed base64 is rejected
- WHEN
base64ToBytesis called with non-base64 characters or bad padding - THEN a
BytesErrorwith codeBYTES.ENCODE.INVALID_BASE64is thrown
Verified by: src/encoding/encoding.test.ts::"round-trips arbitrary bytes" AND src/encoding/encoding.test.ts::"handles empty array" AND src/encoding/encoding.test.ts::"rejects invalid base64" AND src/encoding/encoding.test.ts::"rejects syntactically valid but malformed base64 (bad padding)" AND src/encoding/encoding.test.ts::"base64 round-trip for arbitrary bytes"
Requirement: UTF-8 encoding round-trips arbitrary strings
The package SHALL export bytesToUtf8(bytes: Uint8Array): string backed by TextDecoder("utf-8") and utf8ToBytes(str: string): Uint8Array backed by TextEncoder. Round-tripping multibyte and ASCII strings is lossless.
Scenario: ASCII round-trip
- WHEN any ASCII string is passed through
utf8ToBytesthenbytesToUtf8 - THEN the result equals the input
Scenario: Multibyte round-trip
- WHEN strings containing non-ASCII characters are round-tripped
- THEN code points are preserved
Verified by: src/encoding/encoding.test.ts::"round-trips ASCII" AND src/encoding/encoding.test.ts::"round-trips multibyte characters" AND src/encoding/encoding.test.ts::"handles empty string" AND src/encoding/encoding.test.ts::"utf8 round-trip for arbitrary strings"
Requirement: Uint8Array utility surface
The package SHALL export concatUint8Arrays(...arrays: Uint8Array[]): Uint8Array, sliceUint8Array(arr, start, end?): Uint8Array, and uint8ArraysEqual(a, b): boolean (NOT constant-time — documented). concatUint8Arrays MUST be associative under concatenation and tolerate zero arguments and empty arrays.
Scenario: Concatenation joins arrays end-to-end
- WHEN
concatUint8Arrays(a, b, c)is called - THEN the result has
a.length + b.length + c.lengthbytes in order
Scenario: Concatenation handles edge cases
- WHEN called with no arguments, a single array, or arrays of length zero
- THEN the result is empty (or the lone non-empty array) without throwing
Scenario: Slicing returns a copy of the requested range
- WHEN
sliceUint8Array(arr, start, end?)is called - THEN the result contains bytes
[start, end)(or[start, arr.length)ifendis omitted)
Scenario: Equality compares length and bytes
- WHEN
uint8ArraysEqual(a, b)is called - THEN the result is
trueiff lengths match AND every byte at every index is equal
Verified by: src/uint8/uint8.test.ts::"concatenates multiple arrays" AND src/uint8/uint8.test.ts::"handles empty arrays" AND src/uint8/uint8.test.ts::"handles single array" AND src/uint8/uint8.test.ts::"handles no arguments" AND src/uint8/uint8.test.ts::"concatUint8Arrays associativity" AND src/uint8/uint8.test.ts::"slices correctly" AND src/uint8/uint8.test.ts::"slices from start only" AND src/uint8/uint8.test.ts::"returns empty for zero-length slice" AND src/uint8/uint8.test.ts::"returns true for equal arrays" AND src/uint8/uint8.test.ts::"returns false for different arrays" AND src/uint8/uint8.test.ts::"returns false for different lengths" AND src/uint8/uint8.test.ts::"returns true for two empty arrays"
Requirement: Format detection and validation
The package SHALL export detectInputFormat(input: BytesInput): InputFormat returning "uint8array" for Uint8Array, "hex" for even-length all-hex strings, and "utf8" otherwise; inputToUint8Array(input, format?) coercing input into bytes (auto-detecting when format is omitted); and validateInputTypeFormat(input, format) which MUST throw BytesError with code BYTES.UINT8.FORMAT_MISMATCH when the runtime type does not match the declared format.
Scenario: Heuristic detects the three input formats
- WHEN
detectInputFormatis called with aUint8Array, an even-length hex string, or any other string - THEN the result is
"uint8array","hex", or"utf8"respectively
Scenario: Coercion respects explicit format and falls back to auto-detection
- WHEN
inputToUint8Array(value, format?)is called - THEN the value is decoded using
format(or the detected format) into aUint8Array
Scenario: Validation throws on type/format mismatch
- WHEN
validateInputTypeFormatis called with a value whose runtime type does not match the declared format (e.g. string with"uint8array"format) - THEN a
BytesErrorwith codeBYTES.UINT8.FORMAT_MISMATCHis thrown
Verified by: src/uint8/uint8.test.ts::"detects Uint8Array" AND src/uint8/uint8.test.ts::"detects hex string" AND src/uint8/uint8.test.ts::"detects utf8 for general string" AND src/uint8/uint8.test.ts::"detects utf8 for non-hex even string" AND src/uint8/uint8.test.ts::"returns Uint8Array as-is" AND src/uint8/uint8.test.ts::"converts hex string to Uint8Array" AND src/uint8/uint8.test.ts::"converts utf8 string to Uint8Array" AND src/uint8/uint8.test.ts::"converts base64 string to Uint8Array" AND src/uint8/uint8.test.ts::"auto-detects format when not provided" AND src/uint8/uint8.test.ts::"throws when string given with uint8array format" AND src/uint8/uint8.test.ts::"validates Uint8Array with uint8array format" AND src/uint8/uint8.test.ts::"validates string with hex format" AND src/uint8/uint8.test.ts::"throws when Uint8Array given but string format requested" AND src/uint8/uint8.test.ts::"throws when string given but uint8array format requested"
Requirement: Unified conversion façade with OTel span
The package SHALL export formatOutput(bytes, format) returning a value in the requested representation (uint8array / hex / base64 / utf8) and convertDataFormat(input, outputFormat, inputFormat?) which MUST emit an OpenTelemetry span around the conversion. Auto-detection of inputFormat MUST kick in when omitted.
Scenario: Output formatting covers every supported format
- WHEN
formatOutput(bytes, format)is called with each of"uint8array","hex","base64","utf8" - THEN the result is the corresponding representation of
bytes
Scenario: Format conversion bridges any input to any output
- WHEN
convertDataFormatis called with mismatched input/output formats (e.g. hex → utf8, utf8 → hex, Uint8Array → hex, hex → base64) - THEN the conversion proceeds and the output matches the expected encoding
Scenario: Auto-detection fills in missing input format
- WHEN
convertDataFormat(value, outputFormat)is called withoutinputFormat - THEN the input format is detected via
detectInputFormatbefore conversion
Verified by: src/uint8/uint8.test.ts::"returns Uint8Array for uint8array format" AND src/uint8/uint8.test.ts::"returns hex string for hex format" AND src/uint8/uint8.test.ts::"returns base64 string for base64 format" AND src/uint8/uint8.test.ts::"returns utf8 string for utf8 format" AND src/uint8/uint8.test.ts::"converts hex to utf8" AND src/uint8/uint8.test.ts::"converts utf8 to hex" AND src/uint8/uint8.test.ts::"converts Uint8Array to hex" AND src/uint8/uint8.test.ts::"converts hex to base64" AND src/uint8/uint8.test.ts::"converts Uint8Array to uint8array (identity)" AND src/uint8/uint8.test.ts::"auto-detects input format for hex strings"
Requirement: UUID round-trip and validation
The package SHALL export generateUuid(): string (UUIDv4 via crypto.randomUUID()), uuidToUint8Array(uuid: string): Uint8Array returning 16 bytes, and uint8ArrayToUuid(bytes: Uint8Array): string returning canonical lowercase. Invalid UUIDs MUST throw BytesError with code BYTES.UUID.INVALID_FORMAT; non-16-byte arrays MUST throw BYTES.UUID.INVALID_LENGTH. Round-trip is lossless including all-zero and all-f corner cases.
Scenario: Generated UUIDs are valid and unique
- WHEN
generateUuid()is called multiple times - THEN every result matches the UUIDv4 lowercase canonical shape AND no two results are equal
Scenario: Round-trip preserves the canonical form
- WHEN a UUID string is passed through
uuidToUint8Arraythenuint8ArrayToUuid - THEN the output equals the (lowercased) input — including the all-zero and all-
fcorner cases
Scenario: Invalid UUID strings are rejected
- WHEN
uuidToUint8Arrayis called with a malformed string - THEN a
BytesErrorwith codeBYTES.UUID.INVALID_FORMATis thrown
Scenario: Wrong-length byte arrays are rejected
- WHEN
uint8ArrayToUuidis called with an array whose length is not 16 (including empty) - THEN a
BytesErrorwith codeBYTES.UUID.INVALID_LENGTHis thrown
Verified by: src/uuid/uuid.test.ts::"round-trips a known UUID" AND src/uuid/uuid.test.ts::"uuidToUint8Array produces 16 bytes" AND src/uuid/uuid.test.ts::"throws on invalid UUID" AND src/uuid/uuid.test.ts::"throws on UUID with wrong structure" AND src/uuid/uuid.test.ts::"uint8ArrayToUuid throws on non-16-byte array" AND src/uuid/uuid.test.ts::"uint8ArrayToUuid throws on empty array" AND src/uuid/uuid.test.ts::"round-trips all-zeros UUID" AND src/uuid/uuid.test.ts::"round-trips all-fs UUID" AND src/uuid/uuid.test.ts::"generates a valid UUID v4 string" AND src/uuid/uuid.test.ts::"generates unique values" AND src/uuid/uuid.test.ts::"output is lowercase" AND src/uuid/uuid.test.ts::"generated UUID can round-trip through uint8Array"
Error codes
BYTES.ENCODE.INVALID_HEX, BYTES.ENCODE.INVALID_BASE64, BYTES.ENCODE.INVALID_UTF8 (reserved), BYTES.UINT8.INVALID_INPUT, BYTES.UINT8.FORMAT_MISMATCH, BYTES.UUID.INVALID_LENGTH, BYTES.UUID.INVALID_FORMAT
Version
Published: 0.1.0 (2026-04-20). Spec heading conformance: bytes-spec-conformance (2026-04-26).