Skip to main content

serializer/machine/
footer.rs

1//! DX-Machine footer format (negative-offset)
2//!
3//! The footer is an 8-byte structure appended at the end of the file:
4//! - 4 bytes: Magic ("DXM\0")
5//! - 1 byte: Version
6//! - 1 byte: Flags
7//! - 2 bytes: CRC-16 checksum
8//!
9//! Layout:
10//! ```text
11//! [RKYV data at offset 0][8-byte footer at end]
12//! ```
13
14use std::fmt;
15
16/// DX-Machine footer (8 bytes, appended at end)
17#[repr(C, packed)]
18#[derive(Clone, Copy)]
19pub struct DxFooter {
20    /// Magic bytes: "DXM\0"
21    pub magic: [u8; 4],
22    /// Format version (currently 0x01)
23    pub version: u8,
24    /// Feature flags
25    pub flags: u8,
26    /// CRC-16 checksum of RKYV data
27    pub checksum: u16,
28}
29
30/// Footer magic bytes: "DXM\0"
31pub const FOOTER_MAGIC: [u8; 4] = [b'D', b'X', b'M', 0x00];
32
33/// Footer version
34pub const FOOTER_VERSION: u8 = 0x01;
35
36/// Footer size in bytes
37pub const FOOTER_SIZE: usize = 8;
38
39impl DxFooter {
40    /// Create a new footer with checksum
41    #[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    /// Create footer with specific flags
52    #[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    /// Parse footer from byte slice (reads last 8 bytes)
63    #[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        // Read footer from end of buffer
70        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    /// Validate footer
90    #[inline]
91    pub fn validate(&self) -> Result<(), FooterError> {
92        // Check magic bytes
93        if self.magic != FOOTER_MAGIC {
94            return Err(FooterError::InvalidMagic {
95                expected: FOOTER_MAGIC,
96                found: self.magic,
97            });
98        }
99
100        // Check version
101        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    /// Verify checksum against data
112    #[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    /// Write footer to byte slice
125    #[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    /// Get footer size in bytes
139    #[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        // Copy checksum to avoid unaligned reference to packed field
154        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/// Compute CRC-16 checksum (CCITT variant)
171#[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/// Extract RKYV data from buffer with footer
190#[inline]
191pub fn extract_data(bytes: &[u8]) -> Result<&[u8], FooterError> {
192    if bytes.len() < FOOTER_SIZE {
193        return Err(FooterError::BufferTooSmall);
194    }
195
196    // Data is everything except the last 8 bytes (footer)
197    let data_len = bytes.len() - FOOTER_SIZE;
198    Ok(&bytes[..data_len])
199}
200
201/// Deserialize with footer validation
202///
203/// This function:
204/// 1. Validates the footer at the end
205/// 2. Extracts RKYV data (everything except footer)
206/// 3. Verifies checksum
207/// 4. Returns reference to RKYV data
208#[inline]
209pub fn deserialize_with_footer(bytes: &[u8]) -> Result<&[u8], FooterError> {
210    // Parse and validate footer
211    let footer = DxFooter::from_bytes(bytes)?;
212
213    // Extract RKYV data
214    let data = extract_data(bytes)?;
215
216    // Verify checksum
217    footer.verify_checksum(data)?;
218
219    Ok(data)
220}
221
222/// Footer validation errors
223#[derive(Debug, Clone, PartialEq, Eq)]
224pub enum FooterError {
225    /// Buffer too small to contain footer
226    BufferTooSmall,
227    /// Invalid magic bytes
228    InvalidMagic {
229        /// Expected footer magic bytes.
230        expected: [u8; 4],
231        /// Magic bytes found in the payload.
232        found: [u8; 4],
233    },
234    /// Unsupported format version
235    UnsupportedVersion {
236        /// Version supported by this implementation.
237        supported: u8,
238        /// Version found in the payload.
239        found: u8,
240    },
241    /// Checksum mismatch
242    ChecksumMismatch {
243        /// Footer checksum read from the payload.
244        expected: u16,
245        /// Checksum computed from the data section.
246        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        // Copy to avoid unaligned reference
294        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        // Copy to avoid unaligned reference
310        let checksum1 = parsed.checksum;
311        let checksum2 = footer.checksum;
312        assert_eq!(checksum1, checksum2);
313    }
314
315    #[test]
316    fn test_footer_validation() {
317        // Valid footer
318        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        // Invalid magic
324        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        // Wrong version
331        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); // Should produce non-zero checksum
343
344        // Same data should produce same checksum
345        let crc2 = compute_crc16(data);
346        assert_eq!(crc, crc2);
347
348        // Different data should produce different checksum
349        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        // Append footer
357        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        // Create buffer with data + footer
369        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        // Deserialize
376        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; // Wrong checksum
384
385        // Create buffer with data + footer
386        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        // Should fail checksum verification
393        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}