Skip to main content

audio_plugin_bsd/
proto.rs

1//! IPC message protocol for host↔plugin communication.
2//!
3//! Every message is a single `u8` tag byte followed by tag-specific payloads.
4//! Strings are length-prefixed: `u32` (little-endian) byte count followed by
5//! UTF-8 bytes. Integers are little-endian.
6//!
7//! # Wire format
8//!
9//! | Tag | Direction | Payload |
10//! |---|---|---|
11//! | `0x01` | `Request::Ping` | *(empty)* |
12//! | `0x02` | `Request::GetMetadata` | `plugin_id: u32` |
13//! | `0x03` | `Request::Instantiate` | `plugin_id: u32` |
14//! | `0x04` | `Request::Process` | `plugin_id: u32, sample_count: u32` |
15//! | `0x81` | `Response::Pong` | *(empty)* |
16//! | `0x82` | `Response::Metadata` | `plugin_id: u32` + `PluginMetadataPayload` |
17//! | `0x83` | `Response::InstanceCreated` | `plugin_id: u32` |
18//! | `0x84` | `Response::InstanceDropped` | `plugin_id: u32` |
19//! | `0x85` | `Response::Processed` | `plugin_id: u32` |
20//! | `0xFE` | `Response::Error` | `code: u32, message: String` |
21//!
22//! # Safety
23//!
24//! All decode functions are **panic-free** (fuzz-safe). Malformed input returns
25//! [`PluginError::Sandbox`] rather than panicking.
26
27use crate::error::{PluginError, Result};
28
29// --- Tag constants --------------------------------------------------------
30
31const TAG_REQUEST_PING: u8 = 0x01;
32const TAG_REQUEST_GET_METADATA: u8 = 0x02;
33const TAG_REQUEST_INSTANTIATE: u8 = 0x03;
34const TAG_REQUEST_PROCESS: u8 = 0x04;
35const TAG_RESPONSE_PONG: u8 = 0x81;
36const TAG_RESPONSE_METADATA: u8 = 0x82;
37const TAG_RESPONSE_INSTANCE_CREATED: u8 = 0x83;
38const TAG_RESPONSE_INSTANCE_DROPPED: u8 = 0x84;
39const TAG_RESPONSE_PROCESSED: u8 = 0x85;
40const TAG_RESPONSE_ERROR: u8 = 0xFE;
41
42// --- Plugin metadata payload (wire subset) --------------------------------
43
44/// Wire-format metadata payload carried inside [`Response::Metadata`].
45///
46/// Only the fields required by 0.1.0 IPC are encoded; `author` and `license`
47/// are not part of the wire protocol and will be added in a future revision.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct PluginMetadataPayload {
50    /// Plugin name.
51    pub name: String,
52    /// Plugin version string.
53    pub version: String,
54    /// One-line description.
55    pub description: String,
56    /// Encoded ABI version the plugin was built against.
57    pub abi_version: u32,
58}
59
60// --- Request enum --------------------------------------------------------
61
62/// A host→plugin IPC request.
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub enum Request {
65    /// Liveness check; the plugin responds with [`Response::Pong`].
66    Ping,
67    /// Ask the plugin for its static metadata.
68    GetMetadata {
69        /// Plugin id (assigned by the loader).
70        plugin_id: u32,
71    },
72    /// Create a fresh audio-node instance.
73    Instantiate {
74        /// Plugin id (assigned by the loader).
75        plugin_id: u32,
76    },
77    /// Run one process cycle.
78    Process {
79        /// Plugin id (assigned by the loader).
80        plugin_id: u32,
81        /// Number of samples per frame.
82        sample_count: u32,
83    },
84}
85
86/// A plugin→host IPC response.
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub enum Response {
89    /// Reply to [`Request::Ping`].
90    Pong,
91    /// Static metadata for a plugin.
92    Metadata {
93        /// Plugin id echoed from the request.
94        plugin_id: u32,
95        /// The metadata payload.
96        payload: PluginMetadataPayload,
97    },
98    /// A new instance was created.
99    InstanceCreated {
100        /// Plugin id echoed from the request.
101        plugin_id: u32,
102    },
103    /// An instance was dropped.
104    InstanceDropped {
105        /// Plugin id echoed from the request.
106        plugin_id: u32,
107    },
108    /// A process cycle completed.
109    Processed {
110        /// Plugin id echoed from the request.
111        plugin_id: u32,
112    },
113    /// An error occurred inside the plugin.
114    Error {
115        /// Error code (undefined in 0.1.0; always 0).
116        code: u32,
117        /// Human-readable description.
118        message: String,
119    },
120}
121
122// --- Encode helpers -------------------------------------------------------
123
124fn encode_u32_le(buf: &mut Vec<u8>, val: u32) {
125    buf.extend_from_slice(&val.to_le_bytes());
126}
127
128fn encode_string(buf: &mut Vec<u8>, s: &str) {
129    let len = u32::try_from(s.len()).expect("string length exceeds u32::MAX");
130    encode_u32_le(buf, len);
131    buf.extend_from_slice(s.as_bytes());
132}
133
134// --- Decode helpers -------------------------------------------------------
135
136fn ensure_len(buf: &[u8], need: usize) -> Result<()> {
137    if buf.len() < need {
138        Err(PluginError::Sandbox(format!(
139            "malformed ipc frame: need {need} bytes, got {}",
140            buf.len()
141        )))
142    } else {
143        Ok(())
144    }
145}
146
147fn decode_u32_le(buf: &[u8], offset: usize) -> Result<u32> {
148    ensure_len(buf, offset + 4)?;
149    Ok(u32::from_le_bytes([
150        buf[offset],
151        buf[offset + 1],
152        buf[offset + 2],
153        buf[offset + 3],
154    ]))
155}
156
157fn decode_string(buf: &[u8], offset: usize) -> Result<(String, usize)> {
158    let len = decode_u32_le(buf, offset)? as usize;
159    let start = offset + 4;
160    ensure_len(buf, start + len)?;
161    let bytes = &buf[start..start + len];
162    let s = core::str::from_utf8(bytes).map_err(|e| {
163        PluginError::Sandbox(format!(
164            "malformed ipc frame: invalid utf-8 at offset {start}: {e}"
165        ))
166    })?;
167    Ok((s.to_owned(), start + len))
168}
169
170// --- Public encode / decode -----------------------------------------------
171
172/// Encode a [`Request`] into a byte buffer.
173#[must_use]
174pub fn encode_request(req: &Request) -> Vec<u8> {
175    let mut buf = Vec::with_capacity(64);
176    match req {
177        Request::Ping => buf.push(TAG_REQUEST_PING),
178        Request::GetMetadata { plugin_id } => {
179            buf.push(TAG_REQUEST_GET_METADATA);
180            encode_u32_le(&mut buf, *plugin_id);
181        }
182        Request::Instantiate { plugin_id } => {
183            buf.push(TAG_REQUEST_INSTANTIATE);
184            encode_u32_le(&mut buf, *plugin_id);
185        }
186        Request::Process {
187            plugin_id,
188            sample_count,
189        } => {
190            buf.push(TAG_REQUEST_PROCESS);
191            encode_u32_le(&mut buf, *plugin_id);
192            encode_u32_le(&mut buf, *sample_count);
193        }
194    }
195    buf
196}
197
198/// Decode a [`Request`] from a byte slice.
199///
200/// # Errors
201///
202/// Returns [`PluginError::Sandbox`] on malformed input (truncated frame,
203/// unknown tag, non-UTF-8 string). Never panics.
204pub fn decode_request(buf: &[u8]) -> Result<Request> {
205    ensure_len(buf, 1)?;
206    match buf[0] {
207        TAG_REQUEST_PING => Ok(Request::Ping),
208        TAG_REQUEST_GET_METADATA => {
209            let plugin_id = decode_u32_le(buf, 1)?;
210            Ok(Request::GetMetadata { plugin_id })
211        }
212        TAG_REQUEST_INSTANTIATE => {
213            let plugin_id = decode_u32_le(buf, 1)?;
214            Ok(Request::Instantiate { plugin_id })
215        }
216        TAG_REQUEST_PROCESS => {
217            let plugin_id = decode_u32_le(buf, 1)?;
218            let sample_count = decode_u32_le(buf, 5)?;
219            Ok(Request::Process {
220                plugin_id,
221                sample_count,
222            })
223        }
224        other => Err(PluginError::Sandbox(format!(
225            "malformed ipc frame: unknown request tag {other:#04x}"
226        ))),
227    }
228}
229
230/// Encode a [`Response`] into a byte buffer.
231#[must_use]
232pub fn encode_response(res: &Response) -> Vec<u8> {
233    let mut buf = Vec::with_capacity(128);
234    match res {
235        Response::Pong => buf.push(TAG_RESPONSE_PONG),
236        Response::Metadata { plugin_id, payload } => {
237            buf.push(TAG_RESPONSE_METADATA);
238            encode_u32_le(&mut buf, *plugin_id);
239            encode_string(&mut buf, &payload.name);
240            encode_string(&mut buf, &payload.version);
241            encode_string(&mut buf, &payload.description);
242            encode_u32_le(&mut buf, payload.abi_version);
243        }
244        Response::InstanceCreated { plugin_id } => {
245            buf.push(TAG_RESPONSE_INSTANCE_CREATED);
246            encode_u32_le(&mut buf, *plugin_id);
247        }
248        Response::InstanceDropped { plugin_id } => {
249            buf.push(TAG_RESPONSE_INSTANCE_DROPPED);
250            encode_u32_le(&mut buf, *plugin_id);
251        }
252        Response::Processed { plugin_id } => {
253            buf.push(TAG_RESPONSE_PROCESSED);
254            encode_u32_le(&mut buf, *plugin_id);
255        }
256        Response::Error { code, message } => {
257            buf.push(TAG_RESPONSE_ERROR);
258            encode_u32_le(&mut buf, *code);
259            encode_string(&mut buf, message);
260        }
261    }
262    buf
263}
264
265/// Decode a [`Response`] from a byte slice.
266///
267/// # Errors
268///
269/// Returns [`PluginError::Sandbox`] on malformed input (truncated frame,
270/// unknown tag, non-UTF-8 string). Never panics.
271pub fn decode_response(buf: &[u8]) -> Result<Response> {
272    ensure_len(buf, 1)?;
273    match buf[0] {
274        TAG_RESPONSE_PONG => Ok(Response::Pong),
275        TAG_RESPONSE_METADATA => {
276            let plugin_id = decode_u32_le(buf, 1)?;
277            let (name, offset) = decode_string(buf, 5)?;
278            let (version, offset) = decode_string(buf, offset)?;
279            let (description, offset) = decode_string(buf, offset)?;
280            let abi_version = decode_u32_le(buf, offset)?;
281            Ok(Response::Metadata {
282                plugin_id,
283                payload: PluginMetadataPayload {
284                    name,
285                    version,
286                    description,
287                    abi_version,
288                },
289            })
290        }
291        TAG_RESPONSE_INSTANCE_CREATED => {
292            let plugin_id = decode_u32_le(buf, 1)?;
293            Ok(Response::InstanceCreated { plugin_id })
294        }
295        TAG_RESPONSE_INSTANCE_DROPPED => {
296            let plugin_id = decode_u32_le(buf, 1)?;
297            Ok(Response::InstanceDropped { plugin_id })
298        }
299        TAG_RESPONSE_PROCESSED => {
300            let plugin_id = decode_u32_le(buf, 1)?;
301            Ok(Response::Processed { plugin_id })
302        }
303        TAG_RESPONSE_ERROR => {
304            let code = decode_u32_le(buf, 1)?;
305            let (message, _) = decode_string(buf, 5)?;
306            Ok(Response::Error { code, message })
307        }
308        other => Err(PluginError::Sandbox(format!(
309            "malformed ipc frame: unknown response tag {other:#04x}"
310        ))),
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317
318    // --- Round-trip tests -------------------------------------------------
319
320    #[test]
321    fn ping_roundtrip() {
322        let req = Request::Ping;
323        let buf = encode_request(&req);
324        assert_eq!(buf, vec![TAG_REQUEST_PING]);
325        assert_eq!(decode_request(&buf).unwrap(), req);
326    }
327
328    #[test]
329    fn get_metadata_roundtrip() {
330        let req = Request::GetMetadata { plugin_id: 42 };
331        let buf = encode_request(&req);
332        assert_eq!(decode_request(&buf).unwrap(), req);
333    }
334
335    #[test]
336    fn instantiate_roundtrip() {
337        let req = Request::Instantiate { plugin_id: 7 };
338        let buf = encode_request(&req);
339        assert_eq!(decode_request(&buf).unwrap(), req);
340    }
341
342    #[test]
343    fn process_roundtrip() {
344        let req = Request::Process {
345            plugin_id: 3,
346            sample_count: 256,
347        };
348        let buf = encode_request(&req);
349        assert_eq!(decode_request(&buf).unwrap(), req);
350    }
351
352    #[test]
353    fn pong_roundtrip() {
354        let res = Response::Pong;
355        let buf = encode_response(&res);
356        assert_eq!(buf, vec![TAG_RESPONSE_PONG]);
357        assert_eq!(decode_response(&buf).unwrap(), res);
358    }
359
360    #[test]
361    fn metadata_roundtrip() {
362        let res = Response::Metadata {
363            plugin_id: 1,
364            payload: PluginMetadataPayload {
365                name: "reverb-plate".into(),
366                version: "0.3.1".into(),
367                description: "plate reverb DSP".into(),
368                abi_version: 0x0001_0000,
369            },
370        };
371        let buf = encode_response(&res);
372        assert_eq!(decode_response(&buf).unwrap(), res);
373    }
374
375    #[test]
376    fn instance_created_roundtrip() {
377        let res = Response::InstanceCreated { plugin_id: 99 };
378        let buf = encode_response(&res);
379        assert_eq!(decode_response(&buf).unwrap(), res);
380    }
381
382    #[test]
383    fn instance_dropped_roundtrip() {
384        let res = Response::InstanceDropped { plugin_id: 55 };
385        let buf = encode_response(&res);
386        assert_eq!(decode_response(&buf).unwrap(), res);
387    }
388
389    #[test]
390    fn processed_roundtrip() {
391        let res = Response::Processed { plugin_id: 10 };
392        let buf = encode_response(&res);
393        assert_eq!(decode_response(&buf).unwrap(), res);
394    }
395
396    #[test]
397    fn error_roundtrip() {
398        let res = Response::Error {
399            code: 42,
400            message: "something went wrong".into(),
401        };
402        let buf = encode_response(&res);
403        assert_eq!(decode_response(&buf).unwrap(), res);
404    }
405
406    // --- Edge cases: empty strings, unicode, max u32 ----------------------
407
408    #[test]
409    fn metadata_empty_strings_roundtrip() {
410        let res = Response::Metadata {
411            plugin_id: 0,
412            payload: PluginMetadataPayload {
413                name: String::new(),
414                version: String::new(),
415                description: String::new(),
416                abi_version: u32::MAX,
417            },
418        };
419        let buf = encode_response(&res);
420        assert_eq!(decode_response(&buf).unwrap(), res);
421    }
422
423    #[test]
424    fn metadata_unicode_strings_roundtrip() {
425        let res = Response::Metadata {
426            plugin_id: 1,
427            payload: PluginMetadataPayload {
428                name: "κλαδί".into(),
429                version: "1.0.0-β".into(),
430                description: "テスト プラグイン".into(),
431                abi_version: 0x0002_0001,
432            },
433        };
434        let buf = encode_response(&res);
435        assert_eq!(decode_response(&buf).unwrap(), res);
436    }
437
438    #[test]
439    fn request_max_plugin_id_roundtrip() {
440        let req = Request::GetMetadata {
441            plugin_id: u32::MAX,
442        };
443        let buf = encode_request(&req);
444        assert_eq!(decode_request(&buf).unwrap(), req);
445    }
446
447    // --- Error paths: truncated, bad tag ----------------------------------
448
449    #[test]
450    fn decode_request_empty_is_err() {
451        let err = decode_request(&[]).unwrap_err();
452        assert!(matches!(err, PluginError::Sandbox(_)));
453        assert!(err.to_string().contains("malformed ipc frame"));
454    }
455
456    #[test]
457    fn decode_response_empty_is_err() {
458        let err = decode_response(&[]).unwrap_err();
459        assert!(matches!(err, PluginError::Sandbox(_)));
460    }
461
462    #[test]
463    fn decode_request_unknown_tag_is_err() {
464        let err = decode_request(&[0xFF]).unwrap_err();
465        assert!(matches!(err, PluginError::Sandbox(_)));
466        assert!(err.to_string().contains("unknown request tag"));
467    }
468
469    #[test]
470    fn decode_response_unknown_tag_is_err() {
471        let err = decode_response(&[0xFF]).unwrap_err();
472        assert!(matches!(err, PluginError::Sandbox(_)));
473        assert!(err.to_string().contains("unknown response tag"));
474    }
475
476    #[test]
477    fn decode_get_metadata_truncated_is_err() {
478        // Tag present but only 2 bytes of the 5 needed for a u32.
479        let err = decode_request(&[TAG_REQUEST_GET_METADATA, 0x01]).unwrap_err();
480        assert!(matches!(err, PluginError::Sandbox(_)));
481        assert!(err.to_string().contains("need 5 bytes"));
482    }
483
484    #[test]
485    fn decode_process_truncated_is_err() {
486        // Tag + 4 bytes for plugin_id, but missing sample_count.
487        let err = decode_request(&[TAG_REQUEST_PROCESS, 0x00, 0x00, 0x00, 0x00]).unwrap_err();
488        assert!(matches!(err, PluginError::Sandbox(_)));
489    }
490
491    #[test]
492    fn decode_metadata_string_truncated_is_err() {
493        // Valid tag + plugin_id, but string length says 100 and we only have
494        // 1 byte.
495        let mut buf = vec![TAG_RESPONSE_METADATA];
496        buf.extend_from_slice(&1u32.to_le_bytes()); // plugin_id
497        buf.extend_from_slice(&100u32.to_le_bytes()); // name_len = 100
498        buf.push(0x41); // 'A' — not enough bytes
499        let err = decode_response(&buf).unwrap_err();
500        assert!(matches!(err, PluginError::Sandbox(_)));
501    }
502
503    #[test]
504    fn decode_metadata_invalid_utf8_is_err() {
505        let mut buf = vec![TAG_RESPONSE_METADATA];
506        buf.extend_from_slice(&1u32.to_le_bytes()); // plugin_id
507        buf.extend_from_slice(&3u32.to_le_bytes()); // name_len = 3
508        buf.extend_from_slice(&[0xFF, 0xFE, 0xFD]); // invalid UTF-8
509        let err = decode_response(&buf).unwrap_err();
510        assert!(matches!(err, PluginError::Sandbox(_)));
511        assert!(err.to_string().contains("invalid utf-8"));
512    }
513
514    #[test]
515    fn decode_error_string_truncated_is_err() {
516        let mut buf = vec![TAG_RESPONSE_ERROR];
517        buf.extend_from_slice(&0u32.to_le_bytes()); // code
518        buf.extend_from_slice(&999u32.to_le_bytes()); // message_len = 999
519        let err = decode_response(&buf).unwrap_err();
520        assert!(matches!(err, PluginError::Sandbox(_)));
521    }
522
523    // --- Wire format checks: tag bytes match spec -------------------------
524
525    #[test]
526    fn request_tag_bytes_match_spec() {
527        assert_eq!(encode_request(&Request::Ping), vec![0x01]);
528        let gm = encode_request(&Request::GetMetadata { plugin_id: 0 });
529        assert_eq!(gm[0], 0x02);
530        let inst = encode_request(&Request::Instantiate { plugin_id: 0 });
531        assert_eq!(inst[0], 0x03);
532        let proc = encode_request(&Request::Process {
533            plugin_id: 0,
534            sample_count: 0,
535        });
536        assert_eq!(proc[0], 0x04);
537    }
538
539    #[test]
540    fn response_tag_bytes_match_spec() {
541        assert_eq!(encode_response(&Response::Pong), vec![0x81]);
542        let meta = encode_response(&Response::Metadata {
543            plugin_id: 0,
544            payload: PluginMetadataPayload {
545                name: String::new(),
546                version: String::new(),
547                description: String::new(),
548                abi_version: 0,
549            },
550        });
551        assert_eq!(meta[0], 0x82);
552        let ic = encode_response(&Response::InstanceCreated { plugin_id: 0 });
553        assert_eq!(ic[0], 0x83);
554        let id = encode_response(&Response::InstanceDropped { plugin_id: 0 });
555        assert_eq!(id[0], 0x84);
556        let p = encode_response(&Response::Processed { plugin_id: 0 });
557        assert_eq!(p[0], 0x85);
558        let e = encode_response(&Response::Error {
559            code: 0,
560            message: String::new(),
561        });
562        assert_eq!(e[0], 0xFE);
563    }
564
565    // --- PluginMetadataPayload --------------------------------------------
566
567    #[test]
568    fn plugin_metadata_payload_is_clone_eq_debug() {
569        let a = PluginMetadataPayload {
570            name: "gain".into(),
571            version: "1.0".into(),
572            description: "gain".into(),
573            abi_version: 1,
574        };
575        let b = a.clone();
576        assert_eq!(a, b);
577        assert!(format!("{a:?}").contains("PluginMetadataPayload"));
578    }
579
580    // --- Fuzz safety: decode never panics on arbitrary bytes --------------
581
582    #[test]
583    fn decode_request_arbitrary_byte_does_not_panic() {
584        for byte in 0..=u8::MAX {
585            let _ = decode_request(&[byte]);
586            let _ = decode_request(&[byte, 0x00, 0x00, 0x00, 0x00, 0x00]);
587        }
588    }
589
590    #[test]
591    fn decode_response_arbitrary_byte_does_not_panic() {
592        for byte in 0..=u8::MAX {
593            let _ = decode_response(&[byte]);
594            let _ = decode_response(&[byte, 0x00, 0x00, 0x00, 0x00, 0x00]);
595        }
596    }
597
598    // --- Property tests ---------------------------------------------------
599
600    use proptest::prelude::*;
601
602    proptest! {
603        #[test]
604        fn request_ping_roundtrip_ignored(_seed in 0u32..10) {
605            let req = Request::Ping;
606            let buf = encode_request(&req);
607            prop_assert_eq!(decode_request(&buf).unwrap(), req);
608        }
609
610        #[test]
611        fn request_get_metadata_roundtrip(plugin_id in 0u32..=u32::MAX) {
612            let req = Request::GetMetadata { plugin_id };
613            let buf = encode_request(&req);
614            prop_assert_eq!(decode_request(&buf).unwrap(), req);
615        }
616
617        #[test]
618        fn request_instantiate_roundtrip(plugin_id in 0u32..=u32::MAX) {
619            let req = Request::Instantiate { plugin_id };
620            let buf = encode_request(&req);
621            prop_assert_eq!(decode_request(&buf).unwrap(), req);
622        }
623
624        #[test]
625        fn request_process_roundtrip(
626            plugin_id in 0u32..=u32::MAX,
627            sample_count in 0u32..=u32::MAX,
628        ) {
629            let req = Request::Process { plugin_id, sample_count };
630            let buf = encode_request(&req);
631            prop_assert_eq!(decode_request(&buf).unwrap(), req);
632        }
633
634        #[test]
635        fn response_pong_roundtrip_ignored(_seed in 0u32..10) {
636            let res = Response::Pong;
637            let buf = encode_response(&res);
638            prop_assert_eq!(decode_response(&buf).unwrap(), res);
639        }
640
641        #[test]
642        fn response_metadata_roundtrip(
643            plugin_id in 0u32..=u32::MAX,
644            abi_version in 0u32..=u32::MAX,
645        ) {
646            let payload = PluginMetadataPayload {
647                name: "test".into(),
648                version: "0.1".into(),
649                description: "d".into(),
650                abi_version,
651            };
652            let res = Response::Metadata { plugin_id, payload };
653            let buf = encode_response(&res);
654            prop_assert_eq!(decode_response(&buf).unwrap(), res);
655        }
656
657        #[test]
658        fn response_instance_created_roundtrip(plugin_id in 0u32..=u32::MAX) {
659            let res = Response::InstanceCreated { plugin_id };
660            let buf = encode_response(&res);
661            prop_assert_eq!(decode_response(&buf).unwrap(), res);
662        }
663
664        #[test]
665        fn response_instance_dropped_roundtrip(plugin_id in 0u32..=u32::MAX) {
666            let res = Response::InstanceDropped { plugin_id };
667            let buf = encode_response(&res);
668            prop_assert_eq!(decode_response(&buf).unwrap(), res);
669        }
670
671        #[test]
672        fn response_processed_roundtrip(plugin_id in 0u32..=u32::MAX) {
673            let res = Response::Processed { plugin_id };
674            let buf = encode_response(&res);
675            prop_assert_eq!(decode_response(&buf).unwrap(), res);
676        }
677
678        #[test]
679        fn response_error_roundtrip(
680            code in 0u32..=u32::MAX,
681        ) {
682            let res = Response::Error {
683                code,
684                message: "oops".into(),
685            };
686            let buf = encode_response(&res);
687            prop_assert_eq!(decode_response(&buf).unwrap(), res);
688        }
689
690        #[test]
691        fn request_arbitrary_bytes_never_panic(data in proptest::collection::vec(0u8..=255, 0..1024)) {
692            let _ = decode_request(&data);
693        }
694
695        #[test]
696        fn response_arbitrary_bytes_never_panic(data in proptest::collection::vec(0u8..=255, 0..1024)) {
697            let _ = decode_response(&data);
698        }
699    }
700}