1#![allow(clippy::doc_lazy_continuation)]
2use crate::generated::Packet;
38use ciborium::value::Value as CborValue;
39use serde::{de::DeserializeOwned, Serialize};
40
41pub const TFBUNDLE_MAGIC: [u8; 8] = [0x54, 0x46, 0x42, 0x4e, 0x44, 0x01, 0x00, 0x00];
42pub const TFPKT_MAGIC: [u8; 8] = [0x54, 0x46, 0x50, 0x4b, 0x54, 0x01, 0x00, 0x00];
43
44#[derive(Debug, thiserror::Error)]
45pub enum BinaryFormatError {
46 #[error("bad magic")]
47 BadMagic,
48 #[error("truncated at offset {0}")]
49 Truncated(usize),
50 #[error("cbor: {0}")]
51 Cbor(String),
52 #[error("length out of range: {0}")]
53 LengthOutOfRange(u64),
54}
55
56fn put_u32_be(buf: &mut Vec<u8>, n: usize) -> Result<(), BinaryFormatError> {
57 if n > u32::MAX as usize {
58 return Err(BinaryFormatError::LengthOutOfRange(n as u64));
59 }
60 let n = n as u32;
61 buf.extend_from_slice(&n.to_be_bytes());
62 Ok(())
63}
64
65fn read_u32_be(buf: &[u8], off: usize) -> Result<u32, BinaryFormatError> {
66 if off + 4 > buf.len() {
67 return Err(BinaryFormatError::Truncated(off));
68 }
69 Ok(u32::from_be_bytes([
70 buf[off],
71 buf[off + 1],
72 buf[off + 2],
73 buf[off + 3],
74 ]))
75}
76
77fn canonicalize_json(v: serde_json::Value) -> serde_json::Value {
78 use serde_json::Value;
79 match v {
80 Value::Object(map) => {
81 let mut entries: Vec<(String, Value)> = map
82 .into_iter()
83 .map(|(k, val)| (k, canonicalize_json(val)))
84 .collect();
85 entries.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
86 let mut out = serde_json::Map::with_capacity(entries.len());
87 for (k, val) in entries {
88 out.insert(k, val);
89 }
90 Value::Object(out)
91 }
92 Value::Array(arr) => Value::Array(arr.into_iter().map(canonicalize_json).collect()),
93 other => other,
94 }
95}
96
97fn cbor_encode<T: Serialize>(v: &T) -> Result<Vec<u8>, BinaryFormatError> {
98 let json_value: serde_json::Value =
105 serde_json::to_value(v).map_err(|e| BinaryFormatError::Cbor(e.to_string()))?;
106 let canonical = canonicalize_json(json_value);
107 let mut out = Vec::new();
108 ciborium::ser::into_writer(&canonical, &mut out)
109 .map_err(|e| BinaryFormatError::Cbor(e.to_string()))?;
110 Ok(out)
111}
112
113fn cbor_decode<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, BinaryFormatError> {
114 ciborium::de::from_reader(bytes).map_err(|e| BinaryFormatError::Cbor(e.to_string()))
115}
116
117pub fn write_tfbundle<T: Serialize>(
122 body: &T,
123 signature: Option<&[u8]>,
124) -> Result<Vec<u8>, BinaryFormatError> {
125 let body_bytes = cbor_encode(body)?;
126 let mut out = Vec::with_capacity(TFBUNDLE_MAGIC.len() + 4 + body_bytes.len() + 4);
127 out.extend_from_slice(&TFBUNDLE_MAGIC);
128 put_u32_be(&mut out, body_bytes.len())?;
129 out.extend_from_slice(&body_bytes);
130 let sig = signature.unwrap_or(&[]);
131 put_u32_be(&mut out, sig.len())?;
132 out.extend_from_slice(sig);
133 Ok(out)
134}
135
136#[derive(Debug)]
137pub struct TfbundleParts {
138 pub body: CborValue,
142 pub signature: Vec<u8>,
143 pub body_bytes: Vec<u8>,
144}
145
146pub fn read_tfbundle(buf: &[u8]) -> Result<TfbundleParts, BinaryFormatError> {
147 if buf.len() < TFBUNDLE_MAGIC.len() {
148 return Err(BinaryFormatError::BadMagic);
149 }
150 if buf[..TFBUNDLE_MAGIC.len()] != TFBUNDLE_MAGIC {
151 return Err(BinaryFormatError::BadMagic);
152 }
153 let mut off = TFBUNDLE_MAGIC.len();
154 let body_len = read_u32_be(buf, off)? as usize;
155 off += 4;
156 if off + body_len > buf.len() {
157 return Err(BinaryFormatError::Truncated(off));
158 }
159 let body_bytes = buf[off..off + body_len].to_vec();
160 let body: CborValue = cbor_decode(&body_bytes)?;
161 off += body_len;
162 let sig_len = read_u32_be(buf, off)? as usize;
163 off += 4;
164 if off + sig_len > buf.len() {
165 return Err(BinaryFormatError::Truncated(off));
166 }
167 let signature = buf[off..off + sig_len].to_vec();
168 Ok(TfbundleParts {
169 body,
170 signature,
171 body_bytes,
172 })
173}
174
175pub fn read_tfbundle_typed<T: DeserializeOwned>(
177 buf: &[u8],
178) -> Result<(T, Vec<u8>), BinaryFormatError> {
179 let parts = read_tfbundle(buf)?;
180 let body: T = cbor_decode(&parts.body_bytes)?;
181 Ok((body, parts.signature))
182}
183
184pub fn write_tfpkt(packet: &Packet) -> Result<Vec<u8>, BinaryFormatError> {
189 let body_bytes = cbor_encode(packet)?;
190 let mut out = Vec::with_capacity(TFPKT_MAGIC.len() + 4 + body_bytes.len());
191 out.extend_from_slice(&TFPKT_MAGIC);
192 put_u32_be(&mut out, body_bytes.len())?;
193 out.extend_from_slice(&body_bytes);
194 Ok(out)
195}
196
197pub fn read_tfpkt(buf: &[u8]) -> Result<Packet, BinaryFormatError> {
198 if buf.len() < TFPKT_MAGIC.len() {
199 return Err(BinaryFormatError::BadMagic);
200 }
201 if buf[..TFPKT_MAGIC.len()] != TFPKT_MAGIC {
202 return Err(BinaryFormatError::BadMagic);
203 }
204 let mut off = TFPKT_MAGIC.len();
205 let body_len = read_u32_be(buf, off)? as usize;
206 off += 4;
207 if off + body_len > buf.len() {
208 return Err(BinaryFormatError::Truncated(off));
209 }
210 cbor_decode(&buf[off..off + body_len])
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216 use serde_json::json;
217
218 #[test]
219 fn tfbundle_round_trip_unsigned() {
220 let body = json!({
221 "bundle_version": "1",
222 "events": [],
223 });
224 let buf = write_tfbundle(&body, None).expect("write");
225 assert_eq!(buf[..TFBUNDLE_MAGIC.len()], TFBUNDLE_MAGIC);
226 let parts = read_tfbundle(&buf).expect("read");
227 assert_eq!(parts.signature.len(), 0);
228 let mut serialised = Vec::new();
230 ciborium::ser::into_writer(&parts.body, &mut serialised).unwrap();
231 let decoded: serde_json::Value = ciborium::de::from_reader(serialised.as_slice()).unwrap();
233 assert_eq!(decoded["bundle_version"], "1");
234 }
235
236 #[test]
237 fn tfbundle_round_trip_with_signature() {
238 let body = json!({"bundle_version": "1", "events": []});
239 let signature = vec![0xaa; 64];
240 let buf = write_tfbundle(&body, Some(&signature)).expect("write");
241 let parts = read_tfbundle(&buf).expect("read");
242 assert_eq!(parts.signature, signature);
243 }
244
245 #[test]
246 fn tfbundle_bad_magic_rejected() {
247 let buf = b"NOT-A-BUNDLE\x00\x00\x00\x00";
248 let err = read_tfbundle(buf).unwrap_err();
249 assert!(matches!(err, BinaryFormatError::BadMagic));
250 }
251
252 #[test]
253 fn tfpkt_round_trip_envelope() {
254 let pkt: Packet = serde_json::from_value(json!({
257 "packet_version": "1",
258 "packet_id": "pkt-roundtrip",
259 "source": "tf:actor:agent:example.com/x",
260 "destination": "tf:actor:service:example.com/d",
261 "priority": "P3",
262 "created_at": "2026-04-24T12:00:00Z",
263 "encoding": "cbor",
264 "compression": "none",
265 "payload": "AAAA",
266 "signature": {
267 "algorithm": "ed25519",
268 "signer": "tf:actor:agent:example.com/x",
269 "signature": "AAAA",
270 },
271 }))
272 .expect("packet");
273 let buf = write_tfpkt(&pkt).expect("write");
274 assert_eq!(buf[..TFPKT_MAGIC.len()], TFPKT_MAGIC);
275 let decoded = read_tfpkt(&buf).expect("read");
276 assert_eq!(decoded.packet_id, pkt.packet_id);
277 }
278
279 #[test]
280 fn tfpkt_truncated_body_rejected() {
281 let pkt: Packet = serde_json::from_value(json!({
282 "packet_version": "1",
283 "packet_id": "pkt-trunc",
284 "source": "tf:actor:agent:example.com/x",
285 "destination": "tf:actor:service:example.com/d",
286 "priority": "P3",
287 "created_at": "2026-04-24T12:00:00Z",
288 "encoding": "cbor",
289 "compression": "none",
290 "payload": "AAAA",
291 "signature": {
292 "algorithm": "ed25519",
293 "signer": "tf:actor:agent:example.com/x",
294 "signature": "AAAA",
295 },
296 }))
297 .expect("packet");
298 let buf = write_tfpkt(&pkt).expect("write");
299 let chopped = &buf[..buf.len() - 5];
300 let err = read_tfpkt(chopped).unwrap_err();
301 assert!(matches!(err, BinaryFormatError::Truncated(_)));
302 }
303}