Skip to main content

lumen/
reader.rs

1//! Reading a file: [`LumenFile`], including random access to one layer.
2//!
3//! Opening validates, then parses only what is needed: the fixed header, the
4//! directory, `LTBL` and the chunks that describe the print. Layer masks are
5//! decoded on demand, one `LAYR` chunk at a time, so reading layer 1 of a
6//! 2,000-layer print decompresses the one chunk that holds it rather than the
7//! whole file. For a firmware reader that pulls layers through a fixed buffer,
8//! that is the whole point of the chunked design ([`spec/12-compression.md`]
9//! section 6.2), and the cache here holds exactly one decompressed chunk at a
10//! time - the one last asked for.
11//!
12//! A `(layer, sector)` is addressed by its `LTBL` entry: the entry names the
13//! `LAYR` chunk holding the layer's run and the slice of that chunk's output the
14//! layer occupies, so a single-sector layer costs one chunk read and a
15//! multi-sector layer costs as many as it has sectors. A reader that prints one
16//! material reads the layer's first entry and nothing else, which is what
17//! [`LumenFile::layer`] does; [`LumenFile::layer_sectors`] reads every sector,
18//! and [`LumenFile::is_single_material_complete`] says whether the first way
19//! loses anything.
20//!
21//! Opening validates and then parses, so a caller that also runs
22//! [`crate::validate`] separately pays for the container walk twice. Callers
23//! that want only a verdict should not open the file.
24
25use crate::check::Check;
26use crate::chunkio;
27use crate::chunks::extd::Extension;
28use crate::chunks::head::Head;
29use crate::chunks::lhas::{self, LayerHashes};
30use crate::chunks::ltbl::{LayerEntry, LayerTable};
31use crate::chunks::preview::Preview;
32use crate::chunks::zdic::ZstdDictionary;
33use crate::chunks::{self, json_chunks};
34use crate::container::{self, ChunkDescriptor, ChunkType, Directory, FileHeader};
35use crate::crypto::{self, Auth, Cipher, SessionKey};
36use crate::error::{Error, Result};
37use crate::json::{Meta, Profile, Timing};
38use crate::ree::{self, DecodedLayer};
39use crate::timing::{self, Resolved};
40use crate::validate::{Level, Validator};
41use std::cell::RefCell;
42
43/// One sector's decoded mask on one layer.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct DecodedSector {
46    /// The sector this mask belongs to.
47    pub sector_id: u32,
48    /// The decoded mask.
49    pub layer: DecodedLayer,
50}
51
52/// One `LAYR` chunk: a sector's masks for one group of layers.
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct LayrChunk {
55    /// The chunk's index in the directory, which is what an `LTBL` entry names
56    /// and what a sealed frame's associated data binds it to.
57    pub index: u32,
58    /// The chunk's directory entry.
59    pub descriptor: ChunkDescriptor,
60}
61
62/// An open LUMEN file.
63#[derive(Debug)]
64pub struct LumenFile<'a> {
65    buf: &'a [u8],
66    header: FileHeader,
67    directory: Directory,
68    head: Head,
69    meta: Meta,
70    profile: Option<Profile>,
71    previews: Vec<Preview>,
72    extensions: Vec<Extension>,
73    dictionary: Option<ZstdDictionary>,
74    layer_hashes: Option<LayerHashes>,
75    layer_table: LayerTable,
76    layr_chunks: Vec<LayrChunk>,
77    voxl: Option<Vec<u8>>,
78    auth: Option<Auth>,
79    cipher: Option<Cipher>,
80    key: Option<SessionKey>,
81    level: Level,
82    cache: RefCell<Option<(u32, Vec<u8>)>>,
83}
84
85impl<'a> LumenFile<'a> {
86    /// Open without checking the whole of section 11 first.
87    ///
88    /// [`LumenFile::open`] runs every check of section 11 before it returns, and
89    /// one of them walks each slice's stream. A caller that wants a single layer,
90    /// a layer preview for instance, pays for that walk without asking. This
91    /// parses the container and its chunks and checks nothing: it leaves the
92    /// streams to [`LumenFile::layer`], which reports a malformed one when it
93    /// reaches it.
94    pub fn open_unvalidated(buf: &'a [u8]) -> Result<LumenFile<'a>> {
95        LumenFile::assemble(buf, None, None)
96    }
97
98    /// Open a plaintext file, validating it against `level`.
99    pub fn open(buf: &'a [u8], level: Level) -> Result<LumenFile<'a>> {
100        LumenFile::assemble(buf, Some(level), None)
101    }
102
103    /// Open a file whose session key is already known.
104    pub fn open_with_key(buf: &'a [u8], level: Level, key: SessionKey) -> Result<LumenFile<'a>> {
105        LumenFile::assemble(buf, Some(level), Some(key))
106    }
107
108    /// Open a password-protected file.
109    pub fn open_with_password(
110        buf: &'a [u8],
111        level: Level,
112        password: &str,
113    ) -> Result<LumenFile<'a>> {
114        let auth = auth_of(buf)?;
115        let key = crypto::unwrap_password(&auth, password)?;
116        LumenFile::assemble(buf, Some(level), Some(key))
117    }
118
119    /// Open a machine-bound file with the recipient's X25519 private key.
120    pub fn open_with_machine_key(
121        buf: &'a [u8],
122        level: Level,
123        private_key: &[u8; 32],
124    ) -> Result<LumenFile<'a>> {
125        let auth = auth_of(buf)?;
126        let key = crypto::unwrap_machine(&auth, private_key)?;
127        LumenFile::assemble(buf, Some(level), Some(key))
128    }
129
130    fn assemble(
131        buf: &'a [u8],
132        level: Option<Level>,
133        key: Option<SessionKey>,
134    ) -> Result<LumenFile<'a>> {
135        if let Some(level) = level {
136            let mut validator = Validator::new(level);
137            if let Some(key) = key {
138                validator = validator.with_key(key);
139            }
140            validator.validate(buf)?;
141        }
142
143        let header = FileHeader::parse(buf)?;
144        let directory = container::parse_directory(buf, &header)?;
145        let auth = match directory.find(ChunkType::AUTH) {
146            None => None,
147            Some(d) => Some(crypto::parse_auth(chunkio::stored(buf, d)?)?),
148        };
149        let cipher = auth.as_ref().map(|a| a.cipher);
150        let parts = Parts {
151            buf,
152            dir: &directory,
153            cipher,
154            key: key.as_ref(),
155        };
156
157        let head = Head::parse(&parts.required(ChunkType::HEAD)?)?;
158        let meta = json_chunks::parse_meta(&parts.required(ChunkType::META)?)?;
159        let profile = match parts.optional_required(ChunkType::PROF)? {
160            None => None,
161            Some(bytes) => Some(json_chunks::parse_profile(&bytes)?),
162        };
163        let mut previews = Vec::new();
164        for d in directory.find_all(ChunkType::PREV).collect::<Vec<_>>() {
165            let stored = chunkio::stored(buf, d)?;
166            let Some(bytes) = chunkio::payload(d, stored, cipher, key.as_ref())? else {
167                continue;
168            };
169            previews.push(Preview {
170                role: crate::chunks::preview::PreviewRole::from_flags(d.flags)?,
171                png: bytes,
172            });
173        }
174        let mut extensions = Vec::new();
175        for d in directory.find_all(ChunkType::EXTD).collect::<Vec<_>>() {
176            let stored = chunkio::stored(buf, d)?;
177            let Some(frame) = chunkio::payload(d, stored, cipher, key.as_ref())? else {
178                continue;
179            };
180            // Validation has already rejected a malformed frame on a payload it
181            // could read, so a failure here means the descriptor did not say the
182            // payload was a frame - an extension we do not implement, whose bytes
183            // are opaque to us.
184            if let Ok(ext) = Extension::parse(&frame, d.flags) {
185                extensions.push(ext);
186            }
187        }
188        let dictionary = match parts.optional_required(ChunkType::ZDIC)? {
189            None => None,
190            Some(bytes) => Some(ZstdDictionary::parse(&bytes)?),
191        };
192        let layer_hashes = match directory.find(ChunkType::LHAS) {
193            None => None,
194            Some(d) => Some(LayerHashes::parse(chunkio::stored(buf, d)?)?),
195        };
196        let layer_table = LayerTable::parse(&parts.required(ChunkType::LTBL)?)?;
197        let layr_chunks = directory
198            .descriptors
199            .iter()
200            .enumerate()
201            .filter(|(_, d)| !d.is_null() && d.chunk_type == ChunkType::LAYR)
202            .map(|(index, d)| LayrChunk {
203                index: index as u32,
204                descriptor: *d,
205            })
206            .collect();
207        let voxl = parts.optional_required(ChunkType::VOXL)?;
208
209        Ok(LumenFile {
210            buf,
211            header,
212            directory,
213            head,
214            meta,
215            profile,
216            previews,
217            extensions,
218            dictionary,
219            layer_hashes,
220            layer_table,
221            layr_chunks,
222            voxl,
223            auth,
224            cipher,
225            key,
226            // An unvalidated open has no level to carry; reading is then loose,
227            // which is what a caller that skipped validation wants anyway.
228            level: level.unwrap_or_default(),
229            cache: RefCell::new(None),
230        })
231    }
232
233    /// Recover the session key from a password, without reopening.
234    pub fn recover_password(&self, password: &str) -> Result<SessionKey> {
235        let auth = self
236            .auth
237            .as_ref()
238            .ok_or_else(|| Error::new(Check::CryptNoKey, "the file has no AUTH chunk"))?;
239        crypto::unwrap_password(auth, password)
240    }
241
242    /// Recover the session key from a recipient private key, without reopening.
243    pub fn recover_machine_key(&self, private_key: &[u8; 32]) -> Result<SessionKey> {
244        let auth = self
245            .auth
246            .as_ref()
247            .ok_or_else(|| Error::new(Check::CryptNoKey, "the file has no AUTH chunk"))?;
248        crypto::unwrap_machine(auth, private_key)
249    }
250
251    /// The file's bytes.
252    pub fn bytes(&self) -> &'a [u8] {
253        self.buf
254    }
255
256    /// The fixed file header.
257    pub fn header(&self) -> &FileHeader {
258        &self.header
259    }
260
261    /// The chunk directory.
262    pub fn directory(&self) -> &Directory {
263        &self.directory
264    }
265
266    /// The `HEAD` chunk.
267    pub fn head(&self) -> &Head {
268        &self.head
269    }
270
271    /// The `META` chunk.
272    pub fn meta(&self) -> &Meta {
273        &self.meta
274    }
275
276    /// The `PROF` chunk, if present. A file carries at most one.
277    pub fn profile(&self) -> Option<&Profile> {
278        self.profile.as_ref()
279    }
280
281    /// Every sector `META` defines, in the order it lists them.
282    ///
283    /// Sector 0 is implicit and has no entry here.
284    pub fn meta_sectors(&self) -> &[crate::json::Sector] {
285        self.meta.sectors.as_deref().unwrap_or_default()
286    }
287
288    /// Every `PREV` chunk, in file order.
289    pub fn previews(&self) -> &[Preview] {
290        &self.previews
291    }
292
293    /// Every `EXTD` chunk whose frame could be read, in file order.
294    pub fn extensions(&self) -> &[Extension] {
295        &self.extensions
296    }
297
298    /// The `ZDIC` chunk, if present.
299    pub fn dictionary(&self) -> Option<&ZstdDictionary> {
300        self.dictionary.as_ref()
301    }
302
303    /// The `LHAS` chunk, if present.
304    pub fn layer_hashes(&self) -> Option<&LayerHashes> {
305        self.layer_hashes.as_ref()
306    }
307
308    /// The `LTBL` chunk.
309    pub fn layer_table(&self) -> &LayerTable {
310        &self.layer_table
311    }
312
313    /// Every `LAYR` chunk, in directory order.
314    pub fn layr_chunks(&self) -> &[LayrChunk] {
315        &self.layr_chunks
316    }
317
318    /// The `VOXL` payload, if present, verbatim.
319    pub fn voxl(&self) -> Option<&[u8]> {
320        self.voxl.as_deref()
321    }
322
323    /// The `AUTH` chunk, if present.
324    pub fn auth(&self) -> Option<&Auth> {
325        self.auth.as_ref()
326    }
327
328    /// The layer count.
329    pub fn layer_count(&self) -> u32 {
330        self.head.total_layers
331    }
332
333    /// Pixels in one layer mask.
334    pub fn total_pixels(&self) -> u32 {
335        self.head.total_pixels()
336    }
337
338    /// Whether the file declares sector-based layer encoding, i.e. whether some
339    /// layer carries more than one sector (`head.multi_sector_flag`).
340    ///
341    /// This is informational: which sectors a layer carries is read from its
342    /// `LTBL` entries, not from this flag.
343    pub fn multi_sector(&self) -> bool {
344        self.header.multi_sector()
345    }
346
347    /// Whether the validation level this file was opened at is strict.
348    pub fn level(&self) -> Level {
349        self.level
350    }
351
352    /// Whether the file has a session key available.
353    pub fn is_unlocked(&self) -> bool {
354        self.key.is_some()
355    }
356
357    /// The decompressed, decrypted output of the `LAYR` chunk at directory index
358    /// `index`: the concatenation of every slice the table places in it.
359    pub fn layr_chunk_data(&self, index: u32) -> Result<Vec<u8>> {
360        self.with_chunk(index, |data| Ok(data.to_vec()))
361    }
362
363    /// Run `f` over the decompressed output of the `LAYR` chunk at directory
364    /// index `index`, decompressing it first if it is not the cached one.
365    ///
366    /// The cache holds one chunk, so fetching the layers of one chunk is free
367    /// after the first, and walking layers in order costs one decompression per
368    /// chunk a layer touches.
369    fn with_chunk<T>(&self, index: u32, f: impl FnOnce(&[u8]) -> Result<T>) -> Result<T> {
370        if self.cache.borrow().as_ref().map(|(k, _)| *k) != Some(index) {
371            let data = self.decompress_chunk(index)?;
372            *self.cache.borrow_mut() = Some((index, data));
373        }
374        let guard = self.cache.borrow();
375        let (_, data) = guard.as_ref().expect("filled immediately above");
376        f(data)
377    }
378
379    /// The descriptor of the `LAYR` chunk at directory index `index`.
380    fn layr_descriptor(&self, index: u32) -> Result<&ChunkDescriptor> {
381        let descriptor = self
382            .directory
383            .descriptors
384            .get(index as usize)
385            .filter(|d| !d.is_null() && d.chunk_type == ChunkType::LAYR)
386            .ok_or_else(|| {
387                Error::new(
388                    Check::LtblFirstLayrInRange,
389                    format!("directory index {index} is not a LAYR chunk"),
390                )
391            })?;
392        Ok(descriptor)
393    }
394
395    fn decompress_chunk(&self, index: u32) -> Result<Vec<u8>> {
396        let descriptor = self.layr_descriptor(index)?;
397        let stored = chunkio::stored(self.buf, descriptor)?;
398        let frame = chunkio::layr_frame(descriptor, index, stored, self.cipher, self.key.as_ref())?
399            .ok_or_else(|| {
400                Error::new(
401                    Check::CryptNoKey,
402                    "the frame is sealed and no session key is available",
403                )
404            })?;
405        let dict = self.dictionary.as_ref().map(|d| d.dict_bytes.as_slice());
406        // The bound is the file's own: the layers whose entries point into this
407        // chunk, each of which can hold at most a grayscale REE stream of this
408        // display. A frame's bytes cannot say how far it legitimately expands -
409        // a block of repeating layers compresses past any ratio an encoder's own
410        // output could justify - so the layer table is what bounds it, and a
411        // reader that used a ratio instead would refuse conforming files.
412        let bound = chunks::allocation_bound(self.slices_into(index), self.total_pixels());
413        chunks::decompress_frame(&frame, dict, bound)
414    }
415
416    /// How many slices the layer table points into `index`, floored at one.
417    ///
418    /// The same definition the validator uses for `frame.allocation_bound`, so the
419    /// bound this reader allocates against is the bound the specification states:
420    /// an entry that carries bytes counts, an entry that carries none does not,
421    /// and a chunk nothing points into still gets one layer's worth.
422    fn slices_into(&self, index: u32) -> u64 {
423        let count = (0..self.layer_table.layer_count)
424            .map(|layer| {
425                self.layer_table
426                    .layer_entries(layer)
427                    .iter()
428                    .filter(|entry| !entry.is_empty() && entry.first_layr == index)
429                    .count() as u64
430            })
431            .sum::<u64>();
432        count.max(1)
433    }
434
435    /// One entry's slice of its `LAYR` chunk's decompressed output.
436    fn slice(&self, entry: &LayerEntry) -> Result<Vec<u8>> {
437        if entry.is_empty() {
438            return Ok(Vec::new());
439        }
440        let start = entry.data_offset as usize;
441        let len = entry.data_size as usize;
442        self.with_chunk(entry.first_layr, |chunk| {
443            chunk
444                .get(start..start.saturating_add(len))
445                .map(|slice| slice.to_vec())
446                .ok_or_else(|| {
447                    Error::new(
448                        Check::LtblOffsetWithinChunk,
449                        format!(
450                            "a slice of {len} bytes at offset {start} lies outside its chunk's \
451                             {} bytes",
452                            chunk.len()
453                        ),
454                    )
455                })
456        })
457    }
458
459    /// The stored bytes of layer `index`: its sectors' slices, concatenated in
460    /// ascending `sector_id`.
461    ///
462    /// This is the byte range `LHAS` hashes for the layer (section 4.10), which
463    /// is why it is the concatenation rather than any one sector's data.
464    fn layer_data(&self, index: u32) -> Result<Vec<u8>> {
465        let entries = self.entries_of(index)?;
466        let mut out = Vec::new();
467        for entry in entries {
468            out.extend_from_slice(&self.slice(entry)?);
469        }
470        Ok(out)
471    }
472
473    /// The `LTBL` entries of layer `index`.
474    fn entries_of(&self, index: u32) -> Result<&[LayerEntry]> {
475        let entries = self.layer_table.layer_entries(index);
476        if entries.is_empty() {
477            return Err(Error::new(
478                Check::LtblLayerIndexRange,
479                format!("layer {index} does not exist"),
480            ));
481        }
482        Ok(entries)
483    }
484
485    /// The decoded mask of layer `index` as a single-material reader prints it.
486    ///
487    /// That reader reads the layer's first entry - sector 0's, which every layer
488    /// with data has - and skips the rest, so this is sector 0's mask and not the
489    /// union of every sector's. A layer whose first entry carries no data is
490    /// empty here even when a later sector holds data;
491    /// [`LumenFile::is_single_material_complete`] reports that case, and
492    /// [`LumenFile::layer_sectors`] reaches the other sectors.
493    pub fn layer(&self, index: u32) -> Result<DecodedLayer> {
494        let total_pixels = self.total_pixels();
495        let entries = self.entries_of(index)?;
496        let entry = &entries[0];
497        if entry.is_empty() {
498            return Ok(DecodedLayer::empty(total_pixels));
499        }
500        let data = self.slice(entry)?;
501        ree::decode(&data, total_pixels, self.level.is_strict())
502    }
503
504    /// Every sector's decoded mask on layer `index`, in ascending `sector_id`.
505    ///
506    /// A sector with no data on this layer has no mask and is not reported.
507    pub fn layer_sectors(&self, index: u32) -> Result<Vec<DecodedSector>> {
508        let total_pixels = self.total_pixels();
509        let entries = self.entries_of(index)?;
510        let mut out = Vec::new();
511        for entry in entries {
512            if entry.is_empty() {
513                continue;
514            }
515            let data = self.slice(entry)?;
516            out.push(DecodedSector {
517                sector_id: entry.sector_id,
518                layer: ree::decode(&data, total_pixels, self.level.is_strict())?,
519            });
520        }
521        Ok(out)
522    }
523
524    /// Whether a single-material reader would print every layer faithfully.
525    ///
526    /// False when any layer carries data in a sector other than 0, which is
527    /// content such a reader never reads and would drop silently: it must report
528    /// the file incomplete rather than complete (section 7.2).
529    pub fn is_single_material_complete(&self) -> bool {
530        !(0..self.layer_count()).any(|layer| {
531            self.layer_table
532                .layer_entries(layer)
533                .iter()
534                .any(|entry| entry.sector_id != 0 && !entry.is_empty())
535        })
536    }
537
538    /// Resolve one `(layer, sector)`'s timing.
539    ///
540    /// The returned values already carry the sector's `META.sectors` entry, the
541    /// bottom and transition blend with the counts that sector carries or
542    /// inherits, and the `(layer, sector)`'s own `LROV` payload. Overrides are
543    /// not opt-in ([`spec/08-print-control.md`] section 4.5): a reader that does
544    /// not apply them must refuse a file that carries an `LROV` chunk, because
545    /// printing the layer at META's exposure instead of its override fails
546    /// quietly. This method is therefore the only supported way to read a
547    /// layer's timing.
548    pub fn timing_for(&self, layer: u32, sector_id: u32) -> Result<Resolved> {
549        self.entries_of(layer)?;
550        let sector = match sector_id {
551            0 => None,
552            _ => self
553                .meta
554                .sectors
555                .as_deref()
556                .and_then(|sectors| sectors.iter().find(|s| s.sector_id == sector_id)),
557        };
558        let overrides = self.overrides_for(layer, sector_id)?;
559        timing::resolve(&self.meta, sector, overrides.as_ref(), layer, sector_id)
560    }
561
562    /// The `LROV` payload of `(layer, sector)`, when it has one.
563    fn overrides_for(&self, layer: u32, sector_id: u32) -> Result<Option<Timing>> {
564        let Some(entry) = self.layer_table.entry(layer, sector_id) else {
565            return Ok(None);
566        };
567        if entry.first_lrov == 0 {
568            return Ok(None);
569        }
570        let descriptor = self
571            .directory
572            .descriptors
573            .get(entry.first_lrov as usize)
574            .filter(|d| !d.is_null() && d.chunk_type == ChunkType::LROV)
575            .ok_or_else(|| {
576                Error::new(
577                    Check::LtblFirstLrovInRange,
578                    format!(
579                        "layer {layer} sector {sector_id} names directory index {}, which is not \
580                         an LROV chunk",
581                        entry.first_lrov
582                    ),
583                )
584            })?;
585        let stored = chunkio::stored(self.buf, descriptor)?;
586        let bytes = chunkio::payload(descriptor, stored, self.cipher, self.key.as_ref())?
587            .ok_or_else(|| {
588                Error::new(
589                    Check::CryptNoKey,
590                    "the LROV payload is sealed and no session key is available",
591                )
592            })?;
593        Ok(Some(json_chunks::parse_lrov(&bytes)?))
594    }
595
596    /// Verify layer `index` against the `LHAS` leaf table and Merkle path.
597    pub fn verify_layer(&self, index: u32) -> Result<()> {
598        let hashes = self
599            .layer_hashes
600            .as_ref()
601            .ok_or_else(|| Error::new(Check::LhasFrame, "the file carries no LHAS chunk"))?;
602        let stored_leaf = *hashes.layer_hashes.get(index as usize).ok_or_else(|| {
603            Error::new(
604                Check::LhasLayerCount,
605                format!("layer {index} is outside the hash table"),
606            )
607        })?;
608        let data = self.layer_data(index)?;
609        let leaf = lhas::leaf_hash(&data);
610        if leaf != stored_leaf {
611            return Err(Error::new(
612                Check::LhasLeafMatch,
613                format!("layer {index} does not hash to its stored leaf"),
614            ));
615        }
616        let path = lhas::merkle_path(&hashes.layer_hashes, index as usize);
617        if !lhas::verify_leaf(&leaf, &path, index as usize, &hashes.merkle_root) {
618            return Err(Error::new(
619                Check::LhasRootRecompute,
620                format!("layer {index}'s Merkle path does not reach the stored root"),
621            ));
622        }
623        Ok(())
624    }
625
626    /// Verify every layer and the Merkle root.
627    pub fn verify_all(&self) -> Result<()> {
628        let hashes = self
629            .layer_hashes
630            .as_ref()
631            .ok_or_else(|| Error::new(Check::LhasFrame, "the file carries no LHAS chunk"))?;
632        for index in 0..self.layer_count() {
633            let data = self.layer_data(index)?;
634            let leaf = lhas::leaf_hash(&data);
635            match hashes.layer_hashes.get(index as usize) {
636                Some(stored) if *stored == leaf => {}
637                Some(_) => {
638                    return Err(Error::new(
639                        Check::LhasLeafMatch,
640                        format!("layer {index} does not hash to its stored leaf"),
641                    ))
642                }
643                None => {
644                    return Err(Error::new(
645                        Check::LhasLayerCount,
646                        format!("the hash table holds no layer {index}"),
647                    ))
648                }
649            }
650        }
651        if lhas::merkle_root(&hashes.layer_hashes) != hashes.merkle_root {
652            return Err(Error::new(
653                Check::LhasRootRecompute,
654                "the stored Merkle root is not the root of the stored leaf hashes",
655            ));
656        }
657        Ok(())
658    }
659}
660
661/// The chunk-payload reader for one container walk.
662struct Parts<'a> {
663    buf: &'a [u8],
664    dir: &'a Directory,
665    cipher: Option<Cipher>,
666    key: Option<&'a SessionKey>,
667}
668
669impl Parts<'_> {
670    /// A chunk's payload, or `None` when the chunk is absent or unreadable.
671    fn payload(&self, chunk_type: ChunkType) -> Result<Option<Vec<u8>>> {
672        let Some(d) = self.dir.find(chunk_type) else {
673            return Ok(None);
674        };
675        let stored = chunkio::stored(self.buf, d)?;
676        chunkio::payload(d, stored, self.cipher, self.key)
677    }
678
679    /// A required chunk's payload, distinguishing "missing" from "sealed".
680    fn required(&self, chunk_type: ChunkType) -> Result<Vec<u8>> {
681        let Some(bytes) = self.payload(chunk_type)? else {
682            return Err(self.unreadable(chunk_type));
683        };
684        Ok(bytes)
685    }
686
687    /// An optional chunk's payload: `None` when absent, an error when the chunk
688    /// is present but cannot be opened.
689    fn optional_required(&self, chunk_type: ChunkType) -> Result<Option<Vec<u8>>> {
690        if self.dir.find(chunk_type).is_none() {
691            return Ok(None);
692        }
693        match self.payload(chunk_type)? {
694            Some(bytes) => Ok(Some(bytes)),
695            None => Err(self.unreadable(chunk_type)),
696        }
697    }
698
699    /// The error for a chunk that is present but sealed with no key.
700    fn unreadable(&self, chunk_type: ChunkType) -> Error {
701        Error::new(
702            Check::CryptNoKey,
703            format!(
704                "{chunk_type} is sealed and no session key is available; open the file with a password or a recipient key"
705            ),
706        )
707    }
708}
709
710/// Parse the `AUTH` chunk without a key.
711fn auth_of(buf: &[u8]) -> Result<Auth> {
712    let header = FileHeader::parse(buf)?;
713    let directory = container::parse_directory(buf, &header)?;
714    let d = directory
715        .find(ChunkType::AUTH)
716        .ok_or_else(|| Error::new(Check::CryptNoKey, "the file carries no AUTH chunk"))?;
717    crypto::parse_auth(chunkio::stored(buf, d)?)
718}