Files
boxmaker/docs/plans/M4a/08-gatewayd-ws-frames.md
T
kyleandClaude Opus 5.5 0339dc13b2 Plan M4a: gatewayd in 15 tasks, with skeletons and given tests
Each task's tests were run against a reference at its end state; the end states were replayed
from master in order with the gate at each step (650 to 762 tests); each skeleton compiles
against its tests and fails them. The reference is kept off this machine. Lessons T27 (every
wait in a test has a limit) and T28 (mutate the reference before hand-over) come from this work.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 19:05:44 -07:00

74 lines
3.9 KiB
Markdown

# M4a task 08: WebSocket frames
**Branch:** `m4a` (run `git switch m4a`; `git status --short` must be empty, otherwise stop)
**Commit subject:** `gatewayd: ws frames, a strict decoder and a masked encoder`
## Goal
After the handshake, everything is frames (RFC 6455, section 5). This task is the codec, without
I/O: a `Decoder` that is fed bytes as they arrive, in any pieces, and yields whole messages, and
`encode`, which builds our frames. Everything the server sends is untrusted, so the decoder is
strict and **checks every length before it allocates** (spec section 6):
- A frame from the server that is masked, sets a reserved bit, uses an opcode other than
continuation (0), text (1), close (8), ping (9) or pong (10), is a control frame (8 to 10) over
125 bytes or not final, is a continuation with nothing to continue, or a new text frame while a
message is unfinished, is a `Protocol` error.
- A length not in its shortest form (126 for a length under 126; 127 for one that fits in 16
bits), or a 64-bit length with its top bit set, is a `Protocol` error.
- A message over `MAX_MESSAGE` (1 MiB), counted from the length fields of its frames **before**
their payloads arrive, is `TooLarge`.
- Text must be UTF-8 once whole (a character may be split across frames).
- A close frame's payload is empty, or a 2-byte code and a UTF-8 reason; one byte is an error.
Every frame **we** send is final, masked with the 4 bytes we are given.
## Files
- Copy: `crates/gatewayd/tests/ws_frame.rs`, and the skeleton `crates/gatewayd/src/ws/frame.rs`
- Modify: `crates/gatewayd/src/ws/mod.rs` (`pub mod frame;`), `docs/implementer-log.md`
## The skeleton
Written: `MAX_MESSAGE`, the opcode constants, `Incoming` (`Text`, `Ping`, `Pong`,
`Close(Option<u16>, String)`), `Decoder { buf, partial }` with `new` and `feed`, and the private
`Header { fin, opcode, header_len, payload_len }`.
To fill, in this order, each with its steps above it:
1. `Decoder::header(&self) -> Result<Option<Header>, WsError>`: the next frame's header once all of
it is in `buf`, checked against every rule above that does not need the payload. `Ok(None)`
means "wait for more bytes". It only reads `buf`; it removes nothing.
2. `close(payload) -> Result<Incoming, WsError>`.
3. `Decoder::next_message(&mut self)`: uses `header`, and when the whole frame is in `buf`, takes it
out (`drain`) and acts on it. Control frames may come between the frames of a text message and
are returned at once.
4. `encode(opcode, payload, mask) -> Vec<u8>`.
No indexing that can go out of bounds, no `as` casts: read lengths with `get(2..4)` and
`u16::from_be_bytes`, and convert with `usize::try_from` / `u8::try_from`.
## Steps
- [ ] **1. Copy.** `git switch m4a`, then
`cp docs/plans/M4a/files/crates/gatewayd/tests/ws_frame.rs crates/gatewayd/tests/ && cp docs/plans/M4a/files/crates/gatewayd/src/ws/frame.rs crates/gatewayd/src/ws/`.
Add `pub mod frame;` to `crates/gatewayd/src/ws/mod.rs`.
- [ ] **2. See it fail.** `cargo test -p gatewayd --test ws_frame`. Expected: it compiles and 7
tests fail.
- [ ] **3. Fill the four functions**, `cargo check -p gatewayd` after each.
- [ ] **4. See it pass.** `cargo test -p gatewayd --test ws_frame`. Expected: 7 passed. The last
test feeds 300 random streams, half of them with bits flipped, in random pieces, and compares
your decoder with a simple one written inside the test: they must return the same messages, and
both fail or both not.
- [ ] **5. Run the gate.** `cargo fmt --all`, then `make gate`. Expected last line: `gate: ok`.
- [ ] **6. Log and commit.** `git add crates/gatewayd docs/implementer-log.md Cargo.lock && git commit`
## Done when
- `cargo test -p gatewayd --test ws_frame` passes; `make gate` prints `gate: ok`.
## Stop and report if
- The random-stream test fails on a case you believe the naive decoder gets wrong. Report the seed
and the bytes; do not change the test.