Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Native decompressor

The decompression feature enables the public decompressor module, which owns configuration, reusable decoder ownership, operation sessions, typed errors, and synchronous adapters. Its private core owns wire parsing and regeneration. Production decoding uses safe Rust and alloc, without a C or third-party decoder dependency. no_std disables I/O adapters and profiling. Serialized/custom dictionaries require experimental; RAW prefixes, built-in words/transforms, large windows, and ordinary context maps need only decompression. Shared window, backend and retention types remain available without the encoder. Prepared dictionary views additionally require compression; see codec features.

graph TD
    API[decompressor: config, decoder, session, error] --> Core[private decompressor::core]
    IO[decompressor::io: reader / writer] --> API
    Facade[mbrotli::io] --> IO
    Facade --> Finish[private finish_error: shared FinishError]
    Facade -->|compression enabled| EncoderIO[compressor::io]
    Core --> Stored[stored: borrow a validated complete raw member]
    Stored --> Bits[bits / header]
    Core --> Bits
    Core --> Entropy[huffman / block / context_map]
    Core --> Regen[stream / distance / dictionary]
    Core --> Memory[memory: fallible live workspace accounting]
    Regen --> Dispatch[validated backend: command loop or ring-copy stage]
    Dispatch --> Copies[fearless_simd: 16 / 32-byte history copies]
    Regen --> Shared[private crate::shared: wire tables, dictionary bytes, transforms]
    Dict[dictionary: DecodeDictionary / PreparedDictionary] --> View[DictionaryRef borrowed view]
    View --> API
    Dict --> Store[shared::decode_dictionary / experimental decode_serialized]

Public ownership and APIs

Decompressor::new and builder create an allocation-free workspace. A validated Backend is selected once at construction. The scalar state machine dispatches the command loop and bulk ring-copy stages through that validated token. The command loop passes its concrete fearless_simd::Simd token to 16- and 32-byte history-copy helpers; there are no feature checks or dynamic calls per command or vector. Headers, raw blocks, and dictionary resolution outside the command loop remain shared scalar code. fork_empty copies policy/backend into an independent empty workspace. config, retention, retained_bytes, trim, recover, and reconfigure manage reuse without exposing core storage.

DecoderConfig combines WindowLimit, MemberMode, and DecodeLimits. Default is Single, extended windows up to 62 bits, and no numeric budgets; DecoderConfig::new() is the same value as a const fn, and Default delegates to it. DecodeStreamConfig optionally specifies OutputSize::Exact(u64), which is a validation contract, never an allocation hint. Limits count accepted compressed bytes, regenerated output, and live decoder-owned requested heap bytes. Input and output totals span all concatenated members. Caller destinations, immutable borrowed dictionaries, and fixed adapter buffers are excluded from workspace.

A bounded policy example:

use mbrotli::{DecodeLimits, DecoderConfig, Decompressor, WindowLimit};
let config = DecoderConfig::default()
    .with_window_limit(WindowLimit::standard(24)?)
    .with_limits(DecodeLimits::default()
        .with_max_input_bytes(Some(1 << 20))
        .with_max_output_bytes(Some(8 << 20))
        .with_max_workspace_bytes(Some(32 << 20)));
let mut decoder = Decompressor::new(config)?;
Ok::<(), Box<dyn std::error::Error>>(())

