Skip to main content

tensogram/
wire.rs

1// (C) Copyright 2026- ECMWF and individual contributors.
2//
3// This software is licensed under the terms of the Apache Licence Version 2.0
4// which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
5// In applying this licence, ECMWF does not waive the privileges and immunities
6// granted to it by virtue of its status as an intergovernmental organisation nor
7// does it submit to any jurisdiction.
8
9use crate::error::{Result, TensogramError};
10
11// ── Constants ────────────────────────────────────────────────────────────────
12
13/// Message start magic: ASCII "TENSOGRM"
14pub const MAGIC: &[u8; 8] = b"TENSOGRM";
15/// Message end magic: ASCII "39277777"
16pub const END_MAGIC: &[u8; 8] = b"39277777";
17/// Frame start marker: ASCII "FR"
18pub const FRAME_MAGIC: &[u8; 2] = b"FR";
19/// Frame end marker: ASCII "ENDF"
20pub const FRAME_END: &[u8; 4] = b"ENDF";
21
22/// Preamble size: magic(8) + version(2) + flags(2) + reserved(4) + total_length(8) = 24
23pub const PREAMBLE_SIZE: usize = 24;
24/// Frame header size: FR(2) + type(2) + version(2) + flags(2) + total_length(8) = 16
25pub const FRAME_HEADER_SIZE: usize = 16;
26/// Postamble size: first_footer_offset(8) + end_magic(8) = 16
27pub const POSTAMBLE_SIZE: usize = 16;
28/// Data object footer size: cbor_offset(8) + ENDF(4) = 12
29pub const DATA_OBJECT_FOOTER_SIZE: usize = 12;
30
31// ── Frame Types ──────────────────────────────────────────────────────────────
32
33/// Frame type identifiers (uint16).
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35#[repr(u16)]
36pub enum FrameType {
37    HeaderMetadata = 1,
38    HeaderIndex = 2,
39    HeaderHash = 3,
40    DataObject = 4,
41    FooterHash = 5,
42    FooterIndex = 6,
43    FooterMetadata = 7,
44    /// Per-object metadata frame that immediately precedes a DataObject frame.
45    /// Carries a GlobalMetadata CBOR with a single-entry `base` array
46    /// containing metadata for the next data object. `_reserved_` and
47    /// `_extra_` are empty in the preceder.
48    PrecederMetadata = 8,
49}
50
51impl FrameType {
52    pub fn from_u16(v: u16) -> Result<Self> {
53        match v {
54            1 => Ok(FrameType::HeaderMetadata),
55            2 => Ok(FrameType::HeaderIndex),
56            3 => Ok(FrameType::HeaderHash),
57            4 => Ok(FrameType::DataObject),
58            5 => Ok(FrameType::FooterHash),
59            6 => Ok(FrameType::FooterIndex),
60            7 => Ok(FrameType::FooterMetadata),
61            8 => Ok(FrameType::PrecederMetadata),
62            _ => Err(TensogramError::Framing(format!("unknown frame type: {v}"))),
63        }
64    }
65}
66
67// ── Message Flags ────────────────────────────────────────────────────────────
68
69/// Flags in the message preamble indicating which optional frames are present.
70#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
71pub struct MessageFlags(u16);
72
73impl MessageFlags {
74    pub const HEADER_METADATA: u16 = 1 << 0;
75    pub const FOOTER_METADATA: u16 = 1 << 1;
76    pub const HEADER_INDEX: u16 = 1 << 2;
77    pub const FOOTER_INDEX: u16 = 1 << 3;
78    pub const HEADER_HASHES: u16 = 1 << 4;
79    pub const FOOTER_HASHES: u16 = 1 << 5;
80    /// At least one PrecederMetadata frame is present in the data objects section.
81    pub const PRECEDER_METADATA: u16 = 1 << 6;
82
83    pub fn new(bits: u16) -> Self {
84        Self(bits)
85    }
86
87    pub fn bits(self) -> u16 {
88        self.0
89    }
90
91    pub fn has(self, flag: u16) -> bool {
92        self.0 & flag != 0
93    }
94
95    pub fn set(&mut self, flag: u16) {
96        self.0 |= flag;
97    }
98
99    /// Returns true if at least one metadata frame (header or footer) is present.
100    pub fn has_metadata(self) -> bool {
101        self.has(Self::HEADER_METADATA) || self.has(Self::FOOTER_METADATA)
102    }
103}
104
105// ── Data Object Flags ────────────────────────────────────────────────────────
106
107/// Flags in data object frame header.
108pub struct DataObjectFlags;
109
110impl DataObjectFlags {
111    /// Bit 0: CBOR descriptor position. 0 = before payload, 1 = after payload (default).
112    pub const CBOR_AFTER_PAYLOAD: u16 = 1 << 0;
113}
114
115// ── Preamble ─────────────────────────────────────────────────────────────────
116
117/// The fixed 24-byte message preamble.
118#[derive(Debug, Clone)]
119pub struct Preamble {
120    pub version: u16,
121    pub flags: MessageFlags,
122    pub reserved: u32,
123    /// Total message length including preamble and postamble.
124    /// Zero indicates streaming mode (length unknown at write time).
125    pub total_length: u64,
126}
127
128impl Preamble {
129    pub fn read_from(buf: &[u8]) -> Result<Self> {
130        if buf.len() < PREAMBLE_SIZE {
131            return Err(TensogramError::Framing(format!(
132                "buffer too short for preamble: {} < {PREAMBLE_SIZE}",
133                buf.len()
134            )));
135        }
136        if &buf[0..8] != MAGIC {
137            return Err(TensogramError::Framing("invalid magic bytes".to_string()));
138        }
139        let version = read_u16_be(buf, 8);
140        if version < 2 {
141            return Err(TensogramError::Framing(format!(
142                "unsupported message version {version} (versions 0 and 1 are deprecated, minimum is 2)"
143            )));
144        }
145        Ok(Preamble {
146            version,
147            flags: MessageFlags::new(read_u16_be(buf, 10)),
148            reserved: read_u32_be(buf, 12),
149            total_length: read_u64_be(buf, 16),
150        })
151    }
152
153    pub fn write_to(&self, out: &mut Vec<u8>) {
154        out.extend_from_slice(MAGIC);
155        out.extend_from_slice(&self.version.to_be_bytes());
156        out.extend_from_slice(&self.flags.bits().to_be_bytes());
157        out.extend_from_slice(&self.reserved.to_be_bytes());
158        out.extend_from_slice(&self.total_length.to_be_bytes());
159    }
160}
161
162// ── Frame Header ─────────────────────────────────────────────────────────────
163
164/// The fixed 16-byte frame header.
165#[derive(Debug, Clone)]
166pub struct FrameHeader {
167    pub frame_type: FrameType,
168    pub version: u16,
169    pub flags: u16,
170    /// Total length from start of frame header to end of ENDF marker (inclusive).
171    pub total_length: u64,
172}
173
174impl FrameHeader {
175    pub fn read_from(buf: &[u8]) -> Result<Self> {
176        if buf.len() < FRAME_HEADER_SIZE {
177            return Err(TensogramError::Framing(format!(
178                "buffer too short for frame header: {} < {FRAME_HEADER_SIZE}",
179                buf.len()
180            )));
181        }
182        if &buf[0..2] != FRAME_MAGIC {
183            return Err(TensogramError::Framing(format!(
184                "invalid frame magic: {:?}",
185                &buf[0..2]
186            )));
187        }
188        let type_val = read_u16_be(buf, 2);
189        let frame_type = FrameType::from_u16(type_val)?;
190        Ok(FrameHeader {
191            frame_type,
192            version: read_u16_be(buf, 4),
193            flags: read_u16_be(buf, 6),
194            total_length: read_u64_be(buf, 8),
195        })
196    }
197
198    pub fn write_to(&self, out: &mut Vec<u8>) {
199        out.extend_from_slice(FRAME_MAGIC);
200        out.extend_from_slice(&(self.frame_type as u16).to_be_bytes());
201        out.extend_from_slice(&self.version.to_be_bytes());
202        out.extend_from_slice(&self.flags.to_be_bytes());
203        out.extend_from_slice(&self.total_length.to_be_bytes());
204    }
205}
206
207// ── Postamble ────────────────────────────────────────────────────────────────
208
209/// The fixed 16-byte message postamble (footer terminator).
210#[derive(Debug, Clone)]
211pub struct Postamble {
212    /// Byte offset from message start to the first footer frame,
213    /// or to the postamble itself if no footer frames exist.
214    pub first_footer_offset: u64,
215}
216
217impl Postamble {
218    pub fn read_from(buf: &[u8]) -> Result<Self> {
219        if buf.len() < POSTAMBLE_SIZE {
220            return Err(TensogramError::Framing(format!(
221                "buffer too short for postamble: {} < {POSTAMBLE_SIZE}",
222                buf.len()
223            )));
224        }
225        let first_footer_offset = read_u64_be(buf, 0);
226        if &buf[8..16] != END_MAGIC {
227            return Err(TensogramError::Framing(
228                "invalid end magic in postamble".to_string(),
229            ));
230        }
231        Ok(Postamble {
232            first_footer_offset,
233        })
234    }
235
236    pub fn write_to(&self, out: &mut Vec<u8>) {
237        out.extend_from_slice(&self.first_footer_offset.to_be_bytes());
238        out.extend_from_slice(END_MAGIC);
239    }
240}
241
242// ── Helpers ──────────────────────────────────────────────────────────────────
243
244/// Read a big-endian u16 from `buf` at `offset`.
245///
246/// # Safety invariant
247/// Callers must ensure `offset + 2 <= buf.len()`.  All call sites
248/// in this crate validate buffer length before calling these helpers.
249pub(crate) fn read_u16_be(buf: &[u8], offset: usize) -> u16 {
250    let mut bytes = [0u8; 2];
251    bytes.copy_from_slice(&buf[offset..offset + 2]);
252    u16::from_be_bytes(bytes)
253}
254
255/// Read a big-endian u32 from `buf` at `offset`.
256/// See [`read_u16_be`] for safety invariant.
257pub(crate) fn read_u32_be(buf: &[u8], offset: usize) -> u32 {
258    let mut bytes = [0u8; 4];
259    bytes.copy_from_slice(&buf[offset..offset + 4]);
260    u32::from_be_bytes(bytes)
261}
262
263/// Read a big-endian u64 from `buf` at `offset`.
264/// See [`read_u16_be`] for safety invariant.
265pub(crate) fn read_u64_be(buf: &[u8], offset: usize) -> u64 {
266    let mut bytes = [0u8; 8];
267    bytes.copy_from_slice(&buf[offset..offset + 8]);
268    u64::from_be_bytes(bytes)
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    #[test]
276    fn test_preamble_round_trip() {
277        let preamble = Preamble {
278            version: 2,
279            flags: MessageFlags::new(MessageFlags::HEADER_METADATA | MessageFlags::HEADER_INDEX),
280            reserved: 0,
281            total_length: 4096,
282        };
283        let mut buf = Vec::new();
284        preamble.write_to(&mut buf);
285        assert_eq!(buf.len(), PREAMBLE_SIZE);
286
287        let parsed = Preamble::read_from(&buf).unwrap();
288        assert_eq!(parsed.version, 2);
289        assert!(parsed.flags.has(MessageFlags::HEADER_METADATA));
290        assert!(parsed.flags.has(MessageFlags::HEADER_INDEX));
291        assert!(!parsed.flags.has(MessageFlags::FOOTER_INDEX));
292        assert_eq!(parsed.total_length, 4096);
293    }
294
295    #[test]
296    fn test_frame_header_round_trip() {
297        let fh = FrameHeader {
298            frame_type: FrameType::DataObject,
299            version: 1,
300            flags: DataObjectFlags::CBOR_AFTER_PAYLOAD,
301            total_length: 1024,
302        };
303        let mut buf = Vec::new();
304        fh.write_to(&mut buf);
305        assert_eq!(buf.len(), FRAME_HEADER_SIZE);
306
307        let parsed = FrameHeader::read_from(&buf).unwrap();
308        assert_eq!(parsed.frame_type, FrameType::DataObject);
309        assert_eq!(parsed.version, 1);
310        assert_eq!(parsed.flags, DataObjectFlags::CBOR_AFTER_PAYLOAD);
311        assert_eq!(parsed.total_length, 1024);
312    }
313
314    #[test]
315    fn test_postamble_round_trip() {
316        let pa = Postamble {
317            first_footer_offset: 8192,
318        };
319        let mut buf = Vec::new();
320        pa.write_to(&mut buf);
321        assert_eq!(buf.len(), POSTAMBLE_SIZE);
322
323        let parsed = Postamble::read_from(&buf).unwrap();
324        assert_eq!(parsed.first_footer_offset, 8192);
325    }
326
327    #[test]
328    fn test_invalid_magic() {
329        let buf = vec![0u8; PREAMBLE_SIZE];
330        assert!(Preamble::read_from(&buf).is_err());
331    }
332
333    #[test]
334    fn test_invalid_frame_magic() {
335        let buf = vec![0u8; FRAME_HEADER_SIZE];
336        assert!(FrameHeader::read_from(&buf).is_err());
337    }
338
339    #[test]
340    fn test_invalid_end_magic() {
341        let mut buf = vec![0u8; POSTAMBLE_SIZE];
342        // Valid offset but bad magic
343        buf[0..8].copy_from_slice(&100u64.to_be_bytes());
344        assert!(Postamble::read_from(&buf).is_err());
345    }
346
347    #[test]
348    fn test_frame_type_parse() {
349        assert_eq!(FrameType::from_u16(1).unwrap(), FrameType::HeaderMetadata);
350        assert_eq!(FrameType::from_u16(4).unwrap(), FrameType::DataObject);
351        assert_eq!(FrameType::from_u16(7).unwrap(), FrameType::FooterMetadata);
352        assert_eq!(FrameType::from_u16(8).unwrap(), FrameType::PrecederMetadata);
353        assert!(FrameType::from_u16(0).is_err());
354        assert!(FrameType::from_u16(9).is_err());
355    }
356
357    #[test]
358    fn test_message_flags() {
359        let mut flags = MessageFlags::default();
360        assert!(!flags.has_metadata());
361
362        flags.set(MessageFlags::HEADER_METADATA);
363        assert!(flags.has_metadata());
364        assert!(flags.has(MessageFlags::HEADER_METADATA));
365        assert!(!flags.has(MessageFlags::FOOTER_METADATA));
366
367        flags.set(MessageFlags::FOOTER_INDEX);
368        assert!(flags.has(MessageFlags::FOOTER_INDEX));
369    }
370
371    #[test]
372    fn test_preceder_metadata_flag() {
373        let mut flags = MessageFlags::default();
374        assert!(!flags.has(MessageFlags::PRECEDER_METADATA));
375
376        flags.set(MessageFlags::PRECEDER_METADATA);
377        assert!(flags.has(MessageFlags::PRECEDER_METADATA));
378        assert_eq!(flags.bits() & (1 << 6), 1 << 6);
379    }
380
381    #[test]
382    fn test_preceder_metadata_frame_header_round_trip() {
383        let fh = FrameHeader {
384            frame_type: FrameType::PrecederMetadata,
385            version: 1,
386            flags: 0,
387            total_length: 256,
388        };
389        let mut buf = Vec::new();
390        fh.write_to(&mut buf);
391        assert_eq!(buf.len(), FRAME_HEADER_SIZE);
392
393        let parsed = FrameHeader::read_from(&buf).unwrap();
394        assert_eq!(parsed.frame_type, FrameType::PrecederMetadata);
395        assert_eq!(parsed.version, 1);
396        assert_eq!(parsed.flags, 0);
397        assert_eq!(parsed.total_length, 256);
398    }
399
400    #[test]
401    fn test_truncated_preamble() {
402        let buf = vec![0u8; 10];
403        assert!(Preamble::read_from(&buf).is_err());
404    }
405}