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, FLAG_STRING_TABLE};
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 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 {
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)
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
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}