decompress returns a Vec, decompress_into appends and returns its range, and decompress_to_slice fills the caller’s initialized slice, and decompress_to_uninit an uninitialized one, zeroed one meta-block (at most 32 KiB) ahead of the decoder so it can serve as linear history too (see uninitialized output); sessions have process_uninit, which delivers through the ring. Eligible complete stored members use the borrowed-payload shortcut described below, in decompress, decompress_into and, when the payload fits, in decompress_to_uninit; stored_one_shot holds the eligibility check they share. Other Vec shapes first run the session with no destination at all, which parses up to the first byte of output. A fresh decoder writing a fresh vector in single-member mode collects into its window, which the vector takes (see owned decoder output); a member longer than the window continues through the ring. Every other shape (a reused decoder, a vector with contents or capacity, a dictionary, concatenated members) decodes linearly into the vector: it reserves what the meta-block declares (declared_remaining, at least a small geometric step, at most 16 MiB), so a stream usually gets one reservation rather than a chain of copies, but zero-fills only ZERO_AHEAD_STEP (32 KiB) of it ahead of the decoder at a time, and passes dst[start..] with Delivery::Linear, so the bytes already decoded are the first member’s history; later members are delivered from the ring at their offset. The reservation grows only when decoding reaches its end, not per zeroing step: growing it per step left a 32 MiB vector for a 481 KB output (appending_reserves_in_proportion_to_the_output guards this). The unused tail is trimmed. Criterion decompress/reused (one decoder and vector, q0-q11, default release profile) took 0.61 times the time of the earlier ring-and-copy path in geometric mean: 1 MiB text 0.69-0.82, data compressed beyond 100:1 0.42-0.65, stored members, now copied straight from the input, 0.08-0.35, literary text and binary 0.93-1.02, and mapsdatazrh 1.00-1.03, where the extra decoding call per 32 KiB step outweighs the saved copy. A member that outgrew the collect path stays on the ring: switching it to linear delivery measured 1.8 times slower. Any Vec append is rolled back on failure; a slice preserves the written prefix and untouched suffix. Single one-shot calls reject tails. Dictionary variants accept impl Into<DictionaryRef<'dict>> before the compressed source. Sessions borrow the decoder and dictionary independently, without copying/rebuilding indexes.

Session lifecycle and exact progress

stateDiagram-v2
    Idle --> Active: start / start_with_dictionary
    Active --> Active: Process / NeedsInput / NeedsOutput
    Active --> Finishing: first Finish latches total_in + input.len
    Finishing --> Finishing: Finish with exact unconsumed suffix
    Active --> Complete: validated Single member
    Finishing --> Complete: validated member and final input boundary
    Active --> Failed: format / resource error
    Finishing --> Failed: error / truncation / changed final-input contract
    Active --> Abandoned: mem&#58;&#58;forget
    Finishing --> Abandoned: mem&#58;&#58;forget
    Complete --> Abandoned: mem&#58;&#58;forget
    Failed --> Abandoned: mem&#58;&#58;forget
    Active --> Idle: Drop
    Finishing --> Idle: Drop
    Complete --> Idle: Drop
    Failed --> Idle: Drop
    Abandoned --> Idle: recover / successful reconfigure

DecodeProgress and DecodeFailure both report exact per-call consumed/produced prefix lengths. Counters include failing-call progress. A failed session remains terminal; a completed session returns zero progress and Finished again. Single sessions stop exactly at the member boundary. Concatenated sessions reset window, history, distances, contexts and entropy state between members, but keep aggregate counters and policy; a boundary is NeedsInput until final EOF confirms success. At least one member is required.

Per-operation fields live in the private core::session::OperationState, which holds neither the decoder nor the dictionary. DecoderSession wraps it over &mut Decompressor and runs OperationState::release on drop. DecoderSessionOwned wraps the same state over an owned decoder and dictionary; see owned sessions.

A session stores the dictionary borrow itself. The reusable workspace never stores that reference, so forgetting a session cannot leave a dereferenceable stale pointer. The owner active flag survives trim; only explicit recovery or successful reconfiguration clears abandonment. Drop clears operation state and applies the configured retention policy without I/O. Aggressive retains capacity; CurrentConfig additionally releases it on configuration changes; Bounded releases all workspace above its threshold; ReleaseAll releases on each operation drop.

Parsing and regeneration

flowchart TD
    Window[Demand-read window header] --> Header[Transactional meta-block header]
    Header -->|metadata| Skip[Consume aligned bytes without allocation/output]
    Header -->|uncompressed| Raw[Regenerate bytes into caller output and history]
    Header -->|compressed| Types[Three independent block streams]
    Types --> Maps[Context modes and RLE / inverse-MTF maps]
    Maps --> Trees[Resumable simple / complex Huffman descriptions]
    Trees --> Command[Insert/copy command and extra bits]
    Command --> Literals[Context-dependent literal symbols]
    Literals --> Distance[Short cache / direct / extra-bit distance]
    Distance --> History[Overlapping history copy]
    Distance --> Prefix[Borrowed ordered RAW segments]
    Distance --> Static[Resolve static word then apply transform with fixed-size copies]
    Prefix -->|crossing reference| History
    History --> Command
    Static --> Command
    Raw --> End[Validate block end and padding]
    Skip --> End
    Command -->|remaining = 0| End
    End -->|more blocks| Header
    End -->|last| Member[Complete member]

