# M1 task 04: `proto` frame codec **Branch:** `m1` (run `git switch m1`; `git status --short` must be empty, otherwise stop) **Commit subject:** `Add length-prefixed frame codec to proto` ## Goal Read and write one envelope on a byte stream. This is the only I/O in `proto`. It is the first code that touches bytes from another process, so it must never trust a length it has not checked. ## Context From the spec: "A 4-byte big-endian unsigned length `N`, then `N` bytes of UTF-8 JSON holding one envelope. `MAX_FRAME` is 1,048,576. A reader that sees `N == 0` or `N > MAX_FRAME` returns an error without reading or allocating the body." And: "`v` is the protocol version, 1. A reader that sees another value" rejects the frame. ## Files - Copy: `crates/proto/tests/frame.rs`, `crates/proto/tests/fixtures/frame/tool_request.bin` - Create: `crates/proto/src/frame.rs` - Modify: `crates/proto/src/lib.rs`, `docs/implementer-log.md` ## Interfaces Consumes from task 03: `proto::Envelope`, `proto::PROTOCOL_VERSION`. Produces, in `crates/proto/src/frame.rs`, re-exported from the crate root: ```rust pub const MAX_FRAME: usize = 1_048_576; #[derive(Debug)] pub enum FrameError { Closed, // the stream ended before the first byte of a frame Empty, // the length prefix was 0 TooLarge(usize), // the length prefix, or an outgoing body, was larger than MAX_FRAME BadVersion(u32), // the envelope's v was not PROTOCOL_VERSION Json(serde_json::Error), // the body was not a valid envelope, or could not be encoded Io(std::io::Error), // any other I/O failure, including a stream that ends mid-frame } // FrameError implements Display (a short message per variant) and std::error::Error. pub fn write_frame(w: &mut W, env: &Envelope) -> Result<(), FrameError>; pub fn read_frame(r: &mut R) -> Result; ``` Rules the tests check: - `write_frame` encodes first. If the body is larger than `MAX_FRAME` it returns `TooLarge(len)` and writes **nothing**. Otherwise it writes the 4-byte length, the body, and flushes. - `read_frame` reads exactly 4 length bytes. `Read::read` may return fewer bytes than asked for, even one at a time; keep reading until you have all 4. If the stream ends before the first of them, return `Closed`. If it ends after 1 to 3 of them, return `Io`. - It checks `N == 0` (`Empty`) and `N > MAX_FRAME` (`TooLarge(N)`) **before** it allocates a buffer or reads the body. One test uses a reader that panics if the body is read. - A body of exactly `MAX_FRAME` bytes is allowed. - A stream that ends inside the body is `Io`. A body that is not a valid envelope is `Json`. - After decoding, `v != PROTOCOL_VERSION` is `BadVersion(v)`. ## API notes - `u32::from_be_bytes([u8; 4])` and `u32::to_be_bytes()`. - `serde_json::to_vec(&T) -> Result, serde_json::Error>` and `serde_json::from_slice::(&[u8])`. - `Read::read_exact` returns `ErrorKind::UnexpectedEof` when the stream ends early, which is what you want for the body. It cannot tell a clean close from a cut-off header, so do not use it for the 4 length bytes. - A `read` that fails with `ErrorKind::Interrupted` should be retried. - Build an `io::Error` from a kind with `std::io::Error::from(std::io::ErrorKind::UnexpectedEof)`. ## Steps - [ ] **1. Copy the test and fixture.** ```sh git switch m1 cp docs/plans/M1/files/crates/proto/tests/frame.rs crates/proto/tests/ cp -r docs/plans/M1/files/crates/proto/tests/fixtures/frame crates/proto/tests/fixtures/ ``` - [ ] **2. See the test fail.** `cargo test -p proto --test frame`. Expected: it does not compile. - [ ] **3. Write `frame.rs`,** and add to `lib.rs` (alphabetical order, as before): ```rust pub mod frame; pub use frame::{FrameError, MAX_FRAME, read_frame, write_frame}; ``` - [ ] **4. See the test pass.** `cargo test -p proto --test frame`. Expected: `13 passed; 0 failed`. - [ ] **5. Run the gate.** `make gate`. Expected last line: `gate: ok`. - [ ] **6. Log and commit.** ```sh git add crates/proto docs/implementer-log.md git commit ``` ## Done when - `cargo test -p proto --test frame` reports 13 passed. - `make gate` prints `gate: ok`. - `cmp crates/proto/tests/frame.rs docs/plans/M1/files/crates/proto/tests/frame.rs` prints nothing. - `frame.rs` contains no `unwrap`, `expect`, `panic!` or unchecked indexing. ## Stop and report if - `write_matches_the_fixture_byte_for_byte` fails although `cargo test -p proto --test wire` passes.