diff --git a/runtime/core/Cargo.toml b/runtime/core/Cargo.toml index 01b5a8a..530cc42 100644 --- a/runtime/core/Cargo.toml +++ b/runtime/core/Cargo.toml @@ -51,3 +51,4 @@ serde_json = { version = "1.0.104", optional = true } [dev-dependencies] tokio = { version = "1.33.0", features = ["full"] } +serde = { version = "1.0.190", features = ["derive"] } diff --git a/runtime/core/src/contract/envelope.rs b/runtime/core/src/contract/envelope.rs index 61524b8..ccf1e1e 100644 --- a/runtime/core/src/contract/envelope.rs +++ b/runtime/core/src/contract/envelope.rs @@ -44,3 +44,36 @@ impl<'a> Envelope<'a> { out.put_slice(body); } } + +#[cfg(all(test, feature = "alloc"))] +mod tests { + use super::*; + use alloc::vec::Vec; + + #[test] + fn ok_round_trips() { + let mut frame = Vec::new(); + Envelope::encode_ok(b"payload", &mut frame); + assert_eq!(Envelope::decode(&frame), Some(Envelope::Ok(b"payload"))); + } + + #[test] + fn err_round_trips() { + let mut frame = Vec::new(); + Envelope::encode_err(0x0102, b"fields", &mut frame); + assert_eq!( + Envelope::decode(&frame), + Some(Envelope::Err { + id: 0x0102, + body: b"fields", + }), + ); + } + + #[test] + fn rejects_empty_unknown_tag_and_truncated_err() { + assert_eq!(Envelope::decode(&[]), None); + assert_eq!(Envelope::decode(&[9]), None); // unknown tag + assert_eq!(Envelope::decode(&[TAG_ERR, 0]), None); // id truncated + } +} diff --git a/runtime/core/src/format/mod.rs b/runtime/core/src/format/mod.rs new file mode 100644 index 0000000..2473108 --- /dev/null +++ b/runtime/core/src/format/mod.rs @@ -0,0 +1,8 @@ +//! [`WireFormat`](crate::contract::WireFormat) implementations. +//! +//! `std`-gated for now — `rmp-serde` needs `std::io`. A `no_std` MessagePack +//! path (hand-rolled on the `no_std` `rmp` crate) comes later. + +mod msgpack; + +pub use msgpack::MsgPack; diff --git a/runtime/core/src/format/msgpack.rs b/runtime/core/src/format/msgpack.rs new file mode 100644 index 0000000..9c747c1 --- /dev/null +++ b/runtime/core/src/format/msgpack.rs @@ -0,0 +1,101 @@ +use std::io::Write; + +use serde::{Deserialize, Serialize}; + +use crate::contract::{BufMut, RuntimeError, WireFormat}; + +/// [`WireFormat`] over MessagePack (`rmp-serde`): compact, `serde`-native, +/// borrows on decode. Structs encode positionally (array form) — the +/// append-only call/field discipline keeps both ends in step. +#[derive(Debug, Default, Clone, Copy)] +pub struct MsgPack; + +/// `std::io::Write` over a `BufMut`, so `encode` serialises straight into the +/// caller's buffer with no intermediate `Vec`. +struct BufWriter<'a>(&'a mut dyn BufMut); + +impl Write for BufWriter<'_> { + fn write(&mut self, bytes: &[u8]) -> std::io::Result { + self.0.put_slice(bytes); + Ok(bytes.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +impl WireFormat for MsgPack { + fn encode( + &self, + value: &T, + out: &mut dyn BufMut, + ) -> Result<(), RuntimeError> { + let mut serializer = rmp_serde::Serializer::new(BufWriter(out)); + value + .serialize(&mut serializer) + .map_err(|_| RuntimeError::Serialization) + } + + fn decode<'de, T: Deserialize<'de>>(&self, bytes: &'de [u8]) -> Result { + rmp_serde::from_slice(bytes).map_err(|_| RuntimeError::Serialization) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct Owned { + n: u32, + s: String, + xs: Vec, + } + + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct Borrowed<'a> { + n: u32, + #[serde(borrow)] + s: &'a str, + } + + #[test] + fn round_trips_an_owned_struct() { + let value = Owned { + n: 7, + s: "hi".into(), + xs: vec![1, 2, 3], + }; + let mut buf = Vec::new(); + MsgPack.encode(&value, &mut buf).unwrap(); + + let back: Owned = MsgPack.decode(&buf).unwrap(); + assert_eq!(value, back); + } + + #[test] + fn decode_borrows_from_the_input() { + let mut buf = Vec::new(); + MsgPack.encode(&Borrowed { n: 1, s: "borrowed" }, &mut buf).unwrap(); + + let back: Borrowed = MsgPack.decode(&buf).unwrap(); + assert_eq!(back.s, "borrowed"); // `back.s` points into `buf` + } + + #[test] + fn garbage_is_a_serialization_error() { + let err = MsgPack.decode::(&[0xc1, 0x00, 0x13]).unwrap_err(); + assert_eq!(err, RuntimeError::Serialization); + } + + #[test] + fn encode_appends_no_intermediate_alloc() { + // Encoding into a pre-sized buffer must not grow it (proxy for + // "one pass, straight into the buffer"). + let mut buf = Vec::with_capacity(64); + let ptr = buf.as_ptr(); + MsgPack.encode(&(1u8, "x"), &mut buf).unwrap(); + assert_eq!(buf.as_ptr(), ptr, "buffer reallocated"); + } +} diff --git a/runtime/core/src/lib.rs b/runtime/core/src/lib.rs index e0285b9..e70cc03 100644 --- a/runtime/core/src/lib.rs +++ b/runtime/core/src/lib.rs @@ -8,6 +8,10 @@ extern crate alloc; // The `core ↔ target` contract — `no_std`, allocation-free. pub mod contract; +// `WireFormat` implementations (`std`-gated for now — see the module). +#[cfg(feature = "std")] +pub mod format; + // The `std` layer: transport, async, the setup builders, the dylib ABI. #[cfg(feature = "std")] pub mod package_abi;