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/// Mandatory feature bit: the descriptor carries at least one `DEFLATE_REPLAY`
24/// op and so requires a decoder built with exact-DEFLATE-replay support.
25pub const FEATURE_DEFLATE_REPLAY: u32 = 1 << 0;
26
27/// Mandatory feature bit: the descriptor carries at least one `EXTERNAL_REF`
28/// object and so requires a decoder built with a content-addressed object store.
29///
30/// This is **mandatory, not optional**: an external reference is load-bearing,
31/// so a decoder that ignores it cannot reconstruct the source (unlike
32/// `OBSERVATION_INDEX`/`DIRECTORY`, which are advisory). A build without the
33/// `store` cargo feature therefore rejects a store-backed descriptor with
34/// [`crate::ErrorClass::UnsupportedFeature`] rather than reinterpreting it.
35pub const FEATURE_EXTERNAL_OBJECTS: u32 = 1 << 1;
36
37/// Optional feature bit: the descriptor carries an `OBSERVATION_INDEX` record
38/// and so advertises a partial-decode lane.
39///
40/// Optional bits are ignorable: a decoder built without partial-decode support
41/// still materializes the source exactly, because the reconstruction program
42/// alone is complete. Exactness never requires this bit.
43pub const FEATURE_OBSERVATION_INDEX: u32 = 1 << 0;
44
45/// Optional feature bit: the descriptor carries a seek `DIRECTORY` record and so
46/// advertises a seek-based partial-I/O lane.
47///
48/// Optional bits are ignorable: a decoder built without seek support still
49/// materializes the source exactly, because the `DIRECTORY` record is written
50/// with [`crate::container::record::FLAG_OPTIONAL`] and the reconstruction program
51/// alone is complete. Exactness never requires this bit.
52pub const FEATURE_SEEK_DIRECTORY: u32 = 1 << 1;
53
54/// Optional feature bit: the descriptor carries a `CHECKPOINT` record and so
55/// advertises a byte-level partial-materialization checkpoint lane (Phase 13.4).
56///
57/// Optional bits are ignorable: a decoder built without checkpoint support still
58/// materializes the source exactly, because the `CHECKPOINT` record is written
59/// with [`crate::container::record::FLAG_OPTIONAL`] and the reconstruction program
60/// alone is complete. Exactness never requires this bit.
61pub const FEATURE_CHECKPOINTS: u32 = 1 << 2;
62
63/// Feature bits this build understands and supports.
64///
65/// The `deflate-replay` cargo feature adds [`FEATURE_DEFLATE_REPLAY`]; the
66/// `store` cargo feature adds [`FEATURE_EXTERNAL_OBJECTS`]. A descriptor that
67/// declares an unsupported mandatory bit fails closed at header validation with
68/// [`crate::ErrorClass::UnsupportedFeature`] rather than being reinterpreted.
69#[cfg(all(feature = "deflate-replay", feature = "store"))]
70pub const SUPPORTED_MANDATORY_FEATURES: u32 = FEATURE_DEFLATE_REPLAY | FEATURE_EXTERNAL_OBJECTS;
71/// Feature bits this build understands and supports (no object-store support).
72#[cfg(all(feature = "deflate-replay", not(feature = "store")))]
73pub const SUPPORTED_MANDATORY_FEATURES: u32 = FEATURE_DEFLATE_REPLAY;
74/// Feature bits this build understands and supports (no replay support).
75#[cfg(all(not(feature = "deflate-replay"), feature = "store"))]
76pub const SUPPORTED_MANDATORY_FEATURES: u32 = FEATURE_EXTERNAL_OBJECTS;
77/// Feature bits this build understands and supports (bare exact core).
78#[cfg(all(not(feature = "deflate-replay"), not(feature = "store")))]
79pub const SUPPORTED_MANDATORY_FEATURES: u32 = 0;
80
81/// The parsed, validated fixed header.
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct Header {
84    /// Major version.
85    pub major: u16,
86    /// Minor version.
87    pub minor: u16,
88    /// Feature bits a decoder *must* understand.
89    pub mandatory_features: u32,
90    /// Feature bits a decoder *may* ignore.
91    pub optional_features: u32,
92    /// Exactness profile selector (only `EXACT_BYTES` is normative).
93    pub exactness_profile: u8,
94    /// Source-format class selector.
95    pub source_format: u8,
96    /// First 16 bytes of SHA-256 over the universe declaration string.
97    pub universe_id: [u8; 16],
98    /// Declared length of the fully reconstructed source.
99    pub declared_source_len: u64,
100}
101
102impl Header {
103    /// Build a header for the current format version.
104    pub fn new(
105        universe_id: [u8; 16],
106        declared_source_len: u64,
107        exactness_profile: u8,
108        source_format: u8,
109    ) -> Self {
110        Header {
111            major: FORMAT_MAJOR,
112            minor: FORMAT_MINOR,
113            mandatory_features: 0,
114            optional_features: 0,
115            exactness_profile,
116            source_format,
117            universe_id,
118            declared_source_len,
119        }
120    }
121
122    /// Encode the header to exactly [`HEADER_LEN`] bytes, computing the CRC.
123    pub fn encode(&self) -> [u8; HEADER_LEN] {
124        let mut b = [0u8; HEADER_LEN];
125        b[0..8].copy_from_slice(&MAGIC);
126        b[8..10].copy_from_slice(&self.major.to_le_bytes());
127        b[10..12].copy_from_slice(&self.minor.to_le_bytes());
128        b[12..16].copy_from_slice(&self.mandatory_features.to_le_bytes());
129        b[16..20].copy_from_slice(&self.optional_features.to_le_bytes());
130        b[20] = self.exactness_profile;
131        b[21] = self.source_format;
132        // b[22..24] reserved_a = 0
133        b[24..40].copy_from_slice(&self.universe_id);
134        b[40..48].copy_from_slice(&self.declared_source_len.to_le_bytes());
135        // b[48..60] reserved_b = 0
136        let crc = crc32c(&b[0..60]);
137        b[60..64].copy_from_slice(&crc.to_le_bytes());
138        b
139    }
140
141    /// Decode and validate the header from the start of `bytes`.
142    pub fn decode(bytes: &[u8]) -> Result<Header> {
143        if bytes.len() < HEADER_LEN {
144            return Err(Error::invalid_container(format!(
145                "input is {} bytes; need at least {HEADER_LEN} for a header",
146                bytes.len()
147            )));
148        }
149        let b = &bytes[0..HEADER_LEN];
150        if b[0..8] != MAGIC {
151            return Err(Error::invalid_container("bad container magic"));
152        }
153        let want_crc = u32::from_le_bytes([b[60], b[61], b[62], b[63]]);
154        let got_crc = crc32c(&b[0..60]);
155        if want_crc != got_crc {
156            return Err(Error::invalid_container(format!(
157                "header CRC32C mismatch: declared {want_crc:#010x}, computed {got_crc:#010x}"
158            )));
159        }
160        if b[22] != 0 || b[23] != 0 {
161            return Err(Error::invalid_container("header reserved_a must be zero"));
162        }
163        if b[48..60].iter().any(|&x| x != 0) {
164            return Err(Error::invalid_container("header reserved_b must be zero"));
165        }
166
167        let major = u16::from_le_bytes([b[8], b[9]]);
168        let minor = u16::from_le_bytes([b[10], b[11]]);
169        if major != FORMAT_MAJOR || minor > FORMAT_MINOR {
170            return Err(Error::unsupported_version(format!(
171                "container version {major}.{minor} is not supported by this build ({FORMAT_MAJOR}.{FORMAT_MINOR})"
172            )));
173        }
174
175        let mandatory_features = u32::from_le_bytes([b[12], b[13], b[14], b[15]]);
176        let unsupported = mandatory_features & !SUPPORTED_MANDATORY_FEATURES;
177        if unsupported != 0 {
178            return Err(Error::unsupported_feature(format!(
179                "descriptor requires unsupported mandatory feature bits {unsupported:#010x}"
180            )));
181        }
182
183        let exactness_profile = b[20];
184        if exactness_profile != EXACTNESS_PROFILE_EXACT_BYTES {
185            return Err(Error::unsupported_feature(format!(
186                "exactness profile {exactness_profile} is not the normative EXACT_BYTES profile"
187            )));
188        }
189        let source_format = b[21];
190
191        let mut universe_id = [0u8; 16];
192        universe_id.copy_from_slice(&b[24..40]);
193        let declared_source_len =
194            u64::from_le_bytes([b[40], b[41], b[42], b[43], b[44], b[45], b[46], b[47]]);
195
196        Ok(Header {
197            major,
198            minor,
199            mandatory_features,
200            optional_features: u32::from_le_bytes([b[16], b[17], b[18], b[19]]),
201            exactness_profile,
202            source_format,
203            universe_id,
204            declared_source_len,
205        })
206    }
207
208    /// True if this header describes the opaque source-format class.
209    pub fn is_opaque(&self) -> bool {
210        self.source_format == SOURCE_FORMAT_OPAQUE
211    }
212
213    /// True if this build has an adapter for the declared source-format class.
214    ///
215    /// Unknown classes fail closed: a descriptor naming a class this build does
216    /// not implement is refused rather than reinterpreted as opaque.
217    pub fn source_format_supported(&self) -> bool {
218        matches!(self.source_format, 0 | 1)
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    #[test]
227    fn roundtrip() {
228        let h = Header::new(
229            [7u8; 16],
230            1234,
231            EXACTNESS_PROFILE_EXACT_BYTES,
232            SOURCE_FORMAT_OPAQUE,
233        );
234        let enc = h.encode();
235        assert_eq!(enc.len(), HEADER_LEN);
236        let dec = Header::decode(&enc).unwrap();
237        assert_eq!(h, dec);
238    }
239
240    #[test]
241    fn rejects_bad_magic() {
242        let mut enc = Header::new([0u8; 16], 0, 0, 0).encode();
243        enc[0] = b'X';
244        match Header::decode(&enc) {
245            Err(e) => assert_eq!(e.class(), crate::ErrorClass::InvalidContainer),
246            Ok(_) => panic!("expected failure"),
247        }
248    }
249
250    #[test]
251    fn detects_corruption_via_crc() {
252        let mut enc = Header::new([0u8; 16], 5, 0, 0).encode();
253        enc[41] ^= 0x01; // flip a byte inside declared_source_len
254        let e = Header::decode(&enc).unwrap_err();
255        assert_eq!(e.class(), crate::ErrorClass::InvalidContainer);
256    }
257
258    #[test]
259    fn rejects_truncated() {
260        let e = Header::decode(&[0u8; 10]).unwrap_err();
261        assert_eq!(e.class(), crate::ErrorClass::InvalidContainer);
262    }
263
264    #[test]
265    fn rejects_unknown_mandatory_feature() {
266        let mut h = Header::new([0u8; 16], 0, 0, 0);
267        h.mandatory_features = 0x8000_0000;
268        let e = Header::decode(&h.encode()).unwrap_err();
269        assert_eq!(e.class(), crate::ErrorClass::UnsupportedFeature);
270    }
271
272    #[test]
273    fn rejects_future_major_version() {
274        let mut h = Header::new([0u8; 16], 0, 0, 0);
275        h.major = 9;
276        let e = Header::decode(&h.encode()).unwrap_err();
277        assert_eq!(e.class(), crate::ErrorClass::UnsupportedVersion);
278    }
279}