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
82fn take_objects<R: ObjectResolver + ?Sized>(
93 d: &mut Descriptor,
94 resolver: &R,
95) -> Result<Vec<Vec<u8>>> {
96 let mut out: Vec<Vec<u8>> = Vec::with_capacity(d.objects.len());
97 for (i, src) in d.objects.iter_mut().enumerate() {
98 match src {
99 ObjectSource::Inline(bytes) => out.push(std::mem::take(bytes)),
100 ObjectSource::External { id, len } => {
101 let bytes = resolver.get(id, *len)?;
102 if bytes.len() as u64 != *len {
103 return Err(Error::integrity_mismatch(format!(
104 "external object {i} ({id}) has {} bytes, EXTERNAL_REF declared {len}",
105 bytes.len()
106 )));
107 }
108 out.push(bytes);
109 }
110 }
111 }
112 Ok(out)
113}
114
115pub(crate) fn materialize_in_place(
125 parsed: &mut ParsedDescriptor,
126 limits: Limits,
127) -> Result<Vec<u8>> {
128 let d = &mut parsed.descriptor;
129 let channels = decode_channels(d, limits)?;
130 let objects = take_objects(d, &NullResolver)?;
131
132 let out = d.program.eval(&objects, &channels, limits)?;
133 if out.len() as u64 != d.source_len {
134 return Err(Error::reconstruction_mismatch(format!(
135 "materialized {} bytes but {} were declared",
136 out.len(),
137 d.source_len
138 )));
139 }
140 let digest = sha256(&out);
141 if digest != d.source_sha256 {
142 return Err(Error::integrity_mismatch(format!(
143 "materialized SHA-256 {} != declared {}",
144 to_hex(&digest),
145 to_hex(&d.source_sha256)
146 )));
147 }
148 Ok(out)
149}
150
151pub fn materialize_with<R: ObjectResolver>(
158 parsed: &ParsedDescriptor,
159 resolver: &R,
160 limits: Limits,
161) -> Result<Vec<u8>> {
162 let d = &parsed.descriptor;
163
164 let channels = decode_channels(d, limits)?;
165 let objects = resolve_objects(d, resolver)?;
166
167 let out = d.program.eval(&objects, &channels, limits)?;
168 if out.len() as u64 != d.source_len {
169 return Err(Error::reconstruction_mismatch(format!(
170 "materialized {} bytes but {} were declared",
171 out.len(),
172 d.source_len
173 )));
174 }
175 let digest = sha256(&out);
176 if digest != d.source_sha256 {
177 return Err(Error::integrity_mismatch(format!(
178 "materialized SHA-256 {} != declared {}",
179 to_hex(&digest),
180 to_hex(&d.source_sha256)
181 )));
182 }
183 Ok(out)
184}
185
186pub fn materialize(parsed: &ParsedDescriptor, limits: Limits) -> Result<Vec<u8>> {
192 materialize_with(parsed, &NullResolver, limits)
193}
194
195pub fn decode_to_bytes_with<R: ObjectResolver>(
198 bytes: &[u8],
199 resolver: &R,
200 limits: Limits,
201) -> Result<(Vec<u8>, ParsedDescriptor)> {
202 let parsed = Descriptor::parse(bytes, limits)?;
203 let out = materialize_with(&parsed, resolver, limits)?;
204 Ok((out, parsed))
205}
206
207pub fn decode_to_bytes(bytes: &[u8], limits: Limits) -> Result<(Vec<u8>, ParsedDescriptor)> {
209 decode_to_bytes_with(bytes, &NullResolver, limits)
210}
211
212#[derive(Debug, Clone, PartialEq, Eq)]
214pub struct VerifyReport {
215 pub source_len: u64,
217 pub sha256_hex: String,
219 pub object_count: usize,
221 pub graph_ops: usize,
223}
224
225pub fn verify(bytes: &[u8], limits: Limits) -> Result<VerifyReport> {
227 let (out, parsed) = decode_to_bytes(bytes, limits)?;
228 Ok(VerifyReport {
229 source_len: out.len() as u64,
230 sha256_hex: to_hex(&sha256(&out)),
231 object_count: parsed.descriptor.objects.len(),
232 graph_ops: parsed.descriptor.program.ops.len(),
233 })
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239 use crate::dra::Op;
240 use crate::{EXACTNESS_PROFILE_EXACT_BYTES, SOURCE_FORMAT_OPAQUE};
241
242 fn descriptor_for(source: &[u8]) -> Descriptor {
243 Descriptor {
244 universe: crate::container::UNIVERSE.to_string(),
245 source_format: SOURCE_FORMAT_OPAQUE,
246 format_basis: "opaque:test".to_string(),
247 models: vec![],
248 channels: vec![],
249 objects: vec![crate::container::ObjectSource::Inline(source.to_vec())],
250 program: crate::dra::Program::new(vec![Op::EmitObject { object_id: 0 }]),
251 observation_index: None,
252 seek_directory: false,
253 checkpoints: None,
254 source_sha256: sha256(source),
255 source_len: source.len() as u64,
256 }
257 }
258
259 #[test]
260 fn exact_roundtrip() {
261 let source = b"exact bytes must survive";
262 let d = descriptor_for(source);
263 let (bytes, _) = d.serialize().unwrap();
264 let (out, _) = decode_to_bytes(&bytes, Limits::DEFAULT).unwrap();
265 assert_eq!(out, source);
266 let _ = EXACTNESS_PROFILE_EXACT_BYTES;
267 }
268
269 #[test]
270 fn detects_digest_mismatch() {
271 let source = b"abcdef";
272 let mut d = descriptor_for(source);
273 d.source_sha256[0] ^= 0xFF;
274 let (bytes, _) = d.serialize().unwrap();
275 let e = decode_to_bytes(&bytes, Limits::DEFAULT).unwrap_err();
276 assert_eq!(e.class(), crate::ErrorClass::IntegrityMismatch);
277 }
278
279 #[test]
280 fn materialize_in_place_matches_and_empties_inline_objects() {
281 let source = b"in-place must reconstruct identical bytes".repeat(64);
282 let d = descriptor_for(&source);
283 let (bytes, _) = d.serialize().unwrap();
284
285 let mut parsed = Descriptor::parse(&bytes, Limits::DEFAULT).unwrap();
286 let out = materialize_in_place(&mut parsed, Limits::DEFAULT).unwrap();
287 assert_eq!(out, source, "in-place output must equal the source");
288 assert!(
290 parsed
291 .descriptor
292 .objects
293 .iter()
294 .all(|o| o.as_inline().is_some_and(|b| b.is_empty())),
295 "in-place materialization must take, not clone, inline objects"
296 );
297 assert!(materialize_in_place(&mut parsed, Limits::DEFAULT).is_err());
299 }
300}