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