Skip to main content

vole_document/container/
header.rs

1//! The fixed `.voldoc` header.
2//!
3//! A descriptor begins with exactly [`HEADER_LEN`] bytes. The header is
4//! self-checked with CRC32C so that framing corruption is detected before any
5//! record parsing. Unknown *mandatory* feature bits fail closed; unknown
6//! optional bits are recorded and ignored.
7
8use crate::error::{Error, Result};
9use crate::integrity::crc32c;
10use crate::{EXACTNESS_PROFILE_EXACT_BYTES, SOURCE_FORMAT_OPAQUE};
11
12/// Size of the fixed header in bytes.
13pub const HEADER_LEN: usize = 64;
14
15/// Container magic: `VOLDOC` followed by a 0x1A text-EOF marker and NUL.
16pub const MAGIC: [u8; 8] = *b"VOLDOC\x1A\x00";
17
18/// Major format version implemented by this build (provisional pre-1.0).
19pub const FORMAT_MAJOR: u16 = 0;
20/// Minor format version implemented by this build.
21pub const FORMAT_MINOR: u16 = 1;
22
23/// Feature bits this build understands and supports (none yet beyond exact).
24pub const SUPPORTED_MANDATORY_FEATURES: u32 = 0;
25
26/// The parsed, validated fixed header.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct Header {
29    /// Major version.
30    pub major: u16,
31    /// Minor version.
32    pub minor: u16,
33    /// Feature bits a decoder *must* understand.
34    pub mandatory_features: u32,
35    /// Feature bits a decoder *may* ignore.
36    pub optional_features: u32,
37    /// Exactness profile selector (only `EXACT_BYTES` is normative).
38    pub exactness_profile: u8,
39    /// Source-format class selector.
40    pub source_format: u8,
41    /// First 16 bytes of SHA-256 over the universe declaration string.
42    pub universe_id: [u8; 16],
43    /// Declared length of the fully reconstructed source.
44    pub declared_source_len: u64,
45}
46
47impl Header {
48    /// Build a header for the current format version.
49    pub fn new(
50        universe_id: [u8; 16],
51        declared_source_len: u64,
52        exactness_profile: u8,
53        source_format: u8,
54    ) -> Self {
55        Header {
56            major: FORMAT_MAJOR,
57            minor: FORMAT_MINOR,
58            mandatory_features: 0,
59            optional_features: 0,
60            exactness_profile,
61            source_format,
62            universe_id,
63            declared_source_len,
64        }
65    }
66
67    /// Encode the header to exactly [`HEADER_LEN`] bytes, computing the CRC.
68    pub fn encode(&self) -> [u8; HEADER_LEN] {
69        let mut b = [0u8; HEADER_LEN];
70        b[0..8].copy_from_slice(&MAGIC);
71        b[8..10].copy_from_slice(&self.major.to_le_bytes());
72        b[10..12].copy_from_slice(&self.minor.to_le_bytes());
73        b[12..16].copy_from_slice(&self.mandatory_features.to_le_bytes());
74        b[16..20].copy_from_slice(&self.optional_features.to_le_bytes());
75        b[20] = self.exactness_profile;
76        b[21] = self.source_format;
77        // b[22..24] reserved_a = 0
78        b[24..40].copy_from_slice(&self.universe_id);
79        b[40..48].copy_from_slice(&self.declared_source_len.to_le_bytes());
80        // b[48..60] reserved_b = 0
81        let crc = crc32c(&b[0..60]);
82        b[60..64].copy_from_slice(&crc.to_le_bytes());
83        b
84    }
85
86    /// Decode and validate the header from the start of `bytes`.
87    pub fn decode(bytes: &[u8]) -> Result<Header> {
88        if bytes.len() < HEADER_LEN {
89            return Err(Error::invalid_container(format!(
90                "input is {} bytes; need at least {HEADER_LEN} for a header",
91                bytes.len()
92            )));
93        }
94        let b = &bytes[0..HEADER_LEN];
95        if b[0..8] != MAGIC {
96            return Err(Error::invalid_container("bad container magic"));
97        }
98        let want_crc = u32::from_le_bytes([b[60], b[61], b[62], b[63]]);
99        let got_crc = crc32c(&b[0..60]);
100        if want_crc != got_crc {
101            return Err(Error::invalid_container(format!(
102                "header CRC32C mismatch: declared {want_crc:#010x}, computed {got_crc:#010x}"
103            )));
104        }
105        if b[22] != 0 || b[23] != 0 {
106            return Err(Error::invalid_container("header reserved_a must be zero"));
107        }
108        if b[48..60].iter().any(|&x| x != 0) {
109            return Err(Error::invalid_container("header reserved_b must be zero"));
110        }
111
112        let major = u16::from_le_bytes([b[8], b[9]]);
113        let minor = u16::from_le_bytes([b[10], b[11]]);
114        if major != FORMAT_MAJOR || minor > FORMAT_MINOR {
115            return Err(Error::unsupported_version(format!(
116                "container version {major}.{minor} is not supported by this build ({FORMAT_MAJOR}.{FORMAT_MINOR})"
117            )));
118        }
119
120        let mandatory_features = u32::from_le_bytes([b[12], b[13], b[14], b[15]]);
121        let unsupported = mandatory_features & !SUPPORTED_MANDATORY_FEATURES;
122        if unsupported != 0 {
123            return Err(Error::unsupported_feature(format!(
124                "descriptor requires unsupported mandatory feature bits {unsupported:#010x}"
125            )));
126        }
127
128        let exactness_profile = b[20];
129        if exactness_profile != EXACTNESS_PROFILE_EXACT_BYTES {
130            return Err(Error::unsupported_feature(format!(
131                "exactness profile {exactness_profile} is not the normative EXACT_BYTES profile"
132            )));
133        }
134        let source_format = b[21];
135
136        let mut universe_id = [0u8; 16];
137        universe_id.copy_from_slice(&b[24..40]);
138        let declared_source_len =
139            u64::from_le_bytes([b[40], b[41], b[42], b[43], b[44], b[45], b[46], b[47]]);
140
141        Ok(Header {
142            major,
143            minor,
144            mandatory_features,
145            optional_features: u32::from_le_bytes([b[16], b[17], b[18], b[19]]),
146            exactness_profile,
147            source_format,
148            universe_id,
149            declared_source_len,
150        })
151    }
152
153    /// True if this header describes the opaque source-format class.
154    pub fn is_opaque(&self) -> bool {
155        self.source_format == SOURCE_FORMAT_OPAQUE
156    }
157
158    /// True if this build has an adapter for the declared source-format class.
159    ///
160    /// Unknown classes fail closed: a descriptor naming a class this build does
161    /// not implement is refused rather than reinterpreted as opaque.
162    pub fn source_format_supported(&self) -> bool {
163        matches!(self.source_format, 0 | 1)
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn roundtrip() {
173        let h = Header::new(
174            [7u8; 16],
175            1234,
176            EXACTNESS_PROFILE_EXACT_BYTES,
177            SOURCE_FORMAT_OPAQUE,
178        );
179        let enc = h.encode();
180        assert_eq!(enc.len(), HEADER_LEN);
181        let dec = Header::decode(&enc).unwrap();
182        assert_eq!(h, dec);
183    }
184
185    #[test]
186    fn rejects_bad_magic() {
187        let mut enc = Header::new([0u8; 16], 0, 0, 0).encode();
188        enc[0] = b'X';
189        match Header::decode(&enc) {
190            Err(e) => assert_eq!(e.class(), crate::ErrorClass::InvalidContainer),
191            Ok(_) => panic!("expected failure"),
192        }
193    }
194
195    #[test]
196    fn detects_corruption_via_crc() {
197        let mut enc = Header::new([0u8; 16], 5, 0, 0).encode();
198        enc[41] ^= 0x01; // flip a byte inside declared_source_len
199        let e = Header::decode(&enc).unwrap_err();
200        assert_eq!(e.class(), crate::ErrorClass::InvalidContainer);
201    }
202
203    #[test]
204    fn rejects_truncated() {
205        let e = Header::decode(&[0u8; 10]).unwrap_err();
206        assert_eq!(e.class(), crate::ErrorClass::InvalidContainer);
207    }
208
209    #[test]
210    fn rejects_unknown_mandatory_feature() {
211        let mut h = Header::new([0u8; 16], 0, 0, 0);
212        h.mandatory_features = 0x8000_0000;
213        let e = Header::decode(&h.encode()).unwrap_err();
214        assert_eq!(e.class(), crate::ErrorClass::UnsupportedFeature);
215    }
216
217    #[test]
218    fn rejects_future_major_version() {
219        let mut h = Header::new([0u8; 16], 0, 0, 0);
220        h.major = 9;
221        let e = Header::decode(&h.encode()).unwrap_err();
222        assert_eq!(e.class(), crate::ErrorClass::UnsupportedVersion);
223    }
224}