Files
boxmaker/docs/plans/M4a/08-gatewayd-ws-frames.md
T
kyleandClaude Opus 5.5 02681069b6 M4a tasks 08 and 09: finer skeletons after task 08's session wrote nothing
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>
2026-09-23 20:37:48 -07:00

4.2 KiB

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.