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