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