Bits uses a demand-driven 64-bit reservoir. The slow path accepts one byte at a time and never accepts a byte beyond the field that needs it, so limits and per-call progress stay byte-exact; it reads fields up to 56 bits and splits a 62-bit distance extra field across two reads. Small headers are parsed transactionally; partial peeks retain bits, and a validated field commits by dropping exactly its width. Padding must be zero.

The hot path uses a whole-word refill, which loads a full little-endian word while at least eight acceptable bytes remain before the input limit and accepts the whole bytes that keep at least 56 bits buffered ((!count >> 3) & 7 bytes, masked so the slice advance needs no check beyond the eight-byte one, after which the count is count | 56), and a matching unread, which returns every whole speculatively buffered byte. The word is ORed in unmasked: bits above the buffered count then hold the input bytes that follow, which a later byte load ORs in again unchanged, so no mask sits on the decoding chain. Stream::run clears those bits (settle) before every return that keeps the reservoir, and unread clears them at output pauses and member ends, so between calls the reservoir holds only accepted bits. The Input computes its acceptable prefix (fast_end) once at construction. The command loop holds the unread part of that prefix as a shrinking slice and refills through refill_rest, so each refill tests one length (at least eight bytes left) instead of an index and a length; the loop writes the cursor back from the slice’s length when it leaves. The byte-exact stages refill through refill_from with an index cursor. Input pauses retain the reservoir for incomplete fields. Output pauses and member boundaries return whole buffered bytes accepted during that call. Older bytes belonging to a partial field remain buffered. Returning read-ahead at output pauses prevents a long pending copy from stranding the next member in a previous call when it later completes without consuming more input. Every description reader (Builder, ContextMap, Block) decodes symbols through decode_refilling, which takes a whole word when the input allows and otherwise falls back to byte-exact loading.

Huffman codes are two-level lookup tables. A 256-entry root is indexed by the next eight stream bits; a code longer than eight bits selects a second-level table through a root entry that carries the combined width and the absolute offset of that table in the group’s storage. An entry is one padding-free u32 (width in the low byte, symbol or offset above), so storage zero-fills as a plain memory set. Complete codes fill every entry, so decode_fast needs no per-symbol validity check, and a single-symbol code fills the root with zero-width entries and consumes no bits. A Group keeps every code of one kind for a meta-block in one vector: the roots first, one per code, then the second-level tables appended as codes are built, so only tables that exist are written. The vector’s length is a high-water mark that only grows (zeroed only when it grows) and used is the logical end, so a warm meta-block writes no fill at all; prepare restarts the logical end without touching storage. Tables borrows the whole group once so a loop that selects a code per symbol pays one bounds check on the root. A single-slot group is an owned Huffman. The command loop holds codes as Roots, a single pointer to a 256-entry root, and passes each decode its group’s shared second-level storage, so the loop keeps that storage once per group rather than in every table it holds. The resumable Builder reads a simple or complex description into an intrusive linked list per code length, which is already canonical order, so building a table walks only the symbols with a code rather than the alphabet: fill counts canonical codes upward, turns each into its table key with a byte bit-reversal table, and replicates it across the root or the current second-level table, having grown the vector once to the exact number of second-level entries the code appends. second_size counts those entries by replaying the canonical walk over the length counts alone, so a cold build zero-fills only slots the fill overwrites rather than a loose worst-case bound. build or build_slot fills an owned table or a group slot; reset abandons a partial description. The code-length code of a complex description (18 symbols of at most five bits) is a 32-entry table held inline in the builder and rebuilt per description by a counting sort of its 18 lengths, so reading a description allocates nothing and never fills a 256-entry root for it. A description’s start only zeroes the 18 code lengths when it is complex, since those are the only lengths it may leave unset; both shapes clear the per-length symbol lists themselves before filling them.

