#![allow(clippy::doc_lazy_continuation)]
use crate::cbor;
use crate::generated::Packet;
use serde::{de::DeserializeOwned, Serialize};
pub const TFBUNDLE_MAGIC: [u8; 8] = [0x54, 0x46, 0x42, 0x4e, 0x44, 0x01, 0x00, 0x00];
pub const TFPKT_MAGIC: [u8; 8] = [0x54, 0x46, 0x50, 0x4b, 0x54, 0x01, 0x00, 0x00];
#[derive(Debug, thiserror::Error)]
pub enum BinaryFormatError {
#[error("bad magic")]
BadMagic,
#[error("truncated at offset {0}")]
Truncated(usize),
#[error("cbor: {0}")]
Cbor(String),
#[error("length out of range: {0}")]
LengthOutOfRange(u64),
}
fn put_u32_be(buf: &mut Vec<u8>, n: usize) -> Result<(), BinaryFormatError> {
if n > u32::MAX as usize {
return Err(BinaryFormatError::LengthOutOfRange(n as u64));
}
let n = n as u32;
buf.extend_from_slice(&n.to_be_bytes());
Ok(())
}
fn read_u32_be(buf: &[u8], off: usize) -> Result<u32, BinaryFormatError> {
if off + 4 > buf.len() {
return Err(BinaryFormatError::Truncated(off));
}
Ok(u32::from_be_bytes([
buf[off],
buf[off + 1],
buf[off + 2],
buf[off + 3],
]))
}
fn canonicalize_json(v: serde_json::Value) -> serde_json::Value {
use serde_json::Value;
match v {
Value::Object(map) => {
let mut entries: Vec<(String, Value)> = map
.into_iter()
.map(|(k, val)| (k, canonicalize_json(val)))
.collect();
entries.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
let mut out = serde_json::Map::with_capacity(entries.len());
for (k, val) in entries {
out.insert(k, val);
}
Value::Object(out)
}
Value::Array(arr) => Value::Array(arr.into_iter().map(canonicalize_json).collect()),
other => other,
}
}
fn cbor_encode<T: Serialize>(v: &T) -> Result<Vec<u8>, BinaryFormatError> {
let json_value: serde_json::Value =
serde_json::to_value(v).map_err(|e| BinaryFormatError::Cbor(e.to_string()))?;
let canonical = canonicalize_json(json_value);
let value = cbor::from_json(&canonical).map_err(|e| BinaryFormatError::Cbor(e.to_string()))?;
cbor::encode(&value).map_err(|e| BinaryFormatError::Cbor(e.to_string()))
}
fn cbor_decode<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, BinaryFormatError> {
let value = cbor::decode(bytes).map_err(|e| BinaryFormatError::Cbor(e.to_string()))?;
let json = cbor::to_json(&value).map_err(|e| BinaryFormatError::Cbor(e.to_string()))?;
serde_json::from_value(json).map_err(|e| BinaryFormatError::Cbor(e.to_string()))
}
pub fn write_tfbundle<T: Serialize>(
body: &T,
signature: Option<&[u8]>,
) -> Result<Vec<u8>, BinaryFormatError> {
let body_bytes = cbor_encode(body)?;
let mut out = Vec::with_capacity(TFBUNDLE_MAGIC.len() + 4 + body_bytes.len() + 4);
out.extend_from_slice(&TFBUNDLE_MAGIC);
put_u32_be(&mut out, body_bytes.len())?;
out.extend_from_slice(&body_bytes);
let sig = signature.unwrap_or(&[]);
put_u32_be(&mut out, sig.len())?;
out.extend_from_slice(sig);
Ok(out)
}
#[derive(Debug)]
pub struct TfbundleParts {
pub body: cbor::Value,
pub signature: Vec<u8>,
pub body_bytes: Vec<u8>,
}
pub fn read_tfbundle(buf: &[u8]) -> Result<TfbundleParts, BinaryFormatError> {
if buf.len() < TFBUNDLE_MAGIC.len() {
return Err(BinaryFormatError::BadMagic);
}
if buf[..TFBUNDLE_MAGIC.len()] != TFBUNDLE_MAGIC {
return Err(BinaryFormatError::BadMagic);
}
let mut off = TFBUNDLE_MAGIC.len();
let body_len = read_u32_be(buf, off)? as usize;
off += 4;
if off + body_len > buf.len() {
return Err(BinaryFormatError::Truncated(off));
}
let body_bytes = buf[off..off + body_len].to_vec();
let body = cbor::decode(&body_bytes).map_err(|e| BinaryFormatError::Cbor(e.to_string()))?;
off += body_len;
let sig_len = read_u32_be(buf, off)? as usize;
off += 4;
if off + sig_len > buf.len() {
return Err(BinaryFormatError::Truncated(off));
}
let signature = buf[off..off + sig_len].to_vec();
Ok(TfbundleParts {
body,
signature,
body_bytes,
})
}
pub fn read_tfbundle_typed<T: DeserializeOwned>(
buf: &[u8],
) -> Result<(T, Vec<u8>), BinaryFormatError> {
let parts = read_tfbundle(buf)?;
let body: T = cbor_decode(&parts.body_bytes)?;
Ok((body, parts.signature))
}
pub fn write_tfpkt(packet: &Packet) -> Result<Vec<u8>, BinaryFormatError> {
let body_bytes = cbor_encode(packet)?;
let mut out = Vec::with_capacity(TFPKT_MAGIC.len() + 4 + body_bytes.len());
out.extend_from_slice(&TFPKT_MAGIC);
put_u32_be(&mut out, body_bytes.len())?;
out.extend_from_slice(&body_bytes);
Ok(out)
}
pub fn read_tfpkt(buf: &[u8]) -> Result<Packet, BinaryFormatError> {
if buf.len() < TFPKT_MAGIC.len() {
return Err(BinaryFormatError::BadMagic);
}
if buf[..TFPKT_MAGIC.len()] != TFPKT_MAGIC {
return Err(BinaryFormatError::BadMagic);
}
let mut off = TFPKT_MAGIC.len();
let body_len = read_u32_be(buf, off)? as usize;
off += 4;
if off + body_len > buf.len() {
return Err(BinaryFormatError::Truncated(off));
}
cbor_decode(&buf[off..off + body_len])
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn tfbundle_round_trip_unsigned() {
let body = json!({
"bundle_version": "1",
"events": [],
});
let buf = write_tfbundle(&body, None).expect("write");
assert_eq!(buf[..TFBUNDLE_MAGIC.len()], TFBUNDLE_MAGIC);
let parts = read_tfbundle(&buf).expect("read");
assert_eq!(parts.signature.len(), 0);
let serialised = crate::cbor::encode(&parts.body).unwrap();
let decoded = crate::cbor::to_json(&crate::cbor::decode(&serialised).unwrap()).unwrap();
assert_eq!(decoded["bundle_version"], "1");
}
#[test]
fn tfbundle_round_trip_with_signature() {
let body = json!({"bundle_version": "1", "events": []});
let signature = vec![0xaa; 64];
let buf = write_tfbundle(&body, Some(&signature)).expect("write");
let parts = read_tfbundle(&buf).expect("read");
assert_eq!(parts.signature, signature);
}
#[test]
fn tfbundle_bad_magic_rejected() {
let buf = b"NOT-A-BUNDLE\x00\x00\x00\x00";
let err = read_tfbundle(buf).unwrap_err();
assert!(matches!(err, BinaryFormatError::BadMagic));
}
#[test]
fn tfpkt_round_trip_envelope() {
let pkt: Packet = serde_json::from_value(json!({
"packet_version": "1",
"packet_id": "pkt-roundtrip",
"source": "tf:actor:agent:example.com/x",
"destination": "tf:actor:service:example.com/d",
"priority": "P3",
"created_at": "2026-04-24T12:00:00Z",
"encoding": "cbor",
"compression": "none",
"payload": "AAAA",
"signature": {
"algorithm": "ed25519",
"signer": "tf:actor:agent:example.com/x",
"signature": "AAAA",
},
}))
.expect("packet");
let buf = write_tfpkt(&pkt).expect("write");
assert_eq!(buf[..TFPKT_MAGIC.len()], TFPKT_MAGIC);
let decoded = read_tfpkt(&buf).expect("read");
assert_eq!(decoded.packet_id, pkt.packet_id);
}
#[test]
fn tfpkt_truncated_body_rejected() {
let pkt: Packet = serde_json::from_value(json!({
"packet_version": "1",
"packet_id": "pkt-trunc",
"source": "tf:actor:agent:example.com/x",
"destination": "tf:actor:service:example.com/d",
"priority": "P3",
"created_at": "2026-04-24T12:00:00Z",
"encoding": "cbor",
"compression": "none",
"payload": "AAAA",
"signature": {
"algorithm": "ed25519",
"signer": "tf:actor:agent:example.com/x",
"signature": "AAAA",
},
}))
.expect("packet");
let buf = write_tfpkt(&pkt).expect("write");
let chopped = &buf[..buf.len() - 5];
let err = read_tfpkt(chopped).unwrap_err();
assert!(matches!(err, BinaryFormatError::Truncated(_)));
}
}