Skip to main content

ahu/
header.rs

1use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
2use std::io::{Read, Write};
3
4use crate::error::{AhuError, Result};
5
6pub const MAGIC: [u8; 4] = [0x41, 0x48, 0x55, 0x31]; // "AHU1"
7pub const HEADER_SIZE: usize = 64;
8pub const FORMAT_MAJOR: u16 = 0;
9pub const FORMAT_MINOR: u16 = 1;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct FileHeader {
13    pub format_major: u16,
14    pub format_minor: u16,
15    pub manifest_offset: u64,
16    pub manifest_length: u32,
17    pub seal_offset: u64,
18    pub seal_length: u32,
19    pub index_offset: u64,
20    pub index_length: u64,
21    pub data_offset: u64,
22    pub data_length: u64,
23}
24
25impl FileHeader {
26    pub fn read_from<R: Read>(reader: &mut R) -> Result<Self> {
27        let mut magic = [0u8; 4];
28        reader.read_exact(&mut magic)?;
29        if magic != MAGIC {
30            return Err(AhuError::BadMagic { found: magic });
31        }
32
33        let format_major = reader.read_u16::<BigEndian>()?;
34        if format_major != FORMAT_MAJOR {
35            let format_minor = reader.read_u16::<BigEndian>()?;
36            return Err(AhuError::UnsupportedVersion {
37                major: format_major,
38                minor: format_minor,
39            });
40        }
41        let format_minor = reader.read_u16::<BigEndian>()?;
42
43        Ok(FileHeader {
44            format_major,
45            format_minor,
46            manifest_offset: reader.read_u64::<BigEndian>()?,
47            manifest_length: reader.read_u32::<BigEndian>()?,
48            seal_offset: reader.read_u64::<BigEndian>()?,
49            seal_length: reader.read_u32::<BigEndian>()?,
50            index_offset: reader.read_u64::<BigEndian>()?,
51            index_length: reader.read_u64::<BigEndian>()?,
52            data_offset: reader.read_u64::<BigEndian>()?,
53            data_length: reader.read_u64::<BigEndian>()?,
54        })
55    }
56
57    pub fn write_to<W: Write>(&self, writer: &mut W) -> Result<()> {
58        writer.write_all(&MAGIC)?;
59        writer.write_u16::<BigEndian>(self.format_major)?;
60        writer.write_u16::<BigEndian>(self.format_minor)?;
61        writer.write_u64::<BigEndian>(self.manifest_offset)?;
62        writer.write_u32::<BigEndian>(self.manifest_length)?;
63        writer.write_u64::<BigEndian>(self.seal_offset)?;
64        writer.write_u32::<BigEndian>(self.seal_length)?;
65        writer.write_u64::<BigEndian>(self.index_offset)?;
66        writer.write_u64::<BigEndian>(self.index_length)?;
67        writer.write_u64::<BigEndian>(self.data_offset)?;
68        writer.write_u64::<BigEndian>(self.data_length)?;
69        Ok(())
70    }
71
72    pub fn validate_bounds(&self, file_size: u64) -> Result<()> {
73        let check = |field: &'static str, offset: u64, length: u64| -> Result<()> {
74            if offset.checked_add(length).is_none_or(|end| end > file_size) {
75                return Err(AhuError::HeaderOutOfBounds {
76                    field,
77                    offset,
78                    length,
79                    file_size,
80                });
81            }
82            Ok(())
83        };
84
85        check(
86            "manifest",
87            self.manifest_offset,
88            self.manifest_length as u64,
89        )?;
90        check("seal", self.seal_offset, self.seal_length as u64)?;
91        check("index", self.index_offset, self.index_length)?;
92        check("data", self.data_offset, self.data_length)?;
93        Ok(())
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use std::io::Cursor;
101
102    #[test]
103    fn round_trip() {
104        let header = FileHeader {
105            format_major: FORMAT_MAJOR,
106            format_minor: FORMAT_MINOR,
107            manifest_offset: 64,
108            manifest_length: 256,
109            seal_offset: 320,
110            seal_length: 512,
111            index_offset: 832,
112            index_length: 4800,
113            data_offset: 5632,
114            data_length: 102400,
115        };
116
117        let mut buf = Vec::new();
118        header.write_to(&mut buf).unwrap();
119        assert_eq!(buf.len(), HEADER_SIZE);
120
121        let mut cursor = Cursor::new(&buf);
122        let read_back = FileHeader::read_from(&mut cursor).unwrap();
123        assert_eq!(header, read_back);
124    }
125
126    #[test]
127    fn rejects_bad_magic() {
128        let buf = [0x00u8; HEADER_SIZE];
129        let mut cursor = Cursor::new(&buf);
130        let err = FileHeader::read_from(&mut cursor).unwrap_err();
131        assert!(matches!(err, AhuError::BadMagic { .. }));
132    }
133}