refactor(core): make Frame and WireError crate-private
Spec §3 (decision D-b) lists `Frame` and `WireError` as private; lib.rs re-exported both. Only `frame_sender` stays public. - The pinned vectors in tests/vectors.rs now compile in-crate as a `#[cfg(test)] #[path]` module (autotests = false keeps Cargo from also building them as an integration test). Every golden vector now checks `encode() == GOLDEN` as well as the decode, and new literal vectors pin a trailing-byte rejection and an addressed Obs with a nonzero age and an `observed_at`. - A `#[doc(hidden)]` `conformance` module (behind the `conformance` feature) gives the fuzz harness byte-level frame encoders/decoders and core's local limits without exposing `Frame`. The fuzz_twin test target now requires `conformance`, which scripts/ci.sh enables. - compile_fail doctests pin that `Frame`/`WireError` are unreachable. - Remove the THROWAWAY gen_missing_golden_vectors test and fix stale wire.rs comments about the version/tag byte offsets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,10 @@ readme.workspace = true
|
||||
documentation = "https://docs.rs/tines-core"
|
||||
keywords = ["robotics", "gossip", "delay-tolerant", "no-std", "sync"]
|
||||
categories = ["no-std", "embedded", "network-programming", "science::robotics"]
|
||||
# Test targets are listed by hand below: `tests/vectors.rs` is not one. It pins
|
||||
# the wire format against the crate-private `Frame`, so `src/lib.rs` compiles
|
||||
# it into the unit tests instead (`#[path = "../tests/vectors.rs"]`).
|
||||
autotests = false
|
||||
|
||||
[features]
|
||||
default = ["alloc"]
|
||||
@@ -19,6 +23,8 @@ alloc = []
|
||||
# backends run against themselves to prove they honor the `Store` contract.
|
||||
# Std-free so it builds with `--no-default-features`; enabled only by the
|
||||
# backend crates that exercise the suite (MemStore, LogStore, CappedStore).
|
||||
# It also turns on the `#[doc(hidden)]` `conformance` module, which gives the
|
||||
# fuzz harness byte-level frame helpers and core's local limits.
|
||||
conformance = []
|
||||
|
||||
[dependencies]
|
||||
@@ -33,3 +39,16 @@ proptest = "1"
|
||||
# the test suite via a `#[path]` module include to keep it the SAME harness.
|
||||
# That source names `heapless` directly, so restate it here for the twin.
|
||||
heapless = { version = "0.8", features = ["serde"] }
|
||||
|
||||
[[test]]
|
||||
name = "pair"
|
||||
|
||||
[[test]]
|
||||
name = "public_api"
|
||||
|
||||
# The proptest twin of the `fuzz/` harness builds frames through the hidden
|
||||
# `conformance` helpers, so it runs only with that feature (as `scripts/ci.sh`
|
||||
# does) and needs `MemStore` (`alloc`).
|
||||
[[test]]
|
||||
name = "fuzz_twin"
|
||||
required-features = ["alloc", "conformance"]
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
//! Test-only hooks into crate-private wire types and local constants.
|
||||
//!
|
||||
//! **Not public API.** Hidden from the docs, behind the `conformance`
|
||||
//! feature, and outside the 1.x stability promise: anything here may change
|
||||
//! or vanish in any release.
|
||||
//!
|
||||
//! `Frame` and `WireError` are crate-private (spec §3), and the local
|
||||
//! capacities and timings are `pub(crate)` (decision K). The fuzz harness in
|
||||
//! `fuzz/` and its proptest twin (`tines-core/tests/fuzz_twin.rs`) still need
|
||||
//! to build well-formed frames, read the frames a `Node` sends, and size
|
||||
//! their scenarios by core's local limits. These functions give them exactly
|
||||
//! that, as byte-level helpers, without exposing the `Frame` enum.
|
||||
//!
|
||||
//! Every function is `no_std`, allocation-free and panic-free on any input.
|
||||
|
||||
use crate::consts;
|
||||
use crate::wire::Frame;
|
||||
use crate::{NodeId, Observation, ObservationId, SigAlgo, Signature};
|
||||
|
||||
/// Core's local peer-table capacity (`consts::MAX_PEERS`).
|
||||
pub const MAX_PEERS: usize = consts::MAX_PEERS;
|
||||
/// Core's idle-eviction window (`consts::PEER_IDLE_MS`).
|
||||
pub const PEER_IDLE_MS: u64 = consts::PEER_IDLE_MS;
|
||||
/// Core's summary cadence (`consts::SUMMARY_INTERVAL_MS`).
|
||||
pub const SUMMARY_INTERVAL_MS: u64 = consts::SUMMARY_INTERVAL_MS;
|
||||
/// Core's per-peer send-queue and bounce-table capacity (`consts::SEND_QUEUE_CAP`).
|
||||
pub const SEND_QUEUE_CAP: usize = consts::SEND_QUEUE_CAP;
|
||||
/// Core's `Delivered` event-queue capacity (`consts::EVENT_QUEUE_CAP`).
|
||||
pub const EVENT_QUEUE_CAP: usize = consts::EVENT_QUEUE_CAP;
|
||||
/// Core's MTU-sweep lower bound (`consts::MIN_MTU`).
|
||||
pub const MIN_MTU: usize = consts::MIN_MTU;
|
||||
/// Bytes a `Frag` frame may add around its data (`frag::FRAG_OVERHEAD`).
|
||||
pub const FRAG_OVERHEAD: usize = crate::frag::FRAG_OVERHEAD;
|
||||
/// Most fragments one observation may be split into (`frag::MAX_FRAGMENTS`).
|
||||
pub const MAX_FRAGMENTS: usize = crate::frag::MAX_FRAGMENTS;
|
||||
|
||||
fn encode(frame: &Frame, out: &mut [u8]) -> Option<usize> {
|
||||
frame.encode(out).ok()
|
||||
}
|
||||
|
||||
/// Encodes a `Summary` frame into `out`. `None` if `bits` is longer than
|
||||
/// `SUMMARY_BYTES` or `out` is too small.
|
||||
pub fn encode_summary(from: NodeId, salt: u32, bits: &[u8], out: &mut [u8]) -> Option<usize> {
|
||||
let bits = heapless::Vec::from_slice(bits).ok()?;
|
||||
encode(&Frame::Summary { from, salt, bits }, out)
|
||||
}
|
||||
|
||||
/// Encodes an `Obs` frame carrying `obs` into `out`. `None` if `out` is too
|
||||
/// small.
|
||||
pub fn encode_obs(from: NodeId, obs: &Observation, out: &mut [u8]) -> Option<usize> {
|
||||
let obs = obs.clone();
|
||||
encode(&Frame::Obs { from, obs }, out)
|
||||
}
|
||||
|
||||
/// Encodes a `Frag` frame into `out`. `None` if `data` is longer than
|
||||
/// `MAX_FRAME` or `out` is too small. `index`/`count` are not validated: an
|
||||
/// adversarial harness sends inconsistent ones on purpose.
|
||||
pub fn encode_frag(
|
||||
from: NodeId,
|
||||
id: ObservationId,
|
||||
index: u8,
|
||||
count: u8,
|
||||
data: &[u8],
|
||||
out: &mut [u8],
|
||||
) -> Option<usize> {
|
||||
let data = heapless::Vec::from_slice(data).ok()?;
|
||||
encode(
|
||||
&Frame::Frag {
|
||||
from,
|
||||
id,
|
||||
index,
|
||||
count,
|
||||
data,
|
||||
},
|
||||
out,
|
||||
)
|
||||
}
|
||||
|
||||
/// Encodes an `Ack` frame into `out`. `None` if `ids` is longer than
|
||||
/// `ACK_IDS_CAP` or `out` is too small.
|
||||
pub fn encode_ack(from: NodeId, ids: &[ObservationId], out: &mut [u8]) -> Option<usize> {
|
||||
let ids = heapless::Vec::from_slice(ids).ok()?;
|
||||
encode(&Frame::Ack { from, ids }, out)
|
||||
}
|
||||
|
||||
/// Encodes `obs` alone (no frame envelope) into `out`: the byte stream a
|
||||
/// sender cuts into `Frag` data. `None` if `out` is too small.
|
||||
pub fn encode_observation(obs: &Observation, out: &mut [u8]) -> Option<usize> {
|
||||
postcard::to_slice(obs, out).ok().map(|s| s.len())
|
||||
}
|
||||
|
||||
/// The sender of `bytes` if they decode as a complete, canonical frame — the
|
||||
/// same decode the exchange runs — else `None` (the exchange counts such a
|
||||
/// frame as `VersionMismatch` or `Malformed`).
|
||||
pub fn decoded_sender(bytes: &[u8]) -> Option<NodeId> {
|
||||
Frame::decode(bytes).ok().map(|f| f.sender())
|
||||
}
|
||||
|
||||
/// The observation in `bytes` if they decode as an `Obs` frame.
|
||||
pub fn obs_of(bytes: &[u8]) -> Option<Observation> {
|
||||
match Frame::decode(bytes).ok()? {
|
||||
Frame::Obs { obs, .. } => Some(obs),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// `(id, index, count)` of `bytes` if they decode as a `Frag` frame.
|
||||
pub fn frag_of(bytes: &[u8]) -> Option<(ObservationId, u8, u8)> {
|
||||
match Frame::decode(bytes).ok()? {
|
||||
Frame::Frag {
|
||||
id, index, count, ..
|
||||
} => Some((id, index, count)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Calls `f` on each id of `bytes` if they decode as an `Ack` frame, and
|
||||
/// returns whether they did.
|
||||
pub fn ack_of(bytes: &[u8], f: &mut dyn FnMut(&ObservationId)) -> bool {
|
||||
match Frame::decode(bytes) {
|
||||
Ok(Frame::Ack { ids, .. }) => {
|
||||
ids.iter().for_each(f);
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `bytes` decode as a `Summary` frame.
|
||||
pub fn is_summary(bytes: &[u8]) -> bool {
|
||||
matches!(Frame::decode(bytes), Ok(Frame::Summary { .. }))
|
||||
}
|
||||
|
||||
/// Builds a [`Signature`] (a `#[non_exhaustive]` struct only core can
|
||||
/// construct). `None` if `bytes` is longer than `MAX_SIG`. The shape rules
|
||||
/// (version `0`, a non-64-byte Ed25519) are not checked: a harness builds
|
||||
/// invalid signatures on purpose to see them rejected on decode.
|
||||
pub fn signature(version: u8, algo: SigAlgo, bytes: &[u8]) -> Option<Signature> {
|
||||
Some(Signature {
|
||||
version,
|
||||
algo,
|
||||
bytes: heapless::Vec::from_slice(bytes).ok()?,
|
||||
})
|
||||
}
|
||||
+12
-1
@@ -7,6 +7,9 @@ extern crate alloc;
|
||||
|
||||
pub mod belief;
|
||||
mod bloom;
|
||||
#[cfg(feature = "conformance")]
|
||||
#[doc(hidden)]
|
||||
pub mod conformance;
|
||||
pub mod consts;
|
||||
mod exchange;
|
||||
mod frag;
|
||||
@@ -28,10 +31,18 @@ pub use observation::{
|
||||
};
|
||||
pub use store::{Store, StoreError, TrimPolicy, WallClockStore};
|
||||
pub use transport::{Event, FrameBuf, SendError, Transport};
|
||||
pub use wire::{frame_sender, Frame, WireError};
|
||||
pub use wire::frame_sender;
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
pub use mem_store::MemStore;
|
||||
|
||||
#[cfg(test)]
|
||||
extern crate std;
|
||||
|
||||
// The pinned wire vectors (spec §7) need the crate-private `Frame` and
|
||||
// `WireError`, so they compile as a unit-test module rather than as an
|
||||
// integration test (`autotests = false` in Cargo.toml keeps Cargo from also
|
||||
// building them as one).
|
||||
#[cfg(test)]
|
||||
#[path = "../tests/vectors.rs"]
|
||||
mod vectors;
|
||||
|
||||
+21
-64
@@ -14,13 +14,21 @@ use serde::{Deserialize, Serialize};
|
||||
/// avoid per-frame heap allocation in the `no_std` core; this keeps the enum
|
||||
/// large, so the informative `large_enum_variant` lint is suppressed here.
|
||||
///
|
||||
/// Exposed publicly (see [`crate::Frame`]) so that `tines-core/tests/vectors.rs`
|
||||
/// can decode the pinned golden wire vectors and assert on the decoded shape.
|
||||
/// The wire layout (version byte, positional postcard tags, reserved Signature
|
||||
/// slot) is the frozen contract this crate guarantees across 0.x → 1.0.
|
||||
/// Crate-private (spec §3): the type is not API, but its encoding is. The
|
||||
/// wire layout (version byte, positional postcard tags, reserved Signature
|
||||
/// slot) is frozen for wire v1 and pinned byte-for-byte by
|
||||
/// `tines-core/tests/vectors.rs`, which runs as an in-crate test module.
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// use tines_core::Frame; // crate-private: not reachable from outside core
|
||||
/// ```
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// use tines_core::WireError; // crate-private too
|
||||
/// ```
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum Frame {
|
||||
pub(crate) enum Frame {
|
||||
/// A broadcast bloom summary of a node's observation-id set.
|
||||
Summary {
|
||||
/// The sending node.
|
||||
@@ -143,13 +151,10 @@ pub fn frame_sender(bytes: &[u8]) -> Option<NodeId> {
|
||||
Some(from)
|
||||
}
|
||||
|
||||
/// Errors produced while (de)serializing frames on the wire.
|
||||
///
|
||||
/// Exposed publicly (see [`crate::WireError`]) so that `tines-core/tests/vectors.rs`
|
||||
/// can assert on the exact decode error (e.g. `VersionMismatch`) for the pinned
|
||||
/// malformed vectors.
|
||||
/// Errors produced while (de)serializing frames on the wire. Crate-private
|
||||
/// (spec §3); the exchange reports decode failures as [`crate::DropReason`].
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum WireError {
|
||||
pub(crate) 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`.
|
||||
@@ -315,7 +320,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn frame_sender_peeks_from_field() {
|
||||
// Version byte 0, variant tag 1, then `from: [3;16]` at bytes 2..18.
|
||||
// Version byte (`WIRE_VERSION` = 4) at byte 0, variant tag 0
|
||||
// (Summary) at byte 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,
|
||||
@@ -340,9 +346,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn frame_sender_reads_from_fixed_offset_not_just_the_start() {
|
||||
// The leading byte (the externally-tagged variant tag) is skipped, so a
|
||||
// payload whose first byte is not the sender must still yield the `from`
|
||||
// recorded at byte 1.
|
||||
// The two leading bytes (the version byte and the externally-tagged
|
||||
// variant tag) are skipped, so a payload whose first bytes are not the
|
||||
// sender must still yield the `from` recorded at bytes 2..18.
|
||||
let bytes = {
|
||||
let mut b = [0u8; 32];
|
||||
b[0] = consts::WIRE_VERSION; // version slot (skipped by frame_sender)
|
||||
@@ -514,53 +520,4 @@ mod tests {
|
||||
other => panic!("expected Obs, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gen_missing_golden_vectors() {
|
||||
// THROWAWAY: encode canonical frames and print hex for vectors.rs.
|
||||
let enc = |f: Frame| {
|
||||
let mut buf = [0u8; consts::MAX_FRAME];
|
||||
let n = f.encode(&mut buf).unwrap();
|
||||
let hex: std::string::String =
|
||||
buf[..n].iter().map(|b| std::format!("{b:02x}")).collect();
|
||||
std::println!("{n} {hex}");
|
||||
};
|
||||
|
||||
// unknown-algo Obs
|
||||
let f = Frame::Obs {
|
||||
from: [1; 16],
|
||||
obs: Observation {
|
||||
observer: [1; 16],
|
||||
seq: 1,
|
||||
age_ms: 0,
|
||||
observed_at: None,
|
||||
topic: Topic::from_slice(b"door/3").unwrap(),
|
||||
payload: Payload::from_slice(&[0u8; consts::MAX_PAYLOAD]).unwrap(),
|
||||
dest: None,
|
||||
sig: Some(Signature {
|
||||
version: 1,
|
||||
algo: SigAlgo::Unknown(7),
|
||||
bytes: Vec::from_slice(&[0u8; 64]).unwrap(),
|
||||
}),
|
||||
},
|
||||
};
|
||||
enc(f);
|
||||
|
||||
// Frag
|
||||
let f = Frame::Frag {
|
||||
from: [1; 16],
|
||||
id: [2; 16],
|
||||
index: 0,
|
||||
count: 1,
|
||||
data: Vec::from_slice(&[0x5Cu8; 64]).unwrap(),
|
||||
};
|
||||
enc(f);
|
||||
|
||||
// Ack
|
||||
let ids =
|
||||
Vec::<ObservationId, { consts::ACK_IDS_CAP }>::from_slice(&[[2u8; 16], [3u8; 16]])
|
||||
.unwrap();
|
||||
let f = Frame::Ack { from: [1; 16], ids };
|
||||
enc(f);
|
||||
}
|
||||
}
|
||||
|
||||
+115
-49
@@ -1,7 +1,53 @@
|
||||
//! Pinned identity + wire vectors (spec §7 "Evidence"). A change to any byte
|
||||
//! or expected shape below is an identity- or wire-format change by definition.
|
||||
//!
|
||||
//! `Frame` and `WireError` are crate-private (spec §3), so this file is not a
|
||||
//! Cargo integration-test target: `tines-core/src/lib.rs` compiles it into the
|
||||
//! crate's own unit tests with `#[cfg(test)] #[path = "../tests/vectors.rs"]`.
|
||||
//! Each golden vector is checked both ways: the hand-built frame must
|
||||
//! `encode()` to exactly the pinned bytes, and the pinned bytes must
|
||||
//! `decode()` back to that frame.
|
||||
|
||||
use tines_core::{consts, Frame, Observation, SigAlgo, WireError};
|
||||
use crate::consts;
|
||||
use crate::wire::{Frame, WireError};
|
||||
use crate::{Dest, Observation, Payload, SigAlgo, Signature, Topic};
|
||||
use heapless::Vec;
|
||||
|
||||
/// Encodes `frame` and asserts the bytes equal `golden`, then decodes
|
||||
/// `golden` and asserts it equals `frame`.
|
||||
fn assert_golden(frame: &Frame, golden: &[u8]) {
|
||||
let mut buf = [0u8; consts::MAX_FRAME];
|
||||
let n = frame.encode(&mut buf).expect("golden frame encodes");
|
||||
assert_eq!(
|
||||
&buf[..n],
|
||||
golden,
|
||||
"encode() must reproduce the pinned bytes"
|
||||
);
|
||||
assert_eq!(
|
||||
Frame::decode(golden).as_ref(),
|
||||
Ok(frame),
|
||||
"decode() of the pinned bytes must give the frame"
|
||||
);
|
||||
}
|
||||
|
||||
/// The `[1;16]` / seq 1 / `"door/3"` / `MAX_PAYLOAD` zero-byte broadcast both
|
||||
/// signed vectors carry, signed with 64 zero bytes under `algo`.
|
||||
fn signed_door_obs(algo: SigAlgo) -> Observation {
|
||||
Observation {
|
||||
observer: [1; 16],
|
||||
seq: 1,
|
||||
age_ms: 0,
|
||||
observed_at: None,
|
||||
topic: Topic::from_slice(b"door/3").unwrap(),
|
||||
payload: Payload::from_slice(&[0u8; consts::MAX_PAYLOAD]).unwrap(),
|
||||
dest: None,
|
||||
sig: Some(Signature {
|
||||
version: 1,
|
||||
algo,
|
||||
bytes: Vec::from_slice(&[0u8; 64]).unwrap(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn id_known_vector() {
|
||||
@@ -62,12 +108,12 @@ fn summary_golden_vector() {
|
||||
170u8, 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 Frame::Summary { from, salt, bits } = Frame::decode(&GOLDEN).unwrap() else {
|
||||
panic!("expected a Summary frame")
|
||||
let frame = Frame::Summary {
|
||||
from: [3; 16],
|
||||
salt: 0xdeadbeef,
|
||||
bits: Vec::from_slice(&[0xAAu8; consts::SUMMARY_BYTES]).unwrap(),
|
||||
};
|
||||
assert_eq!(from, [3; 16]);
|
||||
assert_eq!(salt, 0xdeadbeef);
|
||||
assert_eq!(&bits[..], &[0xAAu8; consts::SUMMARY_BYTES][..]);
|
||||
assert_golden(&frame, &GOLDEN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -150,22 +196,11 @@ fn obs_golden_vector_ed25519() {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
];
|
||||
let Frame::Obs { from, obs } = Frame::decode(&GOLDEN).unwrap() else {
|
||||
panic!("expected an Obs frame")
|
||||
let frame = Frame::Obs {
|
||||
from: [1; 16],
|
||||
obs: signed_door_obs(SigAlgo::Ed25519),
|
||||
};
|
||||
assert_eq!(from, [1; 16]);
|
||||
assert_eq!(obs.observer, [1; 16]);
|
||||
assert_eq!(obs.seq, 1);
|
||||
assert_eq!(obs.age_ms, 0);
|
||||
assert_eq!(obs.observed_at, None);
|
||||
assert_eq!(&obs.topic[..], b"door/3");
|
||||
assert_eq!(obs.payload.len(), consts::MAX_PAYLOAD);
|
||||
assert_eq!(obs.dest, None);
|
||||
let sig = obs.sig.expect("a signed obs carries a signature");
|
||||
assert_eq!(sig.version, 1);
|
||||
assert_eq!(sig.algo, SigAlgo::Ed25519);
|
||||
assert_eq!(sig.bytes.len(), 64);
|
||||
assert!(sig.bytes.iter().all(|b| *b == 0));
|
||||
assert_golden(&frame, &GOLDEN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -248,17 +283,11 @@ fn obs_golden_vector_unknown_algo() {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
];
|
||||
let Frame::Obs { obs, .. } = Frame::decode(&GOLDEN).unwrap() else {
|
||||
panic!("expected an Obs frame")
|
||||
let frame = Frame::Obs {
|
||||
from: [1; 16],
|
||||
obs: signed_door_obs(SigAlgo::Unknown(7)),
|
||||
};
|
||||
assert_eq!(obs.observer, [1; 16]);
|
||||
assert_eq!(obs.seq, 1);
|
||||
assert_eq!(obs.observed_at, None);
|
||||
let sig = obs.sig.expect("a signed obs carries a signature");
|
||||
assert_eq!(sig.version, 1);
|
||||
assert_eq!(sig.algo, SigAlgo::Unknown(7));
|
||||
assert_eq!(sig.bytes.len(), 64);
|
||||
assert!(sig.bytes.iter().all(|b| *b == 0));
|
||||
assert_golden(&frame, &GOLDEN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -272,21 +301,14 @@ fn frag_golden_vector() {
|
||||
0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c,
|
||||
0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c, 0x5c,
|
||||
];
|
||||
let Frame::Frag {
|
||||
from,
|
||||
id,
|
||||
index,
|
||||
count,
|
||||
data,
|
||||
} = Frame::decode(&GOLDEN).unwrap()
|
||||
else {
|
||||
panic!("expected a Frag frame")
|
||||
let frame = Frame::Frag {
|
||||
from: [1; 16],
|
||||
id: [2; 16],
|
||||
index: 0,
|
||||
count: 1,
|
||||
data: Vec::from_slice(&[0x5Cu8; 64]).unwrap(),
|
||||
};
|
||||
assert_eq!(from, [1; 16]);
|
||||
assert_eq!(id, [2; 16]);
|
||||
assert_eq!(index, 0);
|
||||
assert_eq!(count, 1);
|
||||
assert_eq!(&data[..], &[0x5Cu8; 64][..]);
|
||||
assert_golden(&frame, &GOLDEN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -297,11 +319,55 @@ fn ack_golden_vector() {
|
||||
0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
|
||||
0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
|
||||
];
|
||||
let Frame::Ack { from, ids } = Frame::decode(&GOLDEN).unwrap() else {
|
||||
panic!("expected an Ack frame")
|
||||
let frame = Frame::Ack {
|
||||
from: [1; 16],
|
||||
ids: Vec::from_slice(&[[2u8; 16], [3u8; 16]]).unwrap(),
|
||||
};
|
||||
assert_eq!(from, [1; 16]);
|
||||
assert_eq!(&ids[..], &[[2u8; 16], [3u8; 16]][..]);
|
||||
assert_golden(&frame, &GOLDEN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ack_with_trailing_byte_is_malformed() {
|
||||
// F13 / §10: frames are canonical. This is the Ack golden vector above
|
||||
// plus one trailing 0x00 byte; the version byte matches and postcard can
|
||||
// decode the Ack, but the leftover byte makes the whole frame Malformed.
|
||||
const TRAILING: [u8; 52] = [
|
||||
0x04, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
|
||||
0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
|
||||
0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
|
||||
0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x00,
|
||||
];
|
||||
assert_eq!(Frame::decode(&TRAILING), Err(WireError::Malformed));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn addressed_obs_golden_vector() {
|
||||
// An addressed observation with a nonzero age and an `observed_at`, so
|
||||
// the vectors pin the `Dest` layout and both optional/varint fields.
|
||||
const GOLDEN: [u8; 73] = [
|
||||
0x04, 0x01, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
|
||||
0x05, 0x05, 0x05, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
|
||||
0x06, 0x06, 0x06, 0x06, 0xac, 0x02, 0xd2, 0x09, 0x01, 0x80, 0xd0, 0x95, 0xff, 0xbc, 0x31,
|
||||
0x03, 0x63, 0x6d, 0x64, 0x02, 0x67, 0x6f, 0x01, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
|
||||
0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0xe0, 0xd4, 0x03, 0x00,
|
||||
];
|
||||
let frame = Frame::Obs {
|
||||
from: [5; 16],
|
||||
obs: Observation {
|
||||
observer: [6; 16],
|
||||
seq: 300,
|
||||
age_ms: 1234,
|
||||
observed_at: Some(1_700_000_000_000),
|
||||
topic: Topic::from_slice(b"cmd").unwrap(),
|
||||
payload: Payload::from_slice(b"go").unwrap(),
|
||||
dest: Some(Dest {
|
||||
to: [7; 16],
|
||||
lifetime_ms: 60_000,
|
||||
}),
|
||||
sig: None,
|
||||
},
|
||||
};
|
||||
assert_golden(&frame, &GOLDEN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user