diff --git a/tines-core/src/lib.rs b/tines-core/src/lib.rs index ef66219..ae8ce2f 100644 --- a/tines-core/src/lib.rs +++ b/tines-core/src/lib.rs @@ -6,8 +6,10 @@ extern crate alloc; pub mod consts; pub mod observation; +pub mod wire; pub use observation::{Observation, NodeId, ObservationId, Payload, Tick, Topic}; +pub use wire::{Frame, WireError}; #[cfg(test)] extern crate std; diff --git a/tines-core/src/wire.rs b/tines-core/src/wire.rs new file mode 100644 index 0000000..a308909 --- /dev/null +++ b/tines-core/src/wire.rs @@ -0,0 +1,164 @@ +//! Wire framing for tines-core using postcard serialization. +//! +//! Every malformed input from the wire is dropped as `Err(WireError::Malformed)` +//! — `tines-core` is `#![no_std]` and never panics on untrusted bytes. + +use crate::consts; +use crate::{Observation, NodeId, ObservationId}; +use heapless::Vec; +use serde::{Deserialize, Serialize}; + +/// A single frame carried on the wire. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum Frame { + Summary { + from: NodeId, + salt: u32, + bits: Vec, + }, + Obs { + from: NodeId, + obs: Observation, + }, + Frag { + from: NodeId, + id: ObservationId, + index: u8, + count: u8, + data: Vec, + }, +} + +impl Frame { + /// Returns the originating node of this frame. + pub fn sender(&self) -> NodeId { + match self { + Frame::Summary { from, .. } => *from, + Frame::Obs { from, .. } => *from, + Frame::Frag { from, .. } => *from, + } + } + + /// Serializes this frame into `out` via postcard, returning the number of + /// bytes written. A buffer too small to hold the frame yields + /// `WireError::TooLarge`. + pub fn encode(&self, out: &mut [u8]) -> Result { + match postcard::to_slice(self, out) { + Ok(slice) => Ok(slice.len()), + Err(_) => Err(WireError::TooLarge), + } + } + + /// Deserializes a frame from `bytes`. Any error (including truncation or + /// garbage) becomes `WireError::Malformed` rather than a panic. + pub fn decode(bytes: &[u8]) -> Result { + match postcard::from_bytes::(bytes) { + Ok(frame) => Ok(frame), + Err(_) => Err(WireError::Malformed), + } + } +} + +/// Errors produced while (de)serializing frames on the wire. +#[derive(Debug, PartialEq, Eq)] +pub enum WireError { + /// The output buffer was too small to hold the serialized frame. + TooLarge, + /// The input bytes were malformed, truncated, or not a valid `Frame`. + Malformed, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Payload; + use crate::Topic; + + fn obs_frame() -> Frame { + Frame::Obs { + from: [7; 16], + obs: Observation { + observer: [7; 16], + seq: 42, + age_ms: 123, + observed_at: Some(999), + topic: Topic::from_slice(b"door/3").unwrap(), + payload: Payload::from_slice(b"open").unwrap(), + }, + } + } + + fn big_obs_frame() -> Frame { + Frame::Obs { + from: [1; 16], + obs: Observation { + observer: [1; 16], + seq: 1, + age_ms: 0, + observed_at: None, + topic: Topic::from_slice(b"t").unwrap(), + payload: Payload::from_slice(&[0u8; 1024]).unwrap(), + }, + } + } + + #[test] + fn obs_frame_roundtrip() { + let f = obs_frame(); + let mut buf = [0u8; consts::MAX_FRAME]; + let n = f.encode(&mut buf).unwrap(); + let decoded = Frame::decode(&buf[..n]).unwrap(); + assert_eq!(f, decoded); + } + + #[test] + fn summary_frame_roundtrip() { + let f = Frame::Summary { + from: [3; 16], + salt: 0xdeadbeef, + bits: Vec::from_slice(&[0xAAu8; consts::SUMMARY_BYTES]).unwrap(), + }; + let mut buf = [0u8; consts::MAX_FRAME]; + let n = f.encode(&mut buf).unwrap(); + let decoded = Frame::decode(&buf[..n]).unwrap(); + assert_eq!(f, decoded); + } + + #[test] + fn frag_frame_roundtrip() { + let f = Frame::Frag { + from: [9; 16], + id: [4; 16], + index: 2, + count: 5, + data: Vec::from_slice(&[0x5Cu8; 100]).unwrap(), + }; + let mut buf = [0u8; consts::MAX_FRAME]; + let n = f.encode(&mut buf).unwrap(); + let decoded = Frame::decode(&buf[..n]).unwrap(); + assert_eq!(f, decoded); + } + + #[test] + fn decode_garbage_is_error_not_panic() { + for len in [0usize, 1, 7, 64, 1500] { + let bytes = std::vec![0xFFu8; len]; + assert_eq!(Frame::decode(&bytes).err(), Some(WireError::Malformed)); + } + } + + #[test] + fn decode_truncated_is_error() { + let f = obs_frame(); + let mut buf = [0u8; consts::MAX_FRAME]; + let n = f.encode(&mut buf).unwrap(); + assert!(Frame::decode(&buf[..n / 2]).is_err()); + } + + #[test] + fn encode_into_too_small_buffer_is_too_large() { + let f = big_obs_frame(); + let mut buf = [0u8; 16]; + assert_eq!(f.encode(&mut buf).err(), Some(WireError::TooLarge)); + } +}