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     string_table_index
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, FLAG_STRING_TABLE};
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/// Format major version
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    /// Index of string table entry in section table
73    pub string_table_index: 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 STRING_TABLE flag set
82    pub fn new() -> Self {
83        Self {
84            version_major: VERSION_MAJOR,
85            version_minor: VERSION_MINOR,
86            flags: Flags::from_raw(FLAG_STRING_TABLE),
87            source_checksum: 0,
88            created_timestamp: 0,
89            section_count: 0,
90            section_table_offset: HEADER_SIZE as u32,
91            string_table_index: 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        crc32fast::hash(yaml_source)
109    }
110
111    /// Set the source checksum from YAML content
112    pub fn set_source_checksum(&mut self, yaml_source: &[u8]) {
113        self.source_checksum = Self::compute_checksum(yaml_source);
114    }
115
116    /// Write header to a byte buffer
117    pub fn write<W: Write>(&self, writer: &mut W) -> FafbResult<()> {
118        writer.write_all(&MAGIC)?;
119        writer.write_u8(self.version_major)?;
120        writer.write_u8(self.version_minor)?;
121        writer.write_u16::<LittleEndian>(self.flags.raw())?;
122        writer.write_u32::<LittleEndian>(self.source_checksum)?;
123        writer.write_u64::<LittleEndian>(self.created_timestamp)?;
124        writer.write_u16::<LittleEndian>(self.section_count)?;
125        writer.write_u32::<LittleEndian>(self.section_table_offset)?;
126        writer.write_u16::<LittleEndian>(self.string_table_index)?;
127        writer.write_u32::<LittleEndian>(self.total_size)?;
128        Ok(())
129    }
130
131    /// Write header to a new `Vec<u8>`
132    pub fn to_bytes(&self) -> FafbResult<Vec<u8>> {
133        let mut buf = Vec::with_capacity(HEADER_SIZE);
134        self.write(&mut buf)?;
135        Ok(buf)
136    }
137
138    /// Read header from a byte buffer
139    pub fn read<R: Read>(reader: &mut R) -> FafbResult<Self> {
140        let mut magic = [0u8; 4];
141        reader.read_exact(&mut magic)?;
142
143        let magic_u32 = u32::from_le_bytes(magic);
144        if magic_u32 != MAGIC_U32 {
145            return Err(FafbError::InvalidMagic(magic_u32));
146        }
147
148        let version_major = reader.read_u8()?;
149        let version_minor = reader.read_u8()?;
150
151        if version_major != VERSION_MAJOR {
152            return Err(FafbError::IncompatibleVersion {
153                expected: VERSION_MAJOR,
154                actual: version_major,
155            });
156        }
157
158        let flags = Flags::from_raw(reader.read_u16::<LittleEndian>()?);
159        let source_checksum = reader.read_u32::<LittleEndian>()?;
160        let created_timestamp = reader.read_u64::<LittleEndian>()?;
161
162        let section_count = reader.read_u16::<LittleEndian>()?;
163        if section_count > MAX_SECTIONS {
164            return Err(FafbError::TooManySections {
165                count: section_count,
166                max: MAX_SECTIONS,
167            });
168        }
169
170        let section_table_offset = reader.read_u32::<LittleEndian>()?;
171        let string_table_index = reader.read_u16::<LittleEndian>()?;
172
173        let total_size = reader.read_u32::<LittleEndian>()?;
174        if total_size > MAX_FILE_SIZE {
175            return Err(FafbError::SizeMismatch {
176                header_size: total_size,
177                actual_size: MAX_FILE_SIZE as usize,
178            });
179        }
180
181        Ok(Self {
182            version_major,
183            version_minor,
184            flags,
185            source_checksum,
186            created_timestamp,
187            section_count,
188            section_table_offset,
189            string_table_index,
190            total_size,
191        })
192    }
193
194    /// Read header from a byte slice
195    pub fn from_bytes(data: &[u8]) -> FafbResult<Self> {
196        if data.len() < HEADER_SIZE {
197            return Err(FafbError::FileTooSmall {
198                expected: HEADER_SIZE,
199                actual: data.len(),
200            });
201        }
202
203        let mut cursor = Cursor::new(data);
204        Self::read(&mut cursor)
205    }
206
207    /// Validate header against actual file data
208    pub fn validate(&self, file_data: &[u8]) -> FafbResult<()> {
209        if self.total_size as usize != file_data.len() {
210            return Err(FafbError::SizeMismatch {
211                header_size: self.total_size,
212                actual_size: file_data.len(),
213            });
214        }
215
216        if self.section_table_offset > self.total_size {
217            return Err(FafbError::InvalidSectionTableOffset {
218                offset: self.section_table_offset,
219                file_size: self.total_size,
220            });
221        }
222
223        Ok(())
224    }
225
226    /// Check if this header is compatible with the current version
227    pub fn is_compatible(&self) -> bool {
228        self.version_major == VERSION_MAJOR
229    }
230
231    /// Get version as string (e.g., "1.0")
232    pub fn version_string(&self) -> String {
233        format!("{}.{}", self.version_major, self.version_minor)
234    }
235}
236
237impl Default for FafbHeader {
238    fn default() -> Self {
239        Self::new()
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    #[test]
248    fn test_header_size() {
249        let header = FafbHeader::new();
250        let bytes = header.to_bytes().unwrap();
251        assert_eq!(bytes.len(), HEADER_SIZE);
252        assert_eq!(bytes.len(), 32);
253    }
254
255    #[test]
256    fn test_magic_bytes() {
257        let header = FafbHeader::new();
258        let bytes = header.to_bytes().unwrap();
259        assert_eq!(&bytes[0..4], b"FAFB");
260    }
261
262    #[test]
263    fn test_roundtrip() {
264        let mut original = FafbHeader::with_timestamp();
265        original.source_checksum = 0xDEADBEEF;
266        original.section_count = 5;
267        original.section_table_offset = 1024;
268        original.total_size = 2048;
269        original.string_table_index = 4;
270        original.flags.set_compressed(true);
271        original.flags.set_embeddings(true);
272
273        let bytes = original.to_bytes().unwrap();
274        let recovered = FafbHeader::from_bytes(&bytes).unwrap();
275
276        assert_eq!(original.version_major, recovered.version_major);
277        assert_eq!(original.version_minor, recovered.version_minor);
278        assert_eq!(original.flags, recovered.flags);
279        assert_eq!(original.source_checksum, recovered.source_checksum);
280        assert_eq!(original.created_timestamp, recovered.created_timestamp);
281        assert_eq!(original.section_count, recovered.section_count);
282        assert_eq!(
283            original.section_table_offset,
284            recovered.section_table_offset
285        );
286        assert_eq!(original.string_table_index, recovered.string_table_index);
287        assert_eq!(original.total_size, recovered.total_size);
288    }
289
290    #[test]
291    fn test_invalid_magic() {
292        let mut bytes = FafbHeader::new().to_bytes().unwrap();
293        bytes[0] = 0x00;
294
295        let result = FafbHeader::from_bytes(&bytes);
296        assert!(matches!(result, Err(FafbError::InvalidMagic(_))));
297    }
298
299    #[test]
300    fn test_incompatible_version() {
301        let mut bytes = FafbHeader::new().to_bytes().unwrap();
302        bytes[4] = 99;
303
304        let result = FafbHeader::from_bytes(&bytes);
305        assert!(matches!(
306            result,
307            Err(FafbError::IncompatibleVersion {
308                expected: 1,
309                actual: 99
310            })
311        ));
312    }
313
314    #[test]
315    fn test_file_too_small() {
316        let bytes = vec![0u8; 16];
317
318        let result = FafbHeader::from_bytes(&bytes);
319        assert!(matches!(
320            result,
321            Err(FafbError::FileTooSmall {
322                expected: 32,
323                actual: 16
324            })
325        ));
326    }
327
328    #[test]
329    fn test_too_many_sections() {
330        let mut header = FafbHeader::new();
331        header.section_count = 300;
332
333        let bytes = header.to_bytes().unwrap();
334        let result = FafbHeader::from_bytes(&bytes);
335
336        assert!(matches!(
337            result,
338            Err(FafbError::TooManySections {
339                count: 300,
340                max: 256
341            })
342        ));
343    }
344
345    #[test]
346    fn test_checksum_computation() {
347        let yaml = b"faf_version: 2.5.0\nproject:\n  name: test";
348        let checksum = FafbHeader::compute_checksum(yaml);
349        assert_eq!(checksum, FafbHeader::compute_checksum(yaml));
350
351        let yaml2 = b"faf_version: 2.5.0\nproject:\n  name: different";
352        assert_ne!(checksum, FafbHeader::compute_checksum(yaml2));
353    }
354
355    #[test]
356    fn test_validate_size_mismatch() {
357        let mut header = FafbHeader::new();
358        header.total_size = 100;
359
360        let data = vec![0u8; 50];
361
362        let result = header.validate(&data);
363        assert!(matches!(
364            result,
365            Err(FafbError::SizeMismatch {
366                header_size: 100,
367                actual_size: 50
368            })
369        ));
370    }
371
372    #[test]
373    fn test_validate_invalid_section_offset() {
374        let mut header = FafbHeader::new();
375        header.total_size = 100;
376        header.section_table_offset = 200;
377
378        let data = vec![0u8; 100];
379
380        let result = header.validate(&data);
381        assert!(matches!(
382            result,
383            Err(FafbError::InvalidSectionTableOffset {
384                offset: 200,
385                file_size: 100
386            })
387        ));
388    }
389
390    #[test]
391    fn test_version_string() {
392        let header = FafbHeader::new();
393        assert_eq!(header.version_string(), "1.0");
394    }
395
396    #[test]
397    fn test_flags_preserved() {
398        let mut header = FafbHeader::new();
399        header.flags.set_compressed(true);
400        header.flags.set_signed(true);
401
402        let bytes = header.to_bytes().unwrap();
403        let recovered = FafbHeader::from_bytes(&bytes).unwrap();
404
405        assert!(recovered.flags.is_compressed());
406        assert!(recovered.flags.is_signed());
407        assert!(!recovered.flags.has_embeddings());
408        assert!(recovered.flags.has_string_table());
409    }
410
411    #[test]
412    fn test_unknown_flags_ignored() {
413        let mut header = FafbHeader::new();
414        header.flags = Flags::from_raw(0xFF00 | FLAG_STRING_TABLE);
415
416        let bytes = header.to_bytes().unwrap();
417        let recovered = FafbHeader::from_bytes(&bytes).unwrap();
418        assert_eq!(recovered.flags.raw(), 0xFF00 | FLAG_STRING_TABLE);
419    }
420
421    #[test]
422    fn test_string_table_flag_always_set() {
423        let header = FafbHeader::new();
424        assert!(header.flags.has_string_table());
425    }
426
427    #[test]
428    fn test_string_table_index_roundtrip() {
429        let mut header = FafbHeader::new();
430        header.string_table_index = 7;
431        header.total_size = 1000;
432        header.section_count = 8;
433
434        let bytes = header.to_bytes().unwrap();
435        let recovered = FafbHeader::from_bytes(&bytes).unwrap();
436        assert_eq!(recovered.string_table_index, 7);
437    }
438}