Skip to main content

tf_types/
format.rs

1//! Binary framing for `.tflog` and `.tfproof`. Matches
2//! `tools/tf-types-ts/src/core/format.ts` byte-for-byte via
3//! `conformance/framing-vectors.yaml`.
4//!
5//! `.tflog`  — append-only log of proof events.
6//!   header  = "TFLOG\x01\x00\x00"   (8 bytes)
7//!   frames  = u32 BE length + canonical-JSON event bytes (repeat)
8//!
9//! `.tfproof` — signed bundle container.
10//!   header  = "TFPROOF\x01"           (8 bytes)
11//!   body    = u32 BE length + canonical-JSON bundle bytes
12//!   trailer = u32 BE length + raw signature bytes
13
14use serde_json::Value;
15
16use crate::canonical::canonicalize;
17use crate::generated::proof_bundle::ProofBundle;
18use crate::generated::proof_event::ProofEvent;
19
20pub const TFLOG_MAGIC: &[u8; 8] = b"TFLOG\x01\x00\x00";
21pub const TFPROOF_MAGIC: &[u8; 8] = b"TFPROOF\x01";
22
23#[derive(Debug, thiserror::Error, PartialEq, Eq)]
24pub enum FormatError {
25    #[error("unexpected end of input at offset {0}")]
26    Truncated(usize),
27    #[error("bad magic header at offset {0}")]
28    BadMagic(usize),
29    #[error("length prefix {1} exceeds remaining bytes at offset {0}")]
30    BadLength(usize, u32),
31    #[error("canonical JSON error: {0}")]
32    Canonical(String),
33    #[error("serde error: {0}")]
34    Serde(String),
35    #[error("utf-8 error: {0}")]
36    Utf8(String),
37}
38
39fn put_u32_be(buf: &mut Vec<u8>, n: usize) -> Result<(), FormatError> {
40    let n = u32::try_from(n).map_err(|_| FormatError::BadLength(buf.len(), u32::MAX))?;
41    buf.extend_from_slice(&n.to_be_bytes());
42    Ok(())
43}
44
45fn read_u32_be(buf: &[u8], off: usize) -> Result<u32, FormatError> {
46    if off + 4 > buf.len() {
47        return Err(FormatError::Truncated(off));
48    }
49    let arr: [u8; 4] = buf[off..off + 4].try_into().unwrap();
50    Ok(u32::from_be_bytes(arr))
51}
52
53// ---------- .tflog ----------
54
55pub fn write_tflog(events: &[ProofEvent]) -> Result<Vec<u8>, FormatError> {
56    let mut buf = Vec::with_capacity(256);
57    buf.extend_from_slice(TFLOG_MAGIC);
58    for e in events {
59        let json = serde_json::to_value(e).map_err(|e| FormatError::Serde(e.to_string()))?;
60        let body = canonicalize(&json).map_err(|e| FormatError::Canonical(e.to_string()))?;
61        let bytes = body.into_bytes();
62        put_u32_be(&mut buf, bytes.len())?;
63        buf.extend_from_slice(&bytes);
64    }
65    Ok(buf)
66}
67
68pub fn append_tflog(existing: &mut Vec<u8>, event: &ProofEvent) -> Result<(), FormatError> {
69    if existing.is_empty() {
70        existing.extend_from_slice(TFLOG_MAGIC);
71    } else if existing.len() < TFLOG_MAGIC.len() || &existing[..TFLOG_MAGIC.len()] != TFLOG_MAGIC {
72        return Err(FormatError::BadMagic(0));
73    }
74    let json = serde_json::to_value(event).map_err(|e| FormatError::Serde(e.to_string()))?;
75    let body = canonicalize(&json).map_err(|e| FormatError::Canonical(e.to_string()))?;
76    let bytes = body.into_bytes();
77    put_u32_be(existing, bytes.len())?;
78    existing.extend_from_slice(&bytes);
79    Ok(())
80}
81
82pub fn read_tflog(buf: &[u8]) -> Result<Vec<ProofEvent>, FormatError> {
83    if buf.len() < TFLOG_MAGIC.len() {
84        return Err(FormatError::Truncated(0));
85    }
86    if &buf[..TFLOG_MAGIC.len()] != TFLOG_MAGIC {
87        return Err(FormatError::BadMagic(0));
88    }
89    let mut out = Vec::new();
90    let mut off = TFLOG_MAGIC.len();
91    while off < buf.len() {
92        let len = read_u32_be(buf, off)? as usize;
93        off += 4;
94        if off + len > buf.len() {
95            return Err(FormatError::BadLength(off - 4, len as u32));
96        }
97        let slice = &buf[off..off + len];
98        let text = std::str::from_utf8(slice).map_err(|e| FormatError::Utf8(e.to_string()))?;
99        let value: Value =
100            serde_json::from_str(text).map_err(|e| FormatError::Serde(e.to_string()))?;
101        let event: ProofEvent =
102            serde_json::from_value(value).map_err(|e| FormatError::Serde(e.to_string()))?;
103        out.push(event);
104        off += len;
105    }
106    Ok(out)
107}
108
109// ---------- .tfproof ----------
110
111pub fn write_tfproof(bundle: &ProofBundle, signature: &[u8]) -> Result<Vec<u8>, FormatError> {
112    let mut buf = Vec::with_capacity(1024);
113    buf.extend_from_slice(TFPROOF_MAGIC);
114    let body_json = serde_json::to_value(bundle).map_err(|e| FormatError::Serde(e.to_string()))?;
115    let body = canonicalize(&body_json).map_err(|e| FormatError::Canonical(e.to_string()))?;
116    let body_bytes = body.into_bytes();
117    put_u32_be(&mut buf, body_bytes.len())?;
118    buf.extend_from_slice(&body_bytes);
119    put_u32_be(&mut buf, signature.len())?;
120    buf.extend_from_slice(signature);
121    Ok(buf)
122}
123
124#[derive(Debug)]
125pub struct TfproofParts {
126    pub bundle: ProofBundle,
127    pub signature: Vec<u8>,
128    pub canonical_body: Vec<u8>,
129}
130
131pub fn read_tfproof(buf: &[u8]) -> Result<TfproofParts, FormatError> {
132    if buf.len() < TFPROOF_MAGIC.len() {
133        return Err(FormatError::Truncated(0));
134    }
135    if &buf[..TFPROOF_MAGIC.len()] != TFPROOF_MAGIC {
136        return Err(FormatError::BadMagic(0));
137    }
138    let mut off = TFPROOF_MAGIC.len();
139    let body_len = read_u32_be(buf, off)? as usize;
140    off += 4;
141    if off + body_len > buf.len() {
142        return Err(FormatError::BadLength(off - 4, body_len as u32));
143    }
144    let body_slice = &buf[off..off + body_len];
145    let body_text =
146        std::str::from_utf8(body_slice).map_err(|e| FormatError::Utf8(e.to_string()))?;
147    let body_value: Value =
148        serde_json::from_str(body_text).map_err(|e| FormatError::Serde(e.to_string()))?;
149    let bundle: ProofBundle =
150        serde_json::from_value(body_value).map_err(|e| FormatError::Serde(e.to_string()))?;
151    off += body_len;
152
153    let sig_len = read_u32_be(buf, off)? as usize;
154    off += 4;
155    if off + sig_len > buf.len() {
156        return Err(FormatError::BadLength(off - 4, sig_len as u32));
157    }
158    let signature = buf[off..off + sig_len].to_vec();
159
160    Ok(TfproofParts {
161        bundle,
162        signature,
163        canonical_body: body_slice.to_vec(),
164    })
165}