Skip to main content

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]);