1use std::fmt;
15
16#[repr(C, packed)]
18#[derive(Clone, Copy)]
19pub struct DxFooter {
20 pub magic: [u8; 4],
22 pub version: u8,
24 pub flags: u8,
26 pub checksum: u16,
28}
29
30pub const FOOTER_MAGIC: [u8; 4] = [b'D', b'X', b'M', 0x00];
32
33pub const FOOTER_VERSION: u8 = 0x01;
35
36pub const FOOTER_SIZE: usize = 8;
38
39impl DxFooter {
40 #[inline]
42 pub const fn new(checksum: u16) -> Self {
43 Self {
44 magic: FOOTER_MAGIC,
45 version: FOOTER_VERSION,
46 flags: 0,
47 checksum,
48 }
49 }
50
51 #[inline]
53 pub const fn with_flags(checksum: u16, flags: u8) -> Self {
54 Self {
55 magic: FOOTER_MAGIC,
56 version: FOOTER_VERSION,
57 flags,
58 checksum,
59 }
60 }
61
62 #[inline]
64 pub fn from_bytes(bytes: &[u8]) -> Result<Self, FooterError> {
65 if bytes.len() < FOOTER_SIZE {
66 return Err(FooterError::BufferTooSmall);
67 }
68
69 let footer_start = bytes.len() - FOOTER_SIZE;
71 let footer_bytes = &bytes[footer_start..];
72
73 let footer = Self {
74 magic: [
75 footer_bytes[0],
76 footer_bytes[1],
77 footer_bytes[2],
78 footer_bytes[3],
79 ],
80 version: footer_bytes[4],
81 flags: footer_bytes[5],
82 checksum: u16::from_le_bytes([footer_bytes[6], footer_bytes[7]]),
83 };
84
85 footer.validate()?;
86 Ok(footer)
87 }
88
89 #[inline]
91 pub fn validate(&self) -> Result<(), FooterError> {
92 if self.magic != FOOTER_MAGIC {
94 return Err(FooterError::InvalidMagic {
95 expected: FOOTER_MAGIC,
96 found: self.magic,
97 });
98 }
99
100 if self.version != FOOTER_VERSION {
102 return Err(FooterError::UnsupportedVersion {
103 supported: FOOTER_VERSION,
104 found: self.version,
105 });
106 }
107
108 Ok(())
109 }
110
111 #[inline]
113 pub fn verify_checksum(&self, data: &[u8]) -> Result<(), FooterError> {
114 let computed = compute_crc16(data);
115 if computed != self.checksum {
116 return Err(FooterError::ChecksumMismatch {
117 expected: self.checksum,
118 computed,
119 });
120 }
121 Ok(())
122 }
123
124 #[inline]
126 pub fn write_to(&self, bytes: &mut [u8]) {
127 bytes[0] = self.magic[0];
128 bytes[1] = self.magic[1];
129 bytes[2] = self.magic[2];
130 bytes[3] = self.magic[3];
131 bytes[4] = self.version;
132 bytes[5] = self.flags;
133 let checksum_bytes = self.checksum.to_le_bytes();
134 bytes[6] = checksum_bytes[0];
135 bytes[7] = checksum_bytes[1];
136 }
137
138 #[inline]
140 pub const fn size() -> usize {
141 FOOTER_SIZE
142 }
143}
144
145impl Default for DxFooter {
146 fn default() -> Self {
147 Self::new(0)
148 }
149}
150
151impl fmt::Debug for DxFooter {
152 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153 let checksum = self.checksum;
155 f.debug_struct("DxFooter")
156 .field(
157 "magic",
158 &format!(
159 "{:?}",
160 std::str::from_utf8(&self.magic).unwrap_or("<invalid>")
161 ),
162 )
163 .field("version", &self.version)
164 .field("flags", &format!("0b{:08b}", self.flags))
165 .field("checksum", &format!("0x{:04X}", checksum))
166 .finish()
167 }
168}
169
170#[inline]
172pub fn compute_crc16(data: &[u8]) -> u16 {
173 let mut crc: u16 = 0xFFFF;
174
175 for &byte in data {
176 crc ^= (byte as u16) << 8;
177 for _ in 0..8 {
178 if crc & 0x8000 != 0 {
179 crc = (crc << 1) ^ 0x1021;
180 } else {
181 crc <<= 1;
182 }
183 }
184 }
185
186 crc
187}
188
189#[inline]
191pub fn extract_data(bytes: &[u8]) -> Result<&[u8], FooterError> {
192 if bytes.len() < FOOTER_SIZE {
193 return Err(FooterError::BufferTooSmall);
194 }
195
196 let data_len = bytes.len() - FOOTER_SIZE;
198 Ok(&bytes[..data_len])
199}
200
201#[inline]
209pub fn deserialize_with_footer(bytes: &[u8]) -> Result<&[u8], FooterError> {
210 let footer = DxFooter::from_bytes(bytes)?;
212
213 let data = extract_data(bytes)?;
215
216 footer.verify_checksum(data)?;
218
219 Ok(data)
220}
221
222#[derive(Debug, Clone, PartialEq, Eq)]
224pub enum FooterError {
225 BufferTooSmall,
227 InvalidMagic {
229 expected: [u8; 4],
231 found: [u8; 4],
233 },
234 UnsupportedVersion {
236 supported: u8,
238 found: u8,
240 },
241 ChecksumMismatch {
243 expected: u16,
245 computed: u16,
247 },
248}
249
250impl fmt::Display for FooterError {
251 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252 match self {
253 Self::BufferTooSmall => {
254 write!(
255 f,
256 "Buffer too small to contain footer (need {} bytes)",
257 FOOTER_SIZE
258 )
259 }
260 Self::InvalidMagic { expected, found } => {
261 write!(
262 f,
263 "Invalid magic bytes: expected {:?}, found {:?}",
264 expected, found
265 )
266 }
267 Self::UnsupportedVersion { supported, found } => write!(
268 f,
269 "Unsupported footer version: this implementation supports v{:02X}, found v{:02X}",
270 supported, found
271 ),
272 Self::ChecksumMismatch { expected, computed } => write!(
273 f,
274 "Checksum mismatch: expected 0x{:04X}, computed 0x{:04X}",
275 expected, computed
276 ),
277 }
278 }
279}
280
281impl std::error::Error for FooterError {}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[test]
288 fn test_footer_new() {
289 let footer = DxFooter::new(0x1234);
290 assert_eq!(footer.magic, FOOTER_MAGIC);
291 assert_eq!(footer.version, FOOTER_VERSION);
292 assert_eq!(footer.flags, 0);
293 let checksum = footer.checksum;
295 assert_eq!(checksum, 0x1234);
296 }
297
298 #[test]
299 fn test_footer_roundtrip() {
300 let mut bytes = [0u8; 8];
301 let footer = DxFooter::with_flags(0xABCD, 0b0000_0001);
302
303 footer.write_to(&mut bytes);
304 let parsed = DxFooter::from_bytes(&bytes).unwrap();
305
306 assert_eq!(parsed.magic, footer.magic);
307 assert_eq!(parsed.version, footer.version);
308 assert_eq!(parsed.flags, footer.flags);
309 let checksum1 = parsed.checksum;
311 let checksum2 = footer.checksum;
312 assert_eq!(checksum1, checksum2);
313 }
314
315 #[test]
316 fn test_footer_validation() {
317 let mut bytes = [0u8; 8];
319 let footer = DxFooter::new(0);
320 footer.write_to(&mut bytes);
321 assert!(DxFooter::from_bytes(&bytes).is_ok());
322
323 let bytes = [0x00, 0x00, 0x00, 0x00, FOOTER_VERSION, 0, 0, 0];
325 assert!(matches!(
326 DxFooter::from_bytes(&bytes),
327 Err(FooterError::InvalidMagic { .. })
328 ));
329
330 let bytes = [b'D', b'X', b'M', 0x00, 0x99, 0, 0, 0];
332 assert!(matches!(
333 DxFooter::from_bytes(&bytes),
334 Err(FooterError::UnsupportedVersion { .. })
335 ));
336 }
337
338 #[test]
339 fn test_crc16() {
340 let data = b"Hello, World!";
341 let crc = compute_crc16(data);
342 assert_ne!(crc, 0); let crc2 = compute_crc16(data);
346 assert_eq!(crc, crc2);
347
348 let crc3 = compute_crc16(b"Hello, World?");
350 assert_ne!(crc, crc3);
351 }
352
353 #[test]
354 fn test_extract_data() {
355 let mut buffer = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
356 buffer.extend_from_slice(&[b'D', b'X', b'M', 0x00, FOOTER_VERSION, 0, 0, 0]);
358
359 let data = extract_data(&buffer).unwrap();
360 assert_eq!(data, &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
361 }
362
363 #[test]
364 fn test_deserialize_with_footer() {
365 let data = b"Test RKYV data";
366 let checksum = compute_crc16(data);
367
368 let mut buffer = Vec::from(&data[..]);
370 let footer = DxFooter::new(checksum);
371 let mut footer_bytes = [0u8; 8];
372 footer.write_to(&mut footer_bytes);
373 buffer.extend_from_slice(&footer_bytes);
374
375 let extracted = deserialize_with_footer(&buffer).unwrap();
377 assert_eq!(extracted, data);
378 }
379
380 #[test]
381 fn test_deserialize_with_footer_bad_checksum() {
382 let data = b"Test RKYV data";
383 let wrong_checksum = 0x0000; let mut buffer = Vec::from(&data[..]);
387 let footer = DxFooter::new(wrong_checksum);
388 let mut footer_bytes = [0u8; 8];
389 footer.write_to(&mut footer_bytes);
390 buffer.extend_from_slice(&footer_bytes);
391
392 let result = deserialize_with_footer(&buffer);
394 assert!(matches!(result, Err(FooterError::ChecksumMismatch { .. })));
395 }
396
397 #[test]
398 fn test_footer_size() {
399 assert_eq!(DxFooter::size(), 8);
400 assert_eq!(std::mem::size_of::<DxFooter>(), 8);
401 }
402}