1use crate::segment::error::{Result, SegmentError};
23use crate::segment::to_array;
24
25pub const MAGIC: [u8; 4] = *b"CXSG";
27
28pub const VERSION: u16 = crate::format::SEGMENT_FORMAT_VERSION;
44
45pub const HEADER_SIZE: usize = 4 + 2 + 2 + 8 + 8 + 8 + 8 + 2 + 4 + 1 + 1;
47pub const FOOTER_SIZE: usize = 8 + 4 + 4 + 4 + 4;
53#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct SegmentHeader {
58 pub version: u16,
60 pub flags: u16,
62 pub created_at: i64,
64 pub min_timestamp: i64,
66 pub max_timestamp: i64,
68 pub row_count: u64,
70 pub column_count: u16,
72 pub series_count: u32,
74 pub compression: u8,
76 pub sort_order: u8,
78}
79
80impl SegmentHeader {
81 #[must_use]
83 pub fn to_bytes(&self) -> Vec<u8> {
84 let mut buf = Vec::with_capacity(HEADER_SIZE);
85 buf.extend_from_slice(&MAGIC);
86 buf.extend_from_slice(&self.version.to_le_bytes());
87 buf.extend_from_slice(&self.flags.to_le_bytes());
88 buf.extend_from_slice(&self.created_at.to_le_bytes());
89 buf.extend_from_slice(&self.min_timestamp.to_le_bytes());
90 buf.extend_from_slice(&self.max_timestamp.to_le_bytes());
91 buf.extend_from_slice(&self.row_count.to_le_bytes());
92 buf.extend_from_slice(&self.column_count.to_le_bytes());
93 buf.extend_from_slice(&self.series_count.to_le_bytes());
94 buf.push(self.compression);
95 buf.push(self.sort_order);
96 buf
97 }
98
99 pub fn from_bytes(data: &[u8]) -> Result<Self> {
106 if data.len() < HEADER_SIZE {
107 return Err(SegmentError::CorruptFile {
108 detail: format!(
109 "header too short: expected {HEADER_SIZE}, got {}",
110 data.len()
111 ),
112 });
113 }
114
115 if data[..4] != MAGIC {
116 return Err(SegmentError::InvalidMagic);
117 }
118
119 let version = u16::from_le_bytes([data[4], data[5]]);
120 if version != VERSION {
124 return Err(SegmentError::UnsupportedVersion {
125 version,
126 detail: crate::format::version_mismatch(
127 "this `.csx` segment",
128 u64::from(version),
129 u64::from(VERSION),
130 ),
131 });
132 }
133
134 let flags = u16::from_le_bytes([data[6], data[7]]);
135 let created_at = i64::from_le_bytes(to_array!(data[8..16], "header created_at")?);
136 let min_timestamp = i64::from_le_bytes(to_array!(data[16..24], "header min_timestamp")?);
137 let max_timestamp = i64::from_le_bytes(to_array!(data[24..32], "header max_timestamp")?);
138 let row_count = u64::from_le_bytes(to_array!(data[32..40], "header row_count")?);
139 let column_count = u16::from_le_bytes([data[40], data[41]]);
140 let series_count = u32::from_le_bytes(to_array!(data[42..46], "header series_count")?);
141 let compression = data[46];
142 let sort_order = data[47];
143
144 Ok(Self {
145 version,
146 flags,
147 created_at,
148 min_timestamp,
149 max_timestamp,
150 row_count,
151 column_count,
152 series_count,
153 compression,
154 sort_order,
155 })
156 }
157}
158
159#[derive(Debug, Clone, PartialEq, Eq)]
161pub struct SegmentFooter {
162 pub metadata_offset: u64,
164 pub row_group_count: u32,
166 pub checksum: u32,
168 pub metadata_checksum: u32,
171}
172
173impl SegmentFooter {
174 #[must_use]
176 pub fn to_bytes(&self) -> Vec<u8> {
177 let mut buf = Vec::with_capacity(FOOTER_SIZE);
178 buf.extend_from_slice(&self.metadata_offset.to_le_bytes());
179 buf.extend_from_slice(&self.row_group_count.to_le_bytes());
180 buf.extend_from_slice(&self.checksum.to_le_bytes());
181 buf.extend_from_slice(&self.metadata_checksum.to_le_bytes());
182 buf.extend_from_slice(&MAGIC);
183 buf
184 }
185
186 pub fn from_bytes(data: &[u8]) -> Result<Self> {
193 if data.len() < FOOTER_SIZE {
194 return Err(SegmentError::CorruptFile {
195 detail: format!(
196 "footer too short: expected {FOOTER_SIZE}, got {}",
197 data.len()
198 ),
199 });
200 }
201
202 let metadata_offset = u64::from_le_bytes(to_array!(data[..8], "footer metadata_offset")?);
203 let row_group_count = u32::from_le_bytes(to_array!(data[8..12], "footer row_group_count")?);
204 let checksum = u32::from_le_bytes(to_array!(data[12..16], "footer checksum")?);
205 let metadata_checksum =
206 u32::from_le_bytes(to_array!(data[16..20], "footer metadata_checksum")?);
207
208 if data[20..24] != MAGIC {
209 return Err(SegmentError::InvalidMagic);
210 }
211
212 Ok(Self {
213 metadata_offset,
214 row_group_count,
215 checksum,
216 metadata_checksum,
217 })
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[test]
226 fn header_roundtrip() {
227 let header = SegmentHeader {
228 version: VERSION,
229 flags: 0,
230 created_at: 1_700_000_000_000_000_000,
231 min_timestamp: 1_000_000,
232 max_timestamp: 2_000_000,
233 row_count: 10_000,
234 column_count: 5,
235 series_count: 42,
236 compression: 0,
237 sort_order: 1,
238 };
239 let bytes = header.to_bytes();
240 assert_eq!(bytes.len(), HEADER_SIZE);
241 let recovered = SegmentHeader::from_bytes(&bytes).unwrap();
242 assert_eq!(header, recovered);
243 }
244
245 #[test]
246 fn header_invalid_magic() {
247 let mut bytes = SegmentHeader {
248 version: VERSION,
249 flags: 0,
250 created_at: 0,
251 min_timestamp: 0,
252 max_timestamp: 0,
253 row_count: 0,
254 column_count: 0,
255 series_count: 0,
256 compression: 0,
257 sort_order: 0,
258 }
259 .to_bytes();
260 bytes[0] = b'X';
261 assert!(matches!(
262 SegmentHeader::from_bytes(&bytes),
263 Err(SegmentError::InvalidMagic)
264 ));
265 }
266
267 #[test]
268 fn footer_roundtrip() {
269 let footer = SegmentFooter {
270 metadata_offset: 1024,
271 row_group_count: 3,
272 checksum: 0xDEAD_BEEF,
273 metadata_checksum: 0xCAFE_BABE,
274 };
275 let bytes = footer.to_bytes();
276 assert_eq!(bytes.len(), FOOTER_SIZE);
277 let recovered = SegmentFooter::from_bytes(&bytes).unwrap();
278 assert_eq!(footer, recovered);
279 }
280
281 #[test]
282 fn footer_invalid_magic() {
283 let mut bytes = SegmentFooter {
284 metadata_offset: 0,
285 row_group_count: 0,
286 checksum: 0,
287 metadata_checksum: 0,
288 }
289 .to_bytes();
290 let len = bytes.len();
291 bytes[len - 1] = b'X';
292 assert!(matches!(
293 SegmentFooter::from_bytes(&bytes),
294 Err(SegmentError::InvalidMagic)
295 ));
296 }
297
298 #[test]
299 fn header_too_short() {
300 assert!(SegmentHeader::from_bytes(&[0; 10]).is_err());
301 }
302
303 #[test]
304 fn footer_too_short() {
305 assert!(SegmentFooter::from_bytes(&[0; 5]).is_err());
306 }
307
308 #[test]
320 fn header_version_is_refused_in_both_directions() {
321 let header = SegmentHeader {
322 version: VERSION,
323 flags: 0,
324 created_at: 0,
325 min_timestamp: 0,
326 max_timestamp: 0,
327 row_count: 0,
328 column_count: 0,
329 series_count: 0,
330 compression: 0,
331 sort_order: 0,
332 };
333 for wrong in [VERSION - 1, VERSION + 1, 999] {
334 let mut bytes = header.to_bytes();
335 bytes[4..6].copy_from_slice(&wrong.to_le_bytes());
336 assert!(
337 matches!(
338 SegmentHeader::from_bytes(&bytes),
339 Err(SegmentError::UnsupportedVersion { .. })
340 ),
341 "version {wrong} must be refused, not read as {VERSION}"
342 );
343 }
344 }
345}