vole_document/materialize/
mod.rs1pub 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#[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#[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
57fn 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
82pub 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
117pub fn materialize(parsed: &ParsedDescriptor, limits: Limits) -> Result<Vec<u8>> {
123 materialize_with(parsed, &NullResolver, limits)
124}
125
126pub 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
138pub fn decode_to_bytes(bytes: &[u8], limits: Limits) -> Result<(Vec<u8>, ParsedDescriptor)> {
140 decode_to_bytes_with(bytes, &NullResolver, limits)
141}
142
143#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct VerifyReport {
146 pub source_len: u64,
148 pub sha256_hex: String,
150 pub object_count: usize,
152 pub graph_ops: usize,
154}
155
156pub 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}