flare_mlake · API · qpack

flare.qpack.dynamic

QPACK dynamic table + encoder/decoder stream instructions (RFC 9204).

The static-only codec in :mod:flare.qpack.codec covers field sections that reference the RFC 9204 Appendix A static table or carry literals. This module adds the dynamic table: the per-connection insert log a peer streams over the unidirectional QPACK encoder stream (type 0x02), the decoder stream (type 0x03) acknowledgements that flow back, and the field-section wire shapes that reference dynamic entries by relative / post-base index.

Sans-I/O: pure state + pure byte-in/byte-out functions. The h3 layer owns the QUIC streams and feeds the instruction bytes here; this module never touches a socket.

Pieces:

  • :struct:QpackDynamicTable -- the insert-ordered entry log with capacity-bounded eviction and absolute / relative index resolution (RFC 9204 section 3.2).
  • Encoder-stream instruction codec (section 4.3): Set Dynamic Table Capacity, Insert With Name Reference (static or dynamic), Insert With Literal Name, Duplicate. :func:apply_encoder_instructions replays a byte stream into a table.
  • Decoder-stream instruction codec (section 4.4): Section Acknowledgment, Stream Cancellation, Insert Count Increment.
  • Field-section dynamic codec (section 4.5): :func:encode_field_section_dynamic / :func:decode_field_section_dynamic, including the Required Insert Count encoding of section 4.5.1.1.
  • :struct:QpackEncoder / :struct:QpackDecoder -- thin owners that pair a table with the stream bookkeeping (Known Received Count, the Insert Count Increment to emit).

