1use crate::error::{PluginError, Result};
28
29const 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#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct PluginMetadataPayload {
50 pub name: String,
52 pub version: String,
54 pub description: String,
56 pub abi_version: u32,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
64pub enum Request {
65 Ping,
67 GetMetadata {
69 plugin_id: u32,
71 },
72 Instantiate {
74 plugin_id: u32,
76 },
77 Process {
79 plugin_id: u32,
81 sample_count: u32,
83 },
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
88pub enum Response {
89 Pong,
91 Metadata {
93 plugin_id: u32,
95 payload: PluginMetadataPayload,
97 },
98 InstanceCreated {
100 plugin_id: u32,
102 },
103 InstanceDropped {
105 plugin_id: u32,
107 },
108 Processed {
110 plugin_id: u32,
112 },
113 Error {
115 code: u32,
117 message: String,
119 },
120}
121
122fn 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
134fn 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#[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
198pub 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#[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
265pub 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 #[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 #[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 #[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 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 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 let mut buf = vec![TAG_RESPONSE_METADATA];
496 buf.extend_from_slice(&1u32.to_le_bytes()); buf.extend_from_slice(&100u32.to_le_bytes()); buf.push(0x41); 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()); buf.extend_from_slice(&3u32.to_le_bytes()); buf.extend_from_slice(&[0xFF, 0xFE, 0xFD]); 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()); buf.extend_from_slice(&999u32.to_le_bytes()); let err = decode_response(&buf).unwrap_err();
520 assert!(matches!(err, PluginError::Sandbox(_)));
521 }
522
523 #[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 #[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 #[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 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}