1use 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#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct DecodedSector {
46 pub sector_id: u32,
48 pub layer: DecodedLayer,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct LayrChunk {
55 pub index: u32,
58 pub descriptor: ChunkDescriptor,
60}
61
62#[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 pub fn open_unvalidated(buf: &'a [u8]) -> Result<LumenFile<'a>> {
95 LumenFile::assemble(buf, None, None)
96 }
97
98 pub fn open(buf: &'a [u8], level: Level) -> Result<LumenFile<'a>> {
100 LumenFile::assemble(buf, Some(level), None)
101 }
102
103 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 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 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 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 level: level.unwrap_or_default(),
229 cache: RefCell::new(None),
230 })
231 }
232
233 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 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 pub fn bytes(&self) -> &'a [u8] {
253 self.buf
254 }
255
256 pub fn header(&self) -> &FileHeader {
258 &self.header
259 }
260
261 pub fn directory(&self) -> &Directory {
263 &self.directory
264 }
265
266 pub fn head(&self) -> &Head {
268 &self.head
269 }
270
271 pub fn meta(&self) -> &Meta {
273 &self.meta
274 }
275
276 pub fn profile(&self) -> Option<&Profile> {
278 self.profile.as_ref()
279 }
280
281 pub fn meta_sectors(&self) -> &[crate::json::Sector] {
285 self.meta.sectors.as_deref().unwrap_or_default()
286 }
287
288 pub fn previews(&self) -> &[Preview] {
290 &self.previews
291 }
292
293 pub fn extensions(&self) -> &[Extension] {
295 &self.extensions
296 }
297
298 pub fn dictionary(&self) -> Option<&ZstdDictionary> {
300 self.dictionary.as_ref()
301 }
302
303 pub fn layer_hashes(&self) -> Option<&LayerHashes> {
305 self.layer_hashes.as_ref()
306 }
307
308 pub fn layer_table(&self) -> &LayerTable {
310 &self.layer_table
311 }
312
313 pub fn layr_chunks(&self) -> &[LayrChunk] {
315 &self.layr_chunks
316 }
317
318 pub fn voxl(&self) -> Option<&[u8]> {
320 self.voxl.as_deref()
321 }
322
323 pub fn auth(&self) -> Option<&Auth> {
325 self.auth.as_ref()
326 }
327
328 pub fn layer_count(&self) -> u32 {
330 self.head.total_layers
331 }
332
333 pub fn total_pixels(&self) -> u32 {
335 self.head.total_pixels()
336 }
337
338 pub fn multi_sector(&self) -> bool {
344 self.header.multi_sector()
345 }
346
347 pub fn level(&self) -> Level {
349 self.level
350 }
351
352 pub fn is_unlocked(&self) -> bool {
354 self.key.is_some()
355 }
356
357 pub fn layr_chunk_data(&self, index: u32) -> Result<Vec<u8>> {
360 self.with_chunk(index, |data| Ok(data.to_vec()))
361 }
362
363 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 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 let bound = chunks::allocation_bound(self.slices_into(index), self.total_pixels());
413 chunks::decompress_frame(&frame, dict, bound)
414 }
415
416 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 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 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 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 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 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 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 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 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 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 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
661struct Parts<'a> {
663 buf: &'a [u8],
664 dir: &'a Directory,
665 cipher: Option<Cipher>,
666 key: Option<&'a SessionKey>,
667}
668
669impl Parts<'_> {
670 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 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 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 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
710fn 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}