nodedb_array/segment/format/
header.rs1use crate::error::{ArrayError, ArrayResult};
20
21pub const HEADER_MAGIC: [u8; 8] = *b"NDAS\0\0\0\x01";
23
24pub const FORMAT_VERSION: u16 = 1;
26
27pub const HEADER_SIZE: usize = 24;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct SegmentHeader {
32 pub version: u16,
33 pub flags: u16,
34 pub schema_hash: u64,
35}
36
37impl SegmentHeader {
38 pub fn new(schema_hash: u64) -> Self {
39 Self {
40 version: FORMAT_VERSION,
41 flags: 0,
42 schema_hash,
43 }
44 }
45
46 pub fn encode_to(&self, out: &mut Vec<u8>) -> usize {
49 let start = out.len();
50 out.extend_from_slice(&HEADER_MAGIC);
51 out.extend_from_slice(&self.version.to_le_bytes());
52 out.extend_from_slice(&self.flags.to_le_bytes());
53 out.extend_from_slice(&self.schema_hash.to_le_bytes());
54 let crc = crc32c::crc32c(&out[start..start + 20]);
55 out.extend_from_slice(&crc.to_le_bytes());
56 HEADER_SIZE
57 }
58
59 pub fn decode(bytes: &[u8]) -> ArrayResult<Self> {
60 if bytes.len() < HEADER_SIZE {
61 return Err(ArrayError::SegmentCorruption {
62 detail: format!("segment header truncated: {} bytes", bytes.len()),
63 });
64 }
65 if bytes[..8] != HEADER_MAGIC {
66 return Err(ArrayError::SegmentCorruption {
67 detail: "segment header magic mismatch (not NDAS)".into(),
68 });
69 }
70 let mut u32_buf = [0u8; 4];
71 u32_buf.copy_from_slice(&bytes[20..24]);
72 let crc_stored = u32::from_le_bytes(u32_buf);
73 let crc_calc = crc32c::crc32c(&bytes[..20]);
74 if crc_stored != crc_calc {
75 return Err(ArrayError::SegmentCorruption {
76 detail: format!(
77 "segment header CRC mismatch: stored={crc_stored:08x} \
78 calc={crc_calc:08x}"
79 ),
80 });
81 }
82 let mut u16_buf = [0u8; 2];
83 u16_buf.copy_from_slice(&bytes[8..10]);
84 let version = u16::from_le_bytes(u16_buf);
85 u16_buf.copy_from_slice(&bytes[10..12]);
86 let flags = u16::from_le_bytes(u16_buf);
87 let mut u64_buf = [0u8; 8];
88 u64_buf.copy_from_slice(&bytes[12..20]);
89 let schema_hash = u64::from_le_bytes(u64_buf);
90 if version != FORMAT_VERSION {
91 return Err(ArrayError::UnsupportedSegmentFormat { version });
92 }
93 Ok(Self {
94 version,
95 flags,
96 schema_hash,
97 })
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn header_round_trip() {
107 let h = SegmentHeader::new(0xDEAD_BEEF_CAFE_BABE);
108 let mut buf = Vec::new();
109 let n = h.encode_to(&mut buf);
110 assert_eq!(n, HEADER_SIZE);
111 assert_eq!(buf.len(), HEADER_SIZE);
112 let d = SegmentHeader::decode(&buf).unwrap();
113 assert_eq!(d, h);
114 }
115
116 #[test]
117 fn header_rejects_bad_magic() {
118 let mut buf = vec![0u8; HEADER_SIZE];
119 buf[0] = b'X';
120 assert!(SegmentHeader::decode(&buf).is_err());
121 }
122
123 #[test]
124 fn header_rejects_bad_crc() {
125 let h = SegmentHeader::new(42);
126 let mut buf = Vec::new();
127 h.encode_to(&mut buf);
128 buf[20] ^= 0xFF;
129 assert!(SegmentHeader::decode(&buf).is_err());
130 }
131
132 #[test]
133 fn header_rejects_truncated() {
134 assert!(SegmentHeader::decode(&[0u8; 10]).is_err());
135 }
136
137 #[test]
138 fn header_rejects_v2_segment() {
139 let mut buf = Vec::new();
140 buf.extend_from_slice(&HEADER_MAGIC);
141 buf.extend_from_slice(&2u16.to_le_bytes());
142 buf.extend_from_slice(&0u16.to_le_bytes());
143 buf.extend_from_slice(&0u64.to_le_bytes());
144 let crc = crc32c::crc32c(&buf[..20]);
145 buf.extend_from_slice(&crc.to_le_bytes());
146 let err = SegmentHeader::decode(&buf).unwrap_err();
147 assert!(
148 matches!(
149 err,
150 crate::error::ArrayError::UnsupportedSegmentFormat { version: 2 }
151 ),
152 "expected UnsupportedSegmentFormat {{version: 2}}, got {err:?}"
153 );
154 }
155
156 #[test]
158 fn golden_array_segment_header_format() {
159 let h = SegmentHeader::new(0xDEAD_BEEF_CAFE_BABE);
160 let mut buf = Vec::new();
161 h.encode_to(&mut buf);
162
163 assert_eq!(&buf[0..8], b"NDAS\0\0\0\x01", "magic mismatch");
164
165 let version = u16::from_le_bytes([buf[8], buf[9]]);
166 assert_eq!(version, FORMAT_VERSION, "version mismatch");
167 assert_eq!(version, 1u16, "expected FORMAT_VERSION == 1");
168
169 let stored_crc = u32::from_le_bytes([buf[20], buf[21], buf[22], buf[23]]);
170 let expected_crc = crc32c::crc32c(&buf[..20]);
171 assert_eq!(stored_crc, expected_crc, "header CRC mismatch");
172
173 assert_eq!(buf.len(), HEADER_SIZE);
174 }
175}