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>
This commit is contained in:
2026-09-23 20:37:48 -07:00
co-authored by Claude Opus 5.5
parent d56fcf8611
commit 02681069b6
6 changed files with 227 additions and 54 deletions
@@ -58,19 +58,65 @@ impl Ws {
/// The next text message, or `None` after about `wait` with none. Pings are answered and sent
/// here; a close frame is answered and ends the connection (`Closed`); silence past the
/// dead-after limit is `Dead`.
/// dead-after limit is `Dead`. Written for you: it is the glue.
pub fn poll(&mut self, wait: Duration) -> Result<Option<String>, WsError> {
// Loop, with `until = now + wait`:
// 1. Every whole message the decoder has: Text -> return it; Ping(p) -> send PONG with p;
// Pong -> nothing; Close(code, _) -> send CLOSE with the code as 2 bytes (or empty),
// ignore that error, return Closed.
// 2. Silence since last_heard >= dead_after -> Dead.
// 3. Since last_ping >= ping_every -> send an empty PING, last_ping = now.
// 4. now >= until -> Ok(None).
// 5. Read timeout: the least of (until - now), (last_ping + ping_every - now) and
// (last_heard + dead_after - now), at least 1 ms. Read into a 16 KiB buffer: 0 bytes ->
// Closed; n bytes -> feed them, last_heard = now; WouldBlock, TimedOut or Interrupted ->
// go round; any other error -> Io.
let until = Instant::now() + wait;
loop {
if let Some(text) = self.take_messages()? {
return Ok(Some(text));
}
let now = Instant::now();
self.keep_alive(now)?;
if now >= until {
return Ok(None);
}
let timeout = self.read_timeout(until, now);
self.read_some(timeout)?;
}
}
/// Act on every whole message the decoder holds, until one is text.
fn take_messages(&mut self) -> Result<Option<String>, WsError> {
// `while let Some(message) = self.decoder.next_message()? { match message { ... } }`:
// - Text(text) -> return Ok(Some(text)).
// - Ping(payload) -> `self.send(PONG, &payload)?`.
// - Pong(_) -> nothing.
// - Close(code, _) -> the reply is the code as 2 big-endian bytes, or empty:
// `code.map(|c| c.to_be_bytes().to_vec()).unwrap_or_default()`. Then
// `let _ = self.send(CLOSE, &reply);` (the peer may be gone) and Err(WsError::Closed).
// After the loop: Ok(None).
todo!()
}
/// The dead-peer check, and our ping when one is due.
fn keep_alive(&mut self, now: Instant) -> Result<(), WsError> {
// 1. `now.duration_since(self.last_heard) >= self.timing.dead_after` -> Err(WsError::Dead).
// 2. `now.duration_since(self.last_ping) >= self.timing.ping_every` ->
// `self.send(PING, b"")?` and `self.last_ping = now`.
// 3. Ok(()).
todo!()
}
/// How long the next read may wait: until the soonest of the end of `wait`, the next ping and
/// the dead-after limit, and never less than 1 ms.
fn read_timeout(&self, until: Instant, now: Instant) -> Duration {
// `(self.last_ping + self.timing.ping_every).saturating_duration_since(now)` is the time to
// the next ping; the same with last_heard and dead_after; and
// `until.saturating_duration_since(now)`. The least of the three (`.min`), then
// `.max(Duration::from_millis(1))`.
todo!()
}
/// One read, at most `timeout` long, fed to the decoder.
fn read_some(&mut self, timeout: Duration) -> Result<(), WsError> {
// 1. `self.stream.set_read_timeout(Some(timeout))?`.
// 2. `let mut buf = [0u8; 16 * 1024];` and `match self.stream.read(&mut buf)`:
// - Ok(0) -> Err(WsError::Closed).
// - Ok(n) -> `self.decoder.feed(buf.get(..n).unwrap_or_default())`,
// `self.last_heard = Instant::now()`, Ok(()).
// - Err(e) whose `e.kind()` is WouldBlock, TimedOut or Interrupted -> Ok(()): nothing
// came, and `poll` goes round.
// - any other Err(e) -> Err(WsError::Io(e)).
todo!()
}
@@ -50,42 +50,143 @@ impl Decoder {
}
/// The next whole message, `None` if more bytes are needed, or the error that ends the
/// connection. After an error, do not call again.
/// connection. After an error, do not call again. Written for you: it is the glue.
pub fn next_message(&mut self) -> Result<Option<Incoming>, WsError> {
// Loop: `self.header()?`, None -> Ok(None). If the buffer holds less than header_len +
// payload_len, Ok(None). Otherwise take the payload out and drain the frame from the
// buffer, then by opcode: PING -> Ping, PONG -> Pong, CLOSE -> `close(&payload)`. TEXT
// starts a new message, CONTINUATION extends `partial`; with fin the whole message must be
// UTF-8 (else Protocol) and is returned as Text; without fin it is kept in `partial` and
// the loop goes on. Any other opcode is Protocol.
loop {
let Some(header) = self.header()? else {
return Ok(None);
};
let total = header.header_len.saturating_add(header.payload_len);
if self.buf.len() < total {
return Ok(None);
}
let payload: Vec<u8> = self
.buf
.get(header.header_len..total)
.unwrap_or_default()
.to_vec();
self.buf.drain(..total);
match header.opcode {
PING => return Ok(Some(Incoming::Ping(payload))),
PONG => return Ok(Some(Incoming::Pong(payload))),
CLOSE => return close(&payload).map(Some),
_ => {
// A text or continuation frame: a whole message, or wait for the next frame.
if let Some(text) = self.data_frame(header.opcode, header.fin, payload)? {
return Ok(Some(Incoming::Text(text)));
}
}
}
}
}
/// The next frame's header, once all of it is in `buf`, checked against every rule that does
/// not need the payload. It only reads `buf`. Written for you: it is the glue.
fn header(&self) -> Result<Option<Header>, WsError> {
let (Some(&b0), Some(&b1)) = (self.buf.first(), self.buf.get(1)) else {
return Ok(None);
};
check_first_bytes(b0, b1)?;
let fin = b0 & 0x80 != 0;
let opcode = b0 & 0x0F;
let Some((header_len, payload_len)) = self.length(b1 & 0x7F)? else {
return Ok(None);
};
if opcode & 0x8 != 0 {
check_control(fin, payload_len)?;
} else {
self.check_data(opcode, payload_len)?;
}
Ok(Some(Header {
fin,
opcode,
header_len,
payload_len,
}))
}
/// The header's length and the payload's, from the 7-bit length `short` in byte 1 and the
/// bytes after it. `Ok(None)` while those bytes have not all arrived.
fn length(&self, short: u8) -> Result<Option<(usize, usize)>, WsError> {
// Three forms, a `match short`:
// - 0..=125: Ok(Some((2, usize::from(short)))).
// - 126: `let Some(bytes) = self.buf.get(2..4) else { return Ok(None) };` then
// `let len = u16::from_be_bytes([bytes[0], bytes[1]]);` (`bytes` has exactly 2 bytes, so
// this indexing cannot fail). len < 126 is Protocol("a length not in its shortest
// form"). Else Ok(Some((4, usize::from(len)))).
// - 127: `let Some(bytes) = self.buf.get(2..10) else { return Ok(None) };`, then
// `let mut b = [0u8; 8]; b.copy_from_slice(bytes); let len = u64::from_be_bytes(b);`.
// len >> 63 != 0 is Protocol("a 64-bit length with its top bit set"); len <= 0xFFFF is
// Protocol("a length not in its shortest form"). Else
// Ok(Some((10, usize::try_from(len).unwrap_or(usize::MAX)))).
todo!()
}
/// The next frame's header, once it is all here, checked against every rule that does not need
/// the payload.
fn header(&self) -> Result<Option<Header>, WsError> {
// Return Ok(None) while the bytes the header needs have not all arrived. The rules, each an
// error: a reserved bit (0x70 of byte 0) is Protocol; the mask bit (0x80 of byte 1) is
// Protocol; an opcode that is not CONTINUATION, TEXT, CLOSE, PING or PONG is Protocol.
// Length 126: a u16 in the next 2 bytes, below 126 is Protocol (not the shortest form).
// 127: a u64 in the next 8 bytes; top bit set is Protocol; <= 0xFFFF is Protocol. Control
// frames (opcode & 0x8): not fin, or over 125 bytes, is Protocol. Data frames: TEXT while
// `partial` is Some, or CONTINUATION while it is None, is Protocol; a payload over
// MAX_MESSAGE minus what `partial` holds is TooLarge. All of it before any allocation.
/// The rules for a text or continuation frame that need what came before.
fn check_data(&self, opcode: u8, payload_len: usize) -> Result<(), WsError> {
// 1. TEXT while `self.partial` is Some: Protocol("a new message inside an unfinished one").
// 2. CONTINUATION while `self.partial` is None: Protocol("a continuation with nothing to
// continue").
// 3. `let so_far = self.partial.as_ref().map_or(0, Vec::len);` then
// payload_len > MAX_MESSAGE.saturating_sub(so_far) is TooLarge.
// 4. Ok(()).
todo!()
}
/// A text or continuation frame's payload, taken out of `buf`: the whole message when `fin`
/// is set, or `None` when more frames must come.
fn data_frame(
&mut self,
opcode: u8,
fin: bool,
payload: Vec<u8>,
) -> Result<Option<String>, WsError> {
// 1. The message so far: `Vec::new()` for TEXT, `self.partial.take().unwrap_or_default()`
// for CONTINUATION. Append `payload` to it.
// 2. Not `fin`: `self.partial = Some(message);` and Ok(None).
// 3. `fin`: `String::from_utf8(message)`; an error is Protocol("text is not UTF-8"); else
// Ok(Some(text)).
todo!()
}
}
/// The rules on the first two bytes alone.
fn check_first_bytes(b0: u8, b1: u8) -> Result<(), WsError> {
// In this order, each a Protocol error: b0 & 0x70 != 0 ("a reserved bit is set");
// b1 & 0x80 != 0 ("a frame from the server is masked"); the opcode b0 & 0x0F is not one of
// CONTINUATION, TEXT, CLOSE, PING, PONG ("opcode <n>"). Then Ok(()).
todo!()
}
/// The rules for a control frame (close, ping, pong).
fn check_control(fin: bool, payload_len: usize) -> Result<(), WsError> {
// Not `fin`: Protocol("a fragmented control frame"). payload_len > 125: Protocol("a control
// frame over 125 bytes"). Else Ok(()).
todo!()
}
/// A close frame's payload.
fn close(payload: &[u8]) -> Result<Incoming, WsError> {
// Empty: Close(None, ""). One byte: Protocol. Otherwise a big-endian u16 code and a UTF-8
// reason (else Protocol).
// A `match payload` on slice patterns, no indexing:
// - `[]` -> Ok(Incoming::Close(None, String::new())).
// - `[_]` -> Protocol("a close frame of one byte").
// - `[a, b, reason @ ..]` -> the code is `u16::from_be_bytes([*a, *b])`; the reason is
// `String::from_utf8(reason.to_vec())`, whose error is Protocol("a close reason that is not
// UTF-8"). Ok(Incoming::Close(Some(code), reason)).
todo!()
}
/// One whole frame from us: FIN set, masked with `mask`.
pub fn encode(opcode: u8, payload: &[u8], mask: [u8; 4]) -> Vec<u8> {
// Byte 0: 0x80 | opcode. Byte 1: 0x80 | length, where the length is the 7-bit form below 126,
// 126 then a u16 up to 0xFFFF, else 127 then a u64. Then the 4 mask bytes, then each payload
// byte XOR mask[i % 4]. No `as` casts: use try_from.
// 1. `let mut out = vec![0x80 | (opcode & 0x0F)];` and `let len = payload.len();`.
// 2. The length, with the mask bit 0x80 set in its first byte, in three branches:
// `if len < 126`: push `0x80 | u8::try_from(len).unwrap_or(0)`.
// `else if let Ok(len16) = u16::try_from(len)`: push `0x80 | 126`, then
// `extend_from_slice(&len16.to_be_bytes())`.
// `else`: push `0x80 | 127`, then
// `extend_from_slice(&u64::try_from(len).unwrap_or(u64::MAX).to_be_bytes())`.
// (`unwrap_or` never uses its fallback here; it is how to convert without a panic.)
// 3. `out.extend_from_slice(&mask);`
// 4. `out.extend(payload.iter().zip(mask.iter().cycle()).map(|(b, m)| b ^ m));` and return out.
todo!()
}