i2c-api Wire Protocol Module (wire.rs)Date: 2026-02-15
Module: services/i2c/api/src/wire.rs
Reviewer: AI-assisted review
Status: Review complete — actionable findings
wire.rs defines the binary IPC wire protocol for I2C operations. It provides:
I2cOp)no_std compatible, zero-copy decodeRequest (8 bytes header + payload): ┌────┬─────┬──────┬─────┬──────────┬──────────┐ │ op │ bus │ addr │ res │ write_len│ read_len │ + [write data] │ 1B │ 1B │ 1B │ 1B │ 2B LE │ 2B LE │ └────┴─────┴──────┴─────┴──────────┴──────────┘ Response (4 bytes header + payload): ┌──────┬─────┬──────────┐ │ code │ res │ data_len │ + [read data] │ 1B │ 1B │ 2B LE │ └──────┴─────┴──────────┘
| Aspect | Detail |
|---|---|
| Compact layout | 8-byte request / 4-byte response headers with explicit reserved bytes for alignment — efficient for IPC on constrained targets |
const fn constructors | I2cRequestHeader::write(), ::read(), etc. allow compile-time header construction for static request tables |
| Explicit LE encoding | Uses to_le_bytes() / from_le_bytes() throughout — no reliance on platform endianness |
| Zero-copy decode | get_response_data() / get_request_payload() return &[u8] slices into the caller's buffer with correct lifetime annotations ('a) |
| Good test coverage | Roundtrip tests for both header types, encode helpers, and I2cOp::from_u8 edge cases |
| Clear documentation | ASCII wire diagram in module doc; doc comments on all public items |
No unsafe | Entirely safe Rust |
u8 Fields Bypass the Type SystemLocation: I2cRequestHeader (lines 80–90), I2cResponseHeader (lines 175–182)
Problem: The header structs store op: u8, address: u8, and code: u8 rather than the validated domain types I2cOp, I2cAddress, and ResponseCode that the crate already defines. This means:
I2cRequestHeader { op: 255, .. } — an invalid opcode with no compile-time or runtime error.address field completely bypasses I2cAddress validation (reserved-range checks, 7-bit range enforcement).I2cResponseHeader.code can hold values not in ResponseCode; the response_code() method silently maps unknowns to ServerError.operation() method returns Option<I2cOp>, proving the struct itself doesn't guarantee validity.Recommendation: Store the typed enums/newtypes in the struct fields. Convert to/from u8 only inside to_bytes() / from_bytes():
pub struct I2cRequestHeader { pub op: I2cOp, // was u8 pub bus: BusIndex, // was u8 pub address: I2cAddress, // was u8 pub write_len: u16, pub read_len: u16, } impl I2cRequestHeader { pub fn from_bytes(bytes: &[u8]) -> Result<Self, WireError> { // ... let op = I2cOp::from_u8(bytes[0]) .ok_or(WireError::InvalidOpcode(bytes[0]))?; // ... } }
This makes illegal states unrepresentable at the struct level.
as u16 TruncationLocation: Lines 280, 312, 370 — data.len() as u16
Problem: If data.len() > 65535, the cast silently wraps. The header will advertise a truncated length while the buffer contains more data, causing the receiver to misinterpret the payload boundary.
While unlikely on a small embedded target, this is a correctness bug — it violates the principle that encoding should either succeed faithfully or fail.
Recommendation (option A — fail-safe):
let write_len = u16::try_from(data.len()).ok()?;
Recommendation (option B — debug guard):
debug_assert!(data.len() <= u16::MAX as usize);
MAX_PAYLOAD_SIZE Declared But Never EnforcedLocation: Line 255 — pub const MAX_PAYLOAD_SIZE: usize = 256;
Problem: The constant is defined and documented but none of the encode_* functions check against it. A caller can encode a payload larger than 256 bytes; the serialization will succeed, but the receiver's fixed-size buffer will overflow or reject it.
Recommendation: Add validation at the top of each encode helper:
pub fn encode_write_request(buf: &mut [u8], bus: u8, address: u8, data: &[u8]) -> Option<usize> { if data.len() > MAX_PAYLOAD_SIZE { return None; // or Err(WireError::PayloadTooLarge) } // ... }
Option Return Type Loses Error InformationLocation: All encode/decode functions
Problem: Every function returns Option<...>. Callers cannot distinguish between:
MAX_PAYLOAD_SIZEu16 length overflowThis makes debugging and logging difficult in production.
Recommendation: Introduce a dedicated error enum:
#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WireError { /// Output buffer too small for the encoded message. BufferTooSmall, /// Payload exceeds MAX_PAYLOAD_SIZE. PayloadTooLarge, /// Unrecognized operation code during decode. InvalidOpcode(u8), /// Unrecognized response code during decode. InvalidResponseCode(u8), /// Input buffer too short to contain a complete header. Truncated, }
Change return types from Option<usize> to Result<usize, WireError>.
u8 ParametersLocation: Lines 268–330 — encode_write_request(), encode_read_request(), etc.
Problem: The free-standing helpers take bus: u8, address: u8 as raw bytes. This defeats the type safety provided by BusIndex and I2cAddress elsewhere in the crate. A caller can easily swap the two arguments without a compiler warning.
Recommendation: Accept the domain types:
pub fn encode_write_request( buf: &mut [u8], bus: BusIndex, address: I2cAddress, data: &[u8], ) -> Result<usize, WireError> { ... }
Location: Lines 331–358 — decode_response_header(), decode_request_header(), get_response_data(), get_request_payload()
Problem: These are trivial one-line delegates to existing methods:
pub fn decode_response_header(buf: &[u8]) -> Option<I2cResponseHeader> { I2cResponseHeader::from_bytes(buf) }
They add API surface without adding value. Callers can (and some already do) call the methods directly.
Recommendation: Remove these wrappers. If a flat-function facade is desired, mark them #[inline] and add a doc note explaining why they exist.
Location: I2cOp enum vs. encode helpers
Problem: I2cOp declares seven variants, but encode helpers exist for only four:
| Op | Constructor | Encode helper |
|---|---|---|
Write | ✅ | ✅ encode_write_request |
Read | ✅ | ✅ encode_read_request |
WriteRead | ✅ | ✅ encode_write_read_request |
Transaction | ❌ | ❌ |
Probe | ✅ | ✅ encode_probe_request |
ConfigureSpeed | ❌ | ❌ |
RecoverBus | ✅ | ❌ |
Callers targeting Transaction, ConfigureSpeed, or RecoverBus must manually construct headers, which is error-prone and inconsistent.
Recommendation: Either add the missing helpers or document that these ops are not yet supported/implemented.
Location: from_bytes() implementations
Problem: On decode, the reserved byte (bytes[3] in request, bytes[1] in response) is silently ignored. This is correct today, but if a future protocol version uses these bytes, old decoders will silently accept new-format messages without detecting the version mismatch.
Recommendation: Consider asserting bytes[reserved] == 0 during decode, or reserving one bit as a version flag for forward compatibility.
unsafe code — all operations are safe.Option/None for undersized buffers.buf[..] accesses are preceded by length checks.| # | Priority | Finding | Effort |
|---|---|---|---|
| 1 | High | 3.1 — Use typed enums/newtypes in header structs | Medium — requires updating all call sites |
| 2 | High | 3.2 — Replace as u16 with try_from | Small — mechanical change |
| 3 | Medium | 3.3 — Enforce MAX_PAYLOAD_SIZE in encode functions | Small |
| 4 | Medium | 3.4 — Introduce WireError enum | Medium — touches return types |
| 5 | Medium | 3.5 — Accept BusIndex/I2cAddress in helpers | Small — depends on 3.1 |
| 6 | Low | 3.6 — Remove redundant wrappers | Trivial |
| 7 | Low | 3.7 — Add missing encode helpers | Small |
| 8 | Low | 3.8 — Validate reserved bytes | Trivial |
The module is functional, safe, well-documented, and appropriate for its no_std IPC context. The primary weakness is that the wire layer bypasses the type safety the rest of the crate carefully establishes — accepting and storing raw u8 where validated domain types exist. Fixing findings 3.1 and 3.2 would bring the wire protocol in line with the crate's overall design philosophy of making illegal states unrepresentable. The remaining findings are incremental improvements.