Skip to main content

ninep_proto/
protocol.rs

1use crate::slice_codec::{self, CodecError, LockType};
2use crate::wire_types::*;
3use alloc::vec::Vec;
4use bytes::{Buf, Bytes};
5#[path = "owned_codec.rs"]
6mod owned_codec;
7
8/// Supported semantic operation encoded by the Linux fallocate mode bits.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum FallocateKind {
11    Allocate,
12    PunchHole,
13    ZeroRange { keep_size: bool },
14}
15
16/// Parse the exact fallocate mode combinations supported by ZeroFS.
17pub fn classify_fallocate_mode(mode: u32) -> Option<FallocateKind> {
18    match mode {
19        0 => Some(FallocateKind::Allocate),
20        mode if mode == (FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE) => {
21            Some(FallocateKind::PunchHole)
22        }
23        FALLOC_FL_ZERO_RANGE => Some(FallocateKind::ZeroRange { keep_size: false }),
24        mode if mode == (FALLOC_FL_ZERO_RANGE | FALLOC_FL_KEEP_SIZE) => {
25            Some(FallocateKind::ZeroRange { keep_size: true })
26        }
27        _ => None,
28    }
29}
30
31pub const P9_LOCK_FLAGS_BLOCK: u32 = 1; // blocking request
32
33pub const P9_CHANNEL_SIZE: usize = 1000;
34pub const P9_DEBUG_BUFFER_SIZE: usize = 40;
35pub const P9_READDIR_BATCH_SIZE: usize = 1000;
36pub const P9_NOBODY_UID: u32 = 65534;
37
38/// Userspace string backed by shared frame storage.
39pub type P9String = WireString<Bytes>;
40pub type P9Bytes = WireBytes<Bytes>;
41
42impl WireString<Bytes> {
43    pub fn new(data: impl Into<Bytes>) -> Self {
44        Self::from_storage(data.into())
45    }
46}
47
48impl From<Vec<u8>> for WireBytes<Bytes> {
49    fn from(value: Vec<u8>) -> Self {
50        Self(Bytes::from(value))
51    }
52}
53
54impl From<WireBytes<Bytes>> for Bytes {
55    fn from(value: WireBytes<Bytes>) -> Self {
56        value.0
57    }
58}
59
60#[derive(Debug, Clone)]
61pub struct DirEntry {
62    pub qid: Qid,
63    pub offset: u64,
64    pub type_: u8,
65    pub name: P9String,
66}
67
68impl DirEntry {
69    /// Serialized wire size.
70    pub fn wire_size(&self) -> usize {
71        Qid::WIRE_SIZE + 8 + 1 + self.name.wire_size()
72    }
73}
74
75#[derive(Debug, Clone)]
76pub struct Tversion {
77    pub msize: u32,
78    pub version: P9String,
79}
80
81#[derive(Debug, Clone)]
82pub struct Tattach {
83    pub fid: u32,
84    pub afid: u32,
85    pub uname: P9String,
86    pub aname: P9String,
87    pub n_uname: u32,
88}
89
90#[derive(Debug, Clone)]
91pub struct Twalk {
92    pub fid: u32,
93    pub newfid: u32,
94    pub nwname: u16,
95    pub wnames: Vec<P9String>,
96}
97
98#[derive(Debug, Clone)]
99pub struct Tlopen {
100    pub fid: u32,
101    pub flags: u32,
102}
103
104#[derive(Debug, Clone)]
105pub struct Tlcreate {
106    pub fid: u32,
107    pub name: P9String,
108    pub flags: u32,
109    pub mode: u32,
110    pub gid: u32,
111}
112
113#[derive(Debug, Clone)]
114pub struct Tread {
115    pub fid: u32,
116    pub offset: u64,
117    pub count: u32,
118}
119
120#[derive(Debug, Clone)]
121pub struct Twrite {
122    pub fid: u32,
123    pub offset: u64,
124    pub count: u32,
125    pub data: P9Bytes,
126}
127
128#[derive(Debug, Clone)]
129pub struct Tclunk {
130    pub fid: u32,
131}
132
133#[derive(Debug, Clone)]
134pub struct Treaddir {
135    pub fid: u32,
136    pub offset: u64,
137    pub count: u32,
138}
139
140#[derive(Debug, Clone)]
141pub struct Tgetattr {
142    pub fid: u32,
143    pub request_mask: u64,
144}
145
146#[derive(Debug, Clone)]
147pub struct Tsetattr {
148    pub fid: u32,
149    pub valid: u32,
150    pub mode: u32,
151    pub uid: u32,
152    pub gid: u32,
153    pub size: u64,
154    pub atime_sec: u64,
155    pub atime_nsec: u64,
156    pub mtime_sec: u64,
157    pub mtime_nsec: u64,
158}
159
160/// ZeroFS-private atomic fallocate request.
161#[derive(Debug, Clone)]
162pub struct Tfallocate {
163    pub fid: u32,
164    pub offset: u64,
165    pub length: u64,
166    pub mode: u32,
167}
168
169#[derive(Debug, Clone)]
170pub struct Rfallocate;
171
172#[derive(Debug, Clone)]
173pub struct Tmkdir {
174    pub dfid: u32,
175    pub name: P9String,
176    pub mode: u32,
177    pub gid: u32,
178}
179
180#[derive(Debug, Clone)]
181pub struct Tsymlink {
182    pub dfid: u32,
183    pub name: P9String,
184    pub symtgt: P9String,
185    pub gid: u32,
186}
187
188#[derive(Debug, Clone)]
189pub struct Tmknod {
190    pub dfid: u32,
191    pub name: P9String,
192    pub mode: u32,
193    pub major: u32,
194    pub minor: u32,
195    pub gid: u32,
196}
197
198#[derive(Debug, Clone)]
199pub struct Tlink {
200    pub dfid: u32,
201    pub fid: u32,
202    pub name: P9String,
203}
204
205#[derive(Debug, Clone)]
206pub struct Trename {
207    pub fid: u32,
208    pub dfid: u32,
209    pub name: P9String,
210}
211
212#[derive(Debug, Clone)]
213pub struct Trenameat {
214    pub olddirfid: u32,
215    pub oldname: P9String,
216    pub newdirfid: u32,
217    pub newname: P9String,
218}
219
220#[derive(Debug, Clone)]
221pub struct Tunlinkat {
222    pub dirfid: u32,
223    pub name: P9String,
224    pub flags: u32,
225}
226
227#[derive(Debug, Clone)]
228pub struct Tfsync {
229    pub fid: u32,
230    pub datasync: u32,
231}
232
233#[derive(Debug, Clone)]
234pub struct Treadlink {
235    pub fid: u32,
236}
237
238#[derive(Debug, Clone)]
239pub struct Tstatfs {
240    pub fid: u32,
241}
242
243// Core 9P structures
244#[derive(Debug, Clone)]
245pub struct Tflush {
246    pub oldtag: u16,
247}
248
249#[derive(Debug, Clone)]
250pub struct Rflush;
251
252// Extended attributes
253#[derive(Debug, Clone)]
254pub struct Txattrwalk {
255    pub fid: u32,
256    pub newfid: u32,
257    pub name: P9String,
258}
259
260#[derive(Debug, Clone)]
261pub struct Tlock {
262    pub fid: u32,
263    pub lock_type: LockType,
264    pub flags: u32,
265    pub start: u64,
266    pub length: u64,
267    pub proc_id: u32,
268    pub client_id: P9String,
269}
270
271#[derive(Debug, Clone)]
272pub struct Tgetlock {
273    pub fid: u32,
274    pub lock_type: LockType,
275    pub start: u64,
276    pub length: u64,
277    pub proc_id: u32,
278    pub client_id: P9String,
279}
280
281#[derive(Debug, Clone)]
282pub struct Rxattrwalk {
283    pub size: u64,
284}
285
286#[derive(Debug, Clone)]
287pub struct Rgetlock {
288    pub lock_type: LockType,
289    pub start: u64,
290    pub length: u64,
291    pub proc_id: u32,
292    pub client_id: P9String,
293}
294
295// Response messages
296#[derive(Debug, Clone)]
297pub struct Rversion {
298    pub msize: u32,
299    pub version: P9String,
300}
301
302#[derive(Debug, Clone)]
303pub struct Rattach {
304    pub qid: Qid,
305}
306
307// ZeroFS-private reconnect extension: binds a fresh fid to an existing inode by
308// id (not by re-walking a path), so reconnection survives renames/hardlinks.
309#[derive(Debug, Clone)]
310pub struct Trebind {
311    pub fid: u32,
312    pub inode_id: u64,
313    /// Attach-root inode. A root-self request has `inode_id == root_inode`.
314    pub root_inode: u64,
315    /// Replay flags. `P9_REBIND_OPENED` requires `P9_REBIND_REPLAY`, marks an
316    /// expected in-place reopen, and grants neither access nor an inode pin.
317    pub flags: u8,
318    /// Original `Tattach` username for legacy clients, or a binary credential
319    /// payload beginning with `P9_REBIND_CREDENTIAL_SENTINEL`. The group-count
320    /// byte may carry `P9_REBIND_CREDENTIAL_GROUPS_INCOMPLETE`.
321    pub uname: P9String,
322    /// Numeric uid; kernel credential payloads carry fsuid here.
323    pub n_uname: u32,
324}
325
326// Private compound requests require the ZeroFS dialect.
327#[derive(Debug, Clone)]
328pub struct Twalkgetattr {
329    pub fid: u32,
330    pub newfid: u32,
331    pub nwname: u16,
332    pub wnames: Vec<P9String>,
333}
334
335// Returned only on a full walk; on any miss the server replies Rlerror.
336#[derive(Debug, Clone)]
337pub struct Rwalkgetattr {
338    pub nwqid: u16,
339    pub wqids: Vec<Qid>,
340    pub stat: Stat,
341}
342
343#[derive(Debug, Clone)]
344pub struct Treaddirattr {
345    pub fid: u32,
346    pub offset: u64,
347    pub count: u32,
348}
349
350#[derive(Debug, Clone)]
351pub struct DirEntryPlus {
352    pub qid: Qid,
353    pub offset: u64,
354    pub type_: u8,
355    pub name: P9String,
356    pub stat: Stat,
357}
358
359impl DirEntryPlus {
360    /// Serialized wire size.
361    pub fn wire_size(&self) -> usize {
362        Qid::WIRE_SIZE + 8 + 1 + self.name.wire_size() + Stat::WIRE_SIZE
363    }
364}
365
366#[derive(Debug, Clone)]
367pub struct Rreaddirattr {
368    pub count: u32,
369    pub data: P9Bytes,
370}
371
372// Tlopenat preserves `fid` and opens `newfid`. Tlcreateattr preserves `dfid`,
373// creates and opens `newfid`, and returns stat. Other *attr replies add stat to
374// the corresponding standard request layout.
375#[derive(Debug, Clone)]
376pub struct Tlopenat {
377    pub fid: u32,
378    pub newfid: u32,
379    pub flags: u32,
380}
381
382#[derive(Debug, Clone)]
383pub struct Rlopenat {
384    pub qid: Qid,
385    pub iounit: u32,
386}
387
388// Tlopenatread combines Tlopenat with a best-effort Tread at offset zero.
389#[derive(Debug, Clone)]
390pub struct Tlopenatread {
391    pub fid: u32,
392    pub newfid: u32,
393    pub flags: u32,
394    /// Bytes to prefetch from offset 0. The server clamps this to fit msize.
395    pub count: u32,
396}
397
398#[derive(Debug, Clone)]
399pub struct Rlopenatread {
400    pub qid: Qid,
401    pub iounit: u32,
402    /// One when `data` reaches EOF; zero for an incomplete prefetch.
403    pub eof: u8,
404    pub count: u32,
405    pub data: P9Bytes,
406}
407
408#[derive(Debug, Clone)]
409pub struct Tlcreateattr {
410    pub dfid: u32,
411    pub newfid: u32,
412    pub name: P9String,
413    pub flags: u32,
414    pub mode: u32,
415    pub gid: u32,
416}
417
418#[derive(Debug, Clone)]
419pub struct Rmkdirattr {
420    pub stat: Stat,
421}
422
423#[derive(Debug, Clone)]
424pub struct Rsymlinkattr {
425    pub stat: Stat,
426}
427
428#[derive(Debug, Clone)]
429pub struct Rmknodattr {
430    pub stat: Stat,
431}
432
433#[derive(Debug, Clone)]
434pub struct Rlinkattr {
435    pub stat: Stat,
436}
437
438#[derive(Debug, Clone)]
439pub struct Rsetattrattr {
440    pub stat: Stat,
441}
442
443impl Rreaddirattr {
444    pub fn from_entries(entries: Vec<DirEntryPlus>) -> Result<Self, CodecError> {
445        let size = entries.iter().try_fold(0usize, |n, entry| {
446            n.checked_add(entry.wire_size())
447                .ok_or(CodecError::LengthOverflow)
448        })?;
449        let mut data = alloc::vec![0; size];
450        let mut cursor = 0;
451        for entry in entries {
452            cursor += entry.to_slice(&mut data[cursor..])?;
453        }
454        Ok(Self {
455            count: size.try_into()?,
456            data: data.into(),
457        })
458    }
459    pub fn to_entries(&self) -> Result<Vec<DirEntryPlus>, CodecError> {
460        let mut input = self.data.0.clone();
461        let mut entries = Vec::new();
462        while !input.is_empty() {
463            let (entry, length) = DirEntryPlus::decode(&input)?;
464            entries.push(entry);
465            input.advance(length);
466        }
467        Ok(entries)
468    }
469}
470
471#[derive(Debug, Clone)]
472pub struct Rwalk {
473    pub nwqid: u16,
474    pub wqids: Vec<Qid>,
475}
476
477#[derive(Debug, Clone)]
478pub struct Rlcreate {
479    pub qid: Qid,
480    pub iounit: u32,
481}
482
483#[derive(Debug, Clone)]
484pub struct Rread {
485    pub count: u32,
486    pub data: P9Bytes,
487}
488
489#[derive(Debug, Clone)]
490pub struct Rreaddir {
491    pub count: u32,
492    pub data: P9Bytes,
493}
494
495impl Rreaddir {
496    pub fn from_entries(entries: Vec<DirEntry>) -> Result<Self, CodecError> {
497        let size = entries.iter().try_fold(0usize, |n, entry| {
498            n.checked_add(entry.wire_size())
499                .ok_or(CodecError::LengthOverflow)
500        })?;
501        let mut data = alloc::vec![0; size];
502        let mut cursor = 0;
503        for entry in entries {
504            cursor += entry.to_slice(&mut data[cursor..])?;
505        }
506        Ok(Self {
507            count: size.try_into()?,
508            data: data.into(),
509        })
510    }
511    pub fn to_entries(&self) -> Result<Vec<DirEntry>, CodecError> {
512        let mut input = self.data.0.clone();
513        let mut entries = Vec::new();
514        while !input.is_empty() {
515            let (entry, length) = DirEntry::decode(&input)?;
516            entries.push(entry);
517            input.advance(length);
518        }
519        Ok(entries)
520    }
521}
522
523#[derive(Debug, Clone)]
524pub struct Rmkdir {
525    pub qid: Qid,
526}
527
528#[derive(Debug, Clone)]
529pub struct Rsymlink {
530    pub qid: Qid,
531}
532
533#[derive(Debug, Clone)]
534pub struct Rmknod {
535    pub qid: Qid,
536}
537
538#[derive(Debug, Clone)]
539pub struct Rreadlink {
540    pub target: P9String,
541}
542
543// Empty responses
544#[derive(Debug, Clone)]
545pub struct Rclunk;
546
547#[derive(Debug, Clone)]
548pub struct Rsetattr;
549
550#[derive(Debug, Clone)]
551pub struct Rrename;
552
553#[derive(Debug, Clone)]
554pub struct Rlink;
555
556#[derive(Debug, Clone)]
557pub struct Rrenameat;
558
559#[derive(Debug, Clone)]
560pub struct Runlinkat;
561
562#[derive(Debug, Clone)]
563pub struct Rfsync;
564
565/// Queries the connection's durability lineage and writer epoch.
566#[derive(Debug, Clone)]
567pub struct Tgetlineage;
568
569/// Durability-verified fsync carrying the oldest unverified lineage token.
570/// `datasync` contains `P9_FSYNC_*` flags; without `P9_FSYNC_INODE` the barrier
571/// is filesystem-wide. Token zero denotes no pending mutation in that scope. A
572/// lineage mismatch returns `ESTALE`.
573#[derive(Debug, Clone)]
574pub struct Tfsyncdur {
575    pub fid: u32,
576    pub datasync: u32,
577    pub token: u64,
578}
579
580// These IDs define mutation-envelope coverage and must match the enum layout.
581pub const T_LCREATE: u8 = message_type::TLCREATE;
582pub const T_SYMLINK: u8 = message_type::TSYMLINK;
583pub const T_MKNOD: u8 = message_type::TMKNOD;
584pub const T_RENAME: u8 = message_type::TRENAME;
585pub const T_SETATTR: u8 = message_type::TSETATTR;
586pub const T_WRITE: u8 = message_type::TWRITE;
587pub const T_LINK: u8 = message_type::TLINK;
588pub const T_MKDIR: u8 = message_type::TMKDIR;
589pub const T_RENAMEAT: u8 = message_type::TRENAMEAT;
590pub const T_UNLINKAT: u8 = message_type::TUNLINKAT;
591pub const T_LCREATEATTR: u8 = message_type::TLCREATEATTR;
592pub const T_MKDIRATTR: u8 = message_type::TMKDIRATTR;
593pub const T_SYMLINKATTR: u8 = message_type::TSYMLINKATTR;
594pub const T_MKNODATTR: u8 = message_type::TMKNODATTR;
595pub const T_LINKATTR: u8 = message_type::TLINKATTR;
596pub const T_SETATTRATTR: u8 = message_type::TSETATTRATTR;
597// Delayed fallocate retries can reorder with writes and therefore carry op-ids.
598pub const T_FALLOCATE: u8 = message_type::TFALLOCATE;
599pub const R_FALLOCATE: u8 = message_type::RFALLOCATE;
600
601// Main message enum
602#[derive(Debug, Clone)]
603pub enum Message {
604    Tversion(Tversion),
605    Rversion(Rversion),
606    Tattach(Tattach),
607    Rattach(Rattach),
608    Twalk(Twalk),
609    Rwalk(Rwalk),
610    Tlopen(Tlopen),
611    Rlopen(Rlopen),
612    Tlcreate(Tlcreate),
613    Rlcreate(Rlcreate),
614    Tread(Tread),
615    Rread(Rread),
616    Twrite(Twrite),
617    Rwrite(Rwrite),
618    Tclunk(Tclunk),
619    Rclunk(Rclunk),
620    Treaddir(Treaddir),
621    Rreaddir(Rreaddir),
622    Tgetattr(Tgetattr),
623    Rgetattr(Rgetattr),
624    Tsetattr(Tsetattr),
625    Rsetattr(Rsetattr),
626    Tfallocate(Tfallocate),
627    Rfallocate(Rfallocate),
628    Tmkdir(Tmkdir),
629    Rmkdir(Rmkdir),
630    Tsymlink(Tsymlink),
631    Rsymlink(Rsymlink),
632    Tmknod(Tmknod),
633    Rmknod(Rmknod),
634    Treadlink(Treadlink),
635    Rreadlink(Rreadlink),
636    Tlink(Tlink),
637    Rlink(Rlink),
638    Trename(Trename),
639    Rrename(Rrename),
640    Trenameat(Trenameat),
641    Rrenameat(Rrenameat),
642    Tunlinkat(Tunlinkat),
643    Runlinkat(Runlinkat),
644    Tfsync(Tfsync),
645    Rfsync(Rfsync),
646    Tfsyncdur(Tfsyncdur),
647    Tgetlineage(Tgetlineage),
648    Rgetlineage(Rgetlineage),
649    Tlock(Tlock),
650    Rlock(Rlock),
651    Tgetlock(Tgetlock),
652    Rgetlock(Rgetlock),
653    Rlerror(Rlerror),
654    Tflush(Tflush),
655    Rflush(Rflush),
656    Txattrwalk(Txattrwalk),
657    Rxattrwalk(Rxattrwalk),
658    Tstatfs(Tstatfs),
659    Rstatfs(Rstatfs),
660    // Private compound extensions use IDs outside the standard 9P range. The
661    // *attr requests keep the standard request layout and return a richer reply.
662    Tlopenat(Tlopenat),
663    Rlopenat(Rlopenat),
664    Tlopenatread(Tlopenatread),
665    Rlopenatread(Rlopenatread),
666    Tlcreateattr(Tlcreateattr),
667    Rlcreateattr(Rlcreateattr),
668    Tmkdirattr(Tmkdir),
669    Rmkdirattr(Rmkdirattr),
670    Tsymlinkattr(Tsymlink),
671    Rsymlinkattr(Rsymlinkattr),
672    Tmknodattr(Tmknod),
673    Rmknodattr(Rmknodattr),
674    Tlinkattr(Tlink),
675    Rlinkattr(Rlinkattr),
676    Tsetattrattr(Tsetattr),
677    Rsetattrattr(Rsetattrattr),
678    // ZeroFS-private reconnect extension (ids outside the standard 9P range).
679    Trebind(Trebind),
680    Rrebind(Rrebind),
681    Twalkgetattr(Twalkgetattr),
682    Rwalkgetattr(Rwalkgetattr),
683    Treaddirattr(Treaddirattr),
684    Rreaddirattr(Rreaddirattr),
685}
686
687impl Message {
688    /// Whether this request requires the private ZeroFS dialect.
689    pub fn is_zerofs_private_request(&self) -> bool {
690        match self {
691            Message::Tfallocate(_)
692            | Message::Tfsyncdur(_)
693            | Message::Tgetlineage(_)
694            | Message::Tlopenat(_)
695            | Message::Tlopenatread(_)
696            | Message::Tlcreateattr(_)
697            | Message::Tmkdirattr(_)
698            | Message::Tsymlinkattr(_)
699            | Message::Tmknodattr(_)
700            | Message::Tlinkattr(_)
701            | Message::Tsetattrattr(_)
702            | Message::Trebind(_)
703            | Message::Twalkgetattr(_)
704            | Message::Treaddirattr(_) => true,
705            Message::Tversion(_)
706            | Message::Rversion(_)
707            | Message::Tattach(_)
708            | Message::Rattach(_)
709            | Message::Twalk(_)
710            | Message::Rwalk(_)
711            | Message::Tlopen(_)
712            | Message::Rlopen(_)
713            | Message::Tlcreate(_)
714            | Message::Rlcreate(_)
715            | Message::Tread(_)
716            | Message::Rread(_)
717            | Message::Twrite(_)
718            | Message::Rwrite(_)
719            | Message::Tclunk(_)
720            | Message::Rclunk(_)
721            | Message::Treaddir(_)
722            | Message::Rreaddir(_)
723            | Message::Tgetattr(_)
724            | Message::Rgetattr(_)
725            | Message::Tsetattr(_)
726            | Message::Rsetattr(_)
727            | Message::Rfallocate(_)
728            | Message::Tmkdir(_)
729            | Message::Rmkdir(_)
730            | Message::Tsymlink(_)
731            | Message::Rsymlink(_)
732            | Message::Tmknod(_)
733            | Message::Rmknod(_)
734            | Message::Treadlink(_)
735            | Message::Rreadlink(_)
736            | Message::Tlink(_)
737            | Message::Rlink(_)
738            | Message::Trename(_)
739            | Message::Rrename(_)
740            | Message::Trenameat(_)
741            | Message::Rrenameat(_)
742            | Message::Tunlinkat(_)
743            | Message::Runlinkat(_)
744            | Message::Tfsync(_)
745            | Message::Rfsync(_)
746            | Message::Rgetlineage(_)
747            | Message::Tlock(_)
748            | Message::Rlock(_)
749            | Message::Tgetlock(_)
750            | Message::Rgetlock(_)
751            | Message::Rlerror(_)
752            | Message::Tflush(_)
753            | Message::Rflush(_)
754            | Message::Txattrwalk(_)
755            | Message::Rxattrwalk(_)
756            | Message::Tstatfs(_)
757            | Message::Rstatfs(_)
758            | Message::Rlopenat(_)
759            | Message::Rlopenatread(_)
760            | Message::Rlcreateattr(_)
761            | Message::Rmkdirattr(_)
762            | Message::Rsymlinkattr(_)
763            | Message::Rmknodattr(_)
764            | Message::Rlinkattr(_)
765            | Message::Rsetattrattr(_)
766            | Message::Rrebind(_)
767            | Message::Rwalkgetattr(_)
768            | Message::Rreaddirattr(_) => false,
769        }
770    }
771
772    /// Fids referenced or allocated by this request. The exhaustive match requires
773    /// each new message to declare its fid footprint.
774    pub fn request_fids(&self) -> impl Iterator<Item = u32> {
775        let fids = match self {
776            Message::Tattach(m) => [Some(m.fid), (m.afid != NOFID).then_some(m.afid)],
777            Message::Twalk(m) => [Some(m.fid), Some(m.newfid)],
778            Message::Tlopen(m) => [Some(m.fid), None],
779            Message::Tlcreate(m) => [Some(m.fid), None],
780            Message::Tread(m) => [Some(m.fid), None],
781            Message::Twrite(m) => [Some(m.fid), None],
782            Message::Tclunk(m) => [Some(m.fid), None],
783            Message::Treaddir(m) => [Some(m.fid), None],
784            Message::Tgetattr(m) => [Some(m.fid), None],
785            Message::Tsetattr(m) | Message::Tsetattrattr(m) => [Some(m.fid), None],
786            Message::Tfallocate(m) => [Some(m.fid), None],
787            Message::Tmkdir(m) | Message::Tmkdirattr(m) => [Some(m.dfid), None],
788            Message::Tsymlink(m) | Message::Tsymlinkattr(m) => [Some(m.dfid), None],
789            Message::Tmknod(m) | Message::Tmknodattr(m) => [Some(m.dfid), None],
790            Message::Treadlink(m) => [Some(m.fid), None],
791            Message::Tlink(m) | Message::Tlinkattr(m) => [Some(m.dfid), Some(m.fid)],
792            Message::Trename(m) => [Some(m.fid), Some(m.dfid)],
793            Message::Trenameat(m) => [Some(m.olddirfid), Some(m.newdirfid)],
794            Message::Tunlinkat(m) => [Some(m.dirfid), None],
795            Message::Tfsync(m) => [Some(m.fid), None],
796            Message::Tfsyncdur(m) => [Some(m.fid), None],
797            Message::Tlock(m) => [Some(m.fid), None],
798            Message::Tgetlock(m) => [Some(m.fid), None],
799            Message::Txattrwalk(m) => [Some(m.fid), Some(m.newfid)],
800            Message::Tstatfs(m) => [Some(m.fid), None],
801            Message::Tlopenat(m) => [Some(m.fid), Some(m.newfid)],
802            Message::Tlopenatread(m) => [Some(m.fid), Some(m.newfid)],
803            Message::Tlcreateattr(m) => [Some(m.dfid), Some(m.newfid)],
804            Message::Trebind(m) => [Some(m.fid), None],
805            Message::Twalkgetattr(m) => [Some(m.fid), Some(m.newfid)],
806            Message::Treaddirattr(m) => [Some(m.fid), None],
807            Message::Tversion(_)
808            | Message::Rversion(_)
809            | Message::Rattach(_)
810            | Message::Rwalk(_)
811            | Message::Rlopen(_)
812            | Message::Rlcreate(_)
813            | Message::Rread(_)
814            | Message::Rwrite(_)
815            | Message::Rclunk(_)
816            | Message::Rreaddir(_)
817            | Message::Rgetattr(_)
818            | Message::Rsetattr(_)
819            | Message::Rfallocate(_)
820            | Message::Rmkdir(_)
821            | Message::Rsymlink(_)
822            | Message::Rmknod(_)
823            | Message::Rreadlink(_)
824            | Message::Rlink(_)
825            | Message::Rrename(_)
826            | Message::Rrenameat(_)
827            | Message::Runlinkat(_)
828            | Message::Rfsync(_)
829            | Message::Tgetlineage(_)
830            | Message::Rgetlineage(_)
831            | Message::Rlock(_)
832            | Message::Rgetlock(_)
833            | Message::Rlerror(_)
834            | Message::Tflush(_)
835            | Message::Rflush(_)
836            | Message::Rxattrwalk(_)
837            | Message::Rstatfs(_)
838            | Message::Rlopenat(_)
839            | Message::Rlopenatread(_)
840            | Message::Rlcreateattr(_)
841            | Message::Rmkdirattr(_)
842            | Message::Rsymlinkattr(_)
843            | Message::Rmknodattr(_)
844            | Message::Rlinkattr(_)
845            | Message::Rsetattrattr(_)
846            | Message::Rrebind(_)
847            | Message::Rwalkgetattr(_)
848            | Message::Rreaddirattr(_) => [None, None],
849        };
850        fids.into_iter().flatten()
851    }
852
853    /// Whether this request creates an obligation for durability-verified fsync.
854    pub fn is_mutation(&self) -> bool {
855        self.durability_fid().is_some()
856    }
857
858    /// Fid owning the mutation's primary durability obligation.
859    pub fn durability_fid(&self) -> Option<u32> {
860        match self {
861            Message::Twrite(m) => Some(m.fid),
862            Message::Tsetattr(m) => Some(m.fid),
863            Message::Tsetattrattr(m) => Some(m.fid),
864            Message::Tfallocate(m) => Some(m.fid),
865            Message::Tlcreate(m) => Some(m.fid),
866            Message::Tlcreateattr(m) => Some(m.dfid),
867            Message::Tmkdir(m) => Some(m.dfid),
868            Message::Tmkdirattr(m) => Some(m.dfid),
869            Message::Tsymlink(m) => Some(m.dfid),
870            Message::Tsymlinkattr(m) => Some(m.dfid),
871            Message::Tmknod(m) => Some(m.dfid),
872            Message::Tmknodattr(m) => Some(m.dfid),
873            Message::Tlink(m) => Some(m.dfid),
874            Message::Tlinkattr(m) => Some(m.dfid),
875            Message::Trename(m) => Some(m.dfid),
876            Message::Trenameat(m) => Some(m.newdirfid),
877            Message::Tunlinkat(m) => Some(m.dirfid),
878            _ => None,
879        }
880    }
881
882    /// Fids changed by the mutation. `Trenameat` includes both directories.
883    pub fn durability_fids(&self) -> impl Iterator<Item = u32> {
884        let extra = match self {
885            Message::Trenameat(m) => Some(m.olddirfid),
886            _ => None,
887        };
888        self.durability_fid().into_iter().chain(extra)
889    }
890}
891
892/// An owned 9P frame.
893#[derive(Debug, Clone)]
894pub struct P9Message {
895    pub size: u32,
896    pub type_: u8,
897    pub tag: u16,
898    pub op_id: [u8; 16],
899    pub op_flags: u8,
900    pub op_origin_epoch: u64,
901    pub body: Message,
902}
903
904impl P9Message {
905    pub fn to_bytes(&self) -> Result<Vec<u8>, CodecError> {
906        self.to_bytes_ctx(false)
907    }
908
909    pub fn to_bytes_ctx(&self, op_id_enabled: bool) -> Result<Vec<u8>, CodecError> {
910        Self::encode_body(self.tag, self.envelope(), &self.body, op_id_enabled)
911    }
912
913    pub fn encode_body(
914        tag: u16,
915        envelope: MutationEnvelope,
916        body: &Message,
917        op_id_enabled: bool,
918    ) -> Result<Vec<u8>, CodecError> {
919        let view = body.as_view();
920        let size = slice_codec::encoded_frame_size(&view, op_id_enabled)?;
921        if size > P9_MAX_MSIZE as usize {
922            return Err(CodecError::MessageTooLarge);
923        }
924        let mut output = alloc::vec![0; size];
925        slice_codec::encode_frame(
926            &mut output,
927            P9_MAX_MSIZE,
928            tag,
929            envelope,
930            &view,
931            op_id_enabled,
932        )?;
933        Ok(output)
934    }
935
936    pub fn to_slice_ctx(
937        &self,
938        output: &mut [u8],
939        op_id_enabled: bool,
940    ) -> Result<usize, CodecError> {
941        slice_codec::encode_frame(
942            output,
943            P9_MAX_MSIZE,
944            self.tag,
945            self.envelope(),
946            &self.body.as_view(),
947            op_id_enabled,
948        )
949    }
950
951    fn envelope(&self) -> MutationEnvelope {
952        MutationEnvelope {
953            op_id: self.op_id,
954            flags: self.op_flags,
955            origin_writer_epoch: self.op_origin_epoch,
956        }
957    }
958
959    pub fn from_bytes_ctx(input: &[u8], op_id_enabled: bool) -> Result<Self, CodecError> {
960        let header = slice_codec::decode_header(input, P9_MAX_MSIZE)?;
961        if header.size as usize != input.len() {
962            return Err(CodecError::FrameSizeMismatch);
963        }
964        Self::from_owned_bytes_ctx(Bytes::copy_from_slice(input), op_id_enabled)
965    }
966
967    pub fn from_owned_bytes_ctx(input: Bytes, op_id_enabled: bool) -> Result<Self, CodecError> {
968        let frame = slice_codec::decode_frame(&input, P9_MAX_MSIZE, op_id_enabled)?;
969        Ok(Self {
970            size: frame.header.size,
971            type_: frame.header.type_,
972            tag: frame.header.tag,
973            op_id: frame.envelope.op_id,
974            op_flags: frame.envelope.flags,
975            op_origin_epoch: frame.envelope.origin_writer_epoch,
976            body: Message::from_view(frame.body, &input)?,
977        })
978    }
979
980    pub fn carries_op_id(type_: u8) -> bool {
981        slice_codec::carries_op_id(type_)
982    }
983
984    pub fn new(tag: u16, body: Message) -> Self {
985        let type_ = body.as_view().type_id();
986
987        Self {
988            size: 0,
989            type_,
990            tag,
991            op_id: [0u8; 16],
992            op_flags: 0,
993            op_origin_epoch: 0,
994            body,
995        }
996    }
997
998    /// Construct a request carrying an idempotency op-id (encode with `to_bytes_ctx(true)`).
999    pub fn new_with_op_id(tag: u16, op_id: [u8; 16], body: Message) -> Self {
1000        Self::new_with_op_id_flags_and_origin(tag, op_id, 0, 0, body)
1001    }
1002
1003    /// Constructs an op-id request with flags and origin writer epoch.
1004    pub fn new_with_op_id_flags_and_origin(
1005        tag: u16,
1006        op_id: [u8; 16],
1007        op_flags: u8,
1008        op_origin_epoch: u64,
1009        body: Message,
1010    ) -> Self {
1011        let mut msg = Self::new(tag, body);
1012        msg.op_id = op_id;
1013        msg.op_flags = op_flags;
1014        msg.op_origin_epoch = op_origin_epoch;
1015        msg
1016    }
1017}
1018
1019#[cfg(test)]
1020mod tests {
1021    use super::*;
1022    use alloc::vec;
1023
1024    #[test]
1025    fn caller_owned_output_matches_allocating_encoder() {
1026        let message = P9Message::new(
1027            42,
1028            Message::Tgetattr(Tgetattr {
1029                fid: 7,
1030                request_mask: GETATTR_ALL,
1031            }),
1032        );
1033        let expected = message.to_bytes_ctx(false).unwrap();
1034        let mut output = [0u8; 64];
1035
1036        let written = message.to_slice_ctx(&mut output, false).unwrap();
1037
1038        assert_eq!(&output[..written], expected);
1039    }
1040
1041    fn counted_payload(message: &P9Message) -> (u32, &Bytes) {
1042        match &message.body {
1043            Message::Rread(message) => (message.count, &message.data),
1044            Message::Rreaddir(message) => (message.count, &message.data),
1045            Message::Rlopenatread(message) => (message.count, &message.data),
1046            Message::Twrite(message) => (message.count, &message.data),
1047            Message::Rreaddirattr(message) => (message.count, &message.data),
1048            _ => panic!("expected a counted-payload message"),
1049        }
1050    }
1051
1052    fn assert_owned_payload_aliases(
1053        message: P9Message,
1054        op_id_enabled: bool,
1055        payload_offset: usize,
1056        expected_payload: &[u8],
1057    ) {
1058        let frame = Bytes::from(message.to_bytes_ctx(op_id_enabled).unwrap());
1059        let expected_ptr = frame.as_ptr().wrapping_add(payload_offset);
1060        let expected_frame = frame.clone();
1061        let decoded = P9Message::from_owned_bytes_ctx(frame, op_id_enabled).unwrap();
1062        let (decoded_count, decoded_payload) = counted_payload(&decoded);
1063
1064        assert_eq!(decoded.size, expected_frame.len() as u32);
1065        assert_eq!(decoded_count, expected_payload.len() as u32);
1066        assert_eq!(decoded_payload.as_ref(), expected_payload);
1067        assert_eq!(
1068            decoded_payload.as_ptr(),
1069            expected_ptr,
1070            "the decoded payload must be a slice of the owned frame"
1071        );
1072        assert_eq!(
1073            decoded.to_bytes_ctx(op_id_enabled).unwrap(),
1074            expected_frame,
1075            "owned decoding must preserve every scalar field"
1076        );
1077    }
1078
1079    fn tmkdir() -> Message {
1080        Message::Tmkdir(Tmkdir {
1081            dfid: 1,
1082            name: P9String::new(b"d".to_vec()),
1083            mode: 0o755,
1084            gid: 0,
1085        })
1086    }
1087
1088    #[test]
1089    fn durability_fids_yields_both_directories_for_a_renameat() {
1090        // A renameat changes both directories (source removal, dest add) in one atomic
1091        // op, so a verified fsync of either must account for it: durability_fids yields
1092        // both dir fids, while durability_fid (the primary) is only the dest.
1093        let m = Message::Trenameat(Trenameat {
1094            olddirfid: 11,
1095            oldname: P9String::new(b"a".to_vec()),
1096            newdirfid: 22,
1097            newname: P9String::new(b"b".to_vec()),
1098        });
1099        assert_eq!(
1100            m.durability_fid(),
1101            Some(22),
1102            "the primary fid is the dest dir"
1103        );
1104        let mut fids: Vec<u32> = m.durability_fids().collect();
1105        fids.sort_unstable();
1106        assert_eq!(fids, vec![11, 22], "both source and dest dirs are covered");
1107    }
1108
1109    #[test]
1110    fn durability_fids_is_the_single_fid_for_non_rename_ops() {
1111        let fids: Vec<u32> = tmkdir().durability_fids().collect();
1112        assert_eq!(
1113            fids,
1114            vec![1],
1115            "a single-directory op yields just its own fid"
1116        );
1117    }
1118
1119    #[test]
1120    fn request_fids_omits_the_nofid_attach_sentinel() {
1121        let attach = |afid| {
1122            Message::Tattach(Tattach {
1123                fid: 7,
1124                afid,
1125                uname: P9String::new(Vec::new()),
1126                aname: P9String::new(Vec::new()),
1127                n_uname: 0,
1128            })
1129        };
1130
1131        assert_eq!(attach(NOFID).request_fids().collect::<Vec<_>>(), vec![7]);
1132        assert_eq!(attach(8).request_fids().collect::<Vec<_>>(), vec![7, 8]);
1133    }
1134
1135    #[test]
1136    fn request_fids_includes_both_rename_directories() {
1137        let request = Message::Trenameat(Trenameat {
1138            olddirfid: 11,
1139            oldname: P9String::new(b"old".to_vec()),
1140            newdirfid: 22,
1141            newname: P9String::new(b"new".to_vec()),
1142        });
1143
1144        assert_eq!(request.request_fids().collect::<Vec<_>>(), vec![11, 22]);
1145    }
1146
1147    #[test]
1148    fn request_fids_is_empty_for_fidless_messages() {
1149        assert!(
1150            Message::Tflush(Tflush { oldtag: 1 })
1151                .request_fids()
1152                .next()
1153                .is_none()
1154        );
1155        assert!(Message::Rflush(Rflush).request_fids().next().is_none());
1156    }
1157
1158    #[test]
1159    fn fallocate_round_trips_and_tracks_its_file_fid() {
1160        assert_eq!(classify_fallocate_mode(0), Some(FallocateKind::Allocate));
1161        assert_eq!(
1162            classify_fallocate_mode(FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE),
1163            Some(FallocateKind::PunchHole)
1164        );
1165        assert_eq!(
1166            classify_fallocate_mode(FALLOC_FL_ZERO_RANGE),
1167            Some(FallocateKind::ZeroRange { keep_size: false })
1168        );
1169        assert_eq!(
1170            classify_fallocate_mode(FALLOC_FL_ZERO_RANGE | FALLOC_FL_KEEP_SIZE),
1171            Some(FallocateKind::ZeroRange { keep_size: true })
1172        );
1173        assert_eq!(classify_fallocate_mode(FALLOC_FL_KEEP_SIZE), None);
1174
1175        let msg = P9Message::new(
1176            9,
1177            Message::Tfallocate(Tfallocate {
1178                fid: 42,
1179                offset: 100,
1180                length: 200,
1181                mode: FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE,
1182            }),
1183        );
1184        let bytes = msg.to_bytes().unwrap();
1185        let decoded = P9Message::from_bytes_ctx(&bytes, false).unwrap();
1186        assert_eq!(decoded.type_, T_FALLOCATE);
1187        assert_eq!(decoded.body.durability_fid(), Some(42));
1188        match decoded.body {
1189            Message::Tfallocate(tf) => {
1190                assert_eq!(tf.offset, 100);
1191                assert_eq!(tf.length, 200);
1192                assert_eq!(tf.mode, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE);
1193            }
1194            _ => panic!("expected Tfallocate"),
1195        }
1196    }
1197
1198    #[test]
1199    fn op_id_round_trips_on_a_zerofs_request() {
1200        let op_id = [7u8; 16];
1201        let msg =
1202            P9Message::new_with_op_id_flags_and_origin(5, op_id, P9_OP_FLAG_RETRY, 42, tmkdir());
1203        let bytes = msg.to_bytes_ctx(true).unwrap();
1204        let decoded = P9Message::from_bytes_ctx(&bytes, true).unwrap();
1205        assert_eq!(
1206            decoded.op_id, op_id,
1207            "the op-id must round-trip in the ZeroFS dialect"
1208        );
1209        assert_eq!(decoded.op_flags, P9_OP_FLAG_RETRY);
1210        assert_eq!(decoded.op_origin_epoch, 42);
1211        assert_eq!(decoded.tag, 5);
1212        assert!(matches!(decoded.body, Message::Tmkdir(_)));
1213    }
1214
1215    #[test]
1216    fn owned_rread_payload_aliases_the_input_frame() {
1217        let payload = b"rread payload retained from its frame";
1218        let message = P9Message::new(
1219            7,
1220            Message::Rread(Rread {
1221                count: payload.len() as u32,
1222                data: payload.to_vec().into(),
1223            }),
1224        );
1225
1226        assert_owned_payload_aliases(message, false, P9_IOHDRSZ as usize, payload);
1227    }
1228
1229    #[test]
1230    fn owned_rlopenatread_payload_aliases_the_input_frame() {
1231        let payload = b"prefetched bytes retained from their frame";
1232        let message = P9Message::new(
1233            11,
1234            Message::Rlopenatread(Rlopenatread {
1235                qid: qid(),
1236                iounit: 64 * 1024,
1237                eof: 1,
1238                count: payload.len() as u32,
1239                data: payload.to_vec().into(),
1240            }),
1241        );
1242
1243        assert_owned_payload_aliases(message, false, P9_RLOPENATREAD_HDR as usize, payload);
1244    }
1245
1246    #[test]
1247    fn owned_enveloped_twrite_payload_aliases_the_input_frame() {
1248        let payload = b"write bytes retained from their enveloped frame";
1249        let message = P9Message::new_with_op_id_flags_and_origin(
1250            19,
1251            [0xa5; P9_OP_ID_LEN],
1252            P9_OP_FLAG_RETRY,
1253            42,
1254            Message::Twrite(Twrite {
1255                fid: 23,
1256                offset: 29,
1257                count: payload.len() as u32,
1258                data: payload.to_vec().into(),
1259            }),
1260        );
1261
1262        assert_owned_payload_aliases(
1263            message,
1264            true,
1265            P9_TWRITE_HDR as usize + P9_OP_ENVELOPE_LEN,
1266            payload,
1267        );
1268    }
1269
1270    #[test]
1271    fn owned_standard_twrite_payload_aliases_the_input_frame() {
1272        let payload = b"write bytes retained from a standard frame";
1273        let message = P9Message::new(
1274            21,
1275            Message::Twrite(Twrite {
1276                fid: 23,
1277                offset: 29,
1278                count: payload.len() as u32,
1279                data: payload.to_vec().into(),
1280            }),
1281        );
1282
1283        assert_owned_payload_aliases(message, false, P9_TWRITE_HDR as usize, payload);
1284    }
1285
1286    #[test]
1287    fn owned_directory_payloads_alias_the_input_frame() {
1288        let payload = b"encoded directory entries";
1289        for body in [
1290            Message::Rreaddir(Rreaddir {
1291                count: payload.len() as u32,
1292                data: payload.to_vec().into(),
1293            }),
1294            Message::Rreaddirattr(Rreaddirattr {
1295                count: payload.len() as u32,
1296                data: payload.to_vec().into(),
1297            }),
1298        ] {
1299            assert_owned_payload_aliases(
1300                P9Message::new(25, body),
1301                false,
1302                P9_IOHDRSZ as usize,
1303                payload,
1304            );
1305        }
1306    }
1307
1308    #[test]
1309    fn owned_metadata_and_directory_names_alias_the_input_frame() {
1310        let name = P9String::new(Bytes::from_static(b"name\xff"));
1311        let bodies = [
1312            Message::Twalk(Twalk {
1313                fid: 1,
1314                newfid: 2,
1315                nwname: 1,
1316                wnames: vec![name.clone()],
1317            }),
1318            Message::Tsymlink(Tsymlink {
1319                dfid: 1,
1320                name: name.clone(),
1321                symtgt: P9String::new(Bytes::from_static(b"target")),
1322                gid: 0,
1323            }),
1324            Message::Rreaddir(
1325                Rreaddir::from_entries(vec![DirEntry {
1326                    qid: qid(),
1327                    offset: 1,
1328                    type_: 0,
1329                    name: name.clone(),
1330                }])
1331                .unwrap(),
1332            ),
1333            Message::Rreaddirattr(
1334                Rreaddirattr::from_entries(vec![DirEntryPlus {
1335                    qid: qid(),
1336                    offset: 1,
1337                    type_: 0,
1338                    name,
1339                    stat: stat(),
1340                }])
1341                .unwrap(),
1342            ),
1343        ];
1344        for body in bodies {
1345            let message = P9Message::new(1, body);
1346            for dialect in [false, true] {
1347                let frame = Bytes::from(message.to_bytes_ctx(dialect).unwrap());
1348                let start = frame.as_ptr() as usize;
1349                let end = start + frame.len();
1350                let decoded = P9Message::from_owned_bytes_ctx(frame, dialect).unwrap();
1351                let names = match decoded.body {
1352                    Message::Twalk(walk) => walk.wnames,
1353                    Message::Tsymlink(link) => vec![link.name, link.symtgt],
1354                    Message::Rreaddir(dir) => dir
1355                        .to_entries()
1356                        .unwrap()
1357                        .into_iter()
1358                        .map(|entry| entry.name)
1359                        .collect(),
1360                    Message::Rreaddirattr(dir) => dir
1361                        .to_entries()
1362                        .unwrap()
1363                        .into_iter()
1364                        .map(|entry| entry.name)
1365                        .collect(),
1366                    _ => unreachable!(),
1367                };
1368                assert!(!names.is_empty());
1369                for name in names {
1370                    let ptr = name.data.as_ptr() as usize;
1371                    assert!(!name.is_empty());
1372                    assert!(ptr >= start && ptr + name.len() <= end);
1373                }
1374            }
1375        }
1376    }
1377
1378    #[test]
1379    fn owned_and_borrowed_decoders_reject_trailing_data() {
1380        let mut frame = P9Message::new(
1381            27,
1382            Message::Rread(Rread {
1383                count: 7,
1384                data: b"payload".to_vec().into(),
1385            }),
1386        )
1387        .to_bytes()
1388        .unwrap();
1389        frame[P9_HEADER_SIZE..P9_IOHDRSZ as usize].copy_from_slice(&3u32.to_le_bytes());
1390
1391        assert_eq!(
1392            P9Message::from_bytes_ctx(&frame, false).unwrap_err(),
1393            CodecError::TrailingData
1394        );
1395        assert_eq!(
1396            P9Message::from_owned_bytes_ctx(Bytes::from(frame), false).unwrap_err(),
1397            CodecError::TrailingData
1398        );
1399    }
1400
1401    #[test]
1402    fn owned_counted_decoder_rejects_truncated_payloads() {
1403        let payload = b"payload";
1404        let rread = P9Message::new(
1405            31,
1406            Message::Rread(Rread {
1407                count: payload.len() as u32,
1408                data: payload.to_vec().into(),
1409            }),
1410        );
1411        let valid = rread.to_bytes().unwrap();
1412
1413        let count_cases = [
1414            (rread, false, P9_IOHDRSZ as usize - P9_COUNT_FIELD_LEN),
1415            (
1416                P9Message::new(
1417                    32,
1418                    Message::Rlopenatread(Rlopenatread {
1419                        qid: qid(),
1420                        iounit: 4096,
1421                        eof: 0,
1422                        count: payload.len() as u32,
1423                        data: payload.to_vec().into(),
1424                    }),
1425                ),
1426                false,
1427                P9_RLOPENATREAD_HDR as usize - P9_COUNT_FIELD_LEN,
1428            ),
1429            (
1430                P9Message::new_with_op_id(
1431                    33,
1432                    [0x5a; P9_OP_ID_LEN],
1433                    Message::Twrite(Twrite {
1434                        fid: 1,
1435                        offset: 2,
1436                        count: payload.len() as u32,
1437                        data: payload.to_vec().into(),
1438                    }),
1439                ),
1440                true,
1441                P9_TWRITE_HDR as usize + P9_OP_ENVELOPE_LEN - P9_COUNT_FIELD_LEN,
1442            ),
1443        ];
1444
1445        for (message, op_id_enabled, count_offset) in count_cases {
1446            let mut truncated = message.to_bytes_ctx(op_id_enabled).unwrap();
1447            truncated[count_offset..count_offset + P9_COUNT_FIELD_LEN]
1448                .copy_from_slice(&((payload.len() + 1) as u32).to_le_bytes());
1449            assert!(
1450                P9Message::from_owned_bytes_ctx(Bytes::from(truncated), op_id_enabled).is_err()
1451            );
1452        }
1453
1454        let mut truncated_count = valid[..P9_HEADER_SIZE + P9_COUNT_FIELD_LEN - 1].to_vec();
1455        let truncated_size = truncated_count.len() as u32;
1456        truncated_count[..P9_SIZE_FIELD_LEN].copy_from_slice(&truncated_size.to_le_bytes());
1457        assert!(P9Message::from_owned_bytes_ctx(Bytes::from(truncated_count), false).is_err());
1458    }
1459
1460    #[test]
1461    fn op_id_write_payload_at_advertised_boundary_fits_msize() {
1462        let msize = 4096u32;
1463        let count = msize - P9_TWRITE_HDR - P9_OP_ENVELOPE_LEN as u32;
1464        let msg = P9Message::new_with_op_id(
1465            5,
1466            [7u8; 16],
1467            Message::Twrite(Twrite {
1468                fid: 9,
1469                offset: 0,
1470                count,
1471                data: vec![0u8; count as usize].into(),
1472            }),
1473        );
1474        let bytes = msg.to_bytes_ctx(true).unwrap();
1475        assert_eq!(bytes.len(), msize as usize);
1476        let decoded = P9Message::from_bytes_ctx(&bytes, true).unwrap();
1477        assert_eq!(decoded.op_id, [7u8; 16]);
1478        assert_eq!(decoded.op_flags, 0);
1479        assert_eq!(decoded.op_origin_epoch, 0);
1480        assert!(matches!(decoded.body, Message::Twrite(_)));
1481    }
1482
1483    #[test]
1484    fn op_id_is_absent_in_standard_framing() {
1485        let op_id = [7u8; 16];
1486        let msg = P9Message::new_with_op_id(5, op_id, tmkdir());
1487        let with = msg.to_bytes_ctx(true).unwrap();
1488        let without = msg.to_bytes_ctx(false).unwrap();
1489        assert_eq!(
1490            with.len(),
1491            without.len() + P9_OP_ENVELOPE_LEN,
1492            "the op envelope is present only in the ZeroFS dialect"
1493        );
1494        let decoded = P9Message::from_bytes_ctx(&without, false).unwrap();
1495        assert_eq!(
1496            decoded.op_id, [0u8; 16],
1497            "standard framing carries no op-id"
1498        );
1499        assert_eq!(decoded.op_flags, 0);
1500        let plain = P9Message::from_bytes_ctx(&without, false).unwrap();
1501        assert_eq!(plain.tag, 5);
1502    }
1503
1504    #[test]
1505    fn idempotent_requests_carry_no_op_id() {
1506        let msg = P9Message::new_with_op_id(5, [7u8; 16], Message::Tclunk(Tclunk { fid: 9 }));
1507        let with = msg.to_bytes_ctx(true).unwrap();
1508        let without = msg.to_bytes_ctx(false).unwrap();
1509        assert_eq!(with, without, "an uncovered op must not carry an op-id");
1510    }
1511
1512    #[test]
1513    fn rebind_round_trips_explicit_root_inode() {
1514        let msg = P9Message::new(
1515            17,
1516            Message::Trebind(Trebind {
1517                fid: 9,
1518                inode_id: 42,
1519                root_inode: 7,
1520                flags: P9_REBIND_REPLAY | P9_REBIND_OPENED,
1521                uname: P9String::new(b"root".to_vec()),
1522                n_uname: 1000,
1523            }),
1524        );
1525        let bytes = msg.to_bytes().unwrap();
1526        let decoded = P9Message::from_bytes_ctx(&bytes, false).unwrap();
1527        match decoded.body {
1528            Message::Trebind(rebind) => {
1529                assert_eq!(rebind.fid, 9);
1530                assert_eq!(rebind.inode_id, 42);
1531                assert_eq!(rebind.root_inode, 7);
1532                assert_eq!(rebind.flags, P9_REBIND_REPLAY | P9_REBIND_OPENED);
1533                assert_eq!(rebind.uname.as_str().unwrap(), "root");
1534                assert_eq!(rebind.n_uname, 1000);
1535            }
1536            _ => panic!("expected Trebind"),
1537        }
1538    }
1539
1540    #[test]
1541    fn rebind_round_trips_binary_credentials() {
1542        let credentials = vec![
1543            P9_REBIND_CREDENTIAL_SENTINEL,
1544            P9_REBIND_CREDENTIAL_VERSION,
1545            0xd2,
1546            0x04,
1547            0,
1548            0,
1549            1,
1550            0x2e,
1551            0x16,
1552            0,
1553            0,
1554        ];
1555        let msg = P9Message::new(
1556            18,
1557            Message::Trebind(Trebind {
1558                fid: 10,
1559                inode_id: 43,
1560                root_inode: 0,
1561                flags: 0,
1562                uname: P9String::new(credentials.clone()),
1563                n_uname: 1001,
1564            }),
1565        );
1566        let bytes = msg.to_bytes().unwrap();
1567        let decoded = P9Message::from_bytes_ctx(&bytes, false).unwrap();
1568        match decoded.body {
1569            Message::Trebind(rebind) => {
1570                assert_eq!(rebind.uname.data, credentials);
1571                assert_eq!(rebind.n_uname, 1001);
1572            }
1573            _ => panic!("expected Trebind"),
1574        }
1575    }
1576
1577    #[test]
1578    fn truncated_large_payload_is_rejected() {
1579        let frame = P9Message::new(
1580            7,
1581            Message::Twrite(Twrite {
1582                fid: 1,
1583                offset: 0,
1584                count: u32::MAX,
1585                data: vec![0xde, 0xad, 0xbe, 0xef].into(),
1586            }),
1587        )
1588        .to_bytes()
1589        .unwrap();
1590        assert!(frame.len() < 64);
1591        assert!(P9Message::from_bytes_ctx(&frame, false).is_err());
1592    }
1593
1594    fn qid() -> Qid {
1595        Qid {
1596            type_: 0x80,
1597            version: 7,
1598            path: 42,
1599        }
1600    }
1601
1602    fn stat() -> Stat {
1603        Stat {
1604            qid: qid(),
1605            mode: 0o755,
1606            uid: 1000,
1607            gid: 1000,
1608            nlink: 2,
1609            rdev: 0,
1610            size: 4096,
1611            blksize: 32768,
1612            blocks: 8,
1613            atime_sec: 1,
1614            atime_nsec: 2,
1615            mtime_sec: 3,
1616            mtime_nsec: 4,
1617            ctime_sec: 5,
1618            ctime_nsec: 6,
1619            btime_sec: 7,
1620            btime_nsec: 8,
1621            r#gen: 9,
1622            data_version: 10,
1623        }
1624    }
1625
1626    #[test]
1627    fn wire_size_matches_serialization() {
1628        for name in [&b""[..], b"a", "h\u{e9}llo-\u{4e16}\u{754c}.txt".as_bytes()] {
1629            let entry = DirEntry {
1630                qid: qid(),
1631                offset: 99,
1632                type_: 4,
1633                name: P9String::new(name.to_vec()),
1634            };
1635            assert_eq!(entry.wire_size(), entry.to_bytes().unwrap().len());
1636
1637            let plus = DirEntryPlus {
1638                qid: qid(),
1639                offset: 99,
1640                type_: 4,
1641                name: P9String::new(name.to_vec()),
1642                stat: stat(),
1643            };
1644            assert_eq!(plus.wire_size(), plus.to_bytes().unwrap().len());
1645        }
1646    }
1647}