use crate::error::{PluginError, Result};
const TAG_REQUEST_PING: u8 = 0x01;
const TAG_REQUEST_GET_METADATA: u8 = 0x02;
const TAG_REQUEST_INSTANTIATE: u8 = 0x03;
const TAG_REQUEST_PROCESS: u8 = 0x04;
const TAG_RESPONSE_PONG: u8 = 0x81;
const TAG_RESPONSE_METADATA: u8 = 0x82;
const TAG_RESPONSE_INSTANCE_CREATED: u8 = 0x83;
const TAG_RESPONSE_INSTANCE_DROPPED: u8 = 0x84;
const TAG_RESPONSE_PROCESSED: u8 = 0x85;
const TAG_RESPONSE_ERROR: u8 = 0xFE;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PluginMetadataPayload {
pub name: String,
pub version: String,
pub description: String,
pub abi_version: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Request {
Ping,
GetMetadata {
plugin_id: u32,
},
Instantiate {
plugin_id: u32,
},
Process {
plugin_id: u32,
sample_count: u32,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Response {
Pong,
Metadata {
plugin_id: u32,
payload: PluginMetadataPayload,
},
InstanceCreated {
plugin_id: u32,
},
InstanceDropped {
plugin_id: u32,
},
Processed {
plugin_id: u32,
},
Error {
code: u32,
message: String,
},
}
fn encode_u32_le(buf: &mut Vec<u8>, val: u32) {
buf.extend_from_slice(&val.to_le_bytes());
}
fn encode_string(buf: &mut Vec<u8>, s: &str) {
let len = u32::try_from(s.len()).expect("string length exceeds u32::MAX");
encode_u32_le(buf, len);
buf.extend_from_slice(s.as_bytes());
}
fn ensure_len(buf: &[u8], need: usize) -> Result<()> {
if buf.len() < need {
Err(PluginError::Sandbox(format!(
"malformed ipc frame: need {need} bytes, got {}",
buf.len()
)))
} else {
Ok(())
}
}
fn decode_u32_le(buf: &[u8], offset: usize) -> Result<u32> {
ensure_len(buf, offset + 4)?;
Ok(u32::from_le_bytes([
buf[offset],
buf[offset + 1],
buf[offset + 2],
buf[offset + 3],
]))
}
fn decode_string(buf: &[u8], offset: usize) -> Result<(String, usize)> {
let len = decode_u32_le(buf, offset)? as usize;
let start = offset + 4;
ensure_len(buf, start + len)?;
let bytes = &buf[start..start + len];
let s = core::str::from_utf8(bytes).map_err(|e| {
PluginError::Sandbox(format!(
"malformed ipc frame: invalid utf-8 at offset {start}: {e}"
))
})?;
Ok((s.to_owned(), start + len))
}
#[must_use]
pub fn encode_request(req: &Request) -> Vec<u8> {
let mut buf = Vec::with_capacity(64);
match req {
Request::Ping => buf.push(TAG_REQUEST_PING),
Request::GetMetadata { plugin_id } => {
buf.push(TAG_REQUEST_GET_METADATA);
encode_u32_le(&mut buf, *plugin_id);
}
Request::Instantiate { plugin_id } => {
buf.push(TAG_REQUEST_INSTANTIATE);
encode_u32_le(&mut buf, *plugin_id);
}
Request::Process {
plugin_id,
sample_count,
} => {
buf.push(TAG_REQUEST_PROCESS);
encode_u32_le(&mut buf, *plugin_id);
encode_u32_le(&mut buf, *sample_count);
}
}
buf
}
pub fn decode_request(buf: &[u8]) -> Result<Request> {
ensure_len(buf, 1)?;
match buf[0] {
TAG_REQUEST_PING => Ok(Request::Ping),
TAG_REQUEST_GET_METADATA => {
let plugin_id = decode_u32_le(buf, 1)?;
Ok(Request::GetMetadata { plugin_id })
}
TAG_REQUEST_INSTANTIATE => {
let plugin_id = decode_u32_le(buf, 1)?;
Ok(Request::Instantiate { plugin_id })
}
TAG_REQUEST_PROCESS => {
let plugin_id = decode_u32_le(buf, 1)?;
let sample_count = decode_u32_le(buf, 5)?;
Ok(Request::Process {
plugin_id,
sample_count,
})
}
other => Err(PluginError::Sandbox(format!(
"malformed ipc frame: unknown request tag {other:#04x}"
))),
}
}
#[must_use]
pub fn encode_response(res: &Response) -> Vec<u8> {
let mut buf = Vec::with_capacity(128);
match res {
Response::Pong => buf.push(TAG_RESPONSE_PONG),
Response::Metadata { plugin_id, payload } => {
buf.push(TAG_RESPONSE_METADATA);
encode_u32_le(&mut buf, *plugin_id);
encode_string(&mut buf, &payload.name);
encode_string(&mut buf, &payload.version);
encode_string(&mut buf, &payload.description);
encode_u32_le(&mut buf, payload.abi_version);
}
Response::InstanceCreated { plugin_id } => {
buf.push(TAG_RESPONSE_INSTANCE_CREATED);
encode_u32_le(&mut buf, *plugin_id);
}
Response::InstanceDropped { plugin_id } => {
buf.push(TAG_RESPONSE_INSTANCE_DROPPED);
encode_u32_le(&mut buf, *plugin_id);
}
Response::Processed { plugin_id } => {
buf.push(TAG_RESPONSE_PROCESSED);
encode_u32_le(&mut buf, *plugin_id);
}
Response::Error { code, message } => {
buf.push(TAG_RESPONSE_ERROR);
encode_u32_le(&mut buf, *code);
encode_string(&mut buf, message);
}
}
buf
}
pub fn decode_response(buf: &[u8]) -> Result<Response> {
ensure_len(buf, 1)?;
match buf[0] {
TAG_RESPONSE_PONG => Ok(Response::Pong),
TAG_RESPONSE_METADATA => {
let plugin_id = decode_u32_le(buf, 1)?;
let (name, offset) = decode_string(buf, 5)?;
let (version, offset) = decode_string(buf, offset)?;
let (description, offset) = decode_string(buf, offset)?;
let abi_version = decode_u32_le(buf, offset)?;
Ok(Response::Metadata {
plugin_id,
payload: PluginMetadataPayload {
name,
version,
description,
abi_version,
},
})
}
TAG_RESPONSE_INSTANCE_CREATED => {
let plugin_id = decode_u32_le(buf, 1)?;
Ok(Response::InstanceCreated { plugin_id })
}
TAG_RESPONSE_INSTANCE_DROPPED => {
let plugin_id = decode_u32_le(buf, 1)?;
Ok(Response::InstanceDropped { plugin_id })
}
TAG_RESPONSE_PROCESSED => {
let plugin_id = decode_u32_le(buf, 1)?;
Ok(Response::Processed { plugin_id })
}
TAG_RESPONSE_ERROR => {
let code = decode_u32_le(buf, 1)?;
let (message, _) = decode_string(buf, 5)?;
Ok(Response::Error { code, message })
}
other => Err(PluginError::Sandbox(format!(
"malformed ipc frame: unknown response tag {other:#04x}"
))),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ping_roundtrip() {
let req = Request::Ping;
let buf = encode_request(&req);
assert_eq!(buf, vec![TAG_REQUEST_PING]);
assert_eq!(decode_request(&buf).unwrap(), req);
}
#[test]
fn get_metadata_roundtrip() {
let req = Request::GetMetadata { plugin_id: 42 };
let buf = encode_request(&req);
assert_eq!(decode_request(&buf).unwrap(), req);
}
#[test]
fn instantiate_roundtrip() {
let req = Request::Instantiate { plugin_id: 7 };
let buf = encode_request(&req);
assert_eq!(decode_request(&buf).unwrap(), req);
}
#[test]
fn process_roundtrip() {
let req = Request::Process {
plugin_id: 3,
sample_count: 256,
};
let buf = encode_request(&req);
assert_eq!(decode_request(&buf).unwrap(), req);
}
#[test]
fn pong_roundtrip() {
let res = Response::Pong;
let buf = encode_response(&res);
assert_eq!(buf, vec![TAG_RESPONSE_PONG]);
assert_eq!(decode_response(&buf).unwrap(), res);
}
#[test]
fn metadata_roundtrip() {
let res = Response::Metadata {
plugin_id: 1,
payload: PluginMetadataPayload {
name: "reverb-plate".into(),
version: "0.3.1".into(),
description: "plate reverb DSP".into(),
abi_version: 0x0001_0000,
},
};
let buf = encode_response(&res);
assert_eq!(decode_response(&buf).unwrap(), res);
}
#[test]
fn instance_created_roundtrip() {
let res = Response::InstanceCreated { plugin_id: 99 };
let buf = encode_response(&res);
assert_eq!(decode_response(&buf).unwrap(), res);
}
#[test]
fn instance_dropped_roundtrip() {
let res = Response::InstanceDropped { plugin_id: 55 };
let buf = encode_response(&res);
assert_eq!(decode_response(&buf).unwrap(), res);
}
#[test]
fn processed_roundtrip() {
let res = Response::Processed { plugin_id: 10 };
let buf = encode_response(&res);
assert_eq!(decode_response(&buf).unwrap(), res);
}
#[test]
fn error_roundtrip() {
let res = Response::Error {
code: 42,
message: "something went wrong".into(),
};
let buf = encode_response(&res);
assert_eq!(decode_response(&buf).unwrap(), res);
}
#[test]
fn metadata_empty_strings_roundtrip() {
let res = Response::Metadata {
plugin_id: 0,
payload: PluginMetadataPayload {
name: String::new(),
version: String::new(),
description: String::new(),
abi_version: u32::MAX,
},
};
let buf = encode_response(&res);
assert_eq!(decode_response(&buf).unwrap(), res);
}
#[test]
fn metadata_unicode_strings_roundtrip() {
let res = Response::Metadata {
plugin_id: 1,
payload: PluginMetadataPayload {
name: "κλαδί".into(),
version: "1.0.0-β".into(),
description: "テスト プラグイン".into(),
abi_version: 0x0002_0001,
},
};
let buf = encode_response(&res);
assert_eq!(decode_response(&buf).unwrap(), res);
}
#[test]
fn request_max_plugin_id_roundtrip() {
let req = Request::GetMetadata {
plugin_id: u32::MAX,
};
let buf = encode_request(&req);
assert_eq!(decode_request(&buf).unwrap(), req);
}
#[test]
fn decode_request_empty_is_err() {
let err = decode_request(&[]).unwrap_err();
assert!(matches!(err, PluginError::Sandbox(_)));
assert!(err.to_string().contains("malformed ipc frame"));
}
#[test]
fn decode_response_empty_is_err() {
let err = decode_response(&[]).unwrap_err();
assert!(matches!(err, PluginError::Sandbox(_)));
}
#[test]
fn decode_request_unknown_tag_is_err() {
let err = decode_request(&[0xFF]).unwrap_err();
assert!(matches!(err, PluginError::Sandbox(_)));
assert!(err.to_string().contains("unknown request tag"));
}
#[test]
fn decode_response_unknown_tag_is_err() {
let err = decode_response(&[0xFF]).unwrap_err();
assert!(matches!(err, PluginError::Sandbox(_)));
assert!(err.to_string().contains("unknown response tag"));
}
#[test]
fn decode_get_metadata_truncated_is_err() {
let err = decode_request(&[TAG_REQUEST_GET_METADATA, 0x01]).unwrap_err();
assert!(matches!(err, PluginError::Sandbox(_)));
assert!(err.to_string().contains("need 5 bytes"));
}
#[test]
fn decode_process_truncated_is_err() {
let err = decode_request(&[TAG_REQUEST_PROCESS, 0x00, 0x00, 0x00, 0x00]).unwrap_err();
assert!(matches!(err, PluginError::Sandbox(_)));
}
#[test]
fn decode_metadata_string_truncated_is_err() {
let mut buf = vec![TAG_RESPONSE_METADATA];
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();
assert!(matches!(err, PluginError::Sandbox(_)));
}
#[test]
fn decode_metadata_invalid_utf8_is_err() {
let mut buf = vec![TAG_RESPONSE_METADATA];
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();
assert!(matches!(err, PluginError::Sandbox(_)));
assert!(err.to_string().contains("invalid utf-8"));
}
#[test]
fn decode_error_string_truncated_is_err() {
let mut buf = vec![TAG_RESPONSE_ERROR];
buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&999u32.to_le_bytes()); let err = decode_response(&buf).unwrap_err();
assert!(matches!(err, PluginError::Sandbox(_)));
}
#[test]
fn request_tag_bytes_match_spec() {
assert_eq!(encode_request(&Request::Ping), vec![0x01]);
let gm = encode_request(&Request::GetMetadata { plugin_id: 0 });
assert_eq!(gm[0], 0x02);
let inst = encode_request(&Request::Instantiate { plugin_id: 0 });
assert_eq!(inst[0], 0x03);
let proc = encode_request(&Request::Process {
plugin_id: 0,
sample_count: 0,
});
assert_eq!(proc[0], 0x04);
}
#[test]
fn response_tag_bytes_match_spec() {
assert_eq!(encode_response(&Response::Pong), vec![0x81]);
let meta = encode_response(&Response::Metadata {
plugin_id: 0,
payload: PluginMetadataPayload {
name: String::new(),
version: String::new(),
description: String::new(),
abi_version: 0,
},
});
assert_eq!(meta[0], 0x82);
let ic = encode_response(&Response::InstanceCreated { plugin_id: 0 });
assert_eq!(ic[0], 0x83);
let id = encode_response(&Response::InstanceDropped { plugin_id: 0 });
assert_eq!(id[0], 0x84);
let p = encode_response(&Response::Processed { plugin_id: 0 });
assert_eq!(p[0], 0x85);
let e = encode_response(&Response::Error {
code: 0,
message: String::new(),
});
assert_eq!(e[0], 0xFE);
}
#[test]
fn plugin_metadata_payload_is_clone_eq_debug() {
let a = PluginMetadataPayload {
name: "gain".into(),
version: "1.0".into(),
description: "gain".into(),
abi_version: 1,
};
let b = a.clone();
assert_eq!(a, b);
assert!(format!("{a:?}").contains("PluginMetadataPayload"));
}
#[test]
fn decode_request_arbitrary_byte_does_not_panic() {
for byte in 0..=u8::MAX {
let _ = decode_request(&[byte]);
let _ = decode_request(&[byte, 0x00, 0x00, 0x00, 0x00, 0x00]);
}
}
#[test]
fn decode_response_arbitrary_byte_does_not_panic() {
for byte in 0..=u8::MAX {
let _ = decode_response(&[byte]);
let _ = decode_response(&[byte, 0x00, 0x00, 0x00, 0x00, 0x00]);
}
}
use proptest::prelude::*;
proptest! {
#[test]
fn request_ping_roundtrip_ignored(_seed in 0u32..10) {
let req = Request::Ping;
let buf = encode_request(&req);
prop_assert_eq!(decode_request(&buf).unwrap(), req);
}
#[test]
fn request_get_metadata_roundtrip(plugin_id in 0u32..=u32::MAX) {
let req = Request::GetMetadata { plugin_id };
let buf = encode_request(&req);
prop_assert_eq!(decode_request(&buf).unwrap(), req);
}
#[test]
fn request_instantiate_roundtrip(plugin_id in 0u32..=u32::MAX) {
let req = Request::Instantiate { plugin_id };
let buf = encode_request(&req);
prop_assert_eq!(decode_request(&buf).unwrap(), req);
}
#[test]
fn request_process_roundtrip(
plugin_id in 0u32..=u32::MAX,
sample_count in 0u32..=u32::MAX,
) {
let req = Request::Process { plugin_id, sample_count };
let buf = encode_request(&req);
prop_assert_eq!(decode_request(&buf).unwrap(), req);
}
#[test]
fn response_pong_roundtrip_ignored(_seed in 0u32..10) {
let res = Response::Pong;
let buf = encode_response(&res);
prop_assert_eq!(decode_response(&buf).unwrap(), res);
}
#[test]
fn response_metadata_roundtrip(
plugin_id in 0u32..=u32::MAX,
abi_version in 0u32..=u32::MAX,
) {
let payload = PluginMetadataPayload {
name: "test".into(),
version: "0.1".into(),
description: "d".into(),
abi_version,
};
let res = Response::Metadata { plugin_id, payload };
let buf = encode_response(&res);
prop_assert_eq!(decode_response(&buf).unwrap(), res);
}
#[test]
fn response_instance_created_roundtrip(plugin_id in 0u32..=u32::MAX) {
let res = Response::InstanceCreated { plugin_id };
let buf = encode_response(&res);
prop_assert_eq!(decode_response(&buf).unwrap(), res);
}
#[test]
fn response_instance_dropped_roundtrip(plugin_id in 0u32..=u32::MAX) {
let res = Response::InstanceDropped { plugin_id };
let buf = encode_response(&res);
prop_assert_eq!(decode_response(&buf).unwrap(), res);
}
#[test]
fn response_processed_roundtrip(plugin_id in 0u32..=u32::MAX) {
let res = Response::Processed { plugin_id };
let buf = encode_response(&res);
prop_assert_eq!(decode_response(&buf).unwrap(), res);
}
#[test]
fn response_error_roundtrip(
code in 0u32..=u32::MAX,
) {
let res = Response::Error {
code,
message: "oops".into(),
};
let buf = encode_response(&res);
prop_assert_eq!(decode_response(&buf).unwrap(), res);
}
#[test]
fn request_arbitrary_bytes_never_panic(data in proptest::collection::vec(0u8..=255, 0..1024)) {
let _ = decode_request(&data);
}
#[test]
fn response_arbitrary_bytes_never_panic(data in proptest::collection::vec(0u8..=255, 0..1024)) {
let _ = decode_response(&data);
}
}
}