Pure module (src/ws/frame.rs): bytes in, frames out, zero io/actor
machinery, so the whole RFC 6455 §5 edge surface lives in fast unit
tests ahead of the chunk-3 conn-lifecycle wiring.
- decode(buf, require_masked, max_payload) -> Ok(Some((frame,
consumed))) | Ok(None: incomplete) | Err(fatal). Server mode requires
client masking (§5.1); the flag keeps the codec reusable for a client
mode. Enforced: RSV=0, reserved opcodes, control frames FIN+<=125,
MINIMAL length encodings (16/64-bit), 64-bit MSB clear. The payload
cap is checked from the header BEFORE the payload is buffered — a
hostile 8-byte length can't make the conn actor allocate toward it.
- Frame::encode is unmask-only (server MUST NOT mask); encode_masked
exists for the client side of tests + future client mode.
- Close payloads: parse_close_payload validates the §7.4 wire-code
ranges (1004/1005/1006/1015 and <1000/1012-2999/>=5000 rejected,
3000-4999 app space allowed), 1-byte payload rejected, UTF-8 reason
required; close_payload(code, reason) truncates the reason to the
125-byte control cap on a char boundary.
- Assembler (data frames only; control frames interleave at the caller
per §5.4): orphan continuation and mid-fragmentation data frames are
protocol errors, message cap spans fragments (and the unfragmented
fast path), text UTF-8 validated once on completion (whole-message
delivery makes incremental validation pointless), reusable after
every message/error.
- FrameError -> close_code mapping: Protocol=1002, TooLarge=1009,
BadUtf8=1007.
Tests pin all five §5.7 worked examples (masked Hello bytes verbatim),
round-trip the 125/126 and 65535/65536 boundaries, and prove decode
returns None at EVERY proper prefix of a frame. 57 unit tests total.