classDiagram
    class Code {
        u32: width low byte, symbol or offset above
        bits() u32
        value() usize
    }
    class Group {
        codes: Vec~Code~ roots then second-level tables
        count: usize
        used: usize logical end
        prepare(count, alphabet, memory)
        table(tree) Table
        tables() Tables
    }
    class Tables {
        roots: &[[Code; 256]]
        second: &[Code] whole storage
        table(tree) Table
        root(tree) Root
        second() &[Code]
    }
    class Root {
        &[Code; 256]
        decode_fast(second, bits) usize
    }
    class Table {
        root: &[Code; 256]
        second: &[Code]
        decode_fast(bits) usize
        decode(bits, input) Option~usize~
        decode_refilling(bits, input) Option~usize~
    }
    class Builder {
        next: [u16; 1128]
        head, tail, counts: [u16; 16]
        code: LengthCode inline 32 entries
        read(alphabet, bits, input)
        build_slot(alphabet, group, tree, memory)
        fill(codes, start, used, memory)
    }
    Group *-- Code
    Group --> Tables : borrows
    Tables --> Table : selects
    Tables --> Root : selects
    Table --> Root : decodes through
    Builder --> Group : fills a slot

The block-switch state, context maps, prefix-code groups, distance layout and dictionary-transform scratch a compressed meta-block needs live in a separate workspace the stream creates on its first compressed meta-block and keeps with the rest. A decoder that only ever sees stored members, metadata or an empty stream never allocates or initializes it, so Decompressor::new and every such decode stay allocation-free and copy a small state. The workspace is created by copying the constant Tables::EMPTY (built from the EMPTY constants of its builder, blocks, context maps, groups and distance layout) straight into its allocation, rather than constructing it on the stack and moving it. The builder’s per-symbol links are part of that constant but never read before a symbol is pushed.

The stream keeps a power-of-two history ring. run first attempts a fast path that decodes whole commands while a whole-word refill and output room allow: it reads the command, literal run and distance from the reservoir and writes literals and copies straight into the ring. Stage::Literals re-enters the same loop mid-command whenever a whole-word refill is possible, so a run that paused on output resumes in bulk and only its byte-exact remainder decodes one symbol at a time. Stage::Command enters the loop only while a whole word of input remains, since within the last word it could only pause again. Every hot quantity lives in a local for the duration of the loop and is written back only when the loop leaves: the bit reservoir, the input cursor, position and the four-slot recent-distance cache. The meta-block’s end and the end of the call’s output space are held as positions (meta_end, space_end and their minimum limit_end), so the remaining length and free space are differences from position rather than counters decremented per write.

Every pause and failure leaves through one labeled break to a single write-back after the loop, so the loop has one exit edge. The command fields the resumable stages read (pending literals, copy length, implicit flag, distance and distance code) are not carried between iterations: each iteration decodes them into locals, and only a pause that leaves them pending writes the ones its stage reads (save!). A pause before the next command’s fields, or a failure, which poisons the session, writes none. Rare branches (block switches, ring growth, references beyond history, wrapping copies, and every pause and failure) call an empty #[cold] function, which gives the register allocator the block frequencies it needs to keep the per-command quantities in registers and to place spills on the rare paths. A block switch itself runs out of line in switch_block, which takes and returns the reservoir by value so it never becomes addressable memory. Ring bytes are delivered to the caller when a write reaches the ring end, when the loop pauses and when an error returns, and the output space still free is tracked as a running count that includes pending ring bytes, so no per-command delivery is needed. A linear member makes no per-write ring-end test at all: its history is the caller’s slice, delivered once when the loop leaves. State that depends only on a block type (the literal context lookup, the root of each of the block’s 64 literal contexts, whether those contexts share one tree and that tree’s root, the command root, and the root of each of the four distance contexts) is set when the loop is entered and refreshed only by the block switch that changes it, so neither a command nor a literal compares block types and a contextual literal or a distance symbol pays no bounds check on its code.

Each category’s Block counts down the elements left in its current block. A category with two or more types reads a new type and count when the count is zero and another element is needed. A category with one type starts at 16777216 (RFC 7932 section 10) and has no type code to switch with, so needing an element after that count runs out is a meta-block error. This matches C Brotli’s BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH, the check behind CVE-2026-111497 (fixed in Brotli 1.2.1). Literals cannot exceed the count, because a meta-block holds at most 2^24 bytes. Commands and explicit distances can, through transformed dictionary words that decode to nothing. The fast switch runs only when switch_ready reports a complete header and more than one type. Otherwise the loop pauses and prepare takes the byte-exact switch or returns the error, so the hot loop pays no extra test.

