Skip to main content

crous_core/
header.rs

1//! File header for the Crous binary format.
2//!
3//! Layout (8 bytes total):
4//! ```text
5//! Offset  Size  Description
6//! 0       7     ASCII magic "CROUSv1"
7//! 7       1     Flags byte
8//! ```
9//!
10//! Flags byte layout:
11//! ```text
12//! Bit 0:   Reserved (must be 0)
13//! Bit 1:   Has index block (1 = yes)
14//! Bit 2:   Has schema block (1 = yes)
15//! Bit 3-7: Reserved (must be 0)
16//! ```
17
18use crate::error::{CrousError, Result};
19
20/// The 7-byte magic identifying a Crous file.
21pub const MAGIC: &[u8; 7] = b"CROUSv1";
22
23/// Header size in bytes.
24pub const HEADER_SIZE: usize = 8;
25
26/// No flags set.
27pub const FLAGS_NONE: u8 = 0x00;
28/// Flag: file contains an index block.
29pub const FLAGS_HAS_INDEX: u8 = 0x02;
30/// Flag: file contains a schema block.
31pub const FLAGS_HAS_SCHEMA: u8 = 0x04;
32
33/// Parsed file header.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct FileHeader {
36    /// Flags byte.
37    pub flags: u8,
38}
39
40impl FileHeader {
41    /// Create a new header with the given flags.
42    pub fn new(flags: u8) -> Self {
43        Self { flags }
44    }
45
46    /// Serialize the header to exactly 8 bytes.
47    pub fn encode(&self) -> [u8; HEADER_SIZE] {
48        let mut buf = [0u8; HEADER_SIZE];
49        buf[..7].copy_from_slice(MAGIC);
50        buf[7] = self.flags;
51        buf
52    }
53
54    /// Parse a header from exactly 8 bytes.
55    pub fn decode(data: &[u8]) -> Result<Self> {
56        if data.len() < HEADER_SIZE {
57            return Err(CrousError::UnexpectedEof(data.len()));
58        }
59        if &data[..7] != MAGIC {
60            return Err(CrousError::InvalidMagic);
61        }
62        Ok(Self { flags: data[7] })
63    }
64
65    /// Check if the file has an index block.
66    pub fn has_index(&self) -> bool {
67        self.flags & FLAGS_HAS_INDEX != 0
68    }
69
70    /// Check if the file has a schema block.
71    pub fn has_schema(&self) -> bool {
72        self.flags & FLAGS_HAS_SCHEMA != 0
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn header_roundtrip() {
82        let hdr = FileHeader::new(FLAGS_HAS_INDEX | FLAGS_HAS_SCHEMA);
83        let bytes = hdr.encode();
84        assert_eq!(&bytes[..7], b"CROUSv1");
85        assert_eq!(bytes[7], 0x06);
86        let decoded = FileHeader::decode(&bytes).unwrap();
87        assert_eq!(decoded, hdr);
88        assert!(decoded.has_index());
89        assert!(decoded.has_schema());
90    }
91
92    #[test]
93    fn header_invalid_magic() {
94        let bad = b"CROUSv2\x00";
95        assert!(matches!(
96            FileHeader::decode(bad),
97            Err(CrousError::InvalidMagic)
98        ));
99    }
100
101    #[test]
102    fn header_too_short() {
103        let short = b"CROUS";
104        assert!(FileHeader::decode(short).is_err());
105    }
106}