# utils/decode-string _(no submodules)_ ## Purpose `decodeString` reads a length-prefixed UTF-8 string from a `Uint8Array` at a given offset. The BTF wire format encodes strings as `[uint32 length LE][utf8 bytes]`, and the unpacker calls this helper to read the three header strings (`familyName`, `cssFontWeight`, `cssFontStyle`) before walking into the dictionary section. Centralising the helper here keeps the unpacker focused on the structural layout and lets the boundary-validation logic (offset bounds, length-overrun detection) live in one auditable place. The function is the inverse of `encodeString` from `@suaveplan/font-compress`. Together they form the wire-format agreement for length-prefixed strings used by the BTF header. ## Features - Reads `[uint32 LE length][utf8 bytes]` and returns the decoded string + next offset - Rejects negative offsets via `FontDecompressError` with `FONT_DECOMPRESS.INVALID_DATA` - Rejects offsets where fewer than 4 bytes remain via the same code - Rejects declared lengths that would read past the buffer end via the same code - Handles empty strings (`length === 0`) correctly - Handles multi-byte UTF-8 (BMP and surrogate-pair) correctly via `TextDecoder` ## Usage: Basic ```ts import { decodeString } from "@suaveplan/font-decompress/utils/decode-string"; const bytes = new Uint8Array([5, 0, 0, 0, 104, 101, 108, 108, 111]); // length 5 + "hello" const { value, offset } = decodeString(bytes, 0); console.log(value); // "hello" console.log(offset); // 9 ``` ## Usage: Advanced ```ts import { decodeString } from "@suaveplan/font-decompress/utils/decode-string"; import { FONT_DECOMPRESS_CODES, FontDecompressError } from "@suaveplan/font-decompress"; let cursor = 0; try { const familyName = decodeString(buffer, cursor); cursor = familyName.offset; const cssFontWeight = decodeString(buffer, cursor); cursor = cssFontWeight.offset; const cssFontStyle = decodeString(buffer, cursor); cursor = cssFontStyle.offset; } catch (err) { if (err instanceof FontDecompressError && err.code === FONT_DECOMPRESS_CODES.INVALID_DATA) { // Truncated BTF buffer — log and surface to caller } throw err; } ``` ## API Reference ### `decodeString(data: Uint8Array, offset: number): { value: string; offset: number }` | Parameter | Type | Meaning | | --------- | ------------ | ---------------------------------------- | | `data` | `Uint8Array` | The buffer containing the encoded string | | `offset` | `number` | Byte offset to start reading from | **Returns** `{ value: string; offset: number }` — the decoded string plus the next byte position. The new offset equals `offset + 4 + utf8ByteLength`. **Throws** `FontDecompressError` with `code === "FONT_DECOMPRESS.INVALID_DATA"` when: - `offset < 0` - `offset + 4 > data.length` (not enough bytes for the length prefix) - `offset + 4 + length > data.length` (declared length reads past buffer end) ## Implementation Notes The helper uses a module-level `TextDecoder` rather than constructing one per call. `TextDecoder` is sturdy under reuse and the BTF format never nests its strings, so a single shared decoder is safe and faster. Length is read as little-endian `uint32` via `DataView.getUint32(..., true)`, matching the encoder side. The DataView is constructed with the `byteOffset` of the input array so callers can pass subarrays without extra slicing. Boundary validation happens up-front: the function checks the offset and length BEFORE reading any bytes via `TextDecoder`. This avoids any ambiguity about whether a partial read might emit a surrogate-half or similar misuse — the throw is synchronous and the wire format is either valid or rejected.