strat9_abi/ipc_codec.rs
1//! Low-level helpers for encoding/decoding [`IpcMessage`] payloads.
2//!
3//! # Overview
4//!
5//! IPC messages carry a fixed 240-byte payload. This module provides
6//! safe, bounds-checked helpers to pack and unpack structured data
7//! into that payload without external allocators or serde.
8//!
9//! # Layers
10//!
11//! 1. **Scalar helpers** : `put_u16`/`get_u16`, `put_u32`/`get_u32`, `put_u64`/`get_u64`.
12//! 2. **Variable-length helpers** : `put_bytes`/`get_bytes`, `put_str`/`get_str`.
13//! 3. **Fixed-size helpers** : `encode_fixed`/`decode_fixed` for `repr(C)` zerocopy structs.
14//! 4. **`InlineBlobHeader`** : minimal framing for inline variable-length data.
15//!
16//! All helpers are bounds-checked: they return `Option` instead of panicking.
17//!
18//! # Payload layout conventions
19//!
20//! - **Fixed messages**: the entire `payload[0..48]` (or a prefix) is a
21//! `repr(C)` struct. Use `encode_fixed`/`decode_fixed`.
22//! - **Variable messages**: the fixed-size part goes first (e.g. flags + u64s),
23//! followed by an [`InlineBlobHeader`] (4 bytes) and the inline data.
24//! Use the put/get helpers for the fixed part, then `InlineBlobHeader::write`
25//! for the variable tail.
26//!
27//! # Safety
28//!
29//! All functions are pure safe-Rust: no `unsafe` required at this layer.
30//! The zerocopy traits used by payload structs are derived safely.
31//!
32//! # Examples
33//!
34//! ## Fixed-size message
35//!
36//! ```ignore
37//! use strat9_abi::data::IpcMessage;
38//! use strat9_abi::ipc_codec::{encode_fixed, decode_fixed};
39//!
40//! // Encode a struct into a message
41//! #[derive(FromBytes, IntoBytes)]
42//! #[repr(C)]
43//! struct OpenReq { flags: u32, mode: u32 }
44//!
45//! let msg = encode_fixed(0x01, &OpenReq { flags: 0x02, mode: 0o644 });
46//!
47//! // Decode it back
48//! let req: &OpenReq = decode_fixed(&msg).unwrap();
49//! assert_eq!(req.flags, 0x02);
50//! ```
51//!
52//! ## Variable-length message
53//!
54//! ```ignore
55//! use strat9_abi::ipc_codec::{put_u32, InlineBlobHeader};
56//!
57//! let mut msg = IpcMessage::new(0x03);
58//! put_u32(&mut msg.payload, 0, 0o755).unwrap(); // mode
59//! InlineBlobHeader::write(&mut msg.payload, 4, 0, b"/dev/null").unwrap();
60//! ```
61
62use crate::data::IpcMessage;
63use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
64
65/// Capacity of the `IpcMessage.payload` field (240 bytes).
66pub const PAYLOAD_CAPACITY: usize = IpcMessage::PAYLOAD_CAPACITY;
67
68// ===========================================================================
69// Scalar helpers : bounds-checked get/put for plain integer types
70// ===========================================================================
71
72/// Write a `u16` at offset `off` (little-endian).
73///
74/// Returns `None` if the write would overflow the payload.
75#[inline]
76pub fn put_u16(payload: &mut [u8], off: usize, v: u16) -> Option<()> {
77 let end = off.checked_add(2)?;
78 let buf = payload.get_mut(off..end)?;
79 buf.copy_from_slice(&v.to_le_bytes());
80 Some(())
81}
82
83/// Read a `u16` at offset `off` (little-endian), or `None` if out of bounds.
84#[inline]
85pub fn get_u16(payload: &[u8], off: usize) -> Option<u16> {
86 let end = off.checked_add(2)?;
87 let buf = payload.get(off..end)?;
88 Some(u16::from_le_bytes([buf[0], buf[1]]))
89}
90
91/// Write a `u32` at offset `off` (little-endian).
92///
93/// Returns `None` if the write would overflow the payload.
94#[inline]
95pub fn put_u32(payload: &mut [u8], off: usize, v: u32) -> Option<()> {
96 let end = off.checked_add(4)?;
97 let buf = payload.get_mut(off..end)?;
98 buf.copy_from_slice(&v.to_le_bytes());
99 Some(())
100}
101
102/// Read a `u32` at offset `off` (little-endian), or `None` if out of bounds.
103#[inline]
104pub fn get_u32(payload: &[u8], off: usize) -> Option<u32> {
105 let end = off.checked_add(4)?;
106 let buf = payload.get(off..end)?;
107 Some(u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]))
108}
109
110/// Write an `i32` at offset `off` (little-endian).
111///
112/// Convenience wrapper around [`put_u32`].
113#[inline]
114pub fn put_i32(payload: &mut [u8], off: usize, v: i32) -> Option<()> {
115 put_u32(payload, off, v as u32)
116}
117
118/// Read an `i32` at offset `off` (little-endian), or `None` if out of bounds.
119///
120/// Convenience wrapper around [`get_u32`].
121#[inline]
122pub fn get_i32(payload: &[u8], off: usize) -> Option<i32> {
123 Some(get_u32(payload, off)? as i32)
124}
125
126/// Write a `u64` at offset `off` (little-endian).
127///
128/// Returns `None` if the write would overflow the payload.
129#[inline]
130pub fn put_u64(payload: &mut [u8], off: usize, v: u64) -> Option<()> {
131 let end = off.checked_add(8)?;
132 let buf = payload.get_mut(off..end)?;
133 buf.copy_from_slice(&v.to_le_bytes());
134 Some(())
135}
136
137/// Read a `u64` at offset `off` (little-endian), or `None` if out of bounds.
138#[inline]
139pub fn get_u64(payload: &[u8], off: usize) -> Option<u64> {
140 let end = off.checked_add(8)?;
141 let buf = payload.get(off..end)?;
142 Some(u64::from_le_bytes([
143 buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7],
144 ]))
145}
146
147// ===========================================================================
148// Variable-length helpers
149// ===========================================================================
150
151/// Copy `src` into `payload[off..off + src.len()]`.
152///
153/// Returns `None` if the slice does not fit (overflow or out of bounds).
154#[inline]
155pub fn put_bytes(payload: &mut [u8], off: usize, src: &[u8]) -> Option<()> {
156 let end = off.checked_add(src.len())?;
157 let dst = payload.get_mut(off..end)?;
158 dst.copy_from_slice(src);
159 Some(())
160}
161
162/// Return a reference to `payload[off..off + len]`.
163///
164/// Returns `None` if the range is out of bounds.
165#[inline]
166pub fn get_bytes(payload: &[u8], off: usize, len: usize) -> Option<&[u8]> {
167 let end = off.checked_add(len)?;
168 payload.get(off..end)
169}
170
171/// Encode a UTF-8 string into `payload[off..]`.
172///
173/// Returns `None` if the string does not fit.
174#[inline]
175pub fn put_str(payload: &mut [u8], off: usize, s: &str) -> Option<()> {
176 put_bytes(payload, off, s.as_bytes())
177}
178
179/// Decode a UTF-8 string of `len` bytes from `payload[off..]`.
180///
181/// Returns `None` if out of bounds or invalid UTF-8.
182#[inline]
183pub fn get_str(payload: &[u8], off: usize, len: usize) -> Option<&str> {
184 let bytes = get_bytes(payload, off, len)?;
185 core::str::from_utf8(bytes).ok()
186}
187
188/// Write a raw `u16` length prefix followed by the bytes at `payload[off..]`
189/// (the common `[len: u16][data...]` framing used by OPEN/CREATE/UNLINK).
190///
191/// Returns `None` if the data does not fit (`> u16::MAX` or out of bounds).
192#[inline]
193pub fn put_u16_len_prefixed(payload: &mut [u8], off: usize, data: &[u8]) -> Option<()> {
194 if data.len() > u16::MAX as usize {
195 return None;
196 }
197 put_u16(payload, off, data.len() as u16)?;
198 put_bytes(payload, off + 2, data)
199}
200
201// ===========================================================================
202// Fixed-size payload helpers
203// ===========================================================================
204
205/// Encode a fixed-size `repr(C)` struct as an [`IpcMessage`].
206///
207/// The body is written directly into `payload[0..size_of::<T>()]`.
208///
209/// # Panics
210///
211/// Panics if `T` exceeds [`PAYLOAD_CAPACITY`]. This is a developer error
212/// (an oversized ABI struct), never an attacker-controlled condition:
213/// failing loudly is preferable to silently emitting a truncated message.
214///
215/// # Example
216///
217/// ```ignore
218/// use strat9_abi::ipc_codec::encode_fixed;
219///
220/// #[derive(FromBytes, IntoBytes)]
221/// #[repr(C)]
222/// struct StatReq { ino: u64, flags: u32 }
223///
224/// let msg = encode_fixed(0x10, &StatReq { ino: 42, flags: 0 });
225/// assert_eq!(msg.msg_type, 0x10);
226/// ```
227pub fn encode_fixed<T: IntoBytes + Immutable>(msg_type: u32, body: &T) -> IpcMessage {
228 let mut msg = IpcMessage::new(msg_type);
229 let src = body.as_bytes();
230 assert!(
231 src.len() <= PAYLOAD_CAPACITY,
232 "encode_fixed: struct of {} bytes exceeds payload capacity {PAYLOAD_CAPACITY}",
233 src.len()
234 );
235 msg.payload[..src.len()].copy_from_slice(src);
236 msg
237}
238
239/// Encode a fixed-size reply targeting `sender`.
240///
241/// Same as [`encode_fixed`] but sets `msg.sender` for reply routing.
242pub fn encode_fixed_reply<T: IntoBytes + Immutable>(
243 sender: u64,
244 msg_type: u32,
245 body: &T,
246) -> IpcMessage {
247 let mut msg = encode_fixed(msg_type, body);
248 msg.sender = sender;
249 msg
250}
251
252/// Try to decode a fixed-size `repr(C)` struct from an [`IpcMessage`] payload.
253///
254/// Returns `None` if:
255/// - `T` is larger than [`PAYLOAD_CAPACITY`] (size overflow), or
256/// - the payload slice is not correctly aligned for `T` (alignment mismatch).
257///
258/// # Example
259///
260/// ```ignore
261/// use strat9_abi::ipc_codec::decode_fixed;
262///
263/// let msg: &IpcMessage = /* received message */;
264/// if let Some(reply) = decode_fixed::<MyReply>(msg) {
265/// println!("status: {}", reply.status);
266/// }
267/// ```
268pub fn decode_fixed<T: FromBytes + Immutable + KnownLayout>(msg: &IpcMessage) -> Option<&T> {
269 let size = core::mem::size_of::<T>();
270 if size > PAYLOAD_CAPACITY {
271 return None;
272 }
273 T::ref_from_bytes(&msg.payload[..size]).ok()
274}
275
276// ===========================================================================
277// InlineBlobHeader : minimal framing for variable-length inline data
278// ===========================================================================
279
280/// Minimal framing header for variable-length data embedded in an IPC payload.
281///
282/// Layout (4 bytes): `[len: u16, kind: u16]`.
283///
284/// - `len`: number of data bytes that follow this header.
285/// - `kind`: discriminator (e.g. `0` = path, `1` = blob data).
286///
287/// This lets you embed a variable-length segment in the 240-byte payload
288/// without external allocators or serde.
289///
290/// # Example
291///
292/// ```ignore
293/// use strat9_abi::ipc_codec::InlineBlobHeader;
294///
295/// let mut payload = [0u8; 240];
296/// // Write a file path at offset 4
297/// InlineBlobHeader::write(&mut payload, 4, 0, b"/etc/passwd").unwrap();
298///
299/// // Parse it back
300/// let hdr = InlineBlobHeader::parse(&payload, 4).unwrap();
301/// assert_eq!(hdr.len, 11); // "/etc/passwd".len()
302/// assert_eq!(hdr.kind, 0);
303/// ```
304#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, Immutable)]
305#[repr(C)]
306pub struct InlineBlobHeader {
307 /// Number of data bytes following this header.
308 pub len: u16,
309 /// Discriminator: 0 = path, 1 = blob data, etc.
310 pub kind: u16,
311}
312
313impl InlineBlobHeader {
314 /// Number of wire bytes consumed by the header itself.
315 pub const SIZE: usize = 4;
316
317 /// Write `InlineBlobHeader(len, kind)` followed by `data` into
318 /// `payload[off..]`.
319 ///
320 /// Returns `None` if:
321 /// - `data` is larger than `u16::MAX` bytes (`len` field would truncate), or
322 /// - the header + data do not fit in the payload slice.
323 pub fn write(payload: &mut [u8], off: usize, kind: u16, data: &[u8]) -> Option<()> {
324 if data.len() > u16::MAX as usize {
325 // The `len` field is u16: writing more bytes than representable
326 // would silently corrupt the framing.
327 return None;
328 }
329 let total = Self::SIZE.checked_add(data.len())?;
330 let end = off.checked_add(total)?;
331 let buf = payload.get_mut(off..end)?;
332 buf[..2].copy_from_slice(&(data.len() as u16).to_le_bytes());
333 buf[2..4].copy_from_slice(&kind.to_le_bytes());
334 buf[Self::SIZE..].copy_from_slice(data);
335 Some(())
336 }
337
338 /// Parse an `InlineBlobHeader` from `payload[off..]`.
339 ///
340 /// Returns `None` if the 4 header bytes are out of bounds.
341 pub fn parse(payload: &[u8], off: usize) -> Option<Self> {
342 let end = off.checked_add(Self::SIZE)?;
343 let buf = payload.get(off..end)?;
344 Some(Self {
345 len: u16::from_le_bytes([buf[0], buf[1]]),
346 kind: u16::from_le_bytes([buf[2], buf[3]]),
347 })
348 }
349
350 /// Return the total wire size of this header + its inline data.
351 pub fn total_size(&self) -> usize {
352 Self::SIZE + self.len as usize
353 }
354}
355
356static_assertions::assert_eq_size!(InlineBlobHeader, [u8; 4]);