Files
suaveplan/openspec/archive/2026-05-02-bytes-spec-conformance/specs/bytes/spec.md
2026-05-27 23:29:53 +01:00

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 Uint8Array is passed through bytesToHex then hexToBytes
  • 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 hexToBytes is called with odd-length input or non-hex characters
  • THEN a BytesError with code BYTES.ENCODE.INVALID_HEX is 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 Uint8Array is passed through bytesToBase64 then base64ToBytes
  • THEN the resulting bytes equal the input

Scenario: Malformed base64 is rejected

  • WHEN base64ToBytes is called with non-base64 characters or bad padding
  • THEN a BytesError with code BYTES.ENCODE.INVALID_BASE64 is 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 utf8ToBytes then bytesToUtf8
  • 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.length bytes 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) if end is omitted)

Scenario: Equality compares length and bytes

  • WHEN uint8ArraysEqual(a, b) is called
  • THEN the result is true iff 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 detectInputFormat is called with a Uint8Array, 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 a Uint8Array

Scenario: Validation throws on type/format mismatch

  • WHEN validateInputTypeFormat is called with a value whose runtime type does not match the declared format (e.g. string with "uint8array" format)
  • THEN a BytesError with code BYTES.UINT8.FORMAT_MISMATCH is 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 convertDataFormat is 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 without inputFormat
  • THEN the input format is detected via detectInputFormat before 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 uuidToUint8Array then uint8ArrayToUuid
  • THEN the output equals the (lowercased) input — including the all-zero and all-f corner cases

Scenario: Invalid UUID strings are rejected

  • WHEN uuidToUint8Array is called with a malformed string
  • THEN a BytesError with code BYTES.UUID.INVALID_FORMAT is thrown

Scenario: Wrong-length byte arrays are rejected

  • WHEN uint8ArrayToUuid is called with an array whose length is not 16 (including empty)
  • THEN a BytesError with code BYTES.UUID.INVALID_LENGTH is 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).