zerodds_inspect_endpoint/
frame.rs1use serde::{Deserialize, Serialize};
9use sha2::{Digest, Sha256};
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
13pub enum FrameKind {
14 Dcps,
16 Rtps,
18 Transport,
20}
21
22#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
30pub struct Frame {
31 pub kind: FrameKind,
33 pub topic: String,
35 pub timestamp_ns: u64,
37 pub correlation_id: u64,
40 pub payload: Vec<u8>,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub content_hash: Option<[u8; 32]>,
47}
48
49impl Frame {
50 #[must_use]
52 pub fn dcps(topic: String, timestamp_ns: u64, correlation_id: u64, payload: Vec<u8>) -> Self {
53 Self {
54 kind: FrameKind::Dcps,
55 topic,
56 timestamp_ns,
57 correlation_id,
58 payload,
59 content_hash: None,
60 }
61 }
62
63 #[must_use]
65 pub fn rtps(topic: String, timestamp_ns: u64, correlation_id: u64, payload: Vec<u8>) -> Self {
66 Self {
67 kind: FrameKind::Rtps,
68 topic,
69 timestamp_ns,
70 correlation_id,
71 payload,
72 content_hash: None,
73 }
74 }
75
76 #[must_use]
78 pub fn transport(timestamp_ns: u64, correlation_id: u64, payload: Vec<u8>) -> Self {
79 Self {
80 kind: FrameKind::Transport,
81 topic: String::new(),
82 timestamp_ns,
83 correlation_id,
84 payload,
85 content_hash: None,
86 }
87 }
88
89 pub fn with_content_hash(mut self) -> Self {
95 let mut hasher = Sha256::new();
96 let kind_tag: u8 = match self.kind {
97 FrameKind::Dcps => 1,
98 FrameKind::Rtps => 2,
99 FrameKind::Transport => 3,
100 };
101 hasher.update([kind_tag]);
102 hasher.update(self.topic.as_bytes());
103 hasher.update(b"\0");
104 hasher.update(self.timestamp_ns.to_be_bytes());
105 hasher.update(self.correlation_id.to_be_bytes());
106 hasher.update(&self.payload);
107 let h: [u8; 32] = hasher.finalize().into();
108 self.content_hash = Some(h);
109 self
110 }
111
112 #[must_use]
115 pub fn verify_content_hash(&self) -> bool {
116 let Some(stored) = self.content_hash else {
117 return false;
118 };
119 let mut hasher = Sha256::new();
120 let kind_tag: u8 = match self.kind {
121 FrameKind::Dcps => 1,
122 FrameKind::Rtps => 2,
123 FrameKind::Transport => 3,
124 };
125 hasher.update([kind_tag]);
126 hasher.update(self.topic.as_bytes());
127 hasher.update(b"\0");
128 hasher.update(self.timestamp_ns.to_be_bytes());
129 hasher.update(self.correlation_id.to_be_bytes());
130 hasher.update(&self.payload);
131 let computed: [u8; 32] = hasher.finalize().into();
132 stored == computed
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn dcps_frame_has_dcps_kind() {
142 let f = Frame::dcps("speed".into(), 1000, 42, vec![1, 2, 3]);
143 assert_eq!(f.kind, FrameKind::Dcps);
144 assert_eq!(f.topic, "speed");
145 assert_eq!(f.correlation_id, 42);
146 }
147
148 #[test]
149 fn rtps_frame_has_rtps_kind() {
150 let f = Frame::rtps("speed".into(), 0, 0, vec![]);
151 assert_eq!(f.kind, FrameKind::Rtps);
152 }
153
154 #[test]
155 fn transport_frame_has_no_topic() {
156 let f = Frame::transport(0, 0, vec![0xFF]);
157 assert_eq!(f.kind, FrameKind::Transport);
158 assert!(f.topic.is_empty());
159 }
160
161 #[test]
162 fn frame_clone_preserves_fields() {
163 let f = Frame::dcps("topic".into(), 12345, 99, vec![0xDE, 0xAD]);
164 let cloned = f.clone();
165 assert_eq!(f, cloned);
166 }
167
168 #[test]
169 fn frame_kinds_are_distinct() {
170 let a = Frame::dcps(String::new(), 0, 0, vec![]);
171 let b = Frame::rtps(String::new(), 0, 0, vec![]);
172 let c = Frame::transport(0, 0, vec![]);
173 assert_ne!(a.kind, b.kind);
174 assert_ne!(b.kind, c.kind);
175 }
176
177 #[test]
178 fn unhashed_frame_has_no_content_hash() {
179 let f = Frame::dcps("t".into(), 0, 0, vec![]);
180 assert!(f.content_hash.is_none());
181 }
182
183 #[test]
184 fn with_content_hash_sets_hash() {
185 let f = Frame::dcps("t".into(), 100, 1, vec![1, 2]).with_content_hash();
186 assert!(f.content_hash.is_some());
187 }
188
189 #[test]
190 fn content_hash_is_deterministic() {
191 let f1 = Frame::dcps("t".into(), 100, 1, vec![1, 2]).with_content_hash();
192 let f2 = Frame::dcps("t".into(), 100, 1, vec![1, 2]).with_content_hash();
193 assert_eq!(f1.content_hash, f2.content_hash);
194 }
195
196 #[test]
197 fn payload_change_changes_hash() {
198 let f1 = Frame::dcps("t".into(), 100, 1, vec![1, 2]).with_content_hash();
199 let f2 = Frame::dcps("t".into(), 100, 1, vec![1, 3]).with_content_hash();
200 assert_ne!(f1.content_hash, f2.content_hash);
201 }
202
203 #[test]
204 fn topic_change_changes_hash() {
205 let f1 = Frame::dcps("a".into(), 100, 1, vec![1]).with_content_hash();
206 let f2 = Frame::dcps("b".into(), 100, 1, vec![1]).with_content_hash();
207 assert_ne!(f1.content_hash, f2.content_hash);
208 }
209
210 #[test]
211 fn timestamp_change_changes_hash() {
212 let f1 = Frame::dcps("t".into(), 100, 1, vec![1]).with_content_hash();
213 let f2 = Frame::dcps("t".into(), 200, 1, vec![1]).with_content_hash();
214 assert_ne!(f1.content_hash, f2.content_hash);
215 }
216
217 #[test]
218 fn kind_change_changes_hash() {
219 let f1 = Frame::dcps("t".into(), 100, 1, vec![1]).with_content_hash();
220 let f2 = Frame::rtps("t".into(), 100, 1, vec![1]).with_content_hash();
221 assert_ne!(f1.content_hash, f2.content_hash);
222 }
223
224 #[test]
225 fn verify_content_hash_returns_false_when_unhashed() {
226 let f = Frame::dcps("t".into(), 0, 0, vec![]);
227 assert!(!f.verify_content_hash());
228 }
229
230 #[test]
231 fn verify_content_hash_detects_post_hash_mutation() {
232 let mut f = Frame::dcps("t".into(), 100, 1, vec![1, 2]).with_content_hash();
233 assert!(f.verify_content_hash());
234 f.payload.push(3);
235 assert!(!f.verify_content_hash());
236 }
237}