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>
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
Protocolerror. - 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
Protocolerror. - A message over
MAX_MESSAGE(1 MiB), counted from the length fields of its frames before their payloads arrive, isTooLarge. - 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 skeletoncrates/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:
check_first_bytes(b0, b1): the reserved bits, the mask bit, the opcode.check_control(fin, payload_len).Decoder::length(&self, short): the three length forms.Ok(None)means "wait for more bytes".Decoder::check_data(&self, opcode, payload_len): the rules that depend onpartial.Decoder::data_frame(&mut self, opcode, fin, payload): a text or continuation payload.close(payload).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, thencp 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/. Addpub mod frame;tocrates/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 gatewaydafter 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, thenmake 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_framepasses;make gateprintsgate: 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.