stateDiagram-v2
    [*] --> Counting: header, count = BLEN or 16777216 for one type
    Counting --> Counting: advance (count - 1)
    Counting --> Exhausted: count = 0
    Exhausted --> Counting: element needed, types >= 2 (switch_fast or prepare reads type and BLEN)
    Exhausted --> Error: element needed, one type (prepare reports meta-block error)
    Exhausted --> [*]: meta-block ends

For a trivial literal context map, the loop decodes three symbols per reservoir check when at least 45 bits are available. Each symbol consumes at most 15 bits. The scalar loop handles short input and the final one or two symbols; batching does not cross the existing block, output-space or ring boundary.

flowchart TD
    Resume[Stage::Literals: saved insert, copy, implicit] --> Lit
    Start[refill; command block switch?] --> Cmd[command symbol -> COMMANDS entry]
    Cmd --> Extra[insert extra, copy extra]
    Extra -->|insert > 0| Lit[literal run bounded by literals, block, space, ring end]
    Lit -->|trivial block| One[one table per symbol]
    Lit -->|context block| Ctx[context from p1, p2 -> per-context root]
    One --> More{run done?}
    Ctx --> More
    More -->|reservoir short| PauseL[pause: Stage::Literals]
    More -->|done| Dist
    Extra -->|insert = 0| Dist{implicit?}
    Dist -->|yes| Pop[cache.pop]
    Dist -->|no| Sym[distance block switch?; distance symbol]
    Sym -->|code 0| Pop
    Sym -->|codes 1..15| Short[cache offset]
    Sym -->|others| Tab[layout table entry: base + extra << postfix]
    Pop --> Ref
    Short --> Ref
    Tab --> Ref{distance > position or max backward?}
    Ref -->|yes| Dict[prefix: pause Resolve / static word, 16/32-byte chunk when it fits]
    Ref -->|no| Limit{length > min remaining, space?}
    Limit -->|past remaining| Corrupt[error: meta-block]
    Limit -->|past space| PauseC[cache.push; pause: Stage::Copy]
    Limit -->|fits| Push[cache.push; grow ring once per call]
    Push --> Snap{length <= 32, no overlap, snapshot windows fit?}
    Snap -->|yes| Word[16/32-byte array snapshot]
    Snap -->|no| Copy{no wrap?}
    Copy -->|yes, distance >= length| Exact[copy_within]
    Copy -->|yes, overlapping| Overlap[fill, byte loop, or doubling]
    Copy -->|wraps| Ring[copy_ring pieces]
    Word --> Start
    Exact --> Start
    Overlap --> Start
    Ring --> Start
    Dict --> Start

The copy length is tested once against the smaller of the meta-block’s remaining length and the free output space; only a failing length is then told apart as corrupt input or as a full output, which pushes the distance and pauses at Stage::Copy. A non-overlapping copy of at most 32 bytes goes straight to a fixed-width helper: both of its complete windows fitting implies that neither side of the copy wraps, so the loop tests the copy’s exact extent only when the helper declines. The fixed-width copy helpers (copy_chunk behind copy16 and copy32) move a fixed-size array: they test the later of the two windows against the last start that fits, load the entire source before storing it, and compile to vector loads and stores under the dispatched backend’s target features without a library call. They accept unaligned, initialized ring slices; if either window is short they leave the ring unchanged and the caller uses copy_within for the exact length. A static dictionary word of at most 32 bytes is written from the transform scratch the same way (put_word), with the same fifteen-byte bound. A word that a transform reduces to nothing writes nothing: the loop moves on to the next command before put_word. Growth for a write happens only when the write extends past the ring, so an empty word grows nothing. When position equals the length of a ring still smaller than the window, position & mask is 0, and a chunk store there would overwrite the oldest live history instead of unwritten space. A 16-byte copy serves lengths 1–16; a 32-byte copy serves lengths 17–32, so at most fifteen bytes ahead of actual output are overwritten. The ring’s sixteen unreachable history slots make those extra writes unobservable. The snapshot is also correct when ring slots physically overlap: all actual source bytes precede any stores. Logical LZ77 expansion still uses fill, sequential bytes, or prefix doubling; it cannot use a snapshot of bytes not yet generated. A unit-distance run that must grow the ring grows it with the repeated byte in one pass through repeat_grow, shared with the byte-exact Stage::Copy, rather than zeroing the new region and then overwriting it. On AVX2 a 32-byte move uses one vector; SSE2/SSE4.2 split it into two 128-bit moves. Fallback remains available for internal differential tests. Neither copy helper allocates or uses unsafe.

