---
date: '2026-07-05'
description: and incremental decoding
id: notes
modified: 2026-07-06 13:05:30 GMT-04:00
tags:
  - cs
title: decoding strategy
created: '2026-07-05'
published: '2026-07-05'
pageLayout: default
slug: hinterland/prep/05-decode/notes
permalink: https://aarnphm.xyz/hinterland/prep/05-decode/notes.md
generator:
  quartz: v4.6.0
  hostedProvider: Cloudflare
  baseUrl: aarnphm.xyz
full: https://aarnphm.xyz/llms-full.txt
---
## route

This module turns one-shot decoders into state machines.

1. Read `picture`, `split invariance`, and `varint state`.
2. Solve `StreamingVarintDecoder`.
3. Read `framing`.
4. Solve `FrameDeframer` and `TlvStreamParser`.
5. Skim `utf-8`, `cobs`, and `chunked` for follow-ups.
6. Review [[hinterland/prep/05-decode/notes.fc]].

Depth: `Utf8StreamValidator`, `cobs_encode`, `cobs_decode`, and `ChunkedDecoder`.

## picture

A streaming decoder is a state machine plus retained partial state.

```mermaid
stateDiagram-v2
  [*] --> Ready
  Ready --> Ready: complete value
  Ready --> Partial: valid prefix
  Partial --> Partial: more prefix
  Partial --> Ready: completes
  Ready --> Error: malformed
  Partial --> Error: malformed
  Partial --> Error: finish while partial
```

`feed(chunk)` returns items completed by this chunk and keeps enough state to continue later. The chunk boundary is transport noise.

## split invariance

The invariant:

```text
feed(a + b) emits the same total output as feed(a); feed(b)
```

Test three ways:

1. feed the whole byte stream.
2. feed it one byte at a time.
3. feed every two-part split.

This catches almost every streaming bug: state reset too early, cursor off by one, EOF confused with “need more data”, and buffer slicing that loses bytes.

Two failure modes:

| mode           | meaning                             | action                            |
| -------------- | ----------------------------------- | --------------------------------- |
| need more data | current bytes are a valid prefix    | keep state, emit what is complete |
| malformed      | no possible future bytes can fix it | raise or mark invalid now         |

`finish()` changes dangling partial state from “need more data” into “truncated”.

## varint state

A streaming varint decoder does not need a byte buffer. It digests partial bytes into `(acc, shift, count)`.

<div class="notebook-runtime" data-notebook-runtime="notebook-runtime-1fhu2fq"></div>

<div class="notebook-code-cell" data-notebook-cell-frame="code-cell-1" id="code-cell-1" data-notebook-language="python">

<div class="notebook-runtime-cell" data-notebook-cell="code-cell-1" data-notebook-execution-count=""><span class="notebook-execution-prompt" data-notebook-execution-label="code-cell-1" aria-live="polite">In [ ]:</span></div>