Eviction follows the encoder (evict the oldest entry to make room) and does not track per-entry reference counts -- a decoder mirror trusts the peer encoder not to evict an entry a not-yet-acknowledged field section still references (RFC 9204 section 2.2 makes that the encoder's responsibility). Blocked-stream handling (a field section referencing inserts that have not arrived) is surfaced as a decode error; the HTTP/3 server advertises 0 blocked streams, so it treats that error (like every undecodable field section) as a connection error of type QPACK_DECOMPRESSION_FAILED (RFC 9204 section 2.1.2). Stream-parking for a non-zero budget is the upgrade path.

References: RFC 9204 sections 2-4; RFC 7541 Appendix B (Huffman).

Structs

QpackDynamicTable

struct QpackDynamicTable

Insert-ordered, capacity-bounded dynamic table (RFC 9204 3.2).

entries[0] is the oldest live entry. dropped counts entries evicted over the table's lifetime, so the absolute index of entries[0] is dropped and the next insert lands at absolute index insert_count() (== dropped + len(entries)).

Implements Copyable, Movable

Fields
entries: List[HpackHeader]
capacity: UInt64
max_capacity: UInt64: The ceiling :meth:set_capacity may raise capacity to: the SETTINGS_QPACK_MAX_TABLE_CAPACITY this side advertised (RFC 9204 sec 3.2.3).
size: UInt64
dropped: UInt64

__init__

def __init__(out self, capacity: UInt64 = UInt64(0))

insert_count

def insert_count(self) -> UInt64

Total inserts ever (the absolute index of the next insert).

max_entries

def max_entries(self) -> UInt64

RFC 9204 section 3.2.2 MaxEntries = floor(capacity / 32).

set_capacity

def set_capacity(mut self, cap: UInt64)

Set the table capacity, evicting the oldest entries until the size fits (RFC 9204 section 4.3.1).

Raises QPACK_ENCODER_STREAM_ERROR above :attr:max_capacity (sec 4.3.1). Any value was accepted, so a peer could grow our dynamic table past the limit we advertised, which is 0 by default, and fill it with inserts of any size.

insert

def insert(mut self, var h: HpackHeader) -> Bool

Insert h after evicting to make room. Returns False if the entry cannot fit even in an empty table (RFC 9204 section 3.2.2 -- such an insert is a no-op the caller treats as an error).

get_abs

def get_abs(self, abs_index: UInt64) -> HpackHeader

Resolve an absolute index to its entry.

find

def find(self, h: HpackHeader) -> Int

Absolute index of a full (name, value) match, or -1.

find_name

def find_name(self, name: String) -> Int

Absolute index of a name match, or -1.

DecoderInstruction

struct DecoderInstruction

A parsed decoder-stream instruction: kind is one of the DEC_INSTR_* constants, value is the stream id or increment, offset is the cursor past the instruction.

Implements Copyable, Movable

Fields
kind: Int
value: UInt64
offset: Int

QpackDecoder

struct QpackDecoder

Owns the inbound dynamic table + the Insert Count Increment owed back to the peer on the decoder stream.

Implements Copyable, Movable

Fields
table: QpackDynamicTable
pending_increment: Int

__init__

def __init__(out self, capacity: UInt64 = UInt64(0))

feed_encoder_stream

def feed_encoder_stream(mut self, buf: Span[UInt8]) -> Int

Apply encoder-stream bytes; accumulate the Insert Count Increment to send. Returns the inserts applied.

take_increment

def take_increment(mut self, mut out: List[UInt8]) -> Bool

Emit a pending Insert Count Increment onto the decoder stream (if any). Returns whether anything was written.

decode

def decode(self, buf: Span[UInt8], max_field_section_size: UInt64 = UInt64(65536)) -> List[HpackHeader]

Decode a field section against the dynamic table, capped at max_field_section_size decoded bytes.

QpackEncoder

struct QpackEncoder

Owns the outbound dynamic table; insert appends an encoder-stream instruction and updates the local table mirror.

The encoder does not track acknowledgments yet: it has no Known Received Count, never sees Section Acknowledgment or Insert Count Increment instructions, and is not told the peer's SETTINGS_QPACK_BLOCKED_STREAMS. Until it does, every entry is unacknowledged, so (RFC 9204 sections 2.1.1 and 2.1.2):

  • :meth:encode references no dynamic entry (Required Insert Count 0), so no stream can block a decoder that allows none; and
  • :meth:insert never evicts: an entry that does not fit in the free space is refused, so no entry a decoder may still need disappears.

Implements Copyable, Movable

Fields
table: QpackDynamicTable

__init__

def __init__(out self, capacity: UInt64 = UInt64(0))

set_capacity

def set_capacity(mut self, cap: UInt64, mut enc_stream: List[UInt8])

insert

def insert(mut self, name: String, value: String, mut enc_stream: List[UInt8]) -> Bool

Insert (name, value), emitting an Insert With Literal Name instruction. Returns False, emitting nothing, if the entry does not fit in the table's free space: inserting it would evict an entry that is not known to be unreferenced (QPACK-06).

encode

def encode(self, headers: List[HpackHeader], mut out: List[UInt8])

Encode a field section against no dynamic entries (Required Insert Count 0): static references and literals only, until acknowledgments are tracked (QPACK-06).

Functions

entry_size

def entry_size(h: HpackHeader) -> UInt64

RFC 9204 section 3.2.1 entry size in bytes.

encode_required_insert_count

def encode_required_insert_count(ric: UInt64, max_entries: UInt64) -> UInt64

Encode the Required Insert Count for the field-section prefix.

decode_required_insert_count

def decode_required_insert_count(enc: UInt64, total_inserts: UInt64, max_entries: UInt64) -> UInt64

Decode the Required Insert Count given the wire value, the decoder's current insert count, and MaxEntries (RFC 9204 4.5.1.1).

encode_set_capacity

def encode_set_capacity(mut out: List[UInt8], cap: UInt64)

Set Dynamic Table Capacity: 001xxxxx (5-bit prefix).

encode_insert_with_name_ref

def encode_insert_with_name_ref(mut out: List[UInt8], is_static: Bool, name_index: Int, value: String)

Insert With Name Reference: 1Txxxxxx (T static flag, 6-bit name index) followed by the value string literal.

encode_insert_with_literal_name

def encode_insert_with_literal_name(mut out: List[UInt8], name: String, value: String)

Insert With Literal Name: 01Hxxxxx (5-bit name length) then the name + value string literals.

encode_duplicate

def encode_duplicate(mut out: List[UInt8], rel_index: Int)

Duplicate: 000xxxxx (5-bit relative index).

apply_encoder_instructions

def apply_encoder_instructions(mut table: QpackDynamicTable, buf: Span[UInt8]) -> Int

Replay a complete encoder-stream byte buffer into table. Returns the number of inserts applied (for the Insert Count Increment the decoder owes back). Raises on a malformed / truncated instruction; use :func:apply_encoder_instructions_partial when the buffer may end mid-instruction at a chunk boundary.

apply_encoder_instructions_partial

def apply_encoder_instructions_partial(mut table: QpackDynamicTable, buf: Span[UInt8]) -> Tuple[Int, Int]

Like :func:apply_encoder_instructions but tolerant of a chunk boundary mid-instruction: applies every whole instruction and returns (inserts_applied, bytes_consumed) so the caller can keep the unconsumed tail and retry once more bytes arrive.

An instruction cut short by the chunk boundary (an untagged parse error) stops consumption and is retried. A real encoder-stream error (a reference to a missing or evicted entry, a capacity above the advertised limit, a literal that is not UTF-8, an insert that does not fit) is tagged QPACK_ENCODER_STREAM_ERROR and is re-raised, so the connection fails instead of stalling.

encode_section_ack

def encode_section_ack(mut out: List[UInt8], stream_id: Int)

Section Acknowledgment: 1xxxxxxx (7-bit stream id).

encode_stream_cancel

def encode_stream_cancel(mut out: List[UInt8], stream_id: Int)

Stream Cancellation: 01xxxxxx (6-bit stream id).

encode_insert_count_increment

def encode_insert_count_increment(mut out: List[UInt8], increment: Int)

Insert Count Increment: 00xxxxxx (6-bit increment).

parse_decoder_instruction

def parse_decoder_instruction(buf: Span[UInt8], offset: Int) -> DecoderInstruction

Parse one decoder-stream instruction at offset.

encode_field_section_dynamic

def encode_field_section_dynamic(headers: List[HpackHeader], table: QpackDynamicTable, mut out: List[UInt8])

Encode a field section that may reference table entries.

For each header: a full dynamic match becomes an indexed (relative) line; a dynamic name match becomes a literal-with-name-reference; otherwise it falls back to the static-table path / literals. The Required Insert Count is the largest referenced absolute index + 1 (0 when no dynamic entry is referenced), and Base is set equal to it (delta_base 0), so every reference is pre-base.

decode_field_section_dynamic

def decode_field_section_dynamic(buf: Span[UInt8], table: QpackDynamicTable, max_field_section_size: UInt64 = UInt64(65536)) -> List[HpackHeader]

Decode a field section against table, resolving dynamic indexed / name-reference / post-base lines (RFC 9204 section 4.5). Static-only inputs (Required Insert Count 0) decode without touching the table: every dynamic reference must resolve to an absolute index below the Required Insert Count (and a Sign-1 Delta Base below it), otherwise the section is a QPACK_DECOMPRESSION_FAILED error.

The decoded size (name + value + 32 per line, RFC 9114 section 4.2.2) is capped at max_field_section_size and checked as each line is decoded, so a one-byte reference to a large dynamic-table entry repeated thousands of times is refused after the first few instead of being expanded in full. The caller's limit on the compressed section does not do this.

Aliases

comptime DEC_INSTR_SECTION_ACK = Int(0)
comptime DEC_INSTR_STREAM_CANCEL = Int(1)
comptime DEC_INSTR_INSERT_COUNT_INCREMENT = Int(2)