1use crate::check::Check;
32use crate::chunkio;
33use crate::chunks::extd::{Extension, CRITICAL_BIT, EXTD_RESERVED_MASK};
34use crate::chunks::head::Head;
35use crate::chunks::layr;
36use crate::chunks::lhas::{self, LayerHashes};
37use crate::chunks::ltbl::{LayerEntry, LayerTable};
38use crate::chunks::preview::{self, PreviewRole};
39use crate::chunks::voxl;
40use crate::chunks::zdic::ZstdDictionary;
41use crate::chunks::{self};
42use crate::container::{
43 self, ChunkDescriptor, ChunkType as Tag, Directory, FileHeader, CHUNK_FLAG_ENCRYPTED,
44 FILE_HEADER_SIZE,
45};
46use crate::crypto::{self, Cipher, SessionKey};
47use crate::error::{Error, Result};
48use crate::json::{Material, Meta, Profile, Timing, REQUIRED_META_FIELDS};
49use crate::ree;
50use serde_json::{Map, Value};
51use std::borrow::Cow;
52use std::cell::{Cell, RefCell};
53
54const HEADER_MUST_BE_ZERO: u32 = (1 << 0) | (1 << 2) | (1 << 4);
59
60const PWM_MAX: u32 = 255;
63
64const TIME_MS_FIELDS: [&str; 8] = [
72 "normal_exposure_ms",
73 "bottom_exposure_ms",
74 "wait_time_before_cure_ms",
75 "wait_time_after_cure_ms",
76 "wait_time_after_lift_ms",
77 "bottom_wait_time_before_cure_ms",
78 "bottom_wait_time_after_cure_ms",
79 "bottom_wait_time_after_lift_ms",
80];
81
82const ESTIMATED_PRINT_TIME_SEC: &str = "estimated_print_time_sec";
88
89const CONTENT_CHUNKS: &[Tag] = &[
92 Tag::META,
93 Tag::PROF,
94 Tag::LROV,
95 Tag::VOXL,
96 Tag::ZDIC,
97 Tag::LAYR,
98];
99
100const CLEARTEXT_CHUNKS: &[Tag] = &[Tag::HEAD, Tag::AUTH, Tag::LTBL, Tag::LHAS];
103
104fn expose(
112 covered: &mut [u64],
113 mine: &mut [u64],
114 start: u64,
115 end: u64,
116 layer: u32,
117 sector: u32,
118) -> Result<()> {
119 if start >= end {
120 return Ok(());
121 }
122 let first = start as usize / 64;
123 let last = (end - 1) as usize / 64;
124 for word in first..=last {
125 let low = if word == first {
126 start as usize % 64
127 } else {
128 0
129 };
130 let high = if word == last {
131 (end - 1) as usize % 64
132 } else {
133 63
134 };
135 let bits = (u64::MAX << low) & (u64::MAX >> (63 - high));
136 if covered[word] & bits & !mine[word] != 0 {
137 return Err(Error::new(
138 Check::SectorPartition,
139 format!(
140 "layer {layer}: sector {sector} exposes a pixel another sector already exposes"
141 ),
142 ));
143 }
144 covered[word] |= bits;
145 mine[word] |= bits;
146 }
147 Ok(())
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
152pub enum Level {
153 #[default]
156 Loose,
157 Strict,
160}
161
162impl Level {
163 pub fn is_strict(self) -> bool {
165 matches!(self, Level::Strict)
166 }
167}
168
169#[derive(Debug, Clone, Default)]
171pub struct Validator {
172 level: Level,
173 key: Option<SessionKey>,
174}
175
176impl Validator {
177 pub fn new(level: Level) -> Validator {
179 Validator { level, key: None }
180 }
181
182 pub fn loose() -> Validator {
184 Validator::new(Level::Loose)
185 }
186
187 pub fn strict() -> Validator {
189 Validator::new(Level::Strict)
190 }
191
192 pub fn with_key(mut self, key: SessionKey) -> Validator {
195 self.key = Some(key);
196 self
197 }
198
199 pub fn level(&self) -> Level {
201 self.level
202 }
203
204 pub fn key(&self) -> Option<&SessionKey> {
206 self.key.as_ref()
207 }
208
209 pub fn validate(&self, buf: &[u8]) -> Result<()> {
211 let mut ctx = Ctx::new(buf, self.level, self.key)?;
212 ctx.check_framing()?;
213 ctx.check_chunk_flags()?;
214
215 let head = ctx.read_hdr()?;
216 check_hdr(&head)?;
217 ctx.display_pixels.set(head.total_pixels());
218
219 let auth = ctx.read_auth()?;
220 ctx.cipher = auth.as_ref().map(|a| a.cipher);
221
222 let meta = ctx.read_meta()?;
223 if let Some(meta) = meta.as_ref() {
224 check_meta(meta)?;
225 }
226 let profile = ctx.read_profile()?;
227 if let Some(profile) = profile.as_ref() {
228 check_profile(profile)?;
229 }
230
231 ctx.check_lrov_payloads()?;
232
233 ctx.check_previews()?;
234 ctx.check_extensions()?;
235 ctx.check_voxl()?;
236
237 let ltbl = ctx.read_ltbl()?;
238 let zdic = ctx.read_zdic()?;
239 ctx.check_layer_data(&head, <bl, &zdic)?;
240 let hashes = ctx.check_hashes(&head)?;
241 if let Some(hashes) = hashes.as_ref() {
242 ctx.check_leaves(<bl, &zdic, hashes)?;
243 }
244
245 if let (Some(key), Some(cipher)) = (ctx.key.as_ref(), ctx.cipher) {
246 ctx.check_sealed_units(key, cipher)?;
247 }
248 Ok(())
249 }
250}
251
252pub fn validate(buf: &[u8], level: Level) -> Result<()> {
254 Validator::new(level).validate(buf)
255}
256
257pub fn validate_with_key(buf: &[u8], level: Level, key: SessionKey) -> Result<()> {
259 Validator::new(level).with_key(key).validate(buf)
260}
261
262struct Ctx<'a> {
264 buf: &'a [u8],
265 header: FileHeader,
266 dir: Directory,
267 level: Level,
268 key: Option<SessionKey>,
269 cipher: Option<Cipher>,
270 cache: RefCell<Option<(u32, Vec<u8>)>>,
274 display_pixels: Cell<u32>,
278}
279
280impl<'a> Ctx<'a> {
281 fn new(buf: &'a [u8], level: Level, key: Option<SessionKey>) -> Result<Ctx<'a>> {
282 container::verify_trailer_crc(buf)?;
283 let header = FileHeader::parse(buf)?;
284 let dir = container::parse_directory(buf, &header)?;
285 Ok(Ctx {
286 buf,
287 header,
288 dir,
289 level,
290 key,
291 cipher: None,
292 cache: RefCell::new(None),
293 display_pixels: Cell::new(0),
294 })
295 }
296
297 fn stored(&self, d: &ChunkDescriptor) -> Result<&'a [u8]> {
299 chunkio::stored(self.buf, d)
300 }
301
302 fn plaintext(&self, d: &ChunkDescriptor) -> Result<Option<Vec<u8>>> {
305 let stored = self.stored(d)?;
306 chunkio::plaintext(d, stored, self.cipher, self.key.as_ref())
307 }
308
309 fn payload(&self, d: &ChunkDescriptor) -> Result<Option<Vec<u8>>> {
312 let stored = self.stored(d)?;
313 chunkio::payload(d, stored, self.cipher, self.key.as_ref())
314 }
315
316 fn check_framing(&mut self) -> Result<()> {
319 if self.header.flags & HEADER_MUST_BE_ZERO != 0 {
320 return Err(Error::new(
321 Check::HeaderFlagsReserved,
322 format!(
323 "header.flags {:#010x} sets a reserved bit",
324 self.header.flags
325 ),
326 ));
327 }
328
329 let dir_start = self.header.dir_offset;
330 let mut ranges: Vec<(u64, u64, Tag)> = Vec::new();
331 for d in self.dir.entries() {
332 let (start, end) = d.extent();
333 if start < FILE_HEADER_SIZE as u64 || end > dir_start {
334 return Err(Error::new(
335 Check::DirChunkExtent,
336 format!(
337 "chunk {} occupies {start}..{end}, outside the {}..{dir_start} payload region",
338 d.chunk_type, FILE_HEADER_SIZE
339 ),
340 ));
341 }
342 ranges.push((start, end, d.chunk_type));
343 }
344 ranges.sort_by_key(|r| r.0);
345 for pair in ranges.windows(2) {
346 if pair[1].0 < pair[0].1 {
347 return Err(Error::new(
348 Check::DirOverlap,
349 format!(
350 "chunk {} at {}..{} overlaps chunk {} at {}..{}",
351 pair[0].2, pair[0].0, pair[0].1, pair[1].2, pair[1].0, pair[1].1
352 ),
353 ));
354 }
355 }
356
357 if self.header.total_uncompressed_size != 0 {
358 let sum = self.dir.total_uncompressed_size();
359 if sum != self.header.total_uncompressed_size {
360 return Err(Error::new(
361 Check::HeaderTotalUncompressedSize,
362 format!(
363 "header declares {} uncompressed bytes, the directory sums to {sum}",
364 self.header.total_uncompressed_size
365 ),
366 ));
367 }
368 }
369
370 if self.dir.find(Tag::HEAD).is_none() {
371 return Err(Error::new(Check::PresenceHead, "no HEAD chunk"));
372 }
373 match self.dir.first_entry_index() {
374 Some(i) if self.dir.descriptors[i].chunk_type == Tag::HEAD => {}
375 _ => {
376 return Err(Error::new(
377 Check::DirHeadFirst,
378 "the first chunk is not HEAD",
379 ))
380 }
381 }
382 if self.dir.find(Tag::META).is_none() {
383 return Err(Error::new(Check::PresenceMeta, "no META chunk"));
384 }
385 if self.dir.find(Tag::LTBL).is_none() {
386 return Err(Error::new(Check::PresenceLtbl, "no LTBL chunk"));
387 }
388 let tables = self.dir.find_all(Tag::LTBL).count();
392 if tables > 1 {
393 return Err(Error::new(
394 Check::PresenceLtbl,
395 format!("{tables} LTBL chunks; a file carries exactly one layer table"),
396 ));
397 }
398 if self.dir.find(Tag::LAYR).is_none() {
399 return Err(Error::new(Check::PresenceLayr, "no LAYR chunk"));
400 }
401 if self.header.encrypted() && self.dir.find(Tag::AUTH).is_none() {
402 return Err(Error::new(
403 Check::PresenceAuth,
404 "the header sets ENCRYPTED but the file carries no AUTH chunk",
405 ));
406 }
407 Ok(())
408 }
409
410 fn check_chunk_flags(&self) -> Result<()> {
412 let encrypted_file = self.header.encrypted();
413 for d in self.dir.entries() {
414 let sealed = d.flags & CHUNK_FLAG_ENCRYPTED != 0;
415 if !encrypted_file {
416 if sealed {
417 return Err(Error::new(
418 Check::CryptChunkFlags,
419 format!(
420 "{} sets the sealed bit, but the file is not encrypted and no key could open it",
421 d.chunk_type
422 ),
423 ));
424 }
425 continue;
426 }
427 if CLEARTEXT_CHUNKS.contains(&d.chunk_type) {
428 if sealed {
429 return Err(Error::new(
430 Check::CryptChunkFlags,
431 format!("{} must stay plaintext", d.chunk_type),
432 ));
433 }
434 } else if CONTENT_CHUNKS.contains(&d.chunk_type) && !sealed {
435 return Err(Error::new(
436 Check::CryptChunkFlags,
437 format!(
438 "{} is content and must be sealed in an encrypted file",
439 d.chunk_type
440 ),
441 ));
442 }
443 }
444 Ok(())
445 }
446
447 fn read_hdr(&self) -> Result<Head> {
450 let d = self.dir.find(Tag::HEAD).expect("presence checked");
451 let payload = self
452 .plaintext(d)?
453 .expect("HEAD is never sealed, so it is always readable");
454 Head::parse(&payload)
455 }
456
457 fn read_auth(&self) -> Result<Option<crypto::Auth>> {
458 let Some(d) = self.dir.find(Tag::AUTH) else {
459 return Ok(None);
460 };
461 let payload = self
462 .plaintext(d)?
463 .expect("AUTH is never sealed, so it is always readable");
464 let auth = crypto::parse_auth(&payload)?;
465 if let Some(password) = auth.password.as_ref() {
466 crypto::check_argon2_budget(password)?;
469 }
470 Ok(Some(auth))
471 }
472
473 fn layr_chunks(&self) -> Vec<(u32, ChunkDescriptor)> {
475 self.dir
476 .descriptors
477 .iter()
478 .enumerate()
479 .filter(|(_, d)| !d.is_null() && d.chunk_type == Tag::LAYR)
480 .map(|(index, d)| (index as u32, *d))
481 .collect()
482 }
483
484 fn layr_frame<'b>(&'b self, index: u32, d: &ChunkDescriptor) -> Result<Option<Cow<'b, [u8]>>> {
490 chunkio::layr_frame(
491 d,
492 index,
493 chunkio::stored(self.buf, d)?,
494 self.cipher,
495 self.key.as_ref(),
496 )
497 }
498
499 fn with_chunk<T>(
502 &self,
503 index: u32,
504 dictionary: Option<&[u8]>,
505 f: impl FnOnce(&[u8]) -> Result<T>,
506 ) -> Result<T> {
507 if self.cache.borrow().as_ref().map(|(k, _)| *k) != Some(index) {
508 let data = {
509 let descriptor = self.layr_descriptor(index)?;
510 let frame = self.layr_frame(index, descriptor)?.ok_or_else(|| {
511 Error::new(
512 Check::CryptNoKey,
513 "the frame is sealed and no session key is available",
514 )
515 })?;
516 let bound = self.chunk_bound(index, frame.len());
523 chunks::decompress_frame(&frame, dictionary, bound)?
524 };
525 *self.cache.borrow_mut() = Some((index, data));
526 }
527 let guard = self.cache.borrow();
528 let (_, data) = guard.as_ref().expect("filled immediately above");
529 f(data)
530 }
531
532 fn chunk_bound(&self, index: u32, frame_len: usize) -> u64 {
539 let pixels = self.display_pixels.get();
540 if pixels == 0 {
541 return (frame_len as u64).saturating_mul(chunks::MAX_ZSTD_RATIO as u64);
545 }
546 chunks::allocation_bound(self.slices_into(index), pixels)
547 }
548
549 fn slices_into(&self, index: u32) -> u64 {
550 let Ok(Some(table)) = self.read_ltbl() else {
551 return 1;
552 };
553 let count = table
554 .entries
555 .iter()
556 .filter(|entry| !entry.is_empty() && entry.first_layr == index)
557 .count() as u64;
558 count.max(1)
559 }
560
561 fn layr_descriptor(&self, index: u32) -> Result<&ChunkDescriptor> {
563 self.dir
564 .descriptors
565 .get(index as usize)
566 .filter(|d| !d.is_null() && d.chunk_type == Tag::LAYR)
567 .ok_or_else(|| {
568 Error::new(
569 Check::LtblFirstLayrInRange,
570 format!("directory index {index} is not a LAYR chunk"),
571 )
572 })
573 }
574
575 fn slice_of(&self, entry: &LayerEntry, dictionary: Option<&[u8]>) -> Result<Vec<u8>> {
577 if entry.is_empty() {
578 return Ok(Vec::new());
579 }
580 let start = entry.data_offset as usize;
581 let len = entry.data_size as usize;
582 self.with_chunk(entry.first_layr, dictionary, |chunk| {
583 chunk
584 .get(start..start.saturating_add(len))
585 .map(|slice| slice.to_vec())
586 .ok_or_else(|| {
587 Error::new(
588 Check::LtblOffsetWithinChunk,
589 format!(
590 "a slice of {len} bytes at offset {start} lies outside its chunk's {} bytes",
591 chunk.len()
592 ),
593 )
594 })
595 })
596 }
597
598 fn layer_bytes(&self, entries: &[LayerEntry], dictionary: Option<&[u8]>) -> Result<Vec<u8>> {
601 let mut out = Vec::new();
602 for entry in entries {
603 out.extend_from_slice(&self.slice_of(entry, dictionary)?);
604 }
605 Ok(out)
606 }
607
608 fn read_ltbl(&self) -> Result<Option<LayerTable>> {
609 match self.dir.find(Tag::LTBL) {
610 None => Ok(None),
611 Some(d) => {
612 let payload = self
613 .plaintext(d)?
614 .expect("LTBL is never sealed, so it is always readable");
615 Ok(Some(LayerTable::parse(&payload)?))
616 }
617 }
618 }
619
620 fn read_zdic(&self) -> Result<Option<ZstdDictionary>> {
621 let mut found = self.dir.find_all(Tag::ZDIC);
622 let Some(d) = found.next() else {
623 return Ok(None);
624 };
625 if found.next().is_some() {
626 return Err(Error::new(
627 Check::ZdicSingle,
628 "more than one ZDIC chunk is present",
629 ));
630 }
631 match self.plaintext(d)? {
632 None => Ok(None),
633 Some(raw) => Ok(Some(ZstdDictionary::parse(&raw)?)),
634 }
635 }
636
637 fn read_meta(&self) -> Result<Option<Meta>> {
642 let Some(d) = self.dir.find(Tag::META) else {
643 return Ok(None);
644 };
645 let Some(bytes) = self.payload(d)? else {
646 return Ok(None);
647 };
648 let value: Value = serde_json::from_slice(&bytes).map_err(json_error)?;
649 let object = value
650 .as_object()
651 .ok_or_else(|| Error::new(Check::MetaJson, "META is not a JSON object"))?;
652 let missing = Timing::missing_from(object, &REQUIRED_META_FIELDS);
653 if !missing.is_empty() {
654 return Err(Error::new(
655 Check::MetaRequiredFields,
656 format!("META is missing {}", missing.join(", ")),
657 ));
658 }
659 check_integer_durations(
660 object,
661 TIME_MS_FIELDS
662 .iter()
663 .copied()
664 .chain(std::iter::once(ESTIMATED_PRINT_TIME_SEC)),
665 Check::MetaTimeInteger,
666 )?;
667 check_sectors_shape(object)?;
668 let meta: Meta = serde_json::from_value(value).map_err(json_error)?;
669 Ok(Some(meta))
670 }
671
672 fn read_profile(&self) -> Result<Option<Profile>> {
673 let Some(d) = self.dir.find(Tag::PROF) else {
674 return Ok(None);
675 };
676 let Some(bytes) = self.payload(d)? else {
677 return Ok(None);
678 };
679 let value: Value = serde_json::from_slice(&bytes).map_err(json_error)?;
680 if let Some(settings) = value.get("settings").and_then(Value::as_object) {
684 check_integer_durations(
685 settings,
686 TIME_MS_FIELDS.iter().copied(),
687 Check::ProfSettingsTimeInteger,
688 )?;
689 }
690 Ok(Some(serde_json::from_value(value).map_err(json_error)?))
691 }
692
693 fn check_lrov_payloads(&self) -> Result<()> {
698 for d in self.dir.find_all(Tag::LROV).collect::<Vec<_>>() {
699 let Some(bytes) = self.payload(d)? else {
700 continue;
701 };
702 let value: Value = serde_json::from_slice(&bytes).map_err(|e| {
703 Error::new(
704 Check::LrovJson,
705 format!("the LROV payload is not a valid object: {e}"),
706 )
707 })?;
708 let object = value
709 .as_object()
710 .ok_or_else(|| Error::new(Check::LrovJson, "LROV is not a JSON object"))?;
711 check_integer_durations(
712 object,
713 TIME_MS_FIELDS.iter().copied(),
714 Check::LrovTimeInteger,
715 )?;
716 check_pwm_object(object, "LROV")?;
717 }
718 Ok(())
719 }
720
721 fn check_previews(&self) -> Result<()> {
722 for d in self.dir.find_all(Tag::PREV).collect::<Vec<_>>() {
723 PreviewRole::from_flags(d.flags)?;
724 if self.level.is_strict() {
725 if let Some(bytes) = self.payload(d)? {
726 preview::check_png(&bytes)?;
727 }
728 }
729 }
730 Ok(())
731 }
732
733 fn check_extensions(&self) -> Result<()> {
740 for d in self.dir.find_all(Tag::EXTD).collect::<Vec<_>>() {
741 if d.is_encrypted() {
742 check_opaque_extension(d.flags)?;
743 continue;
744 }
745 let Some(frame) = self.payload(d)? else {
746 continue;
747 };
748 let ext = Extension::parse(&frame, d.flags)?;
749 check_extension(&ext)?;
750 }
751 Ok(())
752 }
753
754 fn check_voxl(&self) -> Result<()> {
755 if !self.level.is_strict() {
756 return Ok(());
757 }
758 for d in self.dir.find_all(Tag::VOXL).collect::<Vec<_>>() {
759 if let Some(bytes) = self.payload(d)? {
760 voxl::require_voxl(&bytes)?;
761 }
762 }
763 Ok(())
764 }
765
766 fn check_layer_data(
776 &self,
777 head: &Head,
778 ltbl: &Option<LayerTable>,
779 zdic: &Option<ZstdDictionary>,
780 ) -> Result<()> {
781 let Some(ltbl) = ltbl.as_ref() else {
782 return Ok(());
783 };
784 let total_layers = head.total_layers;
785 let total_pixels = head.total_pixels();
786 let dict_bytes = zdic.as_ref().map(|z| z.dict_bytes.as_slice());
787
788 if ltbl.layer_count != total_layers {
789 return Err(Error::new(
790 Check::LtblLayerCount,
791 format!(
792 "LTBL describes {} layers, HEAD says {total_layers}",
793 ltbl.layer_count
794 ),
795 ));
796 }
797
798 if self.header.multi_sector() != ltbl.is_multi_sector() {
802 return Err(Error::new(
803 Check::HeadMultiSectorFlag,
804 if self.header.multi_sector() {
805 "MULTI_SECTOR is set but no layer carries more than one sector".to_string()
806 } else {
807 "a layer carries more than one sector but MULTI_SECTOR is clear".to_string()
808 },
809 ));
810 }
811
812 let mut slices_per_chunk: std::collections::HashMap<u32, u64> =
815 std::collections::HashMap::new();
816 for entry in <bl.entries {
817 if !entry.is_empty() {
818 *slices_per_chunk.entry(entry.first_layr).or_insert(0) += 1;
819 }
820 }
821
822 let chunks = self.layr_chunks();
827 let mut output_size: Vec<Option<u64>> = vec![None; self.dir.descriptors.len()];
828 let mut dictionary_used = false;
829 let mut unreadable = false;
830 for (index, descriptor) in &chunks {
831 let stored = self.stored(descriptor)?;
832 if descriptor.flags & CHUNK_FLAG_ENCRYPTED != 0
834 && stored.len() < layr::LAYR_HEADER_LEN + crypto::UNIT_OVERHEAD
835 {
836 return Err(Error::new(
837 Check::CryptChunkFlags,
838 format!(
839 "the frame at directory index {index} is {} bytes, smaller than one \
840 sealed unit",
841 stored.len()
842 ),
843 ));
844 }
845 let Some(frame) = self.layr_frame(*index, descriptor)? else {
846 unreadable = true;
849 continue;
850 };
851 let size = chunks::frame_content_size(&frame)?;
852 output_size[*index as usize] = Some(size);
853 let slices = slices_per_chunk.get(index).copied().unwrap_or(0).max(1);
854 let bound = chunks::allocation_bound(slices, total_pixels);
855 if size > bound {
856 return Err(Error::new(
857 Check::FrameAllocationBound,
858 format!(
859 "the frame at directory index {index} declares {size} bytes; {slices} \
860 slices of {total_pixels} pixels bound it at {bound}"
861 ),
862 ));
863 }
864 match (chunks::frame_dict_id(&frame)?, zdic.as_ref()) {
865 (0, _) => {}
870 (id, Some(z)) if id == z.dict_id => dictionary_used = true,
871 (id, None) => {
875 return Err(Error::new(
876 Check::LayrDictIdAbsent,
877 format!(
878 "the frame at directory index {index} reports dictionary {id} but \
879 the file carries no ZDIC"
880 ),
881 ))
882 }
883 (id, Some(z)) => {
884 return Err(Error::new(
885 Check::LayrDictIdMatch,
886 format!(
887 "the frame at directory index {index} reports dictionary {id}, ZDIC \
888 declares {}",
889 z.dict_id
890 ),
891 ))
892 }
893 }
894 }
895 if !unreadable && zdic.is_some() && !dictionary_used {
896 return Err(Error::new(
897 Check::PresenceZdic,
898 "ZDIC is present but no LAYR frame uses a dictionary",
899 ));
900 }
901
902 let mut slices: Vec<(u32, u64, u64)> = Vec::with_capacity(ltbl.entries.len());
904 let mut references: std::collections::HashMap<u32, u32> = std::collections::HashMap::new();
905 for (i, entry) in ltbl.entries.iter().enumerate() {
906 if self.layr_descriptor(entry.first_layr).is_err() {
910 return Err(Error::new(
911 Check::LtblFirstLayrInRange,
912 format!(
913 "entry {i} names directory index {} for its run, which is not a LAYR chunk",
914 entry.first_layr
915 ),
916 ));
917 }
918 if let Some(size) = output_size
919 .get(entry.first_layr as usize)
920 .copied()
921 .flatten()
922 {
923 let end = entry.data_offset.saturating_add(u64::from(entry.data_size));
924 if end > size {
925 return Err(Error::new(
926 Check::LtblOffsetWithinChunk,
927 format!(
928 "entry {i} claims {} bytes at offset {}, past its chunk's {size} \
929 bytes",
930 entry.data_size, entry.data_offset
931 ),
932 ));
933 }
934 if !entry.is_empty() {
935 slices.push((entry.first_layr, entry.data_offset, end));
936 }
937 }
938 if entry.first_lrov != 0 {
939 let is_lrov = self
940 .dir
941 .descriptors
942 .get(entry.first_lrov as usize)
943 .is_some_and(|d| !d.is_null() && d.chunk_type == Tag::LROV);
944 if !is_lrov {
945 return Err(Error::new(
946 Check::LtblFirstLrovInRange,
947 format!(
948 "entry {i} names directory index {} for its overrides, which is not \
949 an LROV chunk",
950 entry.first_lrov
951 ),
952 ));
953 }
954 *references.entry(entry.first_lrov).or_insert(0) += 1;
955 }
956 }
957
958 slices.sort_unstable();
960 for pair in slices.windows(2) {
961 if pair[0].0 == pair[1].0 && pair[1].1 < pair[0].2 {
962 return Err(Error::new(
963 Check::LtblSlicesDisjoint,
964 format!(
965 "two slices of the chunk at directory index {} overlap: {}..{} and \
966 {}..{}",
967 pair[0].0, pair[0].1, pair[0].2, pair[1].1, pair[1].2
968 ),
969 ));
970 }
971 }
972
973 let mut named_payloads: Vec<Vec<u8>> = Vec::new();
981 let mut unnamed: Vec<usize> = Vec::new();
982 for (index, descriptor) in self.dir.descriptors.iter().enumerate() {
983 if descriptor.is_null() || descriptor.chunk_type != Tag::LROV {
984 continue;
985 }
986 if references.get(&(index as u32)).copied().unwrap_or(0) > 0 {
987 if let Ok(Some(plain)) = self.plaintext(descriptor) {
988 named_payloads.push(plain);
989 }
990 } else {
991 unnamed.push(index);
992 }
993 }
994 for index in unnamed {
995 let descriptor = &self.dir.descriptors[index];
996 let Ok(Some(plain)) = self.plaintext(descriptor) else {
997 continue;
1001 };
1002 if !named_payloads.contains(&plain) {
1003 return Err(Error::new(
1004 Check::LtblFirstLrovNull,
1005 format!(
1006 "the LROV chunk at directory index {index} carries overrides that no entry \
1007 applies, so an entry's first_lrov of 0 does not hold"
1008 ),
1009 ));
1010 }
1011 }
1012
1013 for (index, descriptor) in self.dir.descriptors.iter().enumerate() {
1018 if descriptor.is_null() || descriptor.chunk_type != Tag::LROV {
1019 continue;
1020 }
1021 if references.get(&(index as u32)).copied().unwrap_or(0) == 0 {
1022 return Err(Error::new(
1023 Check::LrovOrphan,
1024 format!("the LROV chunk at directory index {index} is referenced by no entry"),
1025 ));
1026 }
1027 }
1028
1029 for (index, _) in &chunks {
1031 if output_size
1032 .get(*index as usize)
1033 .copied()
1034 .flatten()
1035 .is_none()
1036 {
1037 continue;
1038 }
1039 self.with_chunk(*index, dict_bytes, |plain| {
1040 for (i, entry) in ltbl.entries.iter().enumerate() {
1041 if entry.first_layr != *index || entry.is_empty() {
1042 continue;
1043 }
1044 let start = entry.data_offset as usize;
1045 let end = start.checked_add(entry.data_size as usize).ok_or_else(|| {
1046 Error::new(Check::LtblOffsetWithinChunk, "the slice range overflows")
1047 })?;
1048 let data = plain.get(start..end).ok_or_else(|| {
1049 Error::new(
1050 Check::LtblOffsetWithinChunk,
1051 format!("entry {i} runs past its chunk's decompressed output"),
1052 )
1053 })?;
1054 self.check_layer_stream(i, data, total_pixels)?;
1055 }
1056 Ok(())
1057 })?;
1058 }
1059
1060 self.check_partition(ltbl, total_pixels, dict_bytes)
1061 }
1062
1063 fn check_layer_stream(&self, i: usize, data: &[u8], total_pixels: u32) -> Result<()> {
1065 let used = ree::validate_stream(data, total_pixels, self.level.is_strict())?;
1066 if used != data.len() {
1067 return Err(Error::new(
1068 Check::ReeNoTrailingBytes,
1069 format!(
1070 "entry {i} stores {} bytes but its stream uses {used}",
1071 data.len()
1072 ),
1073 ));
1074 }
1075 Ok(())
1076 }
1077
1078 fn check_partition(
1089 &self,
1090 ltbl: &LayerTable,
1091 total_pixels: u32,
1092 dictionary: Option<&[u8]>,
1093 ) -> Result<()> {
1094 if !self.level.is_strict() || !self.layer_chunks_readable() {
1095 return Ok(());
1096 }
1097 let words = (total_pixels as usize).div_ceil(64);
1104 let mut covered: Vec<u64> = Vec::new();
1105 let mut mine: Vec<u64> = Vec::new();
1106 for layer in 0..ltbl.layer_count {
1107 let carrying: Vec<&LayerEntry> = ltbl
1108 .layer_entries(layer)
1109 .iter()
1110 .filter(|entry| !entry.is_empty())
1111 .collect();
1112 if carrying.len() < 2 {
1113 continue;
1114 }
1115 if covered.len() != words {
1116 covered.resize(words, 0);
1117 mine.resize(words, 0);
1118 }
1119 covered.fill(0);
1120 for entry in carrying {
1121 mine.fill(0);
1122 let sector = entry.sector_id;
1123 let data = self.slice_of(entry, dictionary)?;
1124 ree::exposed_ranges(&data, total_pixels, |start, end| {
1125 expose(&mut covered, &mut mine, start, end, layer, sector)
1126 })?;
1127 }
1128 }
1129 Ok(())
1130 }
1131
1132 fn layer_chunks_readable(&self) -> bool {
1137 self.key.is_some() || self.layr_chunks().iter().all(|(_, d)| !d.is_encrypted())
1138 }
1139
1140 fn check_hashes(&self, head: &Head) -> Result<Option<LayerHashes>> {
1142 let Some(d) = self.dir.find(Tag::LHAS) else {
1143 return Ok(None);
1144 };
1145 let payload = self
1146 .plaintext(d)?
1147 .expect("LHAS is never sealed, so it is always readable");
1148 let hashes = LayerHashes::parse(&payload)?;
1149 if hashes.layer_count != head.total_layers {
1150 return Err(Error::new(
1151 Check::LhasLayerCount,
1152 format!(
1153 "LHAS covers {} layers, HEAD says {}",
1154 hashes.layer_count, head.total_layers
1155 ),
1156 ));
1157 }
1158 let recomputed = lhas::merkle_root(&hashes.layer_hashes);
1159 if recomputed != hashes.merkle_root {
1160 return Err(Error::new(
1161 Check::LhasRootRecompute,
1162 "the stored Merkle root is not the root of the stored leaf hashes",
1163 ));
1164 }
1165 Ok(Some(hashes))
1166 }
1167
1168 fn check_leaves(
1175 &self,
1176 ltbl: &Option<LayerTable>,
1177 zdic: &Option<ZstdDictionary>,
1178 hashes: &LayerHashes,
1179 ) -> Result<()> {
1180 if !self.level.is_strict() || !self.layer_chunks_readable() {
1181 return Ok(());
1182 }
1183 let Some(ltbl) = ltbl.as_ref() else {
1184 return Ok(());
1185 };
1186 let dict_bytes = zdic.as_ref().map(|z| z.dict_bytes.as_slice());
1187 for layer in 0..ltbl.layer_count {
1188 let Some(stored_leaf) = hashes.layer_hashes.get(layer as usize) else {
1189 continue;
1190 };
1191 let data = self.layer_bytes(ltbl.layer_entries(layer), dict_bytes)?;
1193 if lhas::leaf_hash(&data) != *stored_leaf {
1194 return Err(Error::new(
1195 Check::LhasLeafMatch,
1196 format!("layer {layer} does not hash to its stored leaf"),
1197 ));
1198 }
1199 }
1200 Ok(())
1201 }
1202
1203 fn check_sealed_units(&self, key: &SessionKey, cipher: Cipher) -> Result<()> {
1208 for (index, d) in self.dir.descriptors.iter().enumerate() {
1209 if d.is_null() || d.flags & CHUNK_FLAG_ENCRYPTED == 0 {
1210 continue;
1211 }
1212 if d.chunk_type == Tag::LAYR {
1213 self.layr_frame(index as u32, d)?;
1214 } else {
1215 crypto::open(cipher, key, d.chunk_type, 0, self.stored(d)?)?;
1216 }
1217 }
1218 Ok(())
1219 }
1220}
1221
1222fn json_error(err: serde_json::Error) -> Error {
1223 Error::new(Check::MetaJson, err.to_string())
1224}
1225
1226fn check_integer_durations<'a>(
1238 object: &Map<String, Value>,
1239 names: impl Iterator<Item = &'a str>,
1240 check: Check,
1241) -> Result<()> {
1242 for name in names {
1243 let Some(value) = object.get(name) else {
1244 continue;
1245 };
1246 if value.is_number() && value.as_u64().is_none_or(|n| n > u64::from(u32::MAX)) {
1251 return Err(Error::new(
1252 check,
1253 format!("{name} is {value}, which is not a whole number of milliseconds"),
1254 ));
1255 }
1256 }
1257 Ok(())
1258}
1259
1260fn check_hdr(head: &Head) -> Result<()> {
1262 if head.total_layers == 0 {
1263 return Err(Error::new(
1264 Check::HeadTotalLayers,
1265 "HEAD declares no layers",
1266 ));
1267 }
1268 if head.layer_height_um == 0 {
1269 return Err(Error::new(
1270 Check::HeadLayerHeight,
1271 "layer_height_um is zero",
1272 ));
1273 }
1274 if head.build_width_um == 0 || head.build_depth_um == 0 || head.build_height_um == 0 {
1275 return Err(Error::new(
1276 Check::HeadBuildDims,
1277 "a build dimension is zero",
1278 ));
1279 }
1280 if head.total_pixels() == 0 {
1281 return Err(Error::new(
1282 Check::HeadDisplayPixels,
1283 "the display has no pixels",
1284 ));
1285 }
1286 Ok(())
1287}
1288
1289fn check_pwm(timing: &Timing, at: &str) -> Result<()> {
1295 for (name, value) in [
1296 ("light_pwm", timing.light_pwm),
1297 ("bottom_light_pwm", timing.bottom_light_pwm),
1298 ] {
1299 if let Some(value) = value {
1300 if value > PWM_MAX {
1301 return Err(Error::new(
1302 Check::PwmRange,
1303 format!("{at}: {name} is {value}, outside 0..={PWM_MAX}"),
1304 ));
1305 }
1306 }
1307 }
1308 Ok(())
1309}
1310
1311fn check_pwm_object(object: &Map<String, Value>, at: &str) -> Result<()> {
1314 for name in ["light_pwm", "bottom_light_pwm"] {
1315 let Some(value) = object.get(name) else {
1316 continue;
1317 };
1318 if value.is_null() {
1319 continue;
1320 }
1321 if !value
1322 .as_u64()
1323 .is_some_and(|duty| duty <= u64::from(PWM_MAX))
1324 {
1325 return Err(Error::new(
1326 Check::PwmRange,
1327 format!("{at}: {name} is {value}, outside the integer range 0..={PWM_MAX}"),
1328 ));
1329 }
1330 }
1331 Ok(())
1332}
1333
1334fn check_meta(meta: &Meta) -> Result<()> {
1336 match meta.meta_version {
1337 Some(1) => {}
1338 Some(other) => {
1339 return Err(Error::new(
1340 Check::MetaVersion,
1341 format!("unsupported meta_version {other}"),
1342 ))
1343 }
1344 None => return Err(Error::new(Check::MetaVersion, "no meta_version")),
1345 }
1346 let timing = &meta.timing;
1347 if timing.normal_exposure_ms.unwrap_or(0) == 0 {
1348 return Err(Error::new(
1349 Check::MetaExposure,
1350 "normal_exposure_ms is not positive",
1351 ));
1352 }
1353 if timing.bottom_exposure_ms.unwrap_or(0) == 0 {
1354 return Err(Error::new(
1355 Check::MetaExposure,
1356 "bottom_exposure_ms is not positive",
1357 ));
1358 }
1359 if timing.layer_height_um.unwrap_or(0) == 0 {
1360 return Err(Error::new(
1361 Check::MetaLayerHeight,
1362 "layer_height_um is not positive",
1363 ));
1364 }
1365 if let Some(materials) = meta.materials.as_ref() {
1366 check_materials(materials, Check::MetaMaterialsShape)?;
1367 for sector in meta.sectors.as_deref().unwrap_or_default() {
1370 if let Some(index) = sector.material_index {
1371 if index as usize >= materials.len() {
1372 return Err(Error::new(
1373 Check::MetaSectorMaterialIndex,
1374 format!(
1375 "sector {} names material {index}, which the material library does \
1376 not contain",
1377 sector.sector_id
1378 ),
1379 ));
1380 }
1381 }
1382 }
1383 }
1384 if let Some(curve) = timing.cure_curve.as_ref() {
1385 check_cure_curve(curve, Check::MetaCureCurve)?;
1386 }
1387 check_pwm(timing, "META")?;
1388 for sector in meta.sectors.as_deref().unwrap_or_default() {
1389 check_pwm(
1390 §or.timing,
1391 &format!("META.sectors entry for sector {}", sector.sector_id),
1392 )?;
1393 }
1394 for temperature in [timing.chamber_temperature_c, timing.vat_temperature_c]
1395 .into_iter()
1396 .flatten()
1397 {
1398 if !(0.0..=120.0).contains(&temperature) {
1399 return Err(Error::new(
1400 Check::MetaTemperatureRange,
1401 format!("temperature {temperature} is outside 0..=120 degrees Celsius"),
1402 ));
1403 }
1404 }
1405 Ok(())
1406}
1407
1408fn check_profile(profile: &Profile) -> Result<()> {
1410 match profile.profile_type.as_deref() {
1411 Some("material") | Some("printer") | Some("combined") => {}
1412 Some(other) => {
1413 return Err(Error::new(
1414 Check::ProfProfileType,
1415 format!("unknown profile_type {other:?}"),
1416 ))
1417 }
1418 None => return Err(Error::new(Check::ProfProfileType, "no profile_type")),
1419 }
1420 if profile.profile_name.is_empty() || profile.profile_version.is_empty() {
1421 return Err(Error::new(
1422 Check::ProfProfileIdentity,
1423 "profile_name and profile_version must be non-empty",
1424 ));
1425 }
1426 if profile.settings.normal_exposure_ms.unwrap_or(0) == 0
1427 || profile.settings.bottom_exposure_ms.unwrap_or(0) == 0
1428 {
1429 return Err(Error::new(
1430 Check::ProfSettingsExposure,
1431 "a profile exposure time is not positive",
1432 ));
1433 }
1434 if profile.settings.layer_height_um.unwrap_or(0) == 0 {
1435 return Err(Error::new(
1436 Check::ProfSettingsLayerHeight,
1437 "the profile's layer_height_um is not positive",
1438 ));
1439 }
1440 check_pwm(&profile.settings, "PROF.settings")?;
1441 if let Some(curve) = profile.settings.cure_curve.as_ref() {
1442 check_cure_curve(curve, Check::ProfCureCurve)?;
1443 }
1444 if let Some(uuid) = profile.profile_uuid.as_deref() {
1445 if !is_uuid(uuid) {
1446 return Err(Error::new(
1447 Check::ProfProfileUuid,
1448 format!("{uuid:?} is not a UUID"),
1449 ));
1450 }
1451 }
1452 if let Some(materials) = profile.materials.as_ref() {
1453 check_materials(materials, Check::ProfMaterialsShape)?;
1454 }
1455 Ok(())
1456}
1457
1458fn check_materials(materials: &[Material], check: Check) -> Result<()> {
1460 if materials.is_empty() {
1461 return Err(Error::new(check, "the array is empty"));
1462 }
1463 if let Some(index) = materials.iter().position(|m| m.name.is_empty()) {
1464 return Err(Error::new(check, format!("material {index} has no name")));
1465 }
1466 Ok(())
1467}
1468
1469fn check_sectors_shape(object: &Map<String, Value>) -> Result<()> {
1477 let Some(sectors) = object.get("sectors") else {
1478 return Ok(());
1479 };
1480 let Some(entries) = sectors.as_array() else {
1481 return Err(Error::new(
1482 Check::MetaSectorsShape,
1483 "META.sectors is not an array",
1484 ));
1485 };
1486 let mut seen: Vec<u64> = Vec::with_capacity(entries.len());
1487 for (i, entry) in entries.iter().enumerate() {
1488 let Some(entry) = entry.as_object() else {
1489 return Err(Error::new(
1490 Check::MetaSectorsShape,
1491 format!("META.sectors[{i}] is not an object"),
1492 ));
1493 };
1494 let Some(id) = entry.get("sector_id").and_then(Value::as_u64) else {
1495 return Err(Error::new(
1496 Check::MetaSectorsShape,
1497 format!("META.sectors[{i}] carries no integer sector_id"),
1498 ));
1499 };
1500 if id == 0 {
1501 return Err(Error::new(
1502 Check::MetaSectorsShape,
1503 format!(
1504 "META.sectors[{i}] uses sector_id 0, which names the implicit primary sector"
1505 ),
1506 ));
1507 }
1508 if id > u64::from(u32::MAX) {
1509 return Err(Error::new(
1510 Check::MetaSectorsShape,
1511 format!("META.sectors[{i}] uses sector_id {id}, which does not fit a u32"),
1512 ));
1513 }
1514 if seen.contains(&id) {
1515 return Err(Error::new(
1516 Check::MetaSectorsShape,
1517 format!("sector_id {id} appears twice"),
1518 ));
1519 }
1520 seen.push(id);
1521 check_integer_durations(
1522 entry,
1523 TIME_MS_FIELDS.iter().copied(),
1524 Check::MetaTimeInteger,
1525 )?;
1526 }
1527 Ok(())
1528}
1529
1530fn check_cure_curve(curve: &crate::json::CureCurve, check: Check) -> Result<()> {
1534 if curve.dp_um.is_nan() || curve.dp_um <= 0.0 {
1535 return Err(Error::new(check, "dp_um is not positive"));
1536 }
1537 if curve.ec_mj_cm2.is_nan() || curve.ec_mj_cm2 <= 0.0 {
1538 return Err(Error::new(check, "ec_mj_cm2 is not positive"));
1539 }
1540 if curve.e0_mj_cm2.is_nan() || curve.e0_mj_cm2 < 0.0 {
1541 return Err(Error::new(check, "e0_mj_cm2 is negative"));
1542 }
1543 Ok(())
1544}
1545
1546fn is_uuid(text: &str) -> bool {
1547 let bytes = text.as_bytes();
1548 if bytes.len() != 36 {
1549 return false;
1550 }
1551 for (i, b) in bytes.iter().enumerate() {
1552 let dashed = matches!(i, 8 | 13 | 18 | 23);
1553 if dashed {
1554 if *b != b'-' {
1555 return false;
1556 }
1557 } else if !b.is_ascii_hexdigit() {
1558 return false;
1559 }
1560 }
1561 true
1562}
1563
1564fn check_extension(ext: &Extension) -> Result<()> {
1566 if ext.critical && !ext.is_implemented() {
1567 return Err(Error::new(
1568 Check::ExtdCritical,
1569 format!(
1570 "extension {}{:?} is critical and unimplemented",
1571 ext.tag(),
1572 ext.vendor_id
1573 ),
1574 ));
1575 }
1576 Ok(())
1577}
1578
1579fn check_opaque_extension(flags: u32) -> Result<()> {
1582 if flags & EXTD_RESERVED_MASK != 0 {
1583 return Err(Error::new(
1584 Check::ExtdFlags,
1585 format!("EXTD flags {flags:#010x} set a reserved bit"),
1586 ));
1587 }
1588 if flags & CRITICAL_BIT != 0 {
1589 return Err(Error::new(
1590 Check::ExtdCritical,
1591 "a critical extension is unimplemented",
1592 ));
1593 }
1594 Ok(())
1595}