Skip to main content

nodedb_array/segment/format/
framing.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Per-block framing: 4-byte length prefix + payload + 4-byte CRC32C.
4//!
5//! Used for every variable-length block in a segment (each tile
6//! payload, the footer body, and the per-tile MBR table). CRC covers
7//! the payload only — the length prefix is implicitly checked by the
8//! length itself making sense (and by being inside the header-CRC'd
9//! offset table).
10
11use crate::error::{ArrayError, ArrayResult};
12
13/// Bytes added to a payload by framing (4 length + 4 CRC).
14pub const FRAMING_OVERHEAD: usize = 8;
15
16pub struct BlockFraming;
17
18impl BlockFraming {
19    /// Append `[len_u32_le | payload | crc32c_u32_le]` to `out`. Returns
20    /// total bytes written.
21    pub fn encode(payload: &[u8], out: &mut Vec<u8>) -> usize {
22        let len = payload.len() as u32;
23        out.extend_from_slice(&len.to_le_bytes());
24        out.extend_from_slice(payload);
25        let crc = crc32c::crc32c(payload);
26        out.extend_from_slice(&crc.to_le_bytes());
27        FRAMING_OVERHEAD + payload.len()
28    }
29
30    /// Decode a framed block at the start of `bytes`. Returns
31    /// `(payload_slice, total_bytes_consumed)`.
32    pub fn decode(bytes: &[u8]) -> ArrayResult<(&[u8], usize)> {
33        if bytes.len() < FRAMING_OVERHEAD {
34            return Err(ArrayError::SegmentCorruption {
35                detail: format!("framed block truncated: {} bytes", bytes.len()),
36            });
37        }
38        let len = u32::from_le_bytes(read_u32_le(bytes, 0)) as usize;
39        let total = FRAMING_OVERHEAD + len;
40        if bytes.len() < total {
41            return Err(ArrayError::SegmentCorruption {
42                detail: format!(
43                    "framed block claims len={len} but buffer has {}",
44                    bytes.len() - 4
45                ),
46            });
47        }
48        let payload = &bytes[4..4 + len];
49        let crc_stored = u32::from_le_bytes(read_u32_le(bytes, 4 + len));
50        let crc_calc = crc32c::crc32c(payload);
51        if crc_stored != crc_calc {
52            return Err(ArrayError::SegmentCorruption {
53                detail: format!(
54                    "framed block CRC mismatch: stored={crc_stored:08x} \
55                     calc={crc_calc:08x}"
56                ),
57            });
58        }
59        Ok((payload, total))
60    }
61}
62
63#[inline]
64fn read_u32_le(bytes: &[u8], offset: usize) -> [u8; 4] {
65    let mut out = [0u8; 4];
66    out.copy_from_slice(&bytes[offset..offset + 4]);
67    out
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn framing_round_trip() {
76        let payload = b"hello world";
77        let mut buf = Vec::new();
78        let n = BlockFraming::encode(payload, &mut buf);
79        assert_eq!(n, FRAMING_OVERHEAD + payload.len());
80        let (got, total) = BlockFraming::decode(&buf).unwrap();
81        assert_eq!(got, payload);
82        assert_eq!(total, n);
83    }
84
85    #[test]
86    fn framing_round_trip_empty() {
87        let mut buf = Vec::new();
88        BlockFraming::encode(&[], &mut buf);
89        let (got, total) = BlockFraming::decode(&buf).unwrap();
90        assert_eq!(got, &[] as &[u8]);
91        assert_eq!(total, FRAMING_OVERHEAD);
92    }
93
94    #[test]
95    fn framing_rejects_truncated_header() {
96        assert!(BlockFraming::decode(&[0u8; 3]).is_err());
97    }
98
99    #[test]
100    fn framing_rejects_short_payload() {
101        let mut buf = Vec::new();
102        BlockFraming::encode(b"hi", &mut buf);
103        // Drop the trailing CRC and one payload byte
104        let truncated = &buf[..buf.len() - 3];
105        assert!(BlockFraming::decode(truncated).is_err());
106    }
107
108    #[test]
109    fn framing_rejects_corrupt_payload() {
110        let mut buf = Vec::new();
111        BlockFraming::encode(b"data", &mut buf);
112        // Corrupt the payload (offset 4 = first payload byte)
113        buf[4] ^= 0xFF;
114        assert!(BlockFraming::decode(&buf).is_err());
115    }
116}