faf_rust_sdk/binary/
header.rs1use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
24use std::io::{Cursor, Read, Write};
25
26use super::error::{FafbError, FafbResult};
27use super::flags::Flags;
28
29pub const MAGIC: [u8; 4] = *b"FAFB";
31
32pub const MAGIC_U32: u32 = 0x4246_4146; pub const VERSION_MAJOR: u8 = 1;
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 reserved: 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::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 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 pub fn compute_checksum(yaml_source: &[u8]) -> u32 {
108 crc32fast::hash(yaml_source)
111 }
112
113 pub fn set_source_checksum(&mut self, yaml_source: &[u8]) {
115 self.source_checksum = Self::compute_checksum(yaml_source);
116 }
117
118 pub fn write<W: Write>(&self, writer: &mut W) -> FafbResult<()> {
120 writer.write_all(&MAGIC)?;
125
126 writer.write_u8(self.version_major)?;
128 writer.write_u8(self.version_minor)?;
129
130 writer.write_u16::<LittleEndian>(self.flags.raw())?;
132
133 writer.write_u32::<LittleEndian>(self.source_checksum)?;
135 writer.write_u64::<LittleEndian>(self.created_timestamp)?;
136
137 writer.write_u16::<LittleEndian>(self.section_count)?;
139 writer.write_u32::<LittleEndian>(self.section_table_offset)?;
140 writer.write_u16::<LittleEndian>(self.reserved)?;
142
143 writer.write_u32::<LittleEndian>(self.total_size)?;
145
146 Ok(())
147 }
148
149 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 pub fn read<R: Read>(reader: &mut R) -> FafbResult<Self> {
158 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 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 let flags = Flags::from_raw(reader.read_u16::<LittleEndian>()?);
183
184 let source_checksum = reader.read_u32::<LittleEndian>()?;
186 let created_timestamp = reader.read_u64::<LittleEndian>()?;
187
188 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 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 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 pub fn validate(&self, file_data: &[u8]) -> FafbResult<()> {
237 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 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 pub fn is_compatible(&self) -> bool {
261 self.version_major == VERSION_MAJOR
262 }
263
264 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; 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; 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]; 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; 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 assert_eq!(checksum, FafbHeader::compute_checksum(yaml));
383
384 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]; 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; 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 header.flags = Flags::from_raw(0xFF00);
449
450 let bytes = header.to_bytes().unwrap();
451 let recovered = FafbHeader::from_bytes(&bytes).unwrap();
453 assert_eq!(recovered.flags.raw(), 0xFF00);
454 }
455}