Skip to main content

faf_rust_sdk/binary/
header.rs

1//! FAFB Header Implementation
2//!
3//! The 32-byte header that identifies and describes a .fafb file.
4//!
5//! Layout:
6//! ```text
7//! Offset  Size  Field
8//! ------  ----  -----
9//! 0       4     magic (b"FAFB")
10//! 4       1     version_major
11//! 5       1     version_minor
12//! 6       2     flags
13//! 8       4     source_checksum (CRC32)
14//! 12      8     created_timestamp (Unix)
15//! 20      2     section_count
16//! 22      4     section_table_offset
17//! 26      2     reserved
18//! 28      4     total_size
19//! ------  ----
20//! Total: 32 bytes
21//! ```
22
23use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
24use std::io::{Cursor, Read, Write};
25
26use super::error::{FafbError, FafbResult};
27use super::flags::Flags;
28
29/// Magic number identifying FAFB files: "FAFB" in ASCII
30pub const MAGIC: [u8; 4] = *b"FAFB";
31
32/// Magic number as u32 (little-endian)
33pub const MAGIC_U32: u32 = 0x4246_4146; // "FAFB" little-endian
34
35/// Current format major version (breaking changes)
36pub const VERSION_MAJOR: u8 = 1;
37
38/// Current format minor version (additive changes)
39pub const VERSION_MINOR: u8 = 0;
40
41/// Header size in bytes
42pub const HEADER_SIZE: usize = 32;
43
44/// Maximum allowed section count (DoS protection)
45pub const MAX_SECTIONS: u16 = 256;
46
47/// Maximum allowed file size (DoS protection): 10MB
48pub const MAX_FILE_SIZE: u32 = 10 * 1024 * 1024;
49
50/// The 32-byte FAFB file header
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct FafbHeader {
53    // Identification (8 bytes)
54    /// Format version - major (breaking changes)
55    pub version_major: u8,
56    /// Format version - minor (additive changes)
57    pub version_minor: u8,
58    /// Feature flags
59    pub flags: Flags,
60
61    // Integrity (12 bytes)
62    /// CRC32 checksum of original .faf YAML source
63    pub source_checksum: u32,
64    /// Unix timestamp when .fafb was created
65    pub created_timestamp: u64,
66
67    // Index (8 bytes)
68    /// Number of sections in the file
69    pub section_count: u16,
70    /// Byte offset to section table (from start of file)
71    pub section_table_offset: u32,
72    /// Reserved for future use
73    pub reserved: u16,
74
75    // Size (4 bytes)
76    /// Total file size in bytes
77    pub total_size: u32,
78}
79
80impl FafbHeader {
81    /// Create a new header with default version and empty flags
82    pub fn new() -> Self {
83        Self {
84            version_major: VERSION_MAJOR,
85            version_minor: VERSION_MINOR,
86            flags: Flags::new(),
87            source_checksum: 0,
88            created_timestamp: 0,
89            section_count: 0,
90            section_table_offset: HEADER_SIZE as u32,
91            reserved: 0,
92            total_size: HEADER_SIZE as u32,
93        }
94    }
95
96    /// Create header with current timestamp
97    pub fn with_timestamp() -> Self {
98        let mut header = Self::new();
99        header.created_timestamp = std::time::SystemTime::now()
100            .duration_since(std::time::UNIX_EPOCH)
101            .map(|d| d.as_secs())
102            .unwrap_or(0);
103        header
104    }
105
106    /// Compute CRC32 checksum of source YAML
107    pub fn compute_checksum(yaml_source: &[u8]) -> u32 {
108        // WHY: CRC32 of source YAML (not binary) - enables integrity verification
109        // that the .fafb was created from a specific .faf source file
110        crc32fast::hash(yaml_source)
111    }
112
113    /// Set the source checksum from YAML content
114    pub fn set_source_checksum(&mut self, yaml_source: &[u8]) {
115        self.source_checksum = Self::compute_checksum(yaml_source);
116    }
117
118    /// Write header to a byte buffer
119    pub fn write<W: Write>(&self, writer: &mut W) -> FafbResult<()> {
120        // WHY: Field order matches FAFB spec exactly - parsers depend on this layout
121        // WHY: Little-endian used throughout for cross-platform compatibility (x86/ARM native)
122
123        // Magic (4 bytes) - identifies file type before any other parsing
124        writer.write_all(&MAGIC)?;
125
126        // Version (2 bytes) - enables format evolution without breaking old readers
127        writer.write_u8(self.version_major)?;
128        writer.write_u8(self.version_minor)?;
129
130        // Flags (2 bytes) - feature detection without version bump
131        writer.write_u16::<LittleEndian>(self.flags.raw())?;
132
133        // Integrity (12 bytes) - checksum enables source→binary verification
134        writer.write_u32::<LittleEndian>(self.source_checksum)?;
135        writer.write_u64::<LittleEndian>(self.created_timestamp)?;
136
137        // Index (8 bytes) - section table location for random access
138        writer.write_u16::<LittleEndian>(self.section_count)?;
139        writer.write_u32::<LittleEndian>(self.section_table_offset)?;
140        // WHY: Reserved bytes allow future header extensions without version bump
141        writer.write_u16::<LittleEndian>(self.reserved)?;
142
143        // Size (4 bytes) - enables pre-allocation and bounds validation
144        writer.write_u32::<LittleEndian>(self.total_size)?;
145
146        Ok(())
147    }
148
149    /// Write header to a new `Vec<u8>`
150    pub fn to_bytes(&self) -> FafbResult<Vec<u8>> {
151        let mut buf = Vec::with_capacity(HEADER_SIZE);
152        self.write(&mut buf)?;
153        Ok(buf)
154    }
155
156    /// Read header from a byte buffer
157    pub fn read<R: Read>(reader: &mut R) -> FafbResult<Self> {
158        // WHY: Fail fast on magic check - 4 bytes tells us if this is even FAFB
159        // Checking magic first avoids parsing garbage data as valid fields
160        let mut magic = [0u8; 4];
161        reader.read_exact(&mut magic)?;
162
163        let magic_u32 = u32::from_le_bytes(magic);
164        if magic_u32 != MAGIC_U32 {
165            return Err(FafbError::InvalidMagic(magic_u32));
166        }
167
168        // WHY: Version check before full parse - incompatible versions may have
169        // different field layouts, so we must reject early to avoid misreading
170        let version_major = reader.read_u8()?;
171        let version_minor = reader.read_u8()?;
172
173        if version_major != VERSION_MAJOR {
174            return Err(FafbError::IncompatibleVersion {
175                expected: VERSION_MAJOR,
176                actual: version_major,
177            });
178        }
179
180        // WHY: Unknown flags ignored per spec - enables forward compatibility
181        // New readers can parse old files; old readers can parse new files with new flags
182        let flags = Flags::from_raw(reader.read_u16::<LittleEndian>()?);
183
184        // Integrity fields - read unconditionally, validate later with source
185        let source_checksum = reader.read_u32::<LittleEndian>()?;
186        let created_timestamp = reader.read_u64::<LittleEndian>()?;
187
188        // WHY: DoS protection - reject before allocating large section tables
189        let section_count = reader.read_u16::<LittleEndian>()?;
190        if section_count > MAX_SECTIONS {
191            return Err(FafbError::TooManySections {
192                count: section_count,
193                max: MAX_SECTIONS,
194            });
195        }
196
197        let section_table_offset = reader.read_u32::<LittleEndian>()?;
198        let reserved = reader.read_u16::<LittleEndian>()?;
199
200        // WHY: DoS protection - reject unreasonably large files before processing
201        let total_size = reader.read_u32::<LittleEndian>()?;
202        if total_size > MAX_FILE_SIZE {
203            return Err(FafbError::SizeMismatch {
204                header_size: total_size,
205                actual_size: MAX_FILE_SIZE as usize,
206            });
207        }
208
209        Ok(Self {
210            version_major,
211            version_minor,
212            flags,
213            source_checksum,
214            created_timestamp,
215            section_count,
216            section_table_offset,
217            reserved,
218            total_size,
219        })
220    }
221
222    /// Read header from a byte slice
223    pub fn from_bytes(data: &[u8]) -> FafbResult<Self> {
224        if data.len() < HEADER_SIZE {
225            return Err(FafbError::FileTooSmall {
226                expected: HEADER_SIZE,
227                actual: data.len(),
228            });
229        }
230
231        let mut cursor = Cursor::new(data);
232        Self::read(&mut cursor)
233    }
234
235    /// Validate header against actual file data
236    pub fn validate(&self, file_data: &[u8]) -> FafbResult<()> {
237        // WHY: Validation order is cheapest checks first - size comparison is O(1)
238        // and catches truncated files before more expensive parsing
239
240        // Size check detects truncated downloads or corrupted files
241        if self.total_size as usize != file_data.len() {
242            return Err(FafbError::SizeMismatch {
243                header_size: self.total_size,
244                actual_size: file_data.len(),
245            });
246        }
247
248        // WHY: Offset check prevents out-of-bounds reads when seeking to section table
249        if self.section_table_offset > self.total_size {
250            return Err(FafbError::InvalidSectionTableOffset {
251                offset: self.section_table_offset,
252                file_size: self.total_size,
253            });
254        }
255
256        Ok(())
257    }
258
259    /// Check if this header is compatible with the current version
260    pub fn is_compatible(&self) -> bool {
261        self.version_major == VERSION_MAJOR
262    }
263
264    /// Get version as string (e.g., "1.0")
265    pub fn version_string(&self) -> String {
266        format!("{}.{}", self.version_major, self.version_minor)
267    }
268}
269
270impl Default for FafbHeader {
271    fn default() -> Self {
272        Self::new()
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279
280    #[test]
281    fn test_header_size() {
282        let header = FafbHeader::new();
283        let bytes = header.to_bytes().unwrap();
284        assert_eq!(bytes.len(), HEADER_SIZE);
285        assert_eq!(bytes.len(), 32);
286    }
287
288    #[test]
289    fn test_magic_bytes() {
290        let header = FafbHeader::new();
291        let bytes = header.to_bytes().unwrap();
292        assert_eq!(&bytes[0..4], b"FAFB");
293    }
294
295    #[test]
296    fn test_roundtrip() {
297        let mut original = FafbHeader::with_timestamp();
298        original.source_checksum = 0xDEADBEEF;
299        original.section_count = 5;
300        original.section_table_offset = 1024;
301        original.total_size = 2048;
302        original.flags.set_compressed(true);
303        original.flags.set_embeddings(true);
304
305        let bytes = original.to_bytes().unwrap();
306        let recovered = FafbHeader::from_bytes(&bytes).unwrap();
307
308        assert_eq!(original.version_major, recovered.version_major);
309        assert_eq!(original.version_minor, recovered.version_minor);
310        assert_eq!(original.flags, recovered.flags);
311        assert_eq!(original.source_checksum, recovered.source_checksum);
312        assert_eq!(original.created_timestamp, recovered.created_timestamp);
313        assert_eq!(original.section_count, recovered.section_count);
314        assert_eq!(
315            original.section_table_offset,
316            recovered.section_table_offset
317        );
318        assert_eq!(original.total_size, recovered.total_size);
319    }
320
321    #[test]
322    fn test_invalid_magic() {
323        let mut bytes = FafbHeader::new().to_bytes().unwrap();
324        bytes[0] = 0x00; // Corrupt magic
325
326        let result = FafbHeader::from_bytes(&bytes);
327        assert!(matches!(result, Err(FafbError::InvalidMagic(_))));
328    }
329
330    #[test]
331    fn test_incompatible_version() {
332        let mut bytes = FafbHeader::new().to_bytes().unwrap();
333        bytes[4] = 99; // Set major version to 99
334
335        let result = FafbHeader::from_bytes(&bytes);
336        assert!(matches!(
337            result,
338            Err(FafbError::IncompatibleVersion {
339                expected: 1,
340                actual: 99
341            })
342        ));
343    }
344
345    #[test]
346    fn test_file_too_small() {
347        let bytes = vec![0u8; 16]; // Only 16 bytes, need 32
348
349        let result = FafbHeader::from_bytes(&bytes);
350        assert!(matches!(
351            result,
352            Err(FafbError::FileTooSmall {
353                expected: 32,
354                actual: 16
355            })
356        ));
357    }
358
359    #[test]
360    fn test_too_many_sections() {
361        let mut header = FafbHeader::new();
362        header.section_count = 300; // Over MAX_SECTIONS (256)
363
364        let bytes = header.to_bytes().unwrap();
365        let result = FafbHeader::from_bytes(&bytes);
366
367        assert!(matches!(
368            result,
369            Err(FafbError::TooManySections {
370                count: 300,
371                max: 256
372            })
373        ));
374    }
375
376    #[test]
377    fn test_checksum_computation() {
378        let yaml = b"faf_version: 2.5.0\nproject:\n  name: test";
379        let checksum = FafbHeader::compute_checksum(yaml);
380
381        // CRC32 is deterministic
382        assert_eq!(checksum, FafbHeader::compute_checksum(yaml));
383
384        // Different content = different checksum
385        let yaml2 = b"faf_version: 2.5.0\nproject:\n  name: different";
386        assert_ne!(checksum, FafbHeader::compute_checksum(yaml2));
387    }
388
389    #[test]
390    fn test_validate_size_mismatch() {
391        let mut header = FafbHeader::new();
392        header.total_size = 100;
393
394        let data = vec![0u8; 50]; // Actual size doesn't match header
395
396        let result = header.validate(&data);
397        assert!(matches!(
398            result,
399            Err(FafbError::SizeMismatch {
400                header_size: 100,
401                actual_size: 50
402            })
403        ));
404    }
405
406    #[test]
407    fn test_validate_invalid_section_offset() {
408        let mut header = FafbHeader::new();
409        header.total_size = 100;
410        header.section_table_offset = 200; // Beyond file size
411
412        let data = vec![0u8; 100];
413
414        let result = header.validate(&data);
415        assert!(matches!(
416            result,
417            Err(FafbError::InvalidSectionTableOffset {
418                offset: 200,
419                file_size: 100
420            })
421        ));
422    }
423
424    #[test]
425    fn test_version_string() {
426        let header = FafbHeader::new();
427        assert_eq!(header.version_string(), "1.0");
428    }
429
430    #[test]
431    fn test_flags_preserved() {
432        let mut header = FafbHeader::new();
433        header.flags.set_compressed(true);
434        header.flags.set_signed(true);
435
436        let bytes = header.to_bytes().unwrap();
437        let recovered = FafbHeader::from_bytes(&bytes).unwrap();
438
439        assert!(recovered.flags.is_compressed());
440        assert!(recovered.flags.is_signed());
441        assert!(!recovered.flags.has_embeddings());
442    }
443
444    #[test]
445    fn test_unknown_flags_ignored() {
446        let mut header = FafbHeader::new();
447        // Set some "future" flags in reserved bits
448        header.flags = Flags::from_raw(0xFF00);
449
450        let bytes = header.to_bytes().unwrap();
451        // Should read successfully despite unknown flags
452        let recovered = FafbHeader::from_bytes(&bytes).unwrap();
453        assert_eq!(recovered.flags.raw(), 0xFF00);
454    }
455}