Task 08's `header` was one todo!() with a dozen branches; Ornith planned it in its head until the turn ran out (tip T25). `next_message` and `header` are now written as glue over seven small helpers, and task 09's `poll` over four, each checked to pass when filled from its comments. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
79 lines
4.2 KiB
Markdown
79 lines
4.2 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 }`.
|
|
|
|
Also written, because they are the glue: **`Decoder::next_message`** (take a whole frame out of
|
|
`buf` and act on it) and **`Decoder::header`** (read the first two bytes, then call the checks and
|
|
`length` below). Read both first: they call everything you write.
|
|
|
|
To fill, **one at a time, in this order**, each with its steps above it. Each is a few lines:
|
|
|
|
1. `check_first_bytes(b0, b1)`: the reserved bits, the mask bit, the opcode.
|
|
2. `check_control(fin, payload_len)`.
|
|
3. `Decoder::length(&self, short)`: the three length forms. `Ok(None)` means "wait for more bytes".
|
|
4. `Decoder::check_data(&self, opcode, payload_len)`: the rules that depend on `partial`.
|
|
5. `Decoder::data_frame(&mut self, opcode, fin, payload)`: a text or continuation payload.
|
|
6. `close(payload)`.
|
|
7. `encode(opcode, payload, mask)`.
|
|
|
|
The comment above each `todo!()` gives the constructs to use, down to the expressions for the
|
|
indexing and the conversions. Use them as written: they are checked against the tests, and the
|
|
indexing in them cannot go out of bounds. Do not weigh other ways to write the same thing. Write
|
|
the function, run `cargo check -p gatewayd`, go on to the next.
|
|
|
|
## 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 seven functions** in the order above, `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.
|