Skip to main content

rpi_plugin_sdk/
binary.rs

1//! Optional binary payload support for high-frequency extension data.
2//!
3//! The binary codec is intentionally additive. Existing JSON `StbString` payloads
4//! remain the compatibility format; callers opt into this module only after the
5//! host and extension have negotiated the binary capability. Payloads are
6//! length-bounded before decoding so this module is suitable for untrusted
7//! extension input when the caller also enforces the surrounding frame limit.
8
9use std::fmt;
10use std::io::Cursor;
11
12use serde::{de::DeserializeOwned, Serialize};
13
14/// Default maximum encoded payload size (16 MiB).
15pub const DEFAULT_MAX_PAYLOAD_LENGTH: usize = 16 * 1024 * 1024;
16
17/// Bounds applied to a binary payload before encoding or decoding.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub struct BinaryOptions {
20    pub max_payload_length: usize,
21}
22
23impl Default for BinaryOptions {
24    fn default() -> Self {
25        Self {
26            max_payload_length: DEFAULT_MAX_PAYLOAD_LENGTH,
27        }
28    }
29}
30
31/// Error returned by the binary codec.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum BinaryError {
34    Encode(String),
35    Decode(String),
36    PayloadTooLarge { actual: usize, maximum: usize },
37    TrailingBytes,
38}
39
40impl fmt::Display for BinaryError {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        match self {
43            Self::Encode(message) => write!(f, "binary encode failed: {message}"),
44            Self::Decode(message) => write!(f, "binary decode failed: {message}"),
45            Self::PayloadTooLarge { actual, maximum } => write!(
46                f,
47                "binary payload is {actual} bytes, exceeding the {maximum}-byte limit"
48            ),
49            Self::TrailingBytes => f.write_str("binary payload contains trailing bytes"),
50        }
51    }
52}
53
54impl std::error::Error for BinaryError {}
55
56/// Encode one typed value as a compact CBOR payload.
57pub fn encode<T: Serialize>(value: &T, options: &BinaryOptions) -> Result<Vec<u8>, BinaryError> {
58    let mut bytes = Vec::new();
59    ciborium::ser::into_writer(value, &mut bytes)
60        .map_err(|error| BinaryError::Encode(error.to_string()))?;
61    if bytes.len() > options.max_payload_length {
62        return Err(BinaryError::PayloadTooLarge {
63            actual: bytes.len(),
64            maximum: options.max_payload_length,
65        });
66    }
67    Ok(bytes)
68}
69
70/// Decode one typed value from a complete CBOR payload.
71pub fn decode<T: DeserializeOwned>(
72    bytes: &[u8],
73    options: &BinaryOptions,
74) -> Result<T, BinaryError> {
75    if bytes.len() > options.max_payload_length {
76        return Err(BinaryError::PayloadTooLarge {
77            actual: bytes.len(),
78            maximum: options.max_payload_length,
79        });
80    }
81
82    let mut reader = Cursor::new(bytes);
83    let value = ciborium::de::from_reader(&mut reader)
84        .map_err(|error| BinaryError::Decode(error.to_string()))?;
85    if reader.position() != bytes.len() as u64 {
86        return Err(BinaryError::TrailingBytes);
87    }
88    Ok(value)
89}
90
91/// JSON-shaped convenience API for tool and event payloads that are still
92/// represented by dynamic values at the product boundary.
93#[cfg(feature = "json")]
94pub fn encode_json(
95    value: &serde_json::Value,
96    options: &BinaryOptions,
97) -> Result<Vec<u8>, BinaryError> {
98    encode(value, options)
99}
100
101/// Decode a binary payload into a JSON value without going through a UTF-8
102/// string or JSON parser.
103#[cfg(feature = "json")]
104pub fn decode_json(
105    bytes: &[u8],
106    options: &BinaryOptions,
107) -> Result<serde_json::Value, BinaryError> {
108    decode(bytes, options)
109}
110
111/// An owned byte buffer that can cross an ABI boundary as pointer plus length.
112///
113/// The allocation is always released by the producer's `FreeBytesFn`, just as
114/// `StbString` is released by `FreeStringFn`. This type does not carry an
115/// allocator or lifetime; those are part of the call contract.
116#[repr(C)]
117#[derive(Clone, Copy)]
118pub struct StbBytes {
119    pub ptr: *mut u8,
120    pub len: usize,
121}
122
123// SAFETY: this is a raw pointer/length pair. It does not itself grant access
124// to the allocation; the caller is responsible for the documented lifetime and
125// ownership rules, exactly as for `StbString`.
126unsafe impl Send for StbBytes {}
127unsafe impl Sync for StbBytes {}
128
129/// Producer-side deallocator for [`StbBytes`].
130pub type FreeBytesFn = unsafe extern "C" fn(bytes: StbBytes);
131
132impl StbBytes {
133    /// The empty, non-owning value.
134    pub const fn empty() -> Self {
135        Self {
136            ptr: std::ptr::null_mut(),
137            len: 0,
138        }
139    }
140
141    pub const fn is_empty(&self) -> bool {
142        self.len == 0
143    }
144
145    /// Transfer a boxed byte allocation into an ABI buffer.
146    pub fn from_vec(bytes: Vec<u8>) -> Self {
147        if bytes.is_empty() {
148            return Self::empty();
149        }
150        let boxed = bytes.into_boxed_slice();
151        let len = boxed.len();
152        let ptr = Box::into_raw(boxed) as *mut u8;
153        Self { ptr, len }
154    }
155
156    /// Copy the borrowed ABI buffer into a Rust-owned vector.
157    ///
158    /// # Safety
159    /// `ptr` must be valid for `len` initialized bytes in one allocation and
160    /// remain readable for the duration of this call.
161    pub unsafe fn to_vec(&self) -> Vec<u8> {
162        if self.len == 0 || self.ptr.is_null() {
163            return Vec::new();
164        }
165        // SAFETY: upheld by the caller's ABI buffer contract.
166        unsafe { std::slice::from_raw_parts(self.ptr, self.len).to_vec() }
167    }
168
169    /// Release the buffer through its producer-side deallocator.
170    ///
171    /// # Safety
172    /// The buffer must be a live allocation produced by `free_fn`'s allocator,
173    /// and ownership must be transferred exactly once.
174    pub unsafe fn free_with(self, free_fn: Option<FreeBytesFn>) {
175        if self.is_empty() {
176            return;
177        }
178        if let Some(free_fn) = free_fn {
179            // SAFETY: the caller supplies the matching producer and unique
180            // ownership of this allocation.
181            unsafe { free_fn(self) };
182        }
183    }
184}
185
186/// Default deallocator for buffers created by [`StbBytes::from_vec`].
187///
188/// # Safety
189/// `bytes` must be the unique live allocation returned by `StbBytes::from_vec`.
190pub unsafe extern "C" fn free_bytes(bytes: StbBytes) {
191    if bytes.is_empty() || bytes.ptr.is_null() {
192        return;
193    }
194    // SAFETY: the caller transfers the original boxed-slice allocation once.
195    unsafe {
196        drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(
197            bytes.ptr, bytes.len,
198        )));
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    use serde_json::json;
206
207    #[test]
208    fn json_round_trip_uses_binary_payload() {
209        let value = json!({
210            "type": "tool_result",
211            "tool": "read",
212            "content": ["alpha", "beta"],
213            "ok": true,
214        });
215        let bytes = encode_json(&value, &BinaryOptions::default()).unwrap();
216        assert!(!bytes.is_empty());
217        assert_ne!(bytes, serde_json::to_vec(&value).unwrap());
218        assert_eq!(
219            decode_json(&bytes, &BinaryOptions::default()).unwrap(),
220            value
221        );
222    }
223
224    #[test]
225    fn typed_values_round_trip() {
226        let value = (String::from("tool_result"), 42_u64, true);
227        let bytes = encode(&value, &BinaryOptions::default()).unwrap();
228        assert_eq!(
229            decode::<(String, u64, bool)>(&bytes, &BinaryOptions::default()).unwrap(),
230            value
231        );
232    }
233
234    #[test]
235    fn rejects_oversized_payloads() {
236        let value = json!({"data": "x".repeat(64)});
237        let options = BinaryOptions {
238            max_payload_length: 8,
239        };
240        assert!(matches!(
241            encode_json(&value, &options),
242            Err(BinaryError::PayloadTooLarge { .. })
243        ));
244    }
245
246    #[test]
247    fn rejects_trailing_bytes() {
248        let mut bytes = encode_json(&json!({"ok": true}), &BinaryOptions::default()).unwrap();
249        bytes.push(0);
250        assert_eq!(
251            decode_json(&bytes, &BinaryOptions::default()),
252            Err(BinaryError::TrailingBytes)
253        );
254    }
255
256    #[test]
257    fn abi_buffer_owns_and_releases_bytes() {
258        let buffer = StbBytes::from_vec(vec![1, 2, 3]);
259        // SAFETY: `buffer` owns the live allocation returned above.
260        assert_eq!(unsafe { buffer.to_vec() }, vec![1, 2, 3]);
261        // SAFETY: the default deallocator matches `from_vec`.
262        unsafe { buffer.free_with(Some(free_bytes)) };
263    }
264}