# 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, 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, 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`. 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`. 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.