flowchart TD
    Stage[Scalar run_stages] --> Kind{stage}
    Kind -->|Command / resumable Literals| Dispatch[dispatch stored Backend]
    Dispatch --> Fast[Specialized command loop]
    Fast --> Copy[copy16 / copy32 inside the backend's feature context]
    Fast --> Word{static word length}
    Word -->|0| Next[next command, nothing written]
    Word -->|1-32 and the chunk fits| Put[put_word: 16- or 32-byte store]
    Word -->|otherwise| Ring[write_ring: exact bytes]
    Kind -->|Copy with limited input or output| Bulk[dispatch once for copy_ring]
    Bulk --> Copy
    Kind -->|headers / raw / other stages| Scalar[Shared scalar implementation]
    Copy --> Bounds{both full vector windows fit?}
    Bounds -->|yes| Snapshot[load entire vector, then store]
    Bounds -->|no| Exact[caller copies exact byte count]

The command symbol indexes a 1024-slot COMMANDS table (the 704-symbol alphabet padded to a power of two, so a mask replaces a bounds check) that carries both length bases and extra-bit widths, the implicit-distance flag, the copy code for resumption and the distance context. Distances above the short codes resolve through a per-symbol table that yields the extra width and the base distance in one lookup. The common standard-window layout without postfix or direct codes uses a shared constant table, so a fresh decoder neither fills nor allocates it; other layouts fill a per-meta-block table, reused when the layout repeats. The recent-distance cache is a ring with a push index, like the reference’s dist_rb: an implicit distance or explicit code 0 pops the most recent slot and the copy pushes it back, so every command ends with one slot write and no branch; a command that pauses before its copy, or a prefix or static-dictionary reference, pushes the popped value back first. The fast path returns to the byte-exact stages at every point where input or output can run short, and those stages produce identical results and errors for any chunking. Raw blocks, copies and prefix runs regenerate in bulk ring pieces rather than one byte at a time; metadata is skipped in bulk. A copy near the end of a small, highly expanding input still bulk-copies because Stage::Copy copies a whole run.

Regenerated bytes update caller output and the ring together; the two most recent output bytes for literal context are read back from the ring. Raw blocks update the same ring as compressed blocks. The ring grows on actual output: the fast path grows it once per call to cover what the call can still produce (bounded by the meta-block’s remaining length and the caller’s output room, never by the end of a literal insert longer than that room, so a workspace limit fails only for history the call writes), otherwise it doubles up to the window size, so declaring a large window does not allocate it eagerly, and a full ring wraps rather than growing. Distances and cumulative positions use u64; a meta-block header validates position + MLEN once so the hot path adds without checks, while the byte-exact stages use checked operations and u128 for large-window wire distances. The theoretical large alphabet is 16 + NDIRECT + (124 << NPOSTFIX) even for small large-window headers. Every reachable distance symbol must have a maximum represented distance no larger than (1 << 63) - 4, which also keeps every table entry a decoded symbol can select within u64.

Prefix addresses traverse attachments in order, including empty slots. A copy crossing the prefix end continues through the history present at command start. If its distance exceeds retained window length, reusable prefix_history scratch preserves original history before emitted prefix bytes overwrite it; overlapping copies repeat the original virtual prefix/history sequence. This follows RFC 9841 §3.2; C 1.2.0 rejects references crossing the prefix end, so this path has independent fixture and unit-test evidence. Static addresses are resolved before emitting, and transformed byte count reduces the meta-block’s remaining output. A built-in word is transformed by apply_builtin, which copies prefix, word and suffix as fixed 8- and 32-byte moves into the scratch (shared-primitives.md). Zero-output references with distance <= 120 are invalid, avoiding zero-bit command loops. Above that distance a transform that consumes its whole word — OmitLast4 over a four-byte word, for instance — is legal and emits nothing, so the ring write tolerates an empty write: as a member’s first command it arrives with the ring still unallocated, and the ring mask is only derived once there is something to store. Dictionary references do not enter the history distance cache; normal/prefix references follow short-code cache update rules.

Owned one-shot output

core::stored recognizes a complete empty member or one raw meta-block followed by an empty final block, using the same window/meta-block parsers and padding validation as streaming. decompress borrows that payload and copies it into one fallibly reserved Vec when resource limits are unset, member mode is single, and the decoder is not abandoned. Other shapes use the regular session, which also preserves configured resource-error ordering.

A fresh workspace decoding into a zero-capacity Vec without a dictionary, in single-member mode, can collect output in history. After the ordinary empty-output probe accepts the window, a private session call uses the window size as output capacity. The same state machine emits every byte into the ring and counts progress without copying it to a second slice. On successful completion and exact input consumption, ownership of the ring moves to the result, its length is truncated to actual output, and its capacity leaves workspace accounting. If the window fills before completion, copy that prefix into the caller Vec and resume ordinary delivery while retaining history. Appends, preallocated output, retained workspaces, dictionaries and concatenation use ordinary delivery.

flowchart TD
    Owned[decompress] --> Stored{complete stored member and eligible policy?}
    Stored -->|yes| Borrow[borrow payload; fallible reserve and one copy]
    Stored -->|no| Probe[session with empty output: accept headers]
    Probe --> Fresh{fresh workspace and zero-capacity Vec; single; no dictionary?}
    Fresh -->|yes| Collect[same state machine; ring is destination]
    Collect --> End{member completed before wrap?}
    End -->|yes| Transfer[validate tail; transfer ring allocation to result]
    End -->|no| Prefix[copy prefix; retain ring history]
    Prefix --> Stream[ordinary session output]
    Fresh -->|no| Stream

Before a ring wraps, raw growth reserves the new power-of-two allocation, writes raw bytes into existing initialized slots and appends the remainder, then initializes only padding. A unit-distance copy in the resumable copy stage can fill new ring storage directly with its final byte; the existing unwritten suffix gets the same byte. Both check the workspace reservation before changing history or position. Other copies and wrapped history keep the established SIMD/baseline kernels. No uninitialized slices or new unsafe code are used. See owned decoder output for evidence and limitations.

Allocation and dictionary boundaries

Memory::resize checks arithmetic and workspace policy before try_reserve_exact, growing a non-empty buffer to at least twice its capacity so a workspace that fills in steps reallocates a logarithmic number of times; Memory::reserve is the capacity-only half. Growth conservatively includes both old and replacement buffers, since an allocator may implement realloc as allocate/copy/free. Reported retained bytes count current owned capacities. The history ring, context maps, Huffman groups, the distance symbol table and rare prefix-crossing scratch reuse capacity across operations; Huffman group storage never shrinks, so a smaller meta-block does not reinitialize it. Buffers whose every entry is written before it is read are refilled instead of zeroed first: a context map with more than one tree reserves its size and appends each decoded entry and zero run in order (a one-tree map is the only one zero-filled), the distance table is cleared and extended with every symbol’s split, and prefix-crossing scratch is extended with the history bytes it preserves. Their allocations stay; only their lengths follow the current meta-block.

flowchart LR
    Trees{"context map trees"} -->|one| Zero["Memory::resize: zero-filled map"]
    Trees -->|several| Reserve["Memory::reserve(size)"]
    Reserve --> Append["Entries push / Repeat resize(end, 0)"]
    Append --> Full["len == size before any read"]
    Layout["new distance layout"] --> Clear["table.clear + Memory::reserve"]
    Clear --> Extend["extend with every symbol's split"]
``` There is no allocation per output byte or transform. Header-only and metadata-only
streams need no heap workspace.

`DecodeDictionary` copies RAW payloads without encoder search structures. Source
budget counts all supplied attachment bytes; owned budget includes peak storage
while replacing serialized descriptions. RAW is never magic-sniffed. Empty RAW
attachments consume one of 15 slots; an empty embedded serialized prefix does not.
Experimental `Description` owns source bytes and compact offsets/layouts, applies
C attachment replacement rules, validates list/transform/context indexes, and
resolves combinations through the borrowed view. The old custom mapping remains
live until a successful replacement; prefix-only descriptions reset it to built-in.
Prepared dictionaries resolve through their existing immutable static index and
prefix representation. Common transform application lives in `shared::dictionary`.

## Executable API contracts

Rustdoc examples beside the public decoder APIs exercise complete Vec/slice
decoding, append rollback, unchanged slice tails, output backpressure with a
two-byte buffer, exact-size validation, concatenated members, session recovery,
retention and independent workspaces. The session example advances by per-call
counts and reoffers precisely the remaining suffix under `Finish`; it also
checks aggregate counters and the accepted window header.

Reader examples recover protocol suffixes by chaining `unread_input` before the
returned source. Writer examples explicitly finalize and distinguish codec
completion from delivery and sink flushing. Decode-only dictionary examples run
without compression; the encoder/decoder prefix round-trip example is gated by
`compression`. They document that dictionary identity is a caller contract.

```mermaid
flowchart LR
    Docs[Public rustdoc examples] --> Buffers[Vec rollback and slice prefix checks]
    Docs --> Session[Finish suffix and progress checks]
    Docs --> IO[Reader suffix and writer finalization checks]
    Docs --> Dictionary[Owned dictionary and borrowed session checks]

I/O and error propagation

sequenceDiagram
    participant Caller
    participant Writer as DecoderWriter
    participant Session
    participant Sink
    Caller->>Writer: write(compressed)
    Writer->>Sink: drain pending outbox
    Writer->>Session: process(input, outbox, Process)
    Session-->>Writer: progress or failure with counts
    Writer->>Sink: write output, track exact cursor
    Writer-->>Caller: Ok(consumed) if input accepted, defer error
    Caller->>Writer: try_finish / retry
    Writer->>Session: Finish(empty), once and without new input
    Writer->>Sink: drain suffix, flush
    Writer-->>Caller: success or recoverable sink failure

Reader owns an 8 KiB compressed-input buffer and writes directly into caller output. Empty reads do nothing. Interrupted retries; WouldBlock preserves state; physical EOF declares Finish. An error after produced bytes is deferred to the next nonempty read. Codec errors remain terminal; no false EOF follows them. Single mode preserves read-ahead; into_parts returns source, unread suffix and verified finished state.

Writer owns an 8 KiB output outbox with cursors. An operation accepting input returns Ok(n) even if a codec or sink error follows; that error is deferred. Produced bytes survive errors until delivered. Delivery of a failed codec’s outbox can retry without re-entering the codec. Interrupted retries, short writes advance only their prefix, and write-zero is an error. Flush does not declare EOF. Once finalization begins, new input is refused; only sink delivery/flush can retry. finish uses the original io::FinishError<Self> type and retains the adapter on failure. Drop performs no I/O.

thiserror public non-exhaustive errors categorize wire violations and resource, size, policy, tail and lifecycle failures. Core returns these high-level categories without exposing private implementation errors. DecodeFailure preserves the source chain; conversion to std::io::Error retains the concrete codec error.

Known gaps

The decoder specializes the command loop and its fixed-size history copies, while Huffman symbol decoding retains its serial dependency chain. Long logical overlap uses fill, byte copies or doubling rather than independent SIMD lanes; context-map transforms and table construction remain scalar. The new SIMD kernels have x86_64 execution evidence only; Arm/NEON and AVX-512 still need native validation. See decoder SIMD measurements for the profile, benchmark results and code-size tradeoff. The prefix-crossing history path and short static-dictionary words still emit byte by byte, which does not affect the primary corpora. A first decode on a fresh workspace pays about a dozen separate allocations (ring, three Huffman groups, block and context-map codes, context maps, the distance table) and the one-time zero fill of the storage that is still zero-filled (ring tail, Huffman groups, one-tree context maps), which dominates decoding of payloads of a kilobyte and below in the cold shape; the per-meta-block tables are not yet folded into one arena. Appended and reused Vec destinations still zero-fill newly exposed output slices; fresh owned results can instead take the history allocation. Completely stored members need no history. Small compressed streams still pay entropy-table initialization and session setup. Compatibility evidence and remaining acceptance gaps are tracked separately in decompressor compatibility. There is no container parser, authentication, seek, or async runtime in this raw decoder.

Experimental framed driver

The separate decompressor::framing::core uses the existing private Stream directly. It owns content boundaries and passes short-lived dictionary views. set_framed_workspace_limit adjusts the remaining outer heap ceiling before a framed content call; it changes no raw session transitions or hot loops. See framed decoder for the independent public lifecycle.

graph LR
    Raw[raw DecoderSession] --> Stream[private core::Stream]
    Framed[framing::core driver] --> Stream
    Framed --> Budget[aggregate framing and dictionary budgets]
    Budget --> Limit[remaining raw workspace ceiling]
    Limit --> Stream