vole_document/materialize/
mod.rs1use crate::container::{Descriptor, ParsedDescriptor};
8use crate::error::{Error, Result};
9use crate::integrity::{sha256, to_hex};
10use crate::limits::Limits;
11
12#[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#[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
53pub 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
81pub 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#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct VerifyReport {
91 pub source_len: u64,
93 pub sha256_hex: String,
95 pub object_count: usize,
97 pub graph_ops: usize,
99}
100
101pub 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}