Skip to main content

faf_fafb/
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::{FLAG_STRING_TABLE, 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/// Format major version
36pub const VERSION_MAJOR: u8 = 2;
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 a timestamp.
97    ///
98    /// When `SOURCE_DATE_EPOCH` is set to a decimal Unix-seconds value, that
99    /// value is used. Otherwise wall-clock time.
100    pub fn with_timestamp() -> Self {
101        let mut header = Self::new();
102        header.created_timestamp = source_date_epoch().unwrap_or_else(wall_clock_secs);
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
237/// Honour `SOURCE_DATE_EPOCH` when it is a valid `u64`. Spec MUST.
238pub fn source_date_epoch() -> Option<u64> {
239    std::env::var("SOURCE_DATE_EPOCH")
240        .ok()
241        .and_then(|v| v.parse::<u64>().ok())
242}
243
244fn wall_clock_secs() -> u64 {
245    std::time::SystemTime::now()
246        .duration_since(std::time::UNIX_EPOCH)
247        .map(|d| d.as_secs())
248        .unwrap_or(0)
249}
250
251impl Default for FafbHeader {
252    fn default() -> Self {
253        Self::new()
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[test]
262    fn test_header_size() {
263        let header = FafbHeader::new();
264        let bytes = header.to_bytes().unwrap();
265        assert_eq!(bytes.len(), HEADER_SIZE);
266        assert_eq!(bytes.len(), 32);
267    }
268
269    #[test]
270    fn test_magic_bytes() {
271        let header = FafbHeader::new();
272        let bytes = header.to_bytes().unwrap();
273        assert_eq!(&bytes[0..4], b"FAFB");
274    }
275
276    #[test]
277    fn test_roundtrip() {
278        let mut original = FafbHeader::with_timestamp();
279        original.source_checksum = 0xDEADBEEF;
280        original.section_count = 5;
281        original.section_table_offset = 1024;
282        original.total_size = 2048;
283        original.string_table_index = 4;
284        original.flags.set_compressed(true);
285        original.flags.set_embeddings(true);
286
287        let bytes = original.to_bytes().unwrap();
288        let recovered = FafbHeader::from_bytes(&bytes).unwrap();
289
290        assert_eq!(original.version_major, recovered.version_major);
291        assert_eq!(original.version_minor, recovered.version_minor);
292        assert_eq!(original.flags, recovered.flags);
293        assert_eq!(original.source_checksum, recovered.source_checksum);
294        assert_eq!(original.created_timestamp, recovered.created_timestamp);
295        assert_eq!(original.section_count, recovered.section_count);
296        assert_eq!(
297            original.section_table_offset,
298            recovered.section_table_offset
299        );
300        assert_eq!(original.string_table_index, recovered.string_table_index);
301        assert_eq!(original.total_size, recovered.total_size);
302    }
303
304    #[test]
305    fn test_invalid_magic() {
306        let mut bytes = FafbHeader::new().to_bytes().unwrap();
307        bytes[0] = 0x00;
308
309        let result = FafbHeader::from_bytes(&bytes);
310        assert!(matches!(result, Err(FafbError::InvalidMagic(_))));
311    }
312
313    #[test]
314    fn test_incompatible_version() {
315        let mut bytes = FafbHeader::new().to_bytes().unwrap();
316        bytes[4] = 99;
317
318        let result = FafbHeader::from_bytes(&bytes);
319        assert!(matches!(
320            result,
321            Err(FafbError::IncompatibleVersion {
322                expected: 2,
323                actual: 99
324            })
325        ));
326    }
327
328    #[test]
329    fn test_file_too_small() {
330        let bytes = vec![0u8; 16];
331
332        let result = FafbHeader::from_bytes(&bytes);
333        assert!(matches!(
334            result,
335            Err(FafbError::FileTooSmall {
336                expected: 32,
337                actual: 16
338            })
339        ));
340    }
341
342    #[test]
343    fn test_too_many_sections() {
344        let mut header = FafbHeader::new();
345        header.section_count = 300;
346
347        let bytes = header.to_bytes().unwrap();
348        let result = FafbHeader::from_bytes(&bytes);
349
350        assert!(matches!(
351            result,
352            Err(FafbError::TooManySections {
353                count: 300,
354                max: 256
355            })
356        ));
357    }
358
359    #[test]
360    fn test_checksum_computation() {
361        let yaml = b"faf_version: 2.5.0\nproject:\n  name: test";
362        let checksum = FafbHeader::compute_checksum(yaml);
363        assert_eq!(checksum, FafbHeader::compute_checksum(yaml));
364
365        let yaml2 = b"faf_version: 2.5.0\nproject:\n  name: different";
366        assert_ne!(checksum, FafbHeader::compute_checksum(yaml2));
367    }
368
369    #[test]
370    fn test_validate_size_mismatch() {
371        let mut header = FafbHeader::new();
372        header.total_size = 100;
373
374        let data = vec![0u8; 50];
375
376        let result = header.validate(&data);
377        assert!(matches!(
378            result,
379            Err(FafbError::SizeMismatch {
380                header_size: 100,
381                actual_size: 50
382            })
383        ));
384    }
385
386    #[test]
387    fn test_validate_invalid_section_offset() {
388        let mut header = FafbHeader::new();
389        header.total_size = 100;
390        header.section_table_offset = 200;
391
392        let data = vec![0u8; 100];
393
394        let result = header.validate(&data);
395        assert!(matches!(
396            result,
397            Err(FafbError::InvalidSectionTableOffset {
398                offset: 200,
399                file_size: 100
400            })
401        ));
402    }
403
404    #[test]
405    fn test_version_string() {
406        let header = FafbHeader::new();
407        assert_eq!(header.version_string(), "2.0");
408    }
409
410    #[test]
411    fn test_flags_preserved() {
412        let mut header = FafbHeader::new();
413        header.flags.set_compressed(true);
414        header.flags.set_signed(true);
415
416        let bytes = header.to_bytes().unwrap();
417        let recovered = FafbHeader::from_bytes(&bytes).unwrap();
418
419        assert!(recovered.flags.is_compressed());
420        assert!(recovered.flags.is_signed());
421        assert!(!recovered.flags.has_embeddings());
422        assert!(recovered.flags.has_string_table());
423    }
424
425    #[test]
426    fn test_unknown_flags_ignored() {
427        let mut header = FafbHeader::new();
428        header.flags = Flags::from_raw(0xFF00 | FLAG_STRING_TABLE);
429
430        let bytes = header.to_bytes().unwrap();
431        let recovered = FafbHeader::from_bytes(&bytes).unwrap();
432        assert_eq!(recovered.flags.raw(), 0xFF00 | FLAG_STRING_TABLE);
433    }
434
435    #[test]
436    fn test_string_table_flag_always_set() {
437        let header = FafbHeader::new();
438        assert!(header.flags.has_string_table());
439    }
440
441    #[test]
442    fn test_string_table_index_roundtrip() {
443        let mut header = FafbHeader::new();
444        header.string_table_index = 7;
445        header.total_size = 1000;
446        header.section_count = 8;
447
448        let bytes = header.to_bytes().unwrap();
449        let recovered = FafbHeader::from_bytes(&bytes).unwrap();
450        assert_eq!(recovered.string_table_index, 7);
451    }
452}