Skip to main content

strat9_abi/
ipc_payload.rs

1//! Typed `repr(C)` payload structs for fixed-size IPC messages.
2//!
3//! Each struct derives zerocopy's `FromBytes + IntoBytes + Immutable`,
4//! enabling safe zero-cost cast to/from [`IpcMessage`] payloads via
5//! [`encode_fixed`] / [`decode_fixed`].
6//!
7//! This module also defines the shared VFS scheme opcodes (`OPCODE_*`) used
8//! as `msg_type` values on the wire: scheme servers must import them from
9//! here instead of redeclaring local copies.
10//!
11//! # Conventions
12//!
13//! - **Fixed-size structs** are kept ≤ 48 bytes by convention : well under
14//!   `IpcMessage::PAYLOAD_CAPACITY` (240) : so they can be embedded in
15//!   messages that also carry an inline blob.
16//! - Variable-length path/data fields use an [`InlineBlobHeader`] prefix
17//!   and may exploit the full 240-byte payload capacity.
18//! - `status == 0` means success; non-zero is an errno-compatible error code.
19//! - Padding fields are named `_pad` or `_reserved` and must be zero.
20//!
21//! # Wire format
22//!
23//! Messages are sent through IPC ports or channels as raw bytes.
24//! The `msg_type` field in the `IpcMessage` header identifies the operation.
25//! The `payload` field contains the struct-specific data.
26//!
27//! # Example
28//!
29//! ```ignore
30//! use strat9_abi::ipc_payload::{OpenRequest, OpenReply, OPCODE_OPEN};
31//!
32//! // Encode an open request with path "/tmp/test"
33//! let msg = OpenRequest::encode(OPCODE_OPEN, 0x02, "/tmp/test").unwrap();
34//!
35//! // A server parses the request...
36//! let (flags, path) = OpenRequest::parse(&msg.payload).unwrap();
37//! assert_eq!(path, "/tmp/test");
38//!
39//! // ...and builds a typed reply, which the client parses back.
40//! let reply = OpenReply {
41//!     status: 0,
42//!     file_id: 42,
43//!     size: 1024,
44//!     file_flags: 0x01,
45//! };
46//! ```
47
48use zerocopy::{FromBytes, Immutable, IntoBytes};
49
50use crate::{data::IpcMessage, ipc_codec::InlineBlobHeader};
51
52// ===========================================================================
53// Helpers for compile-time size checks
54// ===========================================================================
55
56macro_rules! assert_payload_size {
57    ($ty:ty) => {
58        static_assertions::const_assert!(core::mem::size_of::<$ty>() <= 48);
59    };
60}
61
62// ===========================================================================
63// VFS scheme protocol : message types and flags
64// ===========================================================================
65
66/// Wire `msg_type` for an open request.
67pub const OPCODE_OPEN: u32 = 0x01;
68/// Wire `msg_type` for a read request.
69pub const OPCODE_READ: u32 = 0x02;
70/// Wire `msg_type` for a write request.
71pub const OPCODE_WRITE: u32 = 0x03;
72/// Wire `msg_type` for a close request.
73pub const OPCODE_CLOSE: u32 = 0x04;
74/// Wire `msg_type` for a readdir request.
75pub const OPCODE_READDIR: u32 = 0x08;
76/// Wire `msg_type` for a create-file request.
77pub const OPCODE_CREATE_FILE: u32 = 0x05;
78/// Wire `msg_type` for a create-directory request.
79pub const OPCODE_CREATE_DIR: u32 = 0x06;
80/// Wire `msg_type` for an unlink request.
81pub const OPCODE_UNLINK: u32 = 0x07;
82
83// ===========================================================================
84// Generic status-only reply (used by every scheme)
85// ===========================================================================
86
87/// Minimal reply carrying only a status code.
88///
89/// Used by scheme handlers that don't need to return additional data.
90///
91/// Wire layout: `status @ 0..4`.
92///
93/// # Example
94///
95/// ```ignore
96/// let reply = StatusReply { status: 0 }; // success
97/// let err = StatusReply { status: 13 };  // EACCES
98/// ```
99#[repr(C)]
100#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, Immutable)]
101pub struct StatusReply {
102    /// Status code: 0 = success, non-zero = errno.
103    pub status: u32,
104}
105assert_payload_size!(StatusReply);
106
107// ===========================================================================
108// VFS / file-system scheme payloads
109// ===========================================================================
110
111/// Open request.
112///
113/// Wire layout:
114///   `flags @ 0..4`, `path_len @ 4..6`, `path_data @ 6..`.
115///
116/// # Wire-compatibility note
117///
118/// Unlike most variable-length fields, the open path is prefixed by a raw
119/// `u16` length : **not** an [`InlineBlobHeader`] (there is no `kind`
120/// field). This historical layout is kept because every existing client
121/// (kernel VFS, net and bus schemes) encodes it this way. Earlier versions
122/// of this documentation wrongly claimed an `InlineBlobHeader` at 4..8;
123/// that wire format never existed.
124#[repr(C, packed(1))]
125#[derive(Debug, Clone, Copy)]
126pub struct OpenRequest {
127    /// Open flags (`O_RDONLY`, `O_WRONLY`, `O_RDWR`, `O_CREAT`, etc.).
128    pub flags: u32,
129    /// Length in bytes of the UTF-8 path starting at offset 6.
130    pub path_len: u16,
131}
132static_assertions::assert_eq_size!(OpenRequest, [u8; 6]);
133
134impl OpenRequest {
135    /// Byte offset at which the inline path starts.
136    ///
137    /// NOTE: this struct is `packed(1)` so its size matches the wire
138    /// prefix exactly (no tail padding). Never take references to its
139    /// fields; read them by copy or use [`OpenRequest::parse`], which
140    /// decodes straight from the payload bytes.
141    pub const PATH_OFFSET: usize = 6;
142
143    /// Parse a full OPEN request payload: fixed prefix + inline path.
144    ///
145    /// Returns `(flags, path)`, or `None` when:
146    /// - the fixed prefix is truncated,
147    /// - `path_len` exceeds the remaining payload,
148    /// - the path bytes are not valid UTF-8.
149    pub fn parse(payload: &[u8]) -> Option<(u32, &str)> {
150        if payload.len() < Self::PATH_OFFSET {
151            return None;
152        }
153        let flags = u32::from_le_bytes(payload[0..4].try_into().ok()?);
154        let path_len = u16::from_le_bytes(payload[4..6].try_into().ok()?) as usize;
155        let path = crate::ipc_codec::get_str(payload, Self::PATH_OFFSET, path_len)?;
156        Some((flags, path))
157    }
158
159    /// Encode an OPEN request into a fresh [`IpcMessage`].
160    /// Returns `None` if the path does not fit the inline capacity.
161    pub fn encode(msg_type: u32, flags: u32, path: &str) -> Option<IpcMessage> {
162        if path.len() > IpcMessage::OPEN_INLINE_CAPACITY {
163            return None;
164        }
165        let mut msg = IpcMessage::new(msg_type);
166        msg.payload[0..4].copy_from_slice(&flags.to_le_bytes());
167        msg.payload[4..6].copy_from_slice(&(path.len() as u16).to_le_bytes());
168        msg.payload[6..6 + path.len()].copy_from_slice(path.as_bytes());
169        Some(msg)
170    }
171}
172
173/// Open reply.
174///
175/// Wire layout:
176///   `status @ 0..4`, `file_id @ 4..12`, `size @ 12..20`, `file_flags @ 20..24`.
177///
178/// # Wire-compatibility note
179///
180/// Earlier documentation wrongly claimed `_pad0 @ 4..8`, `ino @ 8..16`,
181/// `size @ 16..24`, `file_flags @ 24..28` (32 bytes); no server ever
182/// produced that layout. The real layout above is what every scheme
183/// server (net, bus) emits and what the kernel VFS client parses.
184#[repr(C, packed(1))]
185#[derive(Debug, Clone, Copy)]
186pub struct OpenReply {
187    /// Status code: 0 = success.
188    pub status: u32,
189    /// Handle/id of the opened file.
190    pub file_id: u64,
191    /// File size in bytes (`u64::MAX` = size unknown).
192    pub size: u64,
193    /// File flags (device, pipe, chunked read/write, directory...).
194    pub file_flags: u32,
195}
196static_assertions::assert_eq_size!(OpenReply, [u8; 24]);
197
198impl OpenReply {
199    /// Byte at which this struct starts in a reply payload.
200    pub const OFFSET: usize = 0;
201
202    /// Parse an OPEN reply payload.
203    /// Returns `None` if the reply is truncated.
204    pub fn parse(payload: &[u8]) -> Option<Self> {
205        if payload.len() < core::mem::size_of::<Self>() {
206            return None;
207        }
208        Some(Self {
209            status: u32::from_le_bytes(payload[0..4].try_into().ok()?),
210            file_id: u64::from_le_bytes(payload[4..12].try_into().ok()?),
211            size: u64::from_le_bytes(payload[12..20].try_into().ok()?),
212            file_flags: u32::from_le_bytes(payload[20..24].try_into().ok()?),
213        })
214    }
215
216    /// Encode this reply into `payload[0..24]`.
217    pub fn encode_into(&self, payload: &mut [u8]) {
218        payload[0..4].copy_from_slice(&self.status.to_le_bytes());
219        payload[4..12].copy_from_slice(&self.file_id.to_le_bytes());
220        payload[12..20].copy_from_slice(&self.size.to_le_bytes());
221        payload[20..24].copy_from_slice(&self.file_flags.to_le_bytes());
222    }
223}
224
225/// Read request.
226///
227/// Wire layout:
228///   `ino @ 0..8`, `offset @ 8..16`, `count @ 16..20`, `_pad @ 20..24`.
229#[repr(C)]
230#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, Immutable)]
231pub struct ReadRequest {
232    /// Inode number of the file to read.
233    pub ino: u64,
234    /// Byte offset in the file to start reading from.
235    pub offset: u64,
236    /// Maximum number of bytes to read.
237    pub count: u32,
238    pub _pad: u32,
239}
240assert_payload_size!(ReadRequest);
241static_assertions::assert_eq_size!(ReadRequest, [u8; 24]);
242
243impl ReadRequest {
244    /// Parse a READ request payload. Returns `None` if truncated.
245    pub fn parse(payload: &[u8]) -> Option<Self> {
246        if payload.len() < core::mem::size_of::<Self>() {
247            return None;
248        }
249        Some(Self {
250            ino: u64::from_le_bytes(payload[0..8].try_into().ok()?),
251            offset: u64::from_le_bytes(payload[8..16].try_into().ok()?),
252            count: u32::from_le_bytes(payload[16..20].try_into().ok()?),
253            _pad: 0,
254        })
255    }
256
257    /// Encode a READ request into a fresh [`IpcMessage`].
258    pub fn encode(msg_type: u32, ino: u64, offset: u64, count: u32) -> IpcMessage {
259        let mut msg = IpcMessage::new(msg_type);
260        msg.payload[0..8].copy_from_slice(&ino.to_le_bytes());
261        msg.payload[8..16].copy_from_slice(&offset.to_le_bytes());
262        msg.payload[16..20].copy_from_slice(&count.to_le_bytes());
263        msg
264    }
265
266    /// Number of requested bytes (convenience over `count`).
267    pub fn count_usize(&self) -> usize {
268        self.count as usize
269    }
270}
271
272/// Read reply prefix (variable-length data follows at offset 8).
273///
274/// Wire layout:
275///   `status @ 0..4`, `count @ 4..8`, `data @ 8..`.
276///
277/// The `data` field contains `count` bytes of file content.
278#[repr(C)]
279#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, Immutable)]
280pub struct ReadReply {
281    /// Status code: 0 = success.
282    pub status: u32,
283    /// Number of data bytes written starting at offset 8.
284    pub count: u32,
285}
286assert_payload_size!(ReadReply);
287
288impl ReadReply {
289    /// Byte at which inline data starts in a READ reply payload.
290    pub const DATA_OFFSET: usize = 8;
291
292    /// Encode a successful READ reply: prefix + up to
293    /// `IpcMessage::READ_INLINE_CAPACITY` data bytes.
294    /// Returns the message and the number of data bytes packed.
295    pub fn encode_ok(sender: u64, data: &[u8]) -> (IpcMessage, usize) {
296        let mut msg = IpcMessage::new(IpcMessage::REPLY_MSG_TYPE);
297        msg.sender = sender;
298        let n = data.len().min(IpcMessage::READ_INLINE_CAPACITY);
299        let reply = Self {
300            status: 0,
301            count: n as u32,
302        };
303        msg.payload[0..4].copy_from_slice(&reply.status.to_le_bytes());
304        msg.payload[4..8].copy_from_slice(&reply.count.to_le_bytes());
305        msg.payload[8..8 + n].copy_from_slice(&data[..n]);
306        (msg, n)
307    }
308}
309
310/// Write request with variable-length inline data.
311///
312/// Wire layout:
313///   `ino @ 0..8`, `offset @ 8..16`, `data_len @ 16..18`, `data @ 18..`.
314///
315/// # Wire-compatibility note
316///
317/// Same as [`OpenRequest`]: a raw `u16` length prefix, **not** an
318/// [`InlineBlobHeader`]. Earlier documentation wrongly claimed an
319/// `InlineBlobHeader` + pad at 16..24 with data at 24; no component ever
320/// encoded that format.
321#[repr(C, packed(1))]
322#[derive(Debug, Clone, Copy)]
323pub struct WriteRequest {
324    /// Inode number of the file to write.
325    pub ino: u64,
326    /// Byte offset in the file to start writing.
327    pub offset: u64,
328    /// Number of data bytes starting at offset 18.
329    pub data_len: u16,
330}
331static_assertions::assert_eq_size!(WriteRequest, [u8; 18]);
332
333impl WriteRequest {
334    /// Byte offset at which the inline data starts.
335    ///
336    /// NOTE: `packed(1)` so size matches the wire prefix exactly.
337    /// Read fields by copy only (see [`OpenRequest`]).
338    pub const DATA_OFFSET: usize = 18;
339
340    /// Parse the fixed prefix of a WRITE request payload.
341    /// Returns `None` if the prefix is truncated.
342    pub fn parse_prefix(payload: &[u8]) -> Option<Self> {
343        if payload.len() < Self::DATA_OFFSET {
344            return None;
345        }
346        Some(Self {
347            ino: u64::from_le_bytes(payload[0..8].try_into().ok()?),
348            offset: u64::from_le_bytes(payload[8..16].try_into().ok()?),
349            data_len: u16::from_le_bytes(payload[16..18].try_into().ok()?),
350        })
351    }
352
353    /// Return the inline data following the prefix, bounded by `data_len`.
354    pub fn data<'a>(&self, payload: &'a [u8]) -> Option<&'a [u8]> {
355        crate::ipc_codec::get_bytes(payload, Self::DATA_OFFSET, self.data_len as usize)
356    }
357
358    /// Encode a WRITE request into a fresh [`IpcMessage`].
359    ///
360    /// Returns `None` if `data` exceeds [`IpcMessage::WRITE_INLINE_CAPACITY`]
361    /// : chunking is the caller's policy, this helper never truncates.
362    /// Otherwise returns the message and the packed length (== `data.len()`).
363    pub fn encode(
364        msg_type: u32,
365        ino: u64,
366        offset: u64,
367        data: &[u8],
368    ) -> Option<(IpcMessage, usize)> {
369        if data.len() > IpcMessage::WRITE_INLINE_CAPACITY {
370            return None;
371        }
372        let mut msg = IpcMessage::new(msg_type);
373        msg.payload[0..8].copy_from_slice(&ino.to_le_bytes());
374        msg.payload[8..16].copy_from_slice(&offset.to_le_bytes());
375        msg.payload[16..18].copy_from_slice(&(data.len() as u16).to_le_bytes());
376        msg.payload[18..18 + data.len()].copy_from_slice(data);
377        Some((msg, data.len()))
378    }
379}
380
381/// Write reply.
382///
383/// Wire layout: `status @ 0..4`, `written @ 4..8`.
384#[repr(C)]
385#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, Immutable)]
386pub struct WriteReply {
387    /// Status code: 0 = success.
388    pub status: u32,
389    /// Number of bytes actually written.
390    pub written: u32,
391}
392assert_payload_size!(WriteReply);
393
394impl WriteReply {
395    /// Encode a successful WRITE reply carrying the written count.
396    pub fn encode_ok(sender: u64, written: usize) -> IpcMessage {
397        let mut msg = IpcMessage::new(IpcMessage::REPLY_MSG_TYPE);
398        msg.sender = sender;
399        let reply = Self {
400            status: 0,
401            written: written as u32,
402        };
403        msg.payload[0..4].copy_from_slice(&reply.status.to_le_bytes());
404        msg.payload[4..8].copy_from_slice(&reply.written.to_le_bytes());
405        msg
406    }
407}
408
409/// Create request (file or directory).
410///
411/// Wire layout:
412///   `mode @ 0..4`, `path_len @ 4..6`, `path @ 6..`.
413///
414/// # Wire-compatibility note
415///
416/// Same raw `u16` length prefix as [`OpenRequest`] (no InlineBlobHeader).
417#[repr(C, packed(1))]
418#[derive(Debug, Clone, Copy)]
419pub struct CreateRequest {
420    /// Permission mode (e.g. `0o644` for files, `0o755` for directories).
421    pub mode: u32,
422    /// Length in bytes of the UTF-8 path starting at offset 6.
423    pub path_len: u16,
424}
425static_assertions::assert_eq_size!(CreateRequest, [u8; 6]);
426
427impl CreateRequest {
428    /// Byte offset at which the inline path starts.
429    pub const PATH_OFFSET: usize = 6;
430
431    /// Parse a CREATE request payload: `(mode, path)`.
432    pub fn parse(payload: &[u8]) -> Option<(u32, &str)> {
433        if payload.len() < Self::PATH_OFFSET {
434            return None;
435        }
436        let mode = u32::from_le_bytes(payload[0..4].try_into().ok()?);
437        let path_len = u16::from_le_bytes(payload[4..6].try_into().ok()?) as usize;
438        let path = crate::ipc_codec::get_str(payload, Self::PATH_OFFSET, path_len)?;
439        Some((mode, path))
440    }
441
442    /// Encode a CREATE request into a fresh [`IpcMessage`].
443    /// Returns `None` if the path does not fit the inline capacity.
444    pub fn encode(msg_type: u32, mode: u32, path: &str) -> Option<IpcMessage> {
445        if path.len() > IpcMessage::OPEN_INLINE_CAPACITY {
446            return None;
447        }
448        let mut msg = IpcMessage::new(msg_type);
449        msg.payload[0..4].copy_from_slice(&mode.to_le_bytes());
450        msg.payload[4..6].copy_from_slice(&(path.len() as u16).to_le_bytes());
451        msg.payload[6..6 + path.len()].copy_from_slice(path.as_bytes());
452        Some(msg)
453    }
454}
455
456/// Create reply.
457///
458/// Wire layout:
459///   `status @ 0..4`, `ino @ 4..12`.
460///
461/// # Wire-compatibility note
462///
463/// Earlier documentation claimed `_pad @ 4..8`, `ino @ 8..16`; the real
464/// wire packs the inode directly after the status (what the kernel VFS
465/// client parses).
466#[repr(C, packed(1))]
467#[derive(Debug, Clone, Copy)]
468pub struct CreateReply {
469    /// Status code: 0 = success.
470    pub status: u32,
471    /// Inode number of the newly created file/directory.
472    pub ino: u64,
473}
474static_assertions::assert_eq_size!(CreateReply, [u8; 12]);
475
476/// Close request.
477///
478/// Wire layout: `ino @ 0..8`.
479#[repr(C)]
480#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, Immutable)]
481pub struct CloseRequest {
482    /// Inode number of the file to close.
483    pub ino: u64,
484}
485assert_payload_size!(CloseRequest);
486
487impl CloseRequest {
488    /// Encode a CLOSE request into a fresh [`IpcMessage`].
489    pub fn encode(msg_type: u32, ino: u64) -> IpcMessage {
490        let mut msg = IpcMessage::new(msg_type);
491        msg.payload[0..8].copy_from_slice(&ino.to_le_bytes());
492        msg
493    }
494}
495
496/// Stat request.
497///
498/// Wire layout: `ino @ 0..8`.
499#[repr(C)]
500#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, Immutable)]
501pub struct StatRequest {
502    /// Inode number to query.
503    pub ino: u64,
504}
505assert_payload_size!(StatRequest);
506
507/// Lseek request.
508///
509/// Wire layout: `ino @ 0..8`, `offset @ 8..16`, `whence @ 16..20`, `_pad @ 20..24`.
510#[repr(C)]
511#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, Immutable)]
512pub struct LseekRequest {
513    /// Inode number of the file.
514    pub ino: u64,
515    /// Offset relative to `whence` (can be negative).
516    pub offset: i64,
517    /// Whence: `SEEK_SET` (0), `SEEK_CUR` (1), or `SEEK_END` (2).
518    pub whence: u32,
519    pub _pad: u32,
520}
521assert_payload_size!(LseekRequest);
522static_assertions::assert_eq_size!(LseekRequest, [u8; 24]);
523
524/// Lseek reply.
525///
526/// Wire layout:
527///   `status @ 0..4`, `_pad @ 4..8`, `offset @ 8..16`.
528#[repr(C)]
529#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, Immutable)]
530pub struct LseekReply {
531    /// Status code: 0 = success.
532    pub status: u32,
533    pub _pad: u32,
534    /// New file position (absolute offset from beginning of file).
535    pub offset: i64,
536}
537assert_payload_size!(LseekReply);
538static_assertions::assert_eq_size!(LseekReply, [u8; 16]);
539
540// ===========================================================================
541// Network scheme payloads
542// ===========================================================================
543
544/// TCP connect request.
545///
546/// Wire layout:
547///   `port @ 0..4`, `addr_hdr @ 4..8`, `addr_data @ 8..`.
548///
549/// The address is variable-length and follows the header at offset 8.
550/// For IPv4: 4 bytes of network-order octets.
551/// For IPv6: 16 bytes of network-order octets.
552#[repr(C)]
553#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, Immutable)]
554pub struct TcpConnectRequest {
555    /// Target port number (host byte order).
556    pub port: u16,
557    pub _padding: [u8; 2],
558    /// InlineBlobHeader describing the address that follows.
559    pub addr_hdr: InlineBlobHeader,
560}
561assert_payload_size!(TcpConnectRequest);
562
563/// TCP connect reply.
564///
565/// Wire layout:
566///   `status @ 0..4`, `_pad @ 4..8`, `conn_id @ 8..16`.
567#[repr(C)]
568#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, Immutable)]
569pub struct TcpConnectReply {
570    /// Status code: 0 = success.
571    pub status: u32,
572    pub _pad: u32,
573    /// Connection identifier for subsequent read/write operations.
574    pub conn_id: u64,
575}
576assert_payload_size!(TcpConnectReply);
577static_assertions::assert_eq_size!(TcpConnectReply, [u8; 16]);