# flare.qpack.codec

> A module of flare_mlake's API.

QPACK encoder + decoder (RFC 9204) -- static-only.

QPACK is the HPACK-shaped header compression layer that HTTP/3
uses on top of QUIC. The full RFC 9204 codec ships a dynamic
table that streams insertions on a dedicated unidirectional
QUIC stream (the "encoder stream"), with field sections
referencing entries in the dynamic table by index. The dynamic
table introduces head-of-line blocking risk on the QUIC layer
(field sections may need to wait for inserts they reference)
and a non-trivial encoder-stream protocol on top of the simple
field-section codec.

This module is the **static-only** subset:

* Field section prefix: required-insert-count and base both
  fixed at 0 on encode; decoder rejects field sections whose
  required-insert-count is not 0 (no dynamic-table support yet).
* Field lines: indexed-static, literal-with-static-name, and
  literal-with-literal-name. The dynamic-reference variants
  (``1Tppppppp`` with T=0, plus ``001Nhppp`` post-base) are
  rejected on decode.
* String literals: optional Huffman per RFC 7541 Appendix B
  (shared with HPACK; the encoder picks the shorter form).

The static-only subset is enough for an HTTP/3 server that
talks to a real client: the most common request and response
shapes (``:method GET``, ``:status 200``, common content types)
all hit the static table directly, and literal field lines cover
everything else without head-of-line risk.

Public surface:

* :class:`QpackHeader` -- ``(name, value)`` pair, re-exported
  from ``flare.http2.hpack`` for ergonomic interop.
* :func:`encode_field_section` -- emit the field section prefix
  plus all field lines into a fresh byte list.
* :func:`decode_field_section` -- parse a field section into a
  list of :class:`QpackHeader`. Rejects dynamic-table references.
* :data:`QPACK_STATIC_TABLE_SIZE` -- the 99 of the static table.

Sans-I/O contract: zero I/O imports; registered in
``tests/tools/check_sans_io.sh``.

References:
- RFC 9204 "QPACK: Field Compression for HTTP/3".
- RFC 9114 §4.2 "HTTP Fields" (lowercase mandate).
- RFC 7541 Appendix B (Huffman -- shared with HPACK).

## Functions

### `encode_field_section`

```mojo
def encode_field_section(headers: List[HpackHeader], mut out: List[UInt8])
```

Append the QPACK-encoded field section bytes (RFC 9204 §4.5) to ``out``.

The caller owns the buffer and may pass the same
``List[UInt8]`` through repeated encode calls, clearing
between calls so the underlying allocation amortises across
the stream. The encoder appends only; it never reads from or
truncates the existing contents.

The static-only codec emits:

* Field section prefix: required_insert_count=0,
  sign=0 + delta_base=0 (two single-byte 0x00s).
* Per header, the lowest-cost wire shape:
  - full match in the static table -> indexed field line
    (``11pppppp`` for indices < 63, with continuation bytes
    for higher indices; the static-table flag T is always 1).
  - name-only match -> literal-with-name-reference
    (``0101pppp`` with T=1 for static, then string literal).
  - no match -> literal-with-literal-name
    (``0010nnnn`` with H+name length, then name, then value).

Header names must be lowercase per RFC 9114 §4.2; the
encoder emits whatever case the caller provides and trusts
the call site. The decoder mirrors this -- it surfaces the
bytes verbatim.

### `encode_field_line`

```mojo
def encode_field_line(name: String, value: String, mut out: List[UInt8])
```

Append one static-only field line (RFC 9204 §4.5) for ``name: value`` to ``out``: indexed when the pair is in the static table, a name reference when only the name is, otherwise a literal name. Follows the two-byte prefix :func:`encode_field_section` writes; exposed so an encoder can stream fields without building a header list first.

### `decode_field_section`

```mojo
def decode_field_section(buf: Span[UInt8], max_field_section_size: UInt64 = UInt64(65536)) -> List[HpackHeader]
```

Parse a QPACK field section.

Static-only: rejects field sections whose
required-insert-count is not 0 (the encoder never emits one
above 0 either; a peer that sends a non-zero count is
using the dynamic table we don't support yet, and the right
behaviour is to surface a connection error rather than mis-
interpret the payload).

Returns the parsed headers in wire order. Field-line type
dispatch:

* ``1Txxxxxx`` -- indexed; rejects T=0 (dynamic; not supported).
* ``01NTxxxx`` -- literal with name reference; rejects T=0.
* ``001Nhxxx`` -- literal with literal name.
* ``0001xxxx`` -- indexed post-base (dynamic only) -> reject.
* ``0000Nhxx`` -- literal with name reference post-base
  (dynamic only) -> reject.

The decoded size (``name + value + 32`` per line) is capped at
``max_field_section_size``; the section is rejected as soon as a line
would pass it.

## Aliases

### `QPACK_DEFAULT_MAX_FIELD_SECTION_SIZE`

```mojo
comptime QPACK_DEFAULT_MAX_FIELD_SECTION_SIZE = UInt64(65536)
```

Default cap on a *decoded* field section: the sum of ``name + value + 32`` over its field lines, the size RFC 9114 section 4.2.2 gives SETTINGS_MAX_FIELD_SECTION_SIZE. Matches the 64 KiB flare advertises.

### `QPACK_FIELD_SECTION_TOO_LARGE`

```mojo
comptime QPACK_FIELD_SECTION_TOO_LARGE = "qpack: field section above limit"
```

Error text raised when a decoded section passes the cap. Callers match on ``field section above limit``.
