use std::fmt;
use std::io::Cursor;
use serde::{de::DeserializeOwned, Serialize};
pub const DEFAULT_MAX_PAYLOAD_LENGTH: usize = 16 * 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BinaryOptions {
pub max_payload_length: usize,
}
impl Default for BinaryOptions {
fn default() -> Self {
Self {
max_payload_length: DEFAULT_MAX_PAYLOAD_LENGTH,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BinaryError {
Encode(String),
Decode(String),
PayloadTooLarge { actual: usize, maximum: usize },
TrailingBytes,
}
impl fmt::Display for BinaryError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Encode(message) => write!(f, "binary encode failed: {message}"),
Self::Decode(message) => write!(f, "binary decode failed: {message}"),
Self::PayloadTooLarge { actual, maximum } => write!(
f,
"binary payload is {actual} bytes, exceeding the {maximum}-byte limit"
),
Self::TrailingBytes => f.write_str("binary payload contains trailing bytes"),
}
}
}
impl std::error::Error for BinaryError {}
pub fn encode<T: Serialize>(value: &T, options: &BinaryOptions) -> Result<Vec<u8>, BinaryError> {
let mut bytes = Vec::new();
ciborium::ser::into_writer(value, &mut bytes)
.map_err(|error| BinaryError::Encode(error.to_string()))?;
if bytes.len() > options.max_payload_length {
return Err(BinaryError::PayloadTooLarge {
actual: bytes.len(),
maximum: options.max_payload_length,
});
}
Ok(bytes)
}
pub fn decode<T: DeserializeOwned>(
bytes: &[u8],
options: &BinaryOptions,
) -> Result<T, BinaryError> {
if bytes.len() > options.max_payload_length {
return Err(BinaryError::PayloadTooLarge {
actual: bytes.len(),
maximum: options.max_payload_length,
});
}
let mut reader = Cursor::new(bytes);
let value = ciborium::de::from_reader(&mut reader)
.map_err(|error| BinaryError::Decode(error.to_string()))?;
if reader.position() != bytes.len() as u64 {
return Err(BinaryError::TrailingBytes);
}
Ok(value)
}
#[cfg(feature = "json")]
pub fn encode_json(
value: &serde_json::Value,
options: &BinaryOptions,
) -> Result<Vec<u8>, BinaryError> {
encode(value, options)
}
#[cfg(feature = "json")]
pub fn decode_json(
bytes: &[u8],
options: &BinaryOptions,
) -> Result<serde_json::Value, BinaryError> {
decode(bytes, options)
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct StbBytes {
pub ptr: *mut u8,
pub len: usize,
}
unsafe impl Send for StbBytes {}
unsafe impl Sync for StbBytes {}
pub type FreeBytesFn = unsafe extern "C" fn(bytes: StbBytes);
impl StbBytes {
pub const fn empty() -> Self {
Self {
ptr: std::ptr::null_mut(),
len: 0,
}
}
pub const fn is_empty(&self) -> bool {
self.len == 0
}
pub fn from_vec(bytes: Vec<u8>) -> Self {
if bytes.is_empty() {
return Self::empty();
}
let boxed = bytes.into_boxed_slice();
let len = boxed.len();
let ptr = Box::into_raw(boxed) as *mut u8;
Self { ptr, len }
}
pub unsafe fn to_vec(&self) -> Vec<u8> {
if self.len == 0 || self.ptr.is_null() {
return Vec::new();
}
unsafe { std::slice::from_raw_parts(self.ptr, self.len).to_vec() }
}
pub unsafe fn free_with(self, free_fn: Option<FreeBytesFn>) {
if self.is_empty() {
return;
}
if let Some(free_fn) = free_fn {
unsafe { free_fn(self) };
}
}
}
pub unsafe extern "C" fn free_bytes(bytes: StbBytes) {
if bytes.is_empty() || bytes.ptr.is_null() {
return;
}
unsafe {
drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(
bytes.ptr, bytes.len,
)));
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn json_round_trip_uses_binary_payload() {
let value = json!({
"type": "tool_result",
"tool": "read",
"content": ["alpha", "beta"],
"ok": true,
});
let bytes = encode_json(&value, &BinaryOptions::default()).unwrap();
assert!(!bytes.is_empty());
assert_ne!(bytes, serde_json::to_vec(&value).unwrap());
assert_eq!(
decode_json(&bytes, &BinaryOptions::default()).unwrap(),
value
);
}
#[test]
fn typed_values_round_trip() {
let value = (String::from("tool_result"), 42_u64, true);
let bytes = encode(&value, &BinaryOptions::default()).unwrap();
assert_eq!(
decode::<(String, u64, bool)>(&bytes, &BinaryOptions::default()).unwrap(),
value
);
}
#[test]
fn rejects_oversized_payloads() {
let value = json!({"data": "x".repeat(64)});
let options = BinaryOptions {
max_payload_length: 8,
};
assert!(matches!(
encode_json(&value, &options),
Err(BinaryError::PayloadTooLarge { .. })
));
}
#[test]
fn rejects_trailing_bytes() {
let mut bytes = encode_json(&json!({"ok": true}), &BinaryOptions::default()).unwrap();
bytes.push(0);
assert_eq!(
decode_json(&bytes, &BinaryOptions::default()),
Err(BinaryError::TrailingBytes)
);
}
#[test]
fn abi_buffer_owns_and_releases_bytes() {
let buffer = StbBytes::from_vec(vec![1, 2, 3]);
assert_eq!(unsafe { buffer.to_vec() }, vec![1, 2, 3]);
unsafe { buffer.free_with(Some(free_bytes)) };
}
}