Skip to main content

sui_compat/
wire.rs

1//! Nix worker protocol wire format.
2//!
3//! Clean-room implementation. All integers are 64-bit unsigned, little-endian.
4//! Byte buffers are length-prefixed (u64 LE) with zero-padding to 8-byte alignment.
5
6use std::io::{self, Read, Write};
7use thiserror::Error;
8
9/// Worker protocol magic: client sends this.
10pub const WORKER_MAGIC_1: u64 = 0x6e697863; // "nixc"
11/// Worker protocol magic: server responds with this.
12pub const WORKER_MAGIC_2: u64 = 0x6478696f; // "dxio"
13
14/// Current protocol version (major.minor packed as u64).
15pub const PROTOCOL_VERSION: u64 = (1 << 8) | 37; // 1.37
16
17#[derive(Debug, Error)]
18pub enum WireError {
19    #[error("io error: {0}")]
20    Io(#[from] io::Error),
21    #[error("protocol error: {0}")]
22    Protocol(String),
23    #[error("unexpected magic: expected {expected:#x}, got {got:#x}")]
24    BadMagic { expected: u64, got: u64 },
25}
26
27/// Worker protocol operation codes.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29#[non_exhaustive]
30#[repr(u64)]
31pub enum WorkerOp {
32    IsValidPath = 1,
33    HasSubstitutes = 3,
34    QueryPathHash = 4,
35    QueryReferences = 5,
36    QueryReferrers = 6,
37    AddToStore = 7,
38    AddTextToStore = 8,
39    BuildPaths = 9,
40    EnsurePath = 10,
41    AddTempRoot = 11,
42    AddIndirectRoot = 12,
43    SyncWithGC = 13,
44    FindRoots = 14,
45    ExportPath = 16,
46    QueryDeriver = 18,
47    SetOptions = 19,
48    CollectGarbage = 20,
49    QuerySubstitutablePathInfo = 21,
50    QueryDerivationOutputs = 22,
51    QueryAllValidPaths = 23,
52    QueryFailedPaths = 24,
53    ClearFailedPaths = 25,
54    QueryPathInfo = 26,
55    ImportPaths = 27,
56    QueryDerivationOutputNames = 28,
57    QueryPathFromHashPart = 29,
58    QuerySubstitutablePathInfos = 30,
59    QueryValidPaths = 31,
60    QuerySubstitutablePaths = 32,
61    QueryValidDerivers = 33,
62    OptimiseStore = 34,
63    VerifyStore = 35,
64    BuildDerivation = 36,
65    AddSignatures = 37,
66    NarFromPath = 38,
67    AddToStoreNar = 39,
68    QueryMissing = 40,
69    QueryDerivationOutputMap = 41,
70    RegisterDrvOutput = 42,
71    QueryRealisation = 43,
72    AddMultipleToStore = 44,
73    AddBuildLog = 45,
74}
75
76impl WorkerOp {
77    /// Parse an opcode from a u64 value.
78    #[must_use]
79    pub fn from_u64(v: u64) -> Option<Self> {
80        Self::try_from(v).ok()
81    }
82}
83
84impl TryFrom<u64> for WorkerOp {
85    type Error = WireError;
86
87    fn try_from(v: u64) -> Result<Self, Self::Error> {
88        match v {
89            1 => Ok(Self::IsValidPath),
90            3 => Ok(Self::HasSubstitutes),
91            4 => Ok(Self::QueryPathHash),
92            5 => Ok(Self::QueryReferences),
93            6 => Ok(Self::QueryReferrers),
94            7 => Ok(Self::AddToStore),
95            8 => Ok(Self::AddTextToStore),
96            9 => Ok(Self::BuildPaths),
97            10 => Ok(Self::EnsurePath),
98            11 => Ok(Self::AddTempRoot),
99            12 => Ok(Self::AddIndirectRoot),
100            13 => Ok(Self::SyncWithGC),
101            14 => Ok(Self::FindRoots),
102            16 => Ok(Self::ExportPath),
103            18 => Ok(Self::QueryDeriver),
104            19 => Ok(Self::SetOptions),
105            20 => Ok(Self::CollectGarbage),
106            21 => Ok(Self::QuerySubstitutablePathInfo),
107            22 => Ok(Self::QueryDerivationOutputs),
108            23 => Ok(Self::QueryAllValidPaths),
109            24 => Ok(Self::QueryFailedPaths),
110            25 => Ok(Self::ClearFailedPaths),
111            26 => Ok(Self::QueryPathInfo),
112            27 => Ok(Self::ImportPaths),
113            28 => Ok(Self::QueryDerivationOutputNames),
114            29 => Ok(Self::QueryPathFromHashPart),
115            30 => Ok(Self::QuerySubstitutablePathInfos),
116            31 => Ok(Self::QueryValidPaths),
117            32 => Ok(Self::QuerySubstitutablePaths),
118            33 => Ok(Self::QueryValidDerivers),
119            34 => Ok(Self::OptimiseStore),
120            35 => Ok(Self::VerifyStore),
121            36 => Ok(Self::BuildDerivation),
122            37 => Ok(Self::AddSignatures),
123            38 => Ok(Self::NarFromPath),
124            39 => Ok(Self::AddToStoreNar),
125            40 => Ok(Self::QueryMissing),
126            41 => Ok(Self::QueryDerivationOutputMap),
127            42 => Ok(Self::RegisterDrvOutput),
128            43 => Ok(Self::QueryRealisation),
129            44 => Ok(Self::AddMultipleToStore),
130            45 => Ok(Self::AddBuildLog),
131            _ => Err(WireError::Protocol(format!("unknown worker op: {v}"))),
132        }
133    }
134}
135
136/// Stderr message types sent by the daemon during operation processing.
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138#[non_exhaustive]
139#[repr(u64)]
140pub enum StderrMsg {
141    /// Write a string to stderr.
142    Write = 0x6f6c6d67,      // "olmg"
143    /// End of stderr stream — followed by the actual response.
144    Last = 0x616c7473,       // "alts"
145    /// Error message.
146    Error = 0x63787470,      // "cxtp"
147    /// Start activity.
148    StartActivity = 0x53545254, // "STRT"
149    /// Stop activity.
150    StopActivity = 0x53544f50, // "STOP"
151    /// Activity result.
152    Result = 0x52534c54,     // "RSLT"
153}
154
155// ── Wire primitives ──────────────────────────────────────────
156
157/// Write a u64 in little-endian.
158pub fn write_u64(w: &mut impl Write, v: u64) -> io::Result<()> {
159    w.write_all(&v.to_le_bytes())
160}
161
162/// Read a u64 in little-endian.
163pub fn read_u64(r: &mut impl Read) -> io::Result<u64> {
164    let mut buf = [0u8; 8];
165    r.read_exact(&mut buf)?;
166    Ok(u64::from_le_bytes(buf))
167}
168
169/// Zero buffer for 8-byte alignment padding (max 7 bytes needed).
170const PADDING: [u8; 8] = [0u8; 8];
171
172/// Write a length-prefixed, 8-byte-aligned byte buffer.
173pub fn write_bytes(w: &mut impl Write, data: &[u8]) -> io::Result<()> {
174    write_u64(w, data.len() as u64)?;
175    w.write_all(data)?;
176    let pad = (8 - (data.len() % 8)) % 8;
177    if pad > 0 {
178        w.write_all(&PADDING[..pad])?;
179    }
180    Ok(())
181}
182
183/// Read a length-prefixed, 8-byte-aligned byte buffer.
184pub fn read_bytes(r: &mut impl Read) -> io::Result<Vec<u8>> {
185    let len = read_u64(r)? as usize;
186    let mut buf = vec![0u8; len];
187    r.read_exact(&mut buf)?;
188    let pad = (8 - (len % 8)) % 8;
189    if pad > 0 {
190        let mut pad_buf = [0u8; 8];
191        r.read_exact(&mut pad_buf[..pad])?;
192    }
193    Ok(buf)
194}
195
196/// Write a UTF-8 string (as length-prefixed bytes).
197pub fn write_string(w: &mut impl Write, s: impl AsRef<str>) -> io::Result<()> {
198    write_bytes(w, s.as_ref().as_bytes())
199}
200
201/// Read a UTF-8 string.
202pub fn read_string(r: &mut impl Read) -> Result<String, WireError> {
203    let bytes = read_bytes(r)?;
204    String::from_utf8(bytes).map_err(|e| WireError::Protocol(format!("invalid UTF-8: {e}")))
205}
206
207/// Write a bool (as u64: 0 or 1).
208pub fn write_bool(w: &mut impl Write, v: bool) -> io::Result<()> {
209    write_u64(w, u64::from(v))
210}
211
212/// Read a bool (from u64: 0 or 1).
213pub fn read_bool(r: &mut impl Read) -> io::Result<bool> {
214    Ok(read_u64(r)? != 0)
215}
216
217/// Write a list of strings.
218pub fn write_string_list(w: &mut impl Write, list: &[impl AsRef<str>]) -> io::Result<()> {
219    write_u64(w, list.len() as u64)?;
220    for s in list {
221        write_string(w, s)?;
222    }
223    Ok(())
224}
225
226/// Read a list of strings.
227pub fn read_string_list(r: &mut impl Read) -> Result<Vec<String>, WireError> {
228    let count = read_u64(r)? as usize;
229    (0..count).map(|_| read_string(r)).collect()
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235    use std::io::Cursor;
236
237    #[test]
238    fn u64_roundtrip() {
239        let mut buf = Vec::new();
240        write_u64(&mut buf, 42).unwrap();
241        assert_eq!(buf.len(), 8);
242        let v = read_u64(&mut Cursor::new(&buf)).unwrap();
243        assert_eq!(v, 42);
244    }
245
246    #[test]
247    fn bytes_roundtrip() {
248        let data = b"hello";
249        let mut buf = Vec::new();
250        write_bytes(&mut buf, data).unwrap();
251        // 8 (length) + 5 (data) + 3 (padding) = 16
252        assert_eq!(buf.len(), 16);
253        let read = read_bytes(&mut Cursor::new(&buf)).unwrap();
254        assert_eq!(read, data);
255    }
256
257    #[test]
258    fn string_roundtrip() {
259        let s = "hello world";
260        let mut buf = Vec::new();
261        write_string(&mut buf, s).unwrap();
262        let read = read_string(&mut Cursor::new(&buf)).unwrap();
263        assert_eq!(read, s);
264    }
265
266    #[test]
267    fn bool_roundtrip() {
268        for v in [true, false] {
269            let mut buf = Vec::new();
270            write_bool(&mut buf, v).unwrap();
271            let read = read_bool(&mut Cursor::new(&buf)).unwrap();
272            assert_eq!(read, v);
273        }
274    }
275
276    #[test]
277    fn string_list_roundtrip() {
278        let list = vec!["foo".to_string(), "bar".to_string(), "baz".to_string()];
279        let mut buf = Vec::new();
280        write_string_list(&mut buf, &list).unwrap();
281        let read = read_string_list(&mut Cursor::new(&buf)).unwrap();
282        assert_eq!(read, list);
283    }
284
285    #[test]
286    fn empty_string() {
287        let mut buf = Vec::new();
288        write_string(&mut buf, "").unwrap();
289        assert_eq!(buf.len(), 8); // just the length field
290        let read = read_string(&mut Cursor::new(&buf)).unwrap();
291        assert_eq!(read, "");
292    }
293
294    #[test]
295    fn worker_op_from_u64() {
296        assert_eq!(WorkerOp::from_u64(1), Some(WorkerOp::IsValidPath));
297        assert_eq!(WorkerOp::from_u64(26), Some(WorkerOp::QueryPathInfo));
298        assert_eq!(WorkerOp::from_u64(9999), None);
299    }
300
301    #[test]
302    fn magic_constants() {
303        // Verify magic bytes match the ASCII representations
304        assert_eq!(&WORKER_MAGIC_1.to_le_bytes()[..4], b"cxin");
305        assert_eq!(&WORKER_MAGIC_2.to_le_bytes()[..4], b"oixd");
306    }
307
308    #[test]
309    fn eight_byte_alignment() {
310        // "abc" is 3 bytes, needs 5 bytes padding
311        let mut buf = Vec::new();
312        write_bytes(&mut buf, b"abc").unwrap();
313        assert_eq!(buf.len() % 8, 0);
314        assert_eq!(buf.len(), 16); // 8 (len) + 3 (data) + 5 (pad)
315    }
316
317    #[test]
318    fn large_string_roundtrip() {
319        // 1KB+ string
320        let s: String = "abcdefghij".repeat(120); // 1200 bytes
321        assert!(s.len() >= 1024);
322        let mut buf = Vec::new();
323        write_string(&mut buf, &s).unwrap();
324        assert_eq!(buf.len() % 8, 0);
325        let read = read_string(&mut Cursor::new(&buf)).unwrap();
326        assert_eq!(read, s);
327    }
328
329    #[test]
330    fn empty_list_roundtrip() {
331        let list: Vec<String> = vec![];
332        let mut buf = Vec::new();
333        write_string_list(&mut buf, &list).unwrap();
334        // Should be just the count (0) as u64
335        assert_eq!(buf.len(), 8);
336        let read = read_string_list(&mut Cursor::new(&buf)).unwrap();
337        assert!(read.is_empty());
338    }
339
340    #[test]
341    fn binary_data_with_zero_bytes() {
342        let data: Vec<u8> = vec![0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x01, 0x00];
343        let mut buf = Vec::new();
344        write_bytes(&mut buf, &data).unwrap();
345        let read = read_bytes(&mut Cursor::new(&buf)).unwrap();
346        assert_eq!(read, data);
347    }
348
349    #[test]
350    fn protocol_version_constant() {
351        // Protocol version 1.37: major=1 in high byte, minor=37 in low byte
352        assert_eq!(PROTOCOL_VERSION, (1 << 8) | 37);
353        assert_eq!(PROTOCOL_VERSION, 293);
354        // Extract major and minor
355        let major = PROTOCOL_VERSION >> 8;
356        let minor = PROTOCOL_VERSION & 0xFF;
357        assert_eq!(major, 1);
358        assert_eq!(minor, 37);
359    }
360
361    #[test]
362    fn large_u64_roundtrip() {
363        let mut buf = Vec::new();
364        write_u64(&mut buf, u64::MAX).unwrap();
365        let v = read_u64(&mut Cursor::new(&buf)).unwrap();
366        assert_eq!(v, u64::MAX);
367    }
368
369    #[test]
370    fn bytes_exact_8_byte_size() {
371        // 8 bytes exactly: no padding needed
372        let data = b"12345678";
373        let mut buf = Vec::new();
374        write_bytes(&mut buf, data).unwrap();
375        // 8 (length) + 8 (data) + 0 (no padding) = 16
376        assert_eq!(buf.len(), 16);
377        let read = read_bytes(&mut Cursor::new(&buf)).unwrap();
378        assert_eq!(read, data);
379    }
380
381    #[test]
382    fn worker_op_all_valid_codes() {
383        let valid_ops: Vec<(u64, WorkerOp)> = vec![
384            (1, WorkerOp::IsValidPath),
385            (7, WorkerOp::AddToStore),
386            (9, WorkerOp::BuildPaths),
387            (26, WorkerOp::QueryPathInfo),
388            (38, WorkerOp::NarFromPath),
389            (45, WorkerOp::AddBuildLog),
390        ];
391        for (code, expected) in valid_ops {
392            assert_eq!(WorkerOp::from_u64(code), Some(expected));
393        }
394    }
395
396    #[test]
397    fn worker_op_invalid_codes() {
398        assert_eq!(WorkerOp::from_u64(0), None);
399        assert_eq!(WorkerOp::from_u64(2), None);
400        assert_eq!(WorkerOp::from_u64(15), None);
401        assert_eq!(WorkerOp::from_u64(17), None);
402        assert_eq!(WorkerOp::from_u64(46), None);
403        assert_eq!(WorkerOp::from_u64(u64::MAX), None);
404    }
405
406    // ── Additional wire primitive tests ──────────────────
407
408    #[test]
409    fn u64_zero_roundtrip() {
410        let mut buf = Vec::new();
411        write_u64(&mut buf, 0).unwrap();
412        let v = read_u64(&mut Cursor::new(&buf)).unwrap();
413        assert_eq!(v, 0);
414    }
415
416    #[test]
417    fn bytes_empty_roundtrip() {
418        let mut buf = Vec::new();
419        write_bytes(&mut buf, &[]).unwrap();
420        assert_eq!(buf.len(), 8);
421        let read = read_bytes(&mut Cursor::new(&buf)).unwrap();
422        assert!(read.is_empty());
423    }
424
425    #[test]
426    fn string_with_unicode() {
427        let s = "日本語テスト 🎉";
428        let mut buf = Vec::new();
429        write_string(&mut buf, s).unwrap();
430        assert_eq!(buf.len() % 8, 0);
431        let read = read_string(&mut Cursor::new(&buf)).unwrap();
432        assert_eq!(read, s);
433    }
434
435    #[test]
436    fn string_list_single_entry() {
437        let list = vec!["only-one".to_string()];
438        let mut buf = Vec::new();
439        write_string_list(&mut buf, &list).unwrap();
440        let read = read_string_list(&mut Cursor::new(&buf)).unwrap();
441        assert_eq!(read, list);
442    }
443
444    #[test]
445    fn string_list_many_entries() {
446        let list: Vec<String> = (0..100).map(|i| format!("item-{i}")).collect();
447        let mut buf = Vec::new();
448        write_string_list(&mut buf, &list).unwrap();
449        let read = read_string_list(&mut Cursor::new(&buf)).unwrap();
450        assert_eq!(read, list);
451    }
452
453    #[test]
454    fn worker_op_exhaustive_coverage() {
455        let all: Vec<(u64, WorkerOp)> = vec![
456            (1, WorkerOp::IsValidPath),
457            (3, WorkerOp::HasSubstitutes),
458            (4, WorkerOp::QueryPathHash),
459            (5, WorkerOp::QueryReferences),
460            (6, WorkerOp::QueryReferrers),
461            (7, WorkerOp::AddToStore),
462            (8, WorkerOp::AddTextToStore),
463            (9, WorkerOp::BuildPaths),
464            (10, WorkerOp::EnsurePath),
465            (11, WorkerOp::AddTempRoot),
466            (12, WorkerOp::AddIndirectRoot),
467            (13, WorkerOp::SyncWithGC),
468            (14, WorkerOp::FindRoots),
469            (16, WorkerOp::ExportPath),
470            (18, WorkerOp::QueryDeriver),
471            (19, WorkerOp::SetOptions),
472            (20, WorkerOp::CollectGarbage),
473            (21, WorkerOp::QuerySubstitutablePathInfo),
474            (22, WorkerOp::QueryDerivationOutputs),
475            (23, WorkerOp::QueryAllValidPaths),
476            (24, WorkerOp::QueryFailedPaths),
477            (25, WorkerOp::ClearFailedPaths),
478            (26, WorkerOp::QueryPathInfo),
479            (27, WorkerOp::ImportPaths),
480            (28, WorkerOp::QueryDerivationOutputNames),
481            (29, WorkerOp::QueryPathFromHashPart),
482            (30, WorkerOp::QuerySubstitutablePathInfos),
483            (31, WorkerOp::QueryValidPaths),
484            (32, WorkerOp::QuerySubstitutablePaths),
485            (33, WorkerOp::QueryValidDerivers),
486            (34, WorkerOp::OptimiseStore),
487            (35, WorkerOp::VerifyStore),
488            (36, WorkerOp::BuildDerivation),
489            (37, WorkerOp::AddSignatures),
490            (38, WorkerOp::NarFromPath),
491            (39, WorkerOp::AddToStoreNar),
492            (40, WorkerOp::QueryMissing),
493            (41, WorkerOp::QueryDerivationOutputMap),
494            (42, WorkerOp::RegisterDrvOutput),
495            (43, WorkerOp::QueryRealisation),
496            (44, WorkerOp::AddMultipleToStore),
497            (45, WorkerOp::AddBuildLog),
498        ];
499        for (code, expected) in &all {
500            assert_eq!(WorkerOp::from_u64(*code), Some(*expected), "failed for code {code}");
501        }
502        assert_eq!(all.len(), 42);
503    }
504
505    #[test]
506    fn read_u64_truncated_input() {
507        let result = read_u64(&mut Cursor::new(&[0u8; 4]));
508        assert!(result.is_err());
509    }
510
511    #[test]
512    fn read_bytes_truncated_data() {
513        let mut buf = Vec::new();
514        write_u64(&mut buf, 100).unwrap();
515        buf.extend_from_slice(&[0u8; 10]);
516        let result = read_bytes(&mut Cursor::new(&buf));
517        assert!(result.is_err());
518    }
519
520    // ── Alignment edge cases for write_bytes ─────────────
521
522    #[test]
523    fn write_bytes_alignment_for_each_residue() {
524        // Lengths 0..16 — verify the output is always aligned to 8 bytes
525        for len in 0..16 {
526            let data = vec![0xAA_u8; len];
527            let mut buf = Vec::new();
528            write_bytes(&mut buf, &data).unwrap();
529            assert_eq!(buf.len() % 8, 0, "len={len} not 8-byte aligned");
530            // Total size = 8 (length prefix) + ceil(len/8)*8 (data + padding)
531            let aligned_data_size = ((len + 7) / 8) * 8;
532            assert_eq!(buf.len(), 8 + aligned_data_size);
533        }
534    }
535
536    #[test]
537    fn write_bytes_padding_is_zeros() {
538        // 5 bytes → 3 padding bytes which must be zero
539        let mut buf = Vec::new();
540        write_bytes(&mut buf, b"hello").unwrap();
541        // Layout: [u64 len = 5][5 data bytes][3 zero pad]
542        assert_eq!(&buf[8..13], b"hello");
543        assert_eq!(&buf[13..16], &[0u8, 0u8, 0u8]);
544    }
545
546    #[test]
547    fn read_bytes_alignment_for_each_residue() {
548        for len in 0_usize..16 {
549            let data = vec![0xCC_u8; len];
550            let mut buf = Vec::new();
551            write_bytes(&mut buf, &data).unwrap();
552            let read = read_bytes(&mut Cursor::new(&buf)).unwrap();
553            assert_eq!(read, data, "roundtrip failed for len={len}");
554        }
555    }
556
557    // ── read_string error path ───────────────────────────
558
559    #[test]
560    fn read_string_invalid_utf8_returns_protocol_error() {
561        let mut buf = Vec::new();
562        // Invalid UTF-8 sequence: 0xff is never a valid UTF-8 start byte
563        write_bytes(&mut buf, &[0xFF, 0xFE, 0xFD]).unwrap();
564        let result = read_string(&mut Cursor::new(&buf));
565        assert!(result.is_err());
566        match result {
567            Err(WireError::Protocol(_)) => {}
568            other => panic!("expected Protocol error, got {other:?}"),
569        }
570    }
571
572    // ── TryFrom<u64> for WorkerOp ────────────────────────
573
574    #[test]
575    fn worker_op_try_from_known() {
576        let op: WorkerOp = WorkerOp::try_from(26).unwrap();
577        assert_eq!(op, WorkerOp::QueryPathInfo);
578    }
579
580    #[test]
581    fn worker_op_try_from_unknown_returns_protocol_error() {
582        let result = WorkerOp::try_from(999u64);
583        assert!(result.is_err());
584        match result {
585            Err(WireError::Protocol(s)) => assert!(s.contains("999")),
586            other => panic!("expected Protocol error, got {other:?}"),
587        }
588    }
589
590    // ── StderrMsg constants ──────────────────────────────
591
592    #[test]
593    fn stderr_msg_constants_distinct() {
594        let codes = [
595            StderrMsg::Write as u64,
596            StderrMsg::Last as u64,
597            StderrMsg::Error as u64,
598            StderrMsg::StartActivity as u64,
599            StderrMsg::StopActivity as u64,
600            StderrMsg::Result as u64,
601        ];
602        // All codes must be distinct
603        for i in 0..codes.len() {
604            for j in (i + 1)..codes.len() {
605                assert_ne!(codes[i], codes[j]);
606            }
607        }
608    }
609
610    #[test]
611    fn stderr_msg_eq_clone_copy() {
612        let a = StderrMsg::Write;
613        let b = a;
614        let c = a;
615        assert_eq!(a, b);
616        assert_eq!(b, c);
617    }
618
619    // ── WORKER_MAGIC + PROTOCOL_VERSION constants ───────
620
621    #[test]
622    fn worker_magic_constants_distinct() {
623        assert_ne!(WORKER_MAGIC_1, WORKER_MAGIC_2);
624    }
625
626    // ── read_bool truthiness ─────────────────────────────
627
628    #[test]
629    fn read_bool_nonzero_is_true() {
630        let mut buf = Vec::new();
631        write_u64(&mut buf, 42).unwrap();
632        let v = read_bool(&mut Cursor::new(&buf)).unwrap();
633        assert!(v);
634    }
635
636    #[test]
637    fn read_bool_zero_is_false() {
638        let mut buf = Vec::new();
639        write_u64(&mut buf, 0).unwrap();
640        let v = read_bool(&mut Cursor::new(&buf)).unwrap();
641        assert!(!v);
642    }
643
644    // ── read_string_list truncation ──────────────────────
645
646    #[test]
647    fn read_string_list_truncated_count_returns_error() {
648        let buf = [0u8; 4];
649        let result = read_string_list(&mut Cursor::new(&buf));
650        assert!(result.is_err());
651    }
652
653    #[test]
654    fn read_string_list_count_exceeds_data_returns_error() {
655        let mut buf = Vec::new();
656        write_u64(&mut buf, 100).unwrap(); // claim 100 strings
657        let result = read_string_list(&mut Cursor::new(&buf));
658        assert!(result.is_err());
659    }
660
661    // ── write_string_list with mixed-length entries ─────
662
663    #[test]
664    fn write_string_list_mixed_lengths() {
665        let list = vec![
666            String::new(),
667            "a".to_string(),
668            "ab".to_string(),
669            "abcdefgh".to_string(),
670            "abcdefghi".to_string(), // crosses 8-byte boundary
671        ];
672        let mut buf = Vec::new();
673        write_string_list(&mut buf, &list).unwrap();
674        let read = read_string_list(&mut Cursor::new(&buf)).unwrap();
675        assert_eq!(read, list);
676    }
677
678    // ── write_string accepts &str + String + &String ────
679
680    #[test]
681    fn write_string_accepts_str_types() {
682        let mut buf1 = Vec::new();
683        write_string(&mut buf1, "hello").unwrap();
684
685        let mut buf2 = Vec::new();
686        write_string(&mut buf2, String::from("hello")).unwrap();
687
688        let mut buf3 = Vec::new();
689        let s = String::from("hello");
690        write_string(&mut buf3, &s).unwrap();
691
692        assert_eq!(buf1, buf2);
693        assert_eq!(buf2, buf3);
694    }
695
696    // ── Length-prefix at exact byte boundaries ──────────
697
698    #[test]
699    fn bytes_length_seven_padding() {
700        // 7 bytes → needs 1 byte padding
701        let data = b"1234567";
702        let mut buf = Vec::new();
703        write_bytes(&mut buf, data).unwrap();
704        // 8 (len) + 7 (data) + 1 (pad) = 16
705        assert_eq!(buf.len(), 16);
706        let read = read_bytes(&mut Cursor::new(&buf)).unwrap();
707        assert_eq!(read, data);
708    }
709
710    #[test]
711    fn bytes_length_one_padding() {
712        // 1 byte → needs 7 bytes padding
713        let data = b"x";
714        let mut buf = Vec::new();
715        write_bytes(&mut buf, data).unwrap();
716        // 8 + 1 + 7 = 16
717        assert_eq!(buf.len(), 16);
718        let read = read_bytes(&mut Cursor::new(&buf)).unwrap();
719        assert_eq!(read, data);
720    }
721
722    #[test]
723    fn bytes_length_nine_padding() {
724        // 9 bytes → 7 bytes padding (mod 8 = 1)
725        let data = b"123456789";
726        let mut buf = Vec::new();
727        write_bytes(&mut buf, data).unwrap();
728        // 8 + 9 + 7 = 24
729        assert_eq!(buf.len(), 24);
730        let read = read_bytes(&mut Cursor::new(&buf)).unwrap();
731        assert_eq!(read, data);
732    }
733
734    // ── u64 endianness ───────────────────────────────────
735
736    #[test]
737    fn u64_written_in_little_endian() {
738        let mut buf = Vec::new();
739        write_u64(&mut buf, 0x0102_0304_0506_0708).unwrap();
740        // LE: low byte first
741        assert_eq!(buf, vec![0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01]);
742    }
743
744    // ── PROTOCOL_VERSION extraction ─────────────────────
745
746    #[test]
747    fn protocol_version_major_minor_extraction() {
748        let major = PROTOCOL_VERSION >> 8;
749        let minor = PROTOCOL_VERSION & 0xFF;
750        assert_eq!(major, 1);
751        assert_eq!(minor, 37);
752    }
753
754    // ── WorkerOp from_u64 vs try_from agreement ─────────
755
756    #[test]
757    fn worker_op_from_u64_matches_try_from() {
758        for code in [1, 3, 7, 26, 38, 45] {
759            let from_u64 = WorkerOp::from_u64(code);
760            let try_from = WorkerOp::try_from(code).ok();
761            assert_eq!(from_u64, try_from);
762        }
763        // Both reject unknown codes
764        assert_eq!(WorkerOp::from_u64(0), None);
765        assert!(WorkerOp::try_from(0u64).is_err());
766    }
767
768    // ── Error display ────────────────────────────────────
769
770    #[test]
771    fn wire_error_protocol_display() {
772        let err = WireError::Protocol("custom message".to_string());
773        let s = format!("{err}");
774        assert!(s.contains("custom message"));
775    }
776
777    #[test]
778    fn wire_error_bad_magic_display() {
779        let err = WireError::BadMagic { expected: 0x1234, got: 0x5678 };
780        let s = format!("{err}");
781        assert!(s.contains("0x1234"));
782        assert!(s.contains("0x5678"));
783    }
784
785    // ── Empty bytes via wire ─────────────────────────────
786
787    #[test]
788    fn read_bytes_empty_via_zero_length() {
789        let mut buf = Vec::new();
790        write_u64(&mut buf, 0).unwrap();
791        let read = read_bytes(&mut Cursor::new(&buf)).unwrap();
792        assert!(read.is_empty());
793    }
794
795    // ── Multiple sequential reads on one cursor ─────────
796
797    #[test]
798    fn multiple_writes_then_reads_in_order() {
799        let mut buf = Vec::new();
800        write_u64(&mut buf, 11).unwrap();
801        write_string(&mut buf, "first").unwrap();
802        write_u64(&mut buf, 22).unwrap();
803        write_string(&mut buf, "second").unwrap();
804        write_bool(&mut buf, true).unwrap();
805
806        let mut cur = Cursor::new(&buf);
807        assert_eq!(read_u64(&mut cur).unwrap(), 11);
808        assert_eq!(read_string(&mut cur).unwrap(), "first");
809        assert_eq!(read_u64(&mut cur).unwrap(), 22);
810        assert_eq!(read_string(&mut cur).unwrap(), "second");
811        assert!(read_bool(&mut cur).unwrap());
812    }
813}