Skip to main content

vole_document/materialize/
mod.rs

1//! Deterministic materialization.
2//!
3//! The materializer is deliberately boring: it evaluates a bounded program,
4//! checks the reconstructed length, and checks the archival digest. It never
5//! searches, guesses, optimizes, or invokes external tools.
6
7use crate::container::{Descriptor, ParsedDescriptor};
8use crate::error::{Error, Result};
9use crate::integrity::{sha256, to_hex};
10use crate::limits::Limits;
11
12/// Decode every entropy channel in table order, each against the model it
13/// references. Each channel's model must already have been cross-validated by
14/// `Descriptor::parse`.
15#[cfg(feature = "rans")]
16fn decode_channels(d: &Descriptor, limits: Limits) -> Result<Vec<Vec<u8>>> {
17    use crate::entropy::rans::{Capsule, decode_channel};
18
19    let mut channels: Vec<Vec<u8>> = Vec::with_capacity(d.channels.len());
20    for channel in &d.channels {
21        let model = d.models.get(channel.model_id as usize).ok_or_else(|| {
22            Error::invalid_model(format!(
23                "entropy channel references missing model {}",
24                channel.model_id
25            ))
26        })?;
27        let capsule = Capsule {
28            initial_state: channel.initial_state,
29            payload: channel.payload.clone(),
30            symbol_count: channel.symbol_count,
31            decoded_length: channel.decoded_length,
32        };
33        channels.push(decode_channel(model, &capsule, limits)?);
34    }
35    Ok(channels)
36}
37
38/// Without the `rans` feature there is no entropy decoder. A descriptor with no
39/// channels is still exactly materializable (the RAW/RLE floor); one that
40/// declares channels is refused as an explicit capability limit rather than
41/// silently reinterpreted.
42#[cfg(not(feature = "rans"))]
43fn decode_channels(d: &Descriptor, _limits: Limits) -> Result<Vec<Vec<u8>>> {
44    if d.channels.is_empty() {
45        Ok(Vec::new())
46    } else {
47        Err(Error::unsupported_feature(
48            "this build was compiled without the `rans` feature",
49        ))
50    }
51}
52
53/// Materialize the exact source bytes for a parsed descriptor.
54///
55/// Enforces, in order: program bounds (via `eval`), reconstructed length, and
56/// whole-source SHA-256.
57pub fn materialize(parsed: &ParsedDescriptor, limits: Limits) -> Result<Vec<u8>> {
58    let d = &parsed.descriptor;
59
60    let channels = decode_channels(d, limits)?;
61
62    let out = d.program.eval(&d.objects, &channels, limits)?;
63    if out.len() as u64 != d.source_len {
64        return Err(Error::reconstruction_mismatch(format!(
65            "materialized {} bytes but {} were declared",
66            out.len(),
67            d.source_len
68        )));
69    }
70    let digest = sha256(&out);
71    if digest != d.source_sha256 {
72        return Err(Error::integrity_mismatch(format!(
73            "materialized SHA-256 {} != declared {}",
74            to_hex(&digest),
75            to_hex(&d.source_sha256)
76        )));
77    }
78    Ok(out)
79}
80
81/// Parse and materialize in one step.
82pub fn decode_to_bytes(bytes: &[u8], limits: Limits) -> Result<(Vec<u8>, ParsedDescriptor)> {
83    let parsed = Descriptor::parse(bytes, limits)?;
84    let out = materialize(&parsed, limits)?;
85    Ok((out, parsed))
86}
87
88/// The result of a deep verification.
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct VerifyReport {
91    /// Reconstructed source length.
92    pub source_len: u64,
93    /// Lower-case hex SHA-256 of the reconstructed source.
94    pub sha256_hex: String,
95    /// Number of raw byte objects in the descriptor.
96    pub object_count: usize,
97    /// Number of DRA instructions in the reconstruction program.
98    pub graph_ops: usize,
99}
100
101/// Deep verification: parse, materialize, and check the archival digest.
102pub fn verify(bytes: &[u8], limits: Limits) -> Result<VerifyReport> {
103    let (out, parsed) = decode_to_bytes(bytes, limits)?;
104    Ok(VerifyReport {
105        source_len: out.len() as u64,
106        sha256_hex: to_hex(&sha256(&out)),
107        object_count: parsed.descriptor.objects.len(),
108        graph_ops: parsed.descriptor.program.ops.len(),
109    })
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use crate::dra::Op;
116    use crate::{EXACTNESS_PROFILE_EXACT_BYTES, SOURCE_FORMAT_OPAQUE};
117
118    fn descriptor_for(source: &[u8]) -> Descriptor {
119        Descriptor {
120            universe: crate::container::UNIVERSE.to_string(),
121            source_format: SOURCE_FORMAT_OPAQUE,
122            format_basis: "opaque:test".to_string(),
123            models: vec![],
124            channels: vec![],
125            objects: vec![source.to_vec()],
126            program: crate::dra::Program::new(vec![Op::EmitObject { object_id: 0 }]),
127            source_sha256: sha256(source),
128            source_len: source.len() as u64,
129        }
130    }
131
132    #[test]
133    fn exact_roundtrip() {
134        let source = b"exact bytes must survive";
135        let d = descriptor_for(source);
136        let (bytes, _) = d.serialize().unwrap();
137        let (out, _) = decode_to_bytes(&bytes, Limits::DEFAULT).unwrap();
138        assert_eq!(out, source);
139        let _ = EXACTNESS_PROFILE_EXACT_BYTES;
140    }
141
142    #[test]
143    fn detects_digest_mismatch() {
144        let source = b"abcdef";
145        let mut d = descriptor_for(source);
146        d.source_sha256[0] ^= 0xFF;
147        let (bytes, _) = d.serialize().unwrap();
148        let e = decode_to_bytes(&bytes, Limits::DEFAULT).unwrap_err();
149        assert_eq!(e.class(), crate::ErrorClass::IntegrityMismatch);
150    }
151}