Skip to main content

lumen/
check.rs

1//! The named conformance checks.
2//!
3//! Every check carries the `<group>.<rule>` name the specification's conformance
4//! corpus uses in `expected_failure` ([`test-vectors/README.md`]), so a failure
5//! reported by this crate can be compared verbatim against the corpus. Names are
6//! stable: see `spec/17-validation.md` section 11, which groups them.
7
8/// A named conformance check, mirroring the corpus convention `<group>.<rule>`.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
10#[non_exhaustive]
11pub enum Check {
12    // -- group: trailer (section 3.3) --------------------------------------
13    /// `LEND` magic is absent from the last 8 bytes.
14    TrailerMagic,
15    /// Trailer CRC-32C does not match `bytes[0 .. file_len-8]`.
16    TrailerCrc32c,
17
18    // -- group: header (section 3.1) ---------------------------------------
19    /// `LUMN` magic is absent at offset 0.
20    HeaderMagic,
21    /// `header.version` is not recognized.
22    HeaderVersion,
23    /// `header.dir_offset` is outside the file, or the directory does not fit.
24    HeaderDirOffset,
25    /// `header.flags` sets a reserved bit (0, 2 or 4).
26    HeaderFlagsReserved,
27    /// `header.total_uncompressed_size` is non-zero and wrong.
28    HeaderTotalUncompressedSize,
29
30    // -- group: dir (section 3.2) ------------------------------------------
31    /// A chunk descriptor could not be read.
32    DirDescriptor,
33    /// Two chunks overlap.
34    DirOverlap,
35    /// A chunk's stored extent lies outside the file, or inside the directory.
36    DirChunkExtent,
37    /// The first chunk is not `HEAD`.
38    DirHeadFirst,
39
40    // -- group: presence (section 3, section 4) ----------------------------
41    /// A required chunk is missing.
42    PresenceHead,
43    /// `META` is missing.
44    PresenceMeta,
45    /// `LTBL` is missing.
46    PresenceLtbl,
47    /// `LAYR` is missing.
48    PresenceLayr,
49    /// The `ENCRYPTED` flag is set but `AUTH` is absent, or vice versa.
50    PresenceAuth,
51    /// Exactly one `ZDIC` is required (or forbidden) by the `LAYR` frames.
52    PresenceZdic,
53
54    // -- group: head (section 4.1) ------------------------------------------
55    /// `head_version` is not recognized.
56    HeadVersion,
57    /// `encoder_name_len` exceeds 256, or the chunk is not exactly the fixed
58    /// fields plus the name.
59    HeadFrame,
60    /// `total_layers == 0`, or disagrees with `LTBL.layer_count`.
61    HeadTotalLayers,
62    /// `layer_height_um == 0`.
63    HeadLayerHeight,
64    /// A build dimension is zero.
65    HeadBuildDims,
66    /// `display_width_px * display_height_px == 0`.
67    HeadDisplayPixels,
68    /// `MULTI_SECTOR` is set without a layer carrying more than one sector, or
69    /// clear while one does.
70    HeadMultiSectorFlag,
71
72    // -- group: auth (section 4.4) -----------------------------------------
73    /// `auth_version` is not recognized.
74    AuthVersion,
75    /// `cipher_id` is not `A256` or `C20P`.
76    AuthCipherKnown,
77    /// The AUTH payload is too short for its declared sections.
78    AuthFrame,
79
80    // -- group: meta (section 4.2) -----------------------------------------
81    /// The META payload is not a JSON object.
82    MetaJson,
83    /// `meta_version` is absent or unrecognized.
84    MetaVersion,
85    /// A required META field is absent.
86    MetaRequiredFields,
87    /// A META duration is not an integer number of milliseconds.
88    MetaTimeInteger,
89    /// An exposure time is not `> 0`.
90    MetaExposure,
91    /// `layer_height_um` is not `> 0`.
92    MetaLayerHeight,
93    /// `materials`, when present, is not a non-empty array of named entries.
94    MetaMaterialsShape,
95    /// `sectors`, when present, is not an array of objects with unique ids >= 1.
96    MetaSectorsShape,
97    /// A sector's `material_index` addresses no non-empty `materials` array.
98    MetaSectorMaterialIndex,
99    /// A cure curve has a non-positive `dp_um`/`ec_mj_cm2` or a negative `e0_mj_cm2`.
100    MetaCureCurve,
101    /// A temperature is outside `[0.0, 120.0]`.
102    MetaTemperatureRange,
103    /// A `light_pwm` or `bottom_light_pwm` is outside `0..=255`, in any payload
104    /// that carries one (section 4.2).
105    PwmRange,
106
107    // -- group: prof (section 4.3) -----------------------------------------
108    /// `profile_type` is not one of `material`, `printer`, `combined`.
109    ProfProfileType,
110    /// `profile_name` or `profile_version` is empty.
111    ProfProfileIdentity,
112    /// A profile duration is not an integer number of milliseconds.
113    ProfSettingsTimeInteger,
114    /// A profile exposure time is not `> 0`.
115    ProfSettingsExposure,
116    /// A profile `layer_height_um` is not `> 0`.
117    ProfSettingsLayerHeight,
118    /// A profile cure curve is out of range.
119    ProfCureCurve,
120    /// `profile_uuid` is not a well-formed UUID string.
121    ProfProfileUuid,
122    /// Profile materials are malformed.
123    ProfMaterialsShape,
124
125    // -- group: lrov (section 4.5) -----------------------------------------
126    /// The payload is not a JSON object.
127    LrovJson,
128    /// An overridden duration is not an integer number of milliseconds.
129    LrovTimeInteger,
130    /// An `LROV` chunk is referenced by no entry. Several entries naming one
131    /// chunk is how an override set covers a range of pairs, and is legal.
132    LrovOrphan,
133
134    // -- group: prev (section 4.6) -----------------------------------------
135    /// A reserved flag bit is set, or the role is outside 0-3.
136    PrevFlags,
137    /// The payload does not begin with the PNG signature (strict mode).
138    PrevPngSignature,
139
140    // -- group: voxl (section 4.11) ----------------------------------------
141    /// The payload is neither VOXL V2 nor a VOXL V1 document (strict mode).
142    VoxlSignature,
143
144    // -- group: extd (section 4.12) ----------------------------------------
145    /// The payload is shorter than the 8-byte frame.
146    ExtdFrame,
147    /// `ext_type` is not four ASCII characters.
148    ExtdExtType,
149    /// A reserved flag bit is set.
150    ExtdFlags,
151    /// An unimplemented extension carries `critical = 1`.
152    ExtdCritical,
153
154    // -- group: ltbl (section 4.7) -----------------------------------------
155    /// `table_version` is not recognized.
156    LtblVersion,
157    /// `layer_count` disagrees with `HEAD.total_layers`.
158    LtblLayerCount,
159    /// `entry_size < 28`, or the entries do not fit the chunk.
160    LtblEntrySize,
161    /// The entries do not account for every layer, or `entry_count` disagrees
162    /// with the sum of `1 + additional_sector_count` over each layer's first
163    /// entry, or a non-first entry carries a non-zero count.
164    LtblEntryCount,
165    /// A read or a table entry names a layer the table does not describe.
166    LtblLayerIndexRange,
167    /// Sector ids do not ascend within a layer.
168    LtblSectorIdsAscending,
169    /// The same sector id appears twice on one layer.
170    LtblSectorIdUnique,
171    /// A layer's first entry is not sector 0's.
172    LtblFirstEntryIsSectorZero,
173    /// `first_layr` is not the directory index of a `LAYR` chunk.
174    LtblFirstLayrInRange,
175    /// An `LROV` chunk no entry names carries a payload no named chunk carries.
176    LtblFirstLrovNull,
177    /// `first_lrov` is neither `0` nor the directory index of an `LROV` chunk.
178    LtblFirstLrovInRange,
179    /// `data_offset + data_size` exceeds the `LAYR` chunk's decompressed output.
180    LtblOffsetWithinChunk,
181    /// Two slices of one `LAYR` chunk overlap.
182    LtblSlicesDisjoint,
183
184    // -- group: layr (section 4.9) ----------------------------------------
185    /// `layr_version` is not recognized.
186    LayrVersion,
187    /// A frame carries no content size, so a reader cannot size its output.
188    LayrContentSizePresent,
189    /// A frame did not decompress to the size it declares.
190    LayrFrameDecompressedSize,
191    /// A frame's declared output exceeds what its stored size may expand to.
192    FrameAllocationBound,
193    /// A frame's dictionary ID disagrees with `ZDIC`.
194    LayrDictIdMatch,
195    /// A frame reports no dictionary while the file carries `ZDIC`.
196    LayrDictIdAbsent,
197
198    // -- group: zdic (section 4.8) -----------------------------------------
199    /// `zdic_version` is not recognized.
200    ZdicVersion,
201    /// `dict_size` exceeds `ZDIC_DICTSIZE_MAX`, or the bytes are missing.
202    ZdicDictSize,
203    /// `dict_id` disagrees with the `LAYR` frames.
204    ZdicDictIdMatch,
205    /// More than one non-null `ZDIC` chunk is present.
206    ZdicSingle,
207
208    // -- group: lhas (section 4.10) ----------------------------------------
209    /// The payload is too short for its header and hashes.
210    LhasFrame,
211    /// `hash_algorithm` is not `0x01` (SHA-256).
212    LhasHashAlgorithm,
213    /// `layer_count` disagrees with `HEAD.total_layers`.
214    LhasLayerCount,
215    /// The recomputed Merkle root differs from the stored one.
216    LhasRootRecompute,
217    /// A layer's recomputed leaf hash differs from the stored one (strict mode).
218    LhasLeafMatch,
219
220    // -- group: ree (section 5) --------------------------------------------
221    /// A layer carries an encoding tag other than `0x00`-`0x03`.
222    ReeTag,
223    /// A varint is overlong, truncated, or longer than 10 bytes.
224    ReeVarint,
225    /// The four plane lengths of a varint array describe planes that are not
226    /// prefix-closed, or that hold more bytes than the array's varints use
227    /// (strict); or a value needs more than four planes to encode.
228    ReePlanes,
229    /// A binary REE `first_value` is neither `0x00` nor `0xFF`.
230    ReeFirstValue,
231    /// The non-canonical `run_count == 0` form (strict mode).
232    ReeNoRunCountZero,
233    /// A stored run length is zero, or the lengths do not sum to `total_pixels` (strict).
234    ReeRunLengths,
235    /// Adjacent grayscale runs carry the same value, or a run is empty (strict).
236    ReeGrayscaleRuns,
237    /// An all-`0x00`/`0xFF` layer is stored as grayscale REE (strict).
238    ReeGrayscaleAllBinary,
239    /// An all-`0x00`/`0xFF` layer is stored as split REE, with no anti-aliasing
240    /// to overlay (strict).
241    ReeSplitAllBinary,
242    /// The split binary component does not threshold at `v >= 128` (strict).
243    ReeSplitThreshold,
244    /// Split overlay positions are not strictly increasing, or out of range.
245    ReeSplitPositions,
246    /// Attached-form escapes are not strictly increasing, fall outside the layer,
247    /// or do not sit strictly inside the core run they belong to.
248    ReeAttachPositions,
249    /// `aa_pixel_count` disagrees with the pixels the attachment bits and escapes
250    /// describe.
251    ReeAttachCount,
252    /// An attachment bit addresses a run that does not exist, or a run of one
253    /// pixel claims both of its ends, or padding bits past the last run are set
254    /// (strict).
255    ReeAttachBits,
256    /// An attached-form overlay value is `0x00`/`0xFF`, or does not threshold to
257    /// the core run it sits in (strict).
258    ReeAttachThreshold,
259    /// The decoded stream does not end exactly at `total_pixels`.
260    ReeEndPositions,
261    /// Bytes follow the end of a layer's REE stream.
262    ReeNoTrailingBytes,
263    /// A layer's `data_size` does not match its decoded stream.
264    ReeDataSize,
265
266    // -- group: sector (section 7) -----------------------------------------
267    /// Sector masks of one layer overlap (strict mode).
268    SectorPartition,
269
270    // -- group: crypt (section 9) ------------------------------------------
271    /// `AUTH.mode` has no bit set.
272    CryptModeEmpty,
273    /// The password section is shorter than its fixed 65 bytes.
274    CryptPasswordSectionLen,
275    /// The machine section holds no complete recipient entry.
276    CryptMachineSectionLen,
277    /// Argon2id parameters exceed the reader's budget.
278    CryptArgon2Budget,
279    /// A chunk's encrypted flag disagrees with the file's `ENCRYPTED` flag.
280    CryptChunkFlags,
281    /// An AEAD tag did not verify.
282    CryptTagVerify,
283    /// A `LAYR` frame's associated data did not bind it to its directory index.
284    CryptUnitIndexBinding,
285    /// A wrapped session key did not unwrap.
286    CryptKeyUnwrap,
287    /// A machine recipient entry is malformed.
288    CryptRecipientEntry,
289    /// An X25519 exchange produced the all-zero shared secret.
290    CryptLowOrderPoint,
291    /// No key could be recovered: wrong password, or no matching recipient.
292    CryptNoKey,
293}
294
295impl Check {
296    /// The `<group>.<rule>` name, verbatim as the conformance corpus spells it.
297    pub fn name(self) -> &'static str {
298        use Check::*;
299        match self {
300            TrailerMagic => "trailer.magic",
301            TrailerCrc32c => "trailer.crc32c",
302            HeaderMagic => "header.magic",
303            HeaderVersion => "header.version",
304            HeaderDirOffset => "header.dir_offset",
305            HeaderFlagsReserved => "header.flags_reserved",
306            HeaderTotalUncompressedSize => "header.total_uncompressed_size",
307            DirDescriptor => "dir.descriptor",
308            DirOverlap => "dir.overlap",
309            DirChunkExtent => "dir.chunk_extent",
310            DirHeadFirst => "dir.head_first",
311            PresenceHead => "presence.head",
312            PresenceMeta => "presence.meta",
313            PresenceLtbl => "presence.ltbl",
314            PresenceLayr => "presence.layr",
315            PresenceAuth => "presence.auth",
316            PresenceZdic => "presence.zdic",
317            HeadVersion => "head.version",
318            HeadFrame => "head.frame",
319            HeadTotalLayers => "head.total_layers",
320            HeadLayerHeight => "head.layer_height",
321            HeadBuildDims => "head.build_dims",
322            HeadDisplayPixels => "head.display_pixels",
323            HeadMultiSectorFlag => "head.multi_sector_flag",
324            AuthVersion => "auth.version",
325            AuthCipherKnown => "auth.cipher_known",
326            AuthFrame => "auth.frame",
327            MetaJson => "meta.json",
328            MetaVersion => "meta.version",
329            MetaRequiredFields => "meta.required_fields",
330            MetaTimeInteger => "meta.time_integer",
331            MetaExposure => "meta.exposure",
332            MetaLayerHeight => "meta.layer_height",
333            MetaMaterialsShape => "meta.materials_shape",
334            MetaSectorsShape => "meta.sectors_shape",
335            MetaSectorMaterialIndex => "meta.sector_material_index",
336            MetaCureCurve => "meta.cure_curve",
337            MetaTemperatureRange => "meta.temperature_range",
338            PwmRange => "pwm.range",
339            ProfProfileType => "prof.profile_type",
340            ProfProfileIdentity => "prof.profile_identity",
341            ProfSettingsTimeInteger => "prof.settings_time_integer",
342            ProfSettingsExposure => "prof.settings_exposure",
343            ProfSettingsLayerHeight => "prof.settings_layer_height",
344            ProfCureCurve => "prof.cure_curve",
345            ProfProfileUuid => "prof.profile_uuid",
346            ProfMaterialsShape => "prof.materials_shape",
347            LrovJson => "lrov.json",
348            LrovTimeInteger => "lrov.time_integer",
349            LrovOrphan => "lrov.orphan",
350            PrevFlags => "prev.flags",
351            PrevPngSignature => "prev.png_signature",
352            VoxlSignature => "voxl.signature",
353            ExtdFrame => "extd.frame",
354            ExtdExtType => "extd.ext_type",
355            ExtdFlags => "extd.flags",
356            ExtdCritical => "extd.critical",
357            LtblVersion => "ltbl.version",
358            LtblLayerCount => "ltbl.layer_count",
359            LtblEntrySize => "ltbl.entry_size",
360            LtblEntryCount => "ltbl.entry_count",
361            LtblLayerIndexRange => "ltbl.layer_index_range",
362            LtblSectorIdsAscending => "ltbl.sector_ids_ascending",
363            LtblSectorIdUnique => "ltbl.sector_id_unique",
364            LtblFirstEntryIsSectorZero => "ltbl.first_entry_is_sector_zero",
365            LtblFirstLayrInRange => "ltbl.first_layr_in_range",
366            LtblFirstLrovNull => "ltbl.first_lrov_null",
367            LtblFirstLrovInRange => "ltbl.first_lrov_in_range",
368            LtblOffsetWithinChunk => "ltbl.offset_within_chunk",
369            LtblSlicesDisjoint => "ltbl.slices_disjoint",
370            LayrVersion => "layr.version",
371            LayrContentSizePresent => "layr.content_size_present",
372            LayrFrameDecompressedSize => "layr.frame_decompressed_size",
373            FrameAllocationBound => "frame.allocation_bound",
374            LayrDictIdMatch => "layr.dict_id_match",
375            LayrDictIdAbsent => "layr.dict_id_absent",
376            ZdicVersion => "zdic.version",
377            ZdicDictSize => "zdic.dict_size",
378            ZdicDictIdMatch => "zdic.dict_id_match",
379            ZdicSingle => "zdic.single",
380            LhasFrame => "lhas.frame",
381            LhasHashAlgorithm => "lhas.hash_algorithm",
382            LhasLayerCount => "lhas.layer_count",
383            LhasRootRecompute => "lhas.root_recompute",
384            LhasLeafMatch => "lhas.leaf_match",
385            ReeTag => "ree.tag",
386            ReeVarint => "ree.varint",
387            ReePlanes => "ree.planes",
388            ReeFirstValue => "ree.first_value",
389            ReeNoRunCountZero => "ree.no_run_count_zero",
390            ReeRunLengths => "ree.run_lengths",
391            ReeGrayscaleRuns => "ree.grayscale_runs",
392            ReeGrayscaleAllBinary => "ree.grayscale_all_binary",
393            ReeSplitAllBinary => "ree.split_all_binary",
394            ReeSplitThreshold => "ree.split_threshold",
395            ReeSplitPositions => "ree.split_positions",
396            ReeAttachPositions => "ree.attach_positions",
397            ReeAttachCount => "ree.attach_count",
398            ReeAttachBits => "ree.attach_bits",
399            ReeAttachThreshold => "ree.attach_threshold",
400            ReeEndPositions => "ree.end_positions",
401            ReeNoTrailingBytes => "ree.no_trailing_bytes",
402            ReeDataSize => "ree.data_size",
403            SectorPartition => "sector.partition",
404            CryptModeEmpty => "crypt.mode_empty",
405            CryptPasswordSectionLen => "crypt.password_section_len",
406            CryptMachineSectionLen => "crypt.machine_section_len",
407            CryptArgon2Budget => "crypt.argon2_budget",
408            CryptChunkFlags => "crypt.chunk_flags",
409            CryptTagVerify => "crypt.tag_verify",
410            CryptUnitIndexBinding => "crypt.unit_index_binding",
411            CryptKeyUnwrap => "crypt.key_unwrap",
412            CryptRecipientEntry => "crypt.recipient_entry",
413            CryptLowOrderPoint => "crypt.low_order_point",
414            CryptNoKey => "crypt.no_key",
415        }
416    }
417
418    /// Whether the specification marks this check strict-mode only ([section 11.5]).
419    ///
420    /// A loose-mode read must not fail these; a strict validator must.
421    pub fn is_strict_only(self) -> bool {
422        use Check::*;
423        matches!(
424            self,
425            ReeNoRunCountZero
426                | ReePlanes
427                | ReeRunLengths
428                | ReeGrayscaleRuns
429                | ReeGrayscaleAllBinary
430                | ReeSplitAllBinary
431                | ReeSplitThreshold
432                | ReeAttachBits
433                | ReeAttachThreshold
434                | SectorPartition
435                | LhasLeafMatch
436                | PrevPngSignature
437                | VoxlSignature
438        )
439    }
440}
441
442impl core::fmt::Display for Check {
443    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
444        f.write_str(self.name())
445    }
446}