1use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
24use std::io::{Cursor, Read, Write};
25
26use super::error::{FafbError, FafbResult};
27use super::flags::{FLAG_STRING_TABLE, Flags};
28
29pub const MAGIC: [u8; 4] = *b"FAFB";
31
32pub const MAGIC_U32: u32 = 0x4246_4146; pub const VERSION_MAJOR: u8 = 2;
37
38pub const VERSION_MINOR: u8 = 0;
40
41pub const HEADER_SIZE: usize = 32;
43
44pub const MAX_SECTIONS: u16 = 256;
46
47pub const MAX_FILE_SIZE: u32 = 10 * 1024 * 1024;
49
50#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct FafbHeader {
53 pub version_major: u8,
56 pub version_minor: u8,
58 pub flags: Flags,
60
61 pub source_checksum: u32,
64 pub created_timestamp: u64,
66
67 pub section_count: u16,
70 pub section_table_offset: u32,
72 pub string_table_index: u16,
74
75 pub total_size: u32,
78}
79
80impl FafbHeader {
81 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 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 pub fn compute_checksum(yaml_source: &[u8]) -> u32 {
108 crc32fast::hash(yaml_source)
109 }
110
111 pub fn set_source_checksum(&mut self, yaml_source: &[u8]) {
113 self.source_checksum = Self::compute_checksum(yaml_source);
114 }
115
116 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 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 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 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 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 pub fn is_compatible(&self) -> bool {
228 self.version_major == VERSION_MAJOR
229 }
230
231 pub fn version_string(&self) -> String {
233 format!("{}.{}", self.version_major, self.version_minor)
234 }
235}
236
237pub 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}