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
7pub mod observation;
8pub mod seek;
9
10use crate::container::{Descriptor, ObjectSource, ParsedDescriptor};
11use crate::error::{Error, Result};
12use crate::integrity::{sha256, to_hex};
13use crate::limits::Limits;
14use crate::store::{NullResolver, ObjectResolver};
15
16/// Decode every entropy channel in table order, each against the model it
17/// references. Each channel's model must already have been cross-validated by
18/// `Descriptor::parse`.
19#[cfg(feature = "rans")]
20fn decode_channels(d: &Descriptor, limits: Limits) -> Result<Vec<Vec<u8>>> {
21    use crate::entropy::rans::{Capsule, decode_channel};
22
23    let mut channels: Vec<Vec<u8>> = Vec::with_capacity(d.channels.len());
24    for channel in &d.channels {
25        let model = d.models.get(channel.model_id as usize).ok_or_else(|| {
26            Error::invalid_model(format!(
27                "entropy channel references missing model {}",
28                channel.model_id
29            ))
30        })?;
31        let capsule = Capsule {
32            initial_state: channel.initial_state,
33            payload: channel.payload.clone(),
34            symbol_count: channel.symbol_count,
35            decoded_length: channel.decoded_length,
36        };
37        channels.push(decode_channel(model, &capsule, limits)?);
38    }
39    Ok(channels)
40}
41
42/// Without the `rans` feature there is no entropy decoder. A descriptor with no
43/// channels is still exactly materializable (the RAW/RLE floor); one that
44/// declares channels is refused as an explicit capability limit rather than
45/// silently reinterpreted.
46#[cfg(not(feature = "rans"))]
47fn decode_channels(d: &Descriptor, _limits: Limits) -> Result<Vec<Vec<u8>>> {
48    if d.channels.is_empty() {
49        Ok(Vec::new())
50    } else {
51        Err(Error::unsupported_feature(
52            "this build was compiled without the `rans` feature",
53        ))
54    }
55}
56
57/// Resolve every object once, verifying referenced ids and lengths, then hand
58/// the DRA the same plain object vector the standalone path uses.
59fn resolve_objects<R: ObjectResolver + ?Sized>(
60    d: &Descriptor,
61    resolver: &R,
62) -> Result<Vec<Vec<u8>>> {
63    let mut out: Vec<Vec<u8>> = Vec::with_capacity(d.objects.len());
64    for (i, src) in d.objects.iter().enumerate() {
65        match src {
66            ObjectSource::Inline(bytes) => out.push(bytes.clone()),
67            ObjectSource::External { id, len } => {
68                let bytes = resolver.get(id, *len)?;
69                if bytes.len() as u64 != *len {
70                    return Err(Error::integrity_mismatch(format!(
71                        "external object {i} ({id}) has {} bytes, EXTERNAL_REF declared {len}",
72                        bytes.len()
73                    )));
74                }
75                out.push(bytes);
76            }
77        }
78    }
79    Ok(out)
80}
81
82/// Materialize the exact source bytes for a parsed descriptor, resolving any
83/// external object references through `resolver`.
84///
85/// Enforces, in order: program bounds (via `eval`), reconstructed length, and
86/// whole-source SHA-256. The DRA is unchanged: it consumes the same plain
87/// object vector the standalone path uses.
88pub fn materialize_with<R: ObjectResolver>(
89    parsed: &ParsedDescriptor,
90    resolver: &R,
91    limits: Limits,
92) -> Result<Vec<u8>> {
93    let d = &parsed.descriptor;
94
95    let channels = decode_channels(d, limits)?;
96    let objects = resolve_objects(d, resolver)?;
97
98    let out = d.program.eval(&objects, &channels, limits)?;
99    if out.len() as u64 != d.source_len {
100        return Err(Error::reconstruction_mismatch(format!(
101            "materialized {} bytes but {} were declared",
102            out.len(),
103            d.source_len
104        )));
105    }
106    let digest = sha256(&out);
107    if digest != d.source_sha256 {
108        return Err(Error::integrity_mismatch(format!(
109            "materialized SHA-256 {} != declared {}",
110            to_hex(&digest),
111            to_hex(&d.source_sha256)
112        )));
113    }
114    Ok(out)
115}
116
117/// Materialize the exact source bytes for a parsed descriptor.
118///
119/// Standalone entry point: no resolver. Succeeds iff there are no external
120/// references; a descriptor with an [`ObjectSource::External`] object errors
121/// [`crate::ErrorClass::MissingExternalObject`].
122pub fn materialize(parsed: &ParsedDescriptor, limits: Limits) -> Result<Vec<u8>> {
123    materialize_with(parsed, &NullResolver, limits)
124}
125
126/// Parse and materialize in one step, resolving external objects through
127/// `resolver`.
128pub fn decode_to_bytes_with<R: ObjectResolver>(
129    bytes: &[u8],
130    resolver: &R,
131    limits: Limits,
132) -> Result<(Vec<u8>, ParsedDescriptor)> {
133    let parsed = Descriptor::parse(bytes, limits)?;
134    let out = materialize_with(&parsed, resolver, limits)?;
135    Ok((out, parsed))
136}
137
138/// Parse and materialize in one step (no resolver).
139pub fn decode_to_bytes(bytes: &[u8], limits: Limits) -> Result<(Vec<u8>, ParsedDescriptor)> {
140    decode_to_bytes_with(bytes, &NullResolver, limits)
141}
142
143/// The result of a deep verification.
144#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct VerifyReport {
146    /// Reconstructed source length.
147    pub source_len: u64,
148    /// Lower-case hex SHA-256 of the reconstructed source.
149    pub sha256_hex: String,
150    /// Number of raw byte objects in the descriptor.
151    pub object_count: usize,
152    /// Number of DRA instructions in the reconstruction program.
153    pub graph_ops: usize,
154}
155
156/// Deep verification: parse, materialize, and check the archival digest.
157pub fn verify(bytes: &[u8], limits: Limits) -> Result<VerifyReport> {
158    let (out, parsed) = decode_to_bytes(bytes, limits)?;
159    Ok(VerifyReport {
160        source_len: out.len() as u64,
161        sha256_hex: to_hex(&sha256(&out)),
162        object_count: parsed.descriptor.objects.len(),
163        graph_ops: parsed.descriptor.program.ops.len(),
164    })
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use crate::dra::Op;
171    use crate::{EXACTNESS_PROFILE_EXACT_BYTES, SOURCE_FORMAT_OPAQUE};
172
173    fn descriptor_for(source: &[u8]) -> Descriptor {
174        Descriptor {
175            universe: crate::container::UNIVERSE.to_string(),
176            source_format: SOURCE_FORMAT_OPAQUE,
177            format_basis: "opaque:test".to_string(),
178            models: vec![],
179            channels: vec![],
180            objects: vec![crate::container::ObjectSource::Inline(source.to_vec())],
181            program: crate::dra::Program::new(vec![Op::EmitObject { object_id: 0 }]),
182            observation_index: None,
183            seek_directory: false,
184            source_sha256: sha256(source),
185            source_len: source.len() as u64,
186        }
187    }
188
189    #[test]
190    fn exact_roundtrip() {
191        let source = b"exact bytes must survive";
192        let d = descriptor_for(source);
193        let (bytes, _) = d.serialize().unwrap();
194        let (out, _) = decode_to_bytes(&bytes, Limits::DEFAULT).unwrap();
195        assert_eq!(out, source);
196        let _ = EXACTNESS_PROFILE_EXACT_BYTES;
197    }
198
199    #[test]
200    fn detects_digest_mismatch() {
201        let source = b"abcdef";
202        let mut d = descriptor_for(source);
203        d.source_sha256[0] ^= 0xFF;
204        let (bytes, _) = d.serialize().unwrap();
205        let e = decode_to_bytes(&bytes, Limits::DEFAULT).unwrap_err();
206        assert_eq!(e.class(), crate::ErrorClass::IntegrityMismatch);
207    }
208}