<div class="notebook-cell-actions" data-notebook-cell-actions="code-cell-1">
<span class="notebook-language-badge notebook-language-badge-python" data-notebook-language="python" title="Python cell"><span class="notebook-language-icon" aria-hidden="true"><svg class="notebook-language-svg notebook-python-icon" viewBox="0 0 111 112" aria-hidden="true" focusable="false"><path fill="#3776ab" d="M54.918785.00091927421C50.335132.02221727 45.957846.41313697 42.106285 1.0946693 30.760069 3.0991731 28.700036 7.2947714 28.700035 15.032169v10.21875h26.8125v3.40625h-36.875c-7.792459 0-14.6157588 4.683717-16.7499998 13.59375-2.46181998 10.212966-2.57101508 16.586023 0 27.25 1.9059283 7.937852 6.4575432 13.593748 14.2499998 13.59375h9.21875v-12.25c0-8.849902 7.657144-16.656248 16.75-16.65625h26.78125c7.454951 0 13.406253-6.138164 13.40625-13.625v-25.53125c0-7.2663386-6.12998-12.7247771-13.40625-13.9374997C64.281548.32794397 59.502438-.02037903 54.918785.00091927421zM40.418785 8.2196694c2.769547 0 5.03125 2.2986456 5.03125 5.1249996-.000002 2.816336-2.261703 5.09375-5.03125 5.09375-2.779476-.000001-5.03125-2.277415-5.03125-5.09375-.000001-2.826353 2.251774-5.1249996 5.03125-5.1249996z"/><path fill="#ffd43b" d="M85.637535 28.657169v11.90625c0 9.230755-7.825895 16.999999-16.75 17h-26.78125c-7.335833 0-13.406249 6.278483-13.40625 13.625v25.531247c0 7.266344 6.318588 11.540324 13.40625 13.625004 8.487331 2.49561 16.626237 2.94663 26.78125 0 6.750155-1.95439 13.406253-5.88761 13.40625-13.625004V86.500919h-26.78125v-3.40625h40.187504c7.792461 0 10.696251-5.435408 13.406241-13.59375 2.79933-8.398886 2.68022-16.475776 0-27.25-1.92578-7.757441-5.60387-13.59375-13.406241-13.59375zm-15.0625 64.65625c2.779478.000003 5.03125 2.277417 5.03125 5.093747-.000002 2.826354-2.251775 5.125004-5.03125 5.125004-2.76955 0-5.03125-2.29865-5.03125-5.125004.000002-2.81633 2.261697-5.093747 5.03125-5.093747z"/></svg></span><span class="notebook-language-label">Python cell</span></span>
<button type="button" class="notebook-icon-button" data-notebook-run-cell="code-cell-1" aria-label="Run code-cell-1" title="Run code-cell-1"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M8 5v14l11-7z"/></svg></button>
<button type="button" class="notebook-icon-button" data-notebook-edit-cell="code-cell-1" aria-label="Edit code-cell-1" title="Edit code-cell-1"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m4 16.5-.5 4 4-.5L19 8.5 15.5 5z"/><path d="m14 6.5 3.5 3.5"/></svg></button>
<button type="button" class="notebook-icon-button" data-notebook-save-cell="code-cell-1" aria-label="Save code-cell-1 locally" title="Save code-cell-1 locally" hidden><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5 4h11l3 3v13H5z"/><path d="M8 4v6h8V4"/><path d="M8 20v-6h8v6"/></svg></button>
<button type="button" class="notebook-icon-button" data-notebook-revert-cell="code-cell-1" aria-label="Revert code-cell-1 local edit" title="Revert code-cell-1 local edit" hidden><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/></svg></button>
<button type="button" class="notebook-icon-button" data-notebook-vim-cell="code-cell-1" aria-label="Enable Vim mode" title="Enable Vim mode" hidden><svg class="notebook-vim-icon" viewBox="0 0 602 734" aria-hidden="true" focusable="false"><g transform="translate(2 3)"><path class="notebook-vim-icon-left" d="M0 155.5704 155-1l-.000003 728L0 572.237919z"/><path class="notebook-vim-icon-right" d="M443.060403 156.982405 600-1l-3.181208 728L442 572.219941z" transform="translate(521 363.5) scale(-1 1) translate(-521 -363.5)"/><path class="notebook-vim-icon-cross" d="M154.986294 0 558 615.189696 445.224605 728 42 114.172017z"/></g></svg></button>
<span class="notebook-local-source-status" data-notebook-local-source-status="code-cell-1" hidden></span>
</div>

<div class="notebook-source-editor" data-notebook-source-editor="code-cell-1" hidden></div>

```python shell
for b in chunk:
  acc |= (b & 0x7F) << shift
  if b & 0x80:
    shift += 7
  else:
    emit(acc)
    acc = 0
    shift = 0
```

<div class="notebook-runtime-output" data-notebook-output="code-cell-1" hidden></div>

</div>

Walk `ac | 02`:

| feed | state                       | output |
| ---- | --------------------------- | ------ |
| `ac` | `acc = 0x2c`, `shift = 7`   | none   |
| `02` | completes `0x2c + (2 << 7)` | 300    |

Guards:

- at most 10 bytes for u64.
- 10th byte can carry only payload bit 0.
- continuation on the 10th byte is too long.
- multi-byte final `00` is overlong if canonicality is required.

## framing

Framing recovers message boundaries from byte soup.

| strategy           | state                      | risk                    | resync         |
| ------------------ | -------------------------- | ----------------------- | -------------- |
| length prefix      | length, payload bytes left | hostile length          | none           |
| delimiter + escape | escape bit                 | worst-case 2x expansion | next delimiter |
| COBS               | current block length       | truncated block         | next `00`      |
| TLV                | tag, length, value         | hostile length          | none           |

Length-prefix rule: check `length > max_frame` immediately after decoding the length, before buffering or allocating payload. A peer claiming a 1 GiB frame should lose at byte 5, not after your process tries to become a database.

Frame deframer state:

```mermaid
stateDiagram-v2
  [*] --> ReadingLength
  ReadingLength --> ReadingPayload: length complete and <= max_frame
  ReadingLength --> Error: bad length
  ReadingPayload --> ReadingPayload: need more payload
  ReadingPayload --> ReadingLength: payload complete
```

TLV gives forward compatibility: unknown tag plus known length means skip and continue. Protobuf is varint-key TLV: key = `(field_number << 3) | wire_type`.

## utf-8

Strict UTF-8 streaming state:

- remaining continuation bytes
- allowed range for the next byte
- sticky invalid flag

Lead bytes:

| lead | next byte constraint | reason                 |
| ---- | -------------------- | ---------------------- |
| `e0` | `a0..bf`             | reject overlong 3-byte |
| `ed` | `80..9f`             | reject surrogates      |
| `f0` | `90..bf`             | reject overlong 4-byte |
| `f4` | `80..8f`             | reject > U+10ffff      |

Invalid outright:

- lone continuation byte
- `c0`, `c1`
- `f5..ff`

EOF with `remaining > 0` is truncated.

## cobs

COBS removes zero bytes so `00` can delimit frames.

Encoding rule:

1. split input at zeros.
2. emit `code = len(run) + 1`.
3. emit the nonzero run.
4. if run length is 254, code is `ff` and no zero is implied.

Worked:

```text
input:  11 22 00 33
runs:   [11 22] zero [33]
output: 03 11 22 02 33
```

Overhead is one byte per 254 payload bytes, about 0.4%, plus the delimiter. Escaping can double the payload in the worst case.

## chunked transfer

HTTP chunked body:

```text
hex-size CRLF
data CRLF
...
0 CRLF
trailers CRLF
```

State machine:

```mermaid
stateDiagram-v2
  [*] --> SizeLine
  SizeLine --> Data: size > 0
  Data --> DataCRLF
  DataCRLF --> SizeLine
  SizeLine --> Trailers: size == 0
  Trailers --> Done
```

Bugs:

- CRLF split across chunks.
- size line with no maximum length.
- forgetting the CRLF after data.
- trailing bytes after done.

## buffer management

Avoid this:

<div class="notebook-code-cell" data-notebook-cell-frame="code-cell-2" id="code-cell-2" data-notebook-language="python">

<div class="notebook-runtime-cell" data-notebook-cell="code-cell-2" data-notebook-execution-count=""><span class="notebook-execution-prompt" data-notebook-execution-label="code-cell-2" aria-live="polite">In [ ]:</span></div>

