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// ===========================================================================
189// Fixed-size payload helpers
190// ===========================================================================
191
192/// Encode a fixed-size `repr(C)` struct as an [`IpcMessage`].
193///
194/// The body is written directly into `payload[0..size_of::<T>()]`.
195/// Panics in debug if `T` exceeds [`PAYLOAD_CAPACITY`]; in release the write
196/// silently truncates.
197///
198/// # Example
199///
200/// ```ignore
201/// use strat9_abi::ipc_codec::encode_fixed;
202///
203/// #[derive(FromBytes, IntoBytes)]
204/// #[repr(C)]
205/// struct StatReq { ino: u64, flags: u32 }
206///
207/// let msg = encode_fixed(0x10, &StatReq { ino: 42, flags: 0 });
208/// assert_eq!(msg.msg_type, 0x10);
209/// ```
210pub fn encode_fixed<T: IntoBytes + Immutable>(msg_type: u32, body: &T) -> IpcMessage {
211 let mut msg = IpcMessage::new(msg_type);
212 let src = body.as_bytes();
213 debug_assert!(src.len() <= PAYLOAD_CAPACITY, "encode_fixed: T too large");
214 let n = src.len().min(PAYLOAD_CAPACITY);
215 msg.payload[..n].copy_from_slice(&src[..n]);
216 msg
217}
218
219/// Encode a fixed-size reply targeting `sender`.
220///
221/// Same as [`encode_fixed`] but sets `msg.sender` for reply routing.
222pub fn encode_fixed_reply<T: IntoBytes + Immutable>(
223 sender: u64,
224 msg_type: u32,
225 body: &T,
226) -> IpcMessage {
227 let mut msg = encode_fixed(msg_type, body);
228 msg.sender = sender;
229 msg
230}
231
232/// Try to decode a fixed-size `repr(C)` struct from an [`IpcMessage`] payload.
233///
234/// Returns `None` if:
235/// - `T` is larger than [`PAYLOAD_CAPACITY`] (size overflow), or
236/// - the payload slice is not correctly aligned for `T` (alignment mismatch).
237///
238/// # Example
239///
240/// ```ignore
241/// use strat9_abi::ipc_codec::decode_fixed;
242///
243/// let msg: &IpcMessage = /* received message */;
244/// if let Some(reply) = decode_fixed::<MyReply>(msg) {
245/// println!("status: {}", reply.status);
246/// }
247/// ```
248pub fn decode_fixed<T: FromBytes + Immutable + KnownLayout>(
249 msg: &IpcMessage,
250) -> Option<&T> {
251 let size = core::mem::size_of::<T>();
252 if size > PAYLOAD_CAPACITY {
253 return None;
254 }
255 T::ref_from_bytes(&msg.payload[..size]).ok()
256}
257
258// ===========================================================================
259// InlineBlobHeader : minimal framing for variable-length inline data
260// ===========================================================================
261
262/// Minimal framing header for variable-length data embedded in an IPC payload.
263///
264/// Layout (4 bytes): `[len: u16, kind: u16]`.
265///
266/// - `len`: number of data bytes that follow this header.
267/// - `kind`: discriminator (e.g. `0` = path, `1` = blob data).
268///
269/// This lets you embed a variable-length segment in the 240-byte payload
270/// without external allocators or serde.
271///
272/// # Example
273///
274/// ```ignore
275/// use strat9_abi::ipc_codec::InlineBlobHeader;
276///
277/// let mut payload = [0u8; 240];
278/// // Write a file path at offset 4
279/// InlineBlobHeader::write(&mut payload, 4, 0, b"/etc/passwd").unwrap();
280///
281/// // Parse it back
282/// let hdr = InlineBlobHeader::parse(&payload, 4).unwrap();
283/// assert_eq!(hdr.len, 11); // "/etc/passwd".len()
284/// assert_eq!(hdr.kind, 0);
285/// ```
286#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, Immutable)]
287#[repr(C)]
288pub struct InlineBlobHeader {
289 /// Number of data bytes following this header.
290 pub len: u16,
291 /// Discriminator: 0 = path, 1 = blob data, etc.
292 pub kind: u16,
293}
294
295impl InlineBlobHeader {
296 /// Number of wire bytes consumed by the header itself.
297 pub const SIZE: usize = 4;
298
299 /// Write `InlineBlobHeader(len, kind)` followed by `data` into
300 /// `payload[off..]`.
301 ///
302 /// Returns `None` if the header + data do not fit in the payload slice.
303 pub fn write(payload: &mut [u8], off: usize, kind: u16, data: &[u8]) -> Option<()> {
304 let total = Self::SIZE.checked_add(data.len())?;
305 let end = off.checked_add(total)?;
306 let buf = payload.get_mut(off..end)?;
307 buf[..2].copy_from_slice(&(data.len() as u16).to_le_bytes());
308 buf[2..4].copy_from_slice(&kind.to_le_bytes());
309 buf[Self::SIZE..].copy_from_slice(data);
310 Some(())
311 }
312
313 /// Parse an `InlineBlobHeader` from `payload[off..]`.
314 ///
315 /// Returns `None` if the 4 header bytes are out of bounds.
316 pub fn parse(payload: &[u8], off: usize) -> Option<Self> {
317 let end = off.checked_add(Self::SIZE)?;
318 let buf = payload.get(off..end)?;
319 Some(Self {
320 len: u16::from_le_bytes([buf[0], buf[1]]),
321 kind: u16::from_le_bytes([buf[2], buf[3]]),
322 })
323 }
324
325 /// Return the total wire size of this header + its inline data.
326 pub fn total_size(&self) -> usize {
327 Self::SIZE + self.len as usize
328 }
329}
330
331static_assertions::assert_eq_size!(InlineBlobHeader, [u8; 4]);