diff --git a/tines-bt/tests/adv.rs b/tines-bt/tests/adv.rs index 0f9d3679..e4961bd8 100644 --- a/tines-bt/tests/adv.rs +++ b/tines-bt/tests/adv.rs @@ -7,6 +7,7 @@ use std::collections::VecDeque; use tines_bt::{adv, BtTransport, Radio, RadioError, RadioEvent}; +use tines_core::consts::WIRE_VERSION; use tines_core::observation::NodeId; use tines_core::transport::{Event, Transport}; @@ -73,12 +74,14 @@ fn advertising_payload_roundtrips_node_id() { assert_eq!(adv::decode_node_id(&p), Some(node)); } -/// A framed envelope: one tag byte, then the 16-byte `from`. This is the layout -/// `frame_sender` peeks — the transport delivers this whole envelope to core -/// (which decodes it), so the `from` must be stamped with the sender's id. +/// A framed envelope: a `WIRE_VERSION` byte, a variant tag, then the 16-byte +/// `from`. This is the layout `frame_sender` peeks — the transport delivers this +/// whole envelope to core (which decodes it), so the `from` must be stamped with +/// the sender's id. fn frame(from: NodeId, payload: &[u8]) -> Vec { - let mut b = Vec::with_capacity(1 + 16 + payload.len()); - b.push(0u8); // version/tag byte + let mut b = Vec::with_capacity(2 + 16 + payload.len()); + b.push(WIRE_VERSION); // wire version byte + b.push(0u8); // postcard externally-tagged variant tag b.extend_from_slice(&from); b.extend_from_slice(payload); b diff --git a/tines-bt/tests/transport.rs b/tines-bt/tests/transport.rs index de4ab7a3..527f814a 100644 --- a/tines-bt/tests/transport.rs +++ b/tines-bt/tests/transport.rs @@ -9,6 +9,7 @@ use std::collections::{BTreeSet, HashMap, VecDeque}; use tines_bt::{adv, resolve_bound, BtTransport, Radio, RadioError, RadioEvent}; +use tines_core::consts::WIRE_VERSION; use tines_core::observation::NodeId; use tines_core::transport::{Event, Transport}; @@ -58,11 +59,12 @@ impl Drop for MockRadio { } } -/// Build a minimal frame: one leading tag byte, then the 16-byte `from`. -/// `frame_sender` reads `from` from bytes 1..17, so this matches the wire. +/// Build a minimal frame: a `WIRE_VERSION` byte, then the 16-byte `from`. +/// `frame_sender` reads `from` from bytes 2..18, so this matches the wire. fn frame(from: NodeId) -> Vec { - let mut b = vec![0u8; 1 + 16]; - b[1..1 + 16].copy_from_slice(&from); + let mut b = vec![0u8; 2 + 16]; + b[2..2 + 16].copy_from_slice(&from); + b[0] = WIRE_VERSION; b } @@ -164,7 +166,7 @@ fn frame_boundaries_preserved_at_transport() { let peer: NodeId = [2u8; 16]; let frame_a = frame(peer); let mut frame_b = frame(peer); - frame_b[0] = 0xFF; // differ from frame_a via the tag byte, so `from` stays == peer + frame_b[1] = 0xFF; // differ via the variant tag (skipped by frame_sender), so `from` stays == peer let a_expected = frame_a.clone(); let b_expected = frame_b.clone(); diff --git a/tines-core/src/consts.rs b/tines-core/src/consts.rs index c6805ec6..29012322 100644 --- a/tines-core/src/consts.rs +++ b/tines-core/src/consts.rs @@ -3,6 +3,15 @@ //! Every value here is a compile-time `const` so it can drive generic //! parameters (e.g. `heapless::Vec` capacities) and be optimized away. +/// Wire envelope version byte prepended to every serialized `Frame`. +/// +/// The first byte of every frame on the wire; [`crate::wire::Frame::decode`] +/// rejects any frame whose leading byte differs from this +/// (`WireError::VersionMismatch`), and [`crate::wire::frame_sender`] skips it. +/// `4` is the first value in the `4..=0x7F` range that fits a single-byte +/// postcard enum tag and sits above the 0.x tag values (`0..=3`). +pub const WIRE_VERSION: u8 = 4; + /// Maximum topic length in bytes. pub const MAX_TOPIC: usize = 64; diff --git a/tines-core/src/exchange.rs b/tines-core/src/exchange.rs index d12e272c..ee607339 100644 --- a/tines-core/src/exchange.rs +++ b/tines-core/src/exchange.rs @@ -18,9 +18,9 @@ use crate::{ use heapless::Vec; /// Worst-case bytes a [`Frame::Summary`] adds around its bloom bits: the -/// variant tag (1), `from` (16), `salt` as a varint (up to 5), and the bits -/// length as a varint (up to 2). -const SUMMARY_OVERHEAD: usize = 24; +/// `WIRE_VERSION` byte (1), the variant tag (1), `from` (16), `salt` as a +/// varint (up to 5), and the bits length as a varint (up to 2). +const SUMMARY_OVERHEAD: usize = 25; /// A fragmented observation part-way through transmission to one peer. /// diff --git a/tines-core/src/frag.rs b/tines-core/src/frag.rs index aca0e4b1..5887f6e5 100644 --- a/tines-core/src/frag.rs +++ b/tines-core/src/frag.rs @@ -5,7 +5,7 @@ use crate::{Frame, NodeId, Observation, ObservationId, Tick, WireError}; use heapless::Vec; /// Header allowance per fragment (bytes), pinned. -pub const FRAG_OVERHEAD: usize = 64; +pub const FRAG_OVERHEAD: usize = 65; /// The most fragments one observation may be split into: the reassembler /// tracks received indices in a `u64` bitset. diff --git a/tines-core/src/wire.rs b/tines-core/src/wire.rs index 2a6ba957..5d6a80bc 100644 --- a/tines-core/src/wire.rs +++ b/tines-core/src/wire.rs @@ -50,19 +50,33 @@ impl Frame { } /// Serializes this frame into `out` via postcard, returning the number of - /// bytes written. A buffer too small to hold the frame yields - /// `WireError::TooLarge`. + /// bytes written. The leading [`consts::WIRE_VERSION`] byte is prepended, so + /// the total is `1 + postcard_len`. A buffer too small to hold the frame + /// (including a zero-length buffer) yields `WireError::TooLarge`. pub fn encode(&self, out: &mut [u8]) -> Result { - match postcard::to_slice(self, out) { - Ok(slice) => Ok(slice.len()), + // A zero-length `out` must not panic on the `&out[1..]` slice below. + if out.is_empty() { + return Err(WireError::TooLarge); + } + out[0] = consts::WIRE_VERSION; + match postcard::to_slice(self, &mut out[1..]) { + Ok(slice) => Ok(1 + slice.len()), Err(_) => Err(WireError::TooLarge), } } - /// Deserializes a frame from `bytes`. Any error (including truncation or - /// garbage) becomes `WireError::Malformed` rather than a panic. + /// Deserializes a frame from `bytes`. Rejects an empty frame as + /// `WireError::Malformed` and a wrong-version leading byte as + /// `WireError::VersionMismatch`; any other error (truncation, garbage) + /// becomes `Malformed` rather than a panic. pub fn decode(bytes: &[u8]) -> Result { - match postcard::from_bytes::(bytes) { + if bytes.is_empty() { + return Err(WireError::Malformed); + } + if bytes[0] != consts::WIRE_VERSION { + return Err(WireError::VersionMismatch { version: bytes[0] }); + } + match postcard::from_bytes::(&bytes[1..]) { Ok(frame) => Ok(frame), Err(_) => Err(WireError::Malformed), } @@ -71,27 +85,28 @@ impl Frame { /// Peek the [`NodeId`] a wire frame was sent by, without fully decoding it. /// -/// A [`Frame`] envelope is an externally-tagged postcard enum: one tag byte at -/// index `0`, then its fields in declaration order. `from` is the first field, -/// so it occupies the 16 bytes at `bytes[1..17]`. This reads those 16 bytes and -/// returns `None` for anything shorter than a full envelope (so a truncated or -/// non-frame payload reports no sender rather than panicking). -/// -/// The leading byte is skipped on purpose: it is the externally-tagged variant -/// tag today and the reserved version-byte slot in 1.0, so reading `from` from -/// a fixed offset keeps this accessor correct when 1.0 prepends a version byte -/// ahead of the envelope. +/// A [`Frame`] envelope is a `WIRE_VERSION` byte, then an externally-tagged +/// postcard enum: `bytes[0]` is the version, `bytes[1]` the variant tag, and +/// `from` (the first field) occupies the 16 bytes at `bytes[2..18]`. This skips +/// the two leading bytes, reads those 16 bytes, and returns `None` for anything +/// shorter than a full envelope or whose leading byte is not +/// [`consts::WIRE_VERSION`] (so a truncated, wrong-version, or non-frame payload +/// reports no sender rather than panicking). /// /// `tines-bt` uses this to enforce that a frame's declared sender matches the /// NodeId advertised by its transport connection (the from-check), without /// importing the private `Frame` layout. pub fn frame_sender(bytes: &[u8]) -> Option { - // The envelope is one tag byte then the `from` field; `NodeId` is 16 bytes. - const ENVELOPE_OVERHEAD: usize = 1; + // Two leading bytes: `WIRE_VERSION` then the postcard variant tag, then the + // 16-byte `from` field; `NodeId` is 16 bytes. + const ENVELOPE_OVERHEAD: usize = 2; const NODE_ID_BYTES: usize = core::mem::size_of::(); if bytes.len() < ENVELOPE_OVERHEAD + NODE_ID_BYTES { return None; } + if bytes[0] != consts::WIRE_VERSION { + return None; + } let mut from = [0u8; NODE_ID_BYTES]; from.copy_from_slice(&bytes[ENVELOPE_OVERHEAD..ENVELOPE_OVERHEAD + NODE_ID_BYTES]); Some(from) @@ -104,6 +119,8 @@ pub enum WireError { TooLarge, /// The input bytes were malformed, truncated, or not a valid `Frame`. Malformed, + /// The frame's leading `WIRE_VERSION` byte did not match `consts::WIRE_VERSION`. + VersionMismatch { version: u8 }, } #[cfg(test)] @@ -199,10 +216,71 @@ mod tests { #[test] fn summary_golden_vector() { - // Pinned wire bytes for the exact Summary frame below. postcard tags are - // positional, so any future reorder of the existing Frame variants changes - // what these bytes decode to and fails this assertion. - const GOLDEN: [u8; 536] = [ + // Pinned wire bytes for the exact Summary frame below. A `WIRE_VERSION` + // byte leads the frame (byte 0); postcard tags are positional, so any + // future reorder of the existing Frame variants changes what these + // bytes decode to and fails this assertion. + const GOLDEN: [u8; 537] = [ + 4u8, 0u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, + 3u8, 239u8, 253u8, 182u8, 245u8, 13u8, 128u8, 4u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + 170u8, 170u8, 170u8, + ]; + let expected = Frame::Summary { + from: [3; 16], + salt: 0xdeadbeef, + bits: Vec::from_slice(&[0xAAu8; consts::SUMMARY_BYTES]).unwrap(), + }; + assert_eq!(Frame::decode(&GOLDEN).unwrap(), expected); + } + + #[test] + fn rejects_0x_frame() { + // The pre-change (0.x) Summary golden bytes — captured before the + // `WIRE_VERSION` byte was added — decode today as a version mismatch: + // their leading byte is the postcard variant tag (0), not WIRE_VERSION. + const ZERO_X: [u8; 536] = [ 0u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 239u8, 253u8, 182u8, 245u8, 13u8, 128u8, 4u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, @@ -249,20 +327,80 @@ mod tests { 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, ]; - let expected = Frame::Summary { + assert_eq!( + Frame::decode(&ZERO_X).err(), + Some(WireError::VersionMismatch { version: 0 }) + ); + } + + #[test] + fn encode_prepends_version_byte() { + // A Summary serializes with `WIRE_VERSION` as byte 0 and still + // round-trips through `Frame::decode` read across the *whole* buffer + // (version byte included — never `&buf[1..]`, which lacks it). + let f = Frame::Summary { from: [3; 16], salt: 0xdeadbeef, bits: Vec::from_slice(&[0xAAu8; consts::SUMMARY_BYTES]).unwrap(), }; - assert_eq!(Frame::decode(&GOLDEN).unwrap(), expected); + let mut buf = [0u8; consts::MAX_FRAME]; + let n = f.encode(&mut buf).unwrap(); + assert_eq!(buf[0], consts::WIRE_VERSION); + assert_eq!(Frame::decode(&buf[..n]).unwrap(), f); + } + + #[test] + fn decode_wrong_version_is_mismatch() { + // A frame whose leading byte is `WIRE_VERSION + 1` is a version + // mismatch rather than `Malformed`. + let f = obs_frame(); + let mut buf = [0u8; consts::MAX_FRAME]; + let n = f.encode(&mut buf).unwrap(); + buf[0] = consts::WIRE_VERSION + 1; + assert_eq!( + Frame::decode(&buf[..n]).err(), + Some(WireError::VersionMismatch { + version: consts::WIRE_VERSION + 1 + }) + ); + } + + #[test] + fn decode_empty_is_malformed() { + // An empty frame is `Malformed`, never a spurious + // `VersionMismatch { version: 0 }` and never a panic. + assert_eq!(Frame::decode(&[]).err(), Some(WireError::Malformed)); + } + + #[test] + fn frame_sender_skips_version_and_tag() { + // `[WIRE_VERSION, 0(tag), from..]` yields `Some(from)`: the two leading + // bytes (version + variant tag) are skipped. + let from = [9u8; 16]; + let mut b = std::vec![0u8; 2 + 16]; + b[0] = consts::WIRE_VERSION; + b[1] = 0; // variant tag + b[2..2 + 16].copy_from_slice(&from); + assert_eq!(frame_sender(&b), Some(from)); + } + + #[test] + fn frame_sender_wrong_version_is_none() { + // `[WIRE_VERSION + 1, 0, from..]` is not our version, so no sender. + let from = [9u8; 16]; + let mut b = std::vec![0u8; 2 + 16]; + b[0] = consts::WIRE_VERSION + 1; + b[1] = 0; // variant tag + b[2..2 + 16].copy_from_slice(&from); + assert_eq!(frame_sender(&b), None); } #[test] fn frame_sender_peeks_from_field() { - // The golden Summary encodes `from: [3;16]` starting at byte 1. - const GOLDEN: [u8; 200] = [ - 0u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, - 239u8, 253u8, 182u8, 245u8, 13u8, 128u8, 4u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, + // Version byte 0, variant tag 1, then `from: [3;16]` at bytes 2..18. + const GOLDEN: [u8; 201] = [ + 4u8, 0u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, 3u8, + 3u8, 239u8, 253u8, 182u8, 245u8, 13u8, 128u8, 4u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, @@ -277,7 +415,7 @@ mod tests { 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, 170u8, - 170u8, 170u8, + 170u8, 170u8, 170u8, ]; assert_eq!(frame_sender(&GOLDEN), Some([3u8; 16])); } @@ -289,8 +427,9 @@ mod tests { // recorded at byte 1. let bytes = { let mut b = [0u8; 32]; - b[0] = 0xFF; // tag/version slot - b[1..17].copy_from_slice(&[9u8; 16]); // from + b[0] = consts::WIRE_VERSION; // version slot (skipped by frame_sender) + b[1] = 0xFF; // variant tag (skipped by frame_sender) + b[2..18].copy_from_slice(&[9u8; 16]); // from b }; assert_eq!(frame_sender(&bytes), Some([9u8; 16])); @@ -299,16 +438,30 @@ mod tests { #[test] fn frame_sender_truncated_returns_none() { assert_eq!(frame_sender(&[0u8; 0]), None); - assert_eq!(frame_sender(&[0u8; 16]), None); // tag + from missing - assert_eq!(frame_sender(&[0u8; 17]), Some([0u8; 16])); // exactly tag + from + assert_eq!(frame_sender(&[0u8; 16]), None); // version + tag + from missing + // A full envelope (version + tag + `from`) with a matching version + // yields the peeked sender. + let mut full = std::vec![0u8; 2 + 16]; + full[0] = consts::WIRE_VERSION; + assert_eq!(frame_sender(&full), Some([0u8; 16])); } #[test] fn decode_garbage_is_error_not_panic() { - for len in [0usize, 1, 7, 64, 1500] { + // A leading byte that is not `WIRE_VERSION` fails on the version gate + // (`VersionMismatch`) before postcard ever runs... + for len in [1usize, 7, 64, 1500] { let bytes = std::vec![0xFFu8; len]; - assert_eq!(Frame::decode(&bytes).err(), Some(WireError::Malformed)); + assert_eq!( + Frame::decode(&bytes).err(), + Some(WireError::VersionMismatch { version: 0xFF }) + ); } + // ...while same-length bytes that pass the gate but are not a valid + // frame decode as `Malformed` rather than panicking. + let mut garbage = std::vec![consts::WIRE_VERSION]; + garbage.extend(std::vec![0xFFu8; 16]); + assert_eq!(Frame::decode(&garbage).err(), Some(WireError::Malformed)); } #[test]