<div class="notebook-cell-actions" data-notebook-cell-actions="code-cell-2">
<span class="notebook-language-badge notebook-language-badge-python" data-notebook-language="python" title="Python cell"><span class="notebook-language-icon" aria-hidden="true"><svg class="notebook-language-svg notebook-python-icon" viewBox="0 0 111 112" aria-hidden="true" focusable="false"><path fill="#3776ab" d="M54.918785.00091927421C50.335132.02221727 45.957846.41313697 42.106285 1.0946693 30.760069 3.0991731 28.700036 7.2947714 28.700035 15.032169v10.21875h26.8125v3.40625h-36.875c-7.792459 0-14.6157588 4.683717-16.7499998 13.59375-2.46181998 10.212966-2.57101508 16.586023 0 27.25 1.9059283 7.937852 6.4575432 13.593748 14.2499998 13.59375h9.21875v-12.25c0-8.849902 7.657144-16.656248 16.75-16.65625h26.78125c7.454951 0 13.406253-6.138164 13.40625-13.625v-25.53125c0-7.2663386-6.12998-12.7247771-13.40625-13.9374997C64.281548.32794397 59.502438-.02037903 54.918785.00091927421zM40.418785 8.2196694c2.769547 0 5.03125 2.2986456 5.03125 5.1249996-.000002 2.816336-2.261703 5.09375-5.03125 5.09375-2.779476-.000001-5.03125-2.277415-5.03125-5.09375-.000001-2.826353 2.251774-5.1249996 5.03125-5.1249996z"/><path fill="#ffd43b" d="M85.637535 28.657169v11.90625c0 9.230755-7.825895 16.999999-16.75 17h-26.78125c-7.335833 0-13.406249 6.278483-13.40625 13.625v25.531247c0 7.266344 6.318588 11.540324 13.40625 13.625004 8.487331 2.49561 16.626237 2.94663 26.78125 0 6.750155-1.95439 13.406253-5.88761 13.40625-13.625004V86.500919h-26.78125v-3.40625h40.187504c7.792461 0 10.696251-5.435408 13.406241-13.59375 2.79933-8.398886 2.68022-16.475776 0-27.25-1.92578-7.757441-5.60387-13.59375-13.406241-13.59375zm-15.0625 64.65625c2.779478.000003 5.03125 2.277417 5.03125 5.093747-.000002 2.826354-2.251775 5.125004-5.03125 5.125004-2.76955 0-5.03125-2.29865-5.03125-5.125004.000002-2.81633 2.261697-5.093747 5.03125-5.093747z"/></svg></span><span class="notebook-language-label">Python cell</span></span>
<button type="button" class="notebook-icon-button" data-notebook-run-cell="code-cell-2" aria-label="Run code-cell-2" title="Run code-cell-2"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M8 5v14l11-7z"/></svg></button>
<button type="button" class="notebook-icon-button" data-notebook-edit-cell="code-cell-2" aria-label="Edit code-cell-2" title="Edit code-cell-2"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m4 16.5-.5 4 4-.5L19 8.5 15.5 5z"/><path d="m14 6.5 3.5 3.5"/></svg></button>
<button type="button" class="notebook-icon-button" data-notebook-save-cell="code-cell-2" aria-label="Save code-cell-2 locally" title="Save code-cell-2 locally" hidden><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5 4h11l3 3v13H5z"/><path d="M8 4v6h8V4"/><path d="M8 20v-6h8v6"/></svg></button>
<button type="button" class="notebook-icon-button" data-notebook-revert-cell="code-cell-2" aria-label="Revert code-cell-2 local edit" title="Revert code-cell-2 local edit" hidden><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/></svg></button>
<button type="button" class="notebook-icon-button" data-notebook-vim-cell="code-cell-2" aria-label="Enable Vim mode" title="Enable Vim mode" hidden><svg class="notebook-vim-icon" viewBox="0 0 602 734" aria-hidden="true" focusable="false"><g transform="translate(2 3)"><path class="notebook-vim-icon-left" d="M0 155.5704 155-1l-.000003 728L0 572.237919z"/><path class="notebook-vim-icon-right" d="M443.060403 156.982405 600-1l-3.181208 728L442 572.219941z" transform="translate(521 363.5) scale(-1 1) translate(-521 -363.5)"/><path class="notebook-vim-icon-cross" d="M154.986294 0 558 615.189696 445.224605 728 42 114.172017z"/></g></svg></button>
<span class="notebook-local-source-status" data-notebook-local-source-status="code-cell-2" hidden></span>
</div>

<div class="notebook-source-editor" data-notebook-source-editor="code-cell-2" hidden></div>

```python shell
buf = buf[n:]
```

<div class="notebook-runtime-output" data-notebook-output="code-cell-2" hidden></div>

</div>

That copies the tail every time. Use:

- `bytearray` plus cursor.
- compact only when dead prefix is large.
- `memoryview` for zero-copy reads.
- max-buffer guard independent of declared frame length.

Every adversarial stream either completes, waits with bounded state, or fails. Anything else is a memory leak with network access, which is a cursed vending machine.

## guards

- empty chunk is a no-op.
- `finish()` must check dangling state.
- after a parse error, state is undefined unless the API specifies resync.
- return independent `bytes` objects, not views into mutable internal buffers.
- streaming varint overflow is policy in Python but memory safety in C.
- a decoder must not grow unbounded under valid prefixes forever.

## drills

1. State split invariance.
2. Walk `ac` then `02` through streaming varint state.
3. Explain why `80 00` is overlong.
4. Explain where the `max_frame` check belongs.
5. COBS encode `11 22 00 33`.
6. Name the UTF-8 constraints after `e0`, `ed`, `f0`, and `f4`.
7. Explain what `finish()` does.
8. Explain why repeated `buf = buf[n:]` is quadratic.

<script type="application/json" data-notebook-runtime-data>{"id":"notebook-runtime-1fhu2fq","sourcePath":"hinterland/prep/05-decode/notes.md","language":"python","indexUrl":"https://cdn.jsdelivr.net/pyodide/v0.29.4/full/","cells":[{"id":"code-cell-1","source":"for b in chunk:\n  acc |= (b \u0026 0x7F) \u003c\u003c shift\n  if b \u0026 0x80:\n    shift += 7\n  else:\n    emit(acc)\n    acc = 0\n    shift = 0","language":"python","executionIndex":null},{"id":"code-cell-2","source":"buf = buf[n:]","language":"python","executionIndex":null}],"toolbar":false,"debug":true,"vimMode":true}</script>

