lumen-format 1.0.0

Reference encoder, decoder and validator for the LUMEN resin print file format (v1.0)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
//! The named conformance checks.
//!
//! Every check carries the `<group>.<rule>` name the specification's conformance
//! corpus uses in `expected_failure` ([`test-vectors/README.md`]), so a failure
//! reported by this crate can be compared verbatim against the corpus. Names are
//! stable: see `spec/17-validation.md` section 11, which groups them.

/// A named conformance check, mirroring the corpus convention `<group>.<rule>`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum Check {
    // -- group: trailer (section 3.3) --------------------------------------
    /// `LEND` magic is absent from the last 8 bytes.
    TrailerMagic,
    /// Trailer CRC-32C does not match `bytes[0 .. file_len-8]`.
    TrailerCrc32c,

    // -- group: header (section 3.1) ---------------------------------------
    /// `LUMN` magic is absent at offset 0.
    HeaderMagic,
    /// `header.version` is not recognized.
    HeaderVersion,
    /// `header.dir_offset` is outside the file, or the directory does not fit.
    HeaderDirOffset,
    /// `header.flags` sets a reserved bit (0, 2 or 4).
    HeaderFlagsReserved,
    /// `header.total_uncompressed_size` is non-zero and wrong.
    HeaderTotalUncompressedSize,

    // -- group: dir (section 3.2) ------------------------------------------
    /// A chunk descriptor could not be read.
    DirDescriptor,
    /// Two chunks overlap.
    DirOverlap,
    /// A chunk's stored extent lies outside the file, or inside the directory.
    DirChunkExtent,
    /// The first chunk is not `HEAD`.
    DirHeadFirst,

    // -- group: presence (section 3, section 4) ----------------------------
    /// A required chunk is missing.
    PresenceHead,
    /// `META` is missing.
    PresenceMeta,
    /// `LTBL` is missing.
    PresenceLtbl,
    /// `LAYR` is missing.
    PresenceLayr,
    /// The `ENCRYPTED` flag is set but `AUTH` is absent, or vice versa.
    PresenceAuth,
    /// Exactly one `ZDIC` is required (or forbidden) by the `LAYR` frames.
    PresenceZdic,

    // -- group: head (section 4.1) ------------------------------------------
    /// `head_version` is not recognized.
    HeadVersion,
    /// `encoder_name_len` exceeds 256, or the chunk is not exactly the fixed
    /// fields plus the name.
    HeadFrame,
    /// `total_layers == 0`, or disagrees with `LTBL.layer_count`.
    HeadTotalLayers,
    /// `layer_height_um == 0`.
    HeadLayerHeight,
    /// A build dimension is zero.
    HeadBuildDims,
    /// `display_width_px * display_height_px == 0`.
    HeadDisplayPixels,
    /// `MULTI_SECTOR` is set without a layer carrying more than one sector, or
    /// clear while one does.
    HeadMultiSectorFlag,

    // -- group: auth (section 4.4) -----------------------------------------
    /// `auth_version` is not recognized.
    AuthVersion,
    /// `cipher_id` is not `A256` or `C20P`.
    AuthCipherKnown,
    /// The AUTH payload is too short for its declared sections.
    AuthFrame,

    // -- group: meta (section 4.2) -----------------------------------------
    /// The META payload is not a JSON object.
    MetaJson,
    /// `meta_version` is absent or unrecognized.
    MetaVersion,
    /// A required META field is absent.
    MetaRequiredFields,
    /// A META duration is not an integer number of milliseconds.
    MetaTimeInteger,
    /// An exposure time is not `> 0`.
    MetaExposure,
    /// `layer_height_um` is not `> 0`.
    MetaLayerHeight,
    /// `materials`, when present, is not a non-empty array of named entries.
    MetaMaterialsShape,
    /// `sectors`, when present, is not an array of objects with unique ids >= 1.
    MetaSectorsShape,
    /// A sector's `material_index` addresses no non-empty `materials` array.
    MetaSectorMaterialIndex,
    /// A cure curve has a non-positive `dp_um`/`ec_mj_cm2` or a negative `e0_mj_cm2`.
    MetaCureCurve,
    /// A temperature is outside `[0.0, 120.0]`.
    MetaTemperatureRange,
    /// A `light_pwm` or `bottom_light_pwm` is outside `0..=255`, in any payload
    /// that carries one (section 4.2).
    PwmRange,

    // -- group: prof (section 4.3) -----------------------------------------
    /// `profile_type` is not one of `material`, `printer`, `combined`.
    ProfProfileType,
    /// `profile_name` or `profile_version` is empty.
    ProfProfileIdentity,
    /// A profile duration is not an integer number of milliseconds.
    ProfSettingsTimeInteger,
    /// A profile exposure time is not `> 0`.
    ProfSettingsExposure,
    /// A profile `layer_height_um` is not `> 0`.
    ProfSettingsLayerHeight,
    /// A profile cure curve is out of range.
    ProfCureCurve,
    /// `profile_uuid` is not a well-formed UUID string.
    ProfProfileUuid,
    /// Profile materials are malformed.
    ProfMaterialsShape,

    // -- group: lrov (section 4.5) -----------------------------------------
    /// The payload is not a JSON object.
    LrovJson,
    /// An overridden duration is not an integer number of milliseconds.
    LrovTimeInteger,
    /// An `LROV` chunk is referenced by no entry. Several entries naming one
    /// chunk is how an override set covers a range of pairs, and is legal.
    LrovOrphan,

    // -- group: prev (section 4.6) -----------------------------------------
    /// A reserved flag bit is set, or the role is outside 0-3.
    PrevFlags,
    /// The payload does not begin with the PNG signature (strict mode).
    PrevPngSignature,

    // -- group: voxl (section 4.11) ----------------------------------------
    /// The payload is neither VOXL V2 nor a VOXL V1 document (strict mode).
    VoxlSignature,

    // -- group: extd (section 4.12) ----------------------------------------
    /// The payload is shorter than the 8-byte frame.
    ExtdFrame,
    /// `ext_type` is not four ASCII characters.
    ExtdExtType,
    /// A reserved flag bit is set.
    ExtdFlags,
    /// An unimplemented extension carries `critical = 1`.
    ExtdCritical,

    // -- group: ltbl (section 4.7) -----------------------------------------
    /// `table_version` is not recognized.
    LtblVersion,
    /// `layer_count` disagrees with `HEAD.total_layers`.
    LtblLayerCount,
    /// `entry_size < 28`, or the entries do not fit the chunk.
    LtblEntrySize,
    /// The entries do not account for every layer, or `entry_count` disagrees
    /// with the sum of `1 + additional_sector_count` over each layer's first
    /// entry, or a non-first entry carries a non-zero count.
    LtblEntryCount,
    /// A read or a table entry names a layer the table does not describe.
    LtblLayerIndexRange,
    /// Sector ids do not ascend within a layer.
    LtblSectorIdsAscending,
    /// The same sector id appears twice on one layer.
    LtblSectorIdUnique,
    /// A layer's first entry is not sector 0's.
    LtblFirstEntryIsSectorZero,
    /// `first_layr` is not the directory index of a `LAYR` chunk.
    LtblFirstLayrInRange,
    /// An `LROV` chunk no entry names carries a payload no named chunk carries.
    LtblFirstLrovNull,
    /// `first_lrov` is neither `0` nor the directory index of an `LROV` chunk.
    LtblFirstLrovInRange,
    /// `data_offset + data_size` exceeds the `LAYR` chunk's decompressed output.
    LtblOffsetWithinChunk,
    /// Two slices of one `LAYR` chunk overlap.
    LtblSlicesDisjoint,

    // -- group: layr (section 4.9) ----------------------------------------
    /// `layr_version` is not recognized.
    LayrVersion,
    /// A frame carries no content size, so a reader cannot size its output.
    LayrContentSizePresent,
    /// A frame did not decompress to the size it declares.
    LayrFrameDecompressedSize,
    /// A frame's declared output exceeds what its stored size may expand to.
    FrameAllocationBound,
    /// A frame's dictionary ID disagrees with `ZDIC`.
    LayrDictIdMatch,
    /// A frame reports no dictionary while the file carries `ZDIC`.
    LayrDictIdAbsent,

    // -- group: zdic (section 4.8) -----------------------------------------
    /// `zdic_version` is not recognized.
    ZdicVersion,
    /// `dict_size` exceeds `ZDIC_DICTSIZE_MAX`, or the bytes are missing.
    ZdicDictSize,
    /// `dict_id` disagrees with the `LAYR` frames.
    ZdicDictIdMatch,
    /// More than one non-null `ZDIC` chunk is present.
    ZdicSingle,

    // -- group: lhas (section 4.10) ----------------------------------------
    /// The payload is too short for its header and hashes.
    LhasFrame,
    /// `hash_algorithm` is not `0x01` (SHA-256).
    LhasHashAlgorithm,
    /// `layer_count` disagrees with `HEAD.total_layers`.
    LhasLayerCount,
    /// The recomputed Merkle root differs from the stored one.
    LhasRootRecompute,
    /// A layer's recomputed leaf hash differs from the stored one (strict mode).
    LhasLeafMatch,

    // -- group: ree (section 5) --------------------------------------------
    /// A layer carries an encoding tag other than `0x00`-`0x03`.
    ReeTag,
    /// A varint is overlong, truncated, or longer than 10 bytes.
    ReeVarint,
    /// The four plane lengths of a varint array describe planes that are not
    /// prefix-closed, or that hold more bytes than the array's varints use
    /// (strict); or a value needs more than four planes to encode.
    ReePlanes,
    /// A binary REE `first_value` is neither `0x00` nor `0xFF`.
    ReeFirstValue,
    /// The non-canonical `run_count == 0` form (strict mode).
    ReeNoRunCountZero,
    /// A stored run length is zero, or the lengths do not sum to `total_pixels` (strict).
    ReeRunLengths,
    /// Adjacent grayscale runs carry the same value, or a run is empty (strict).
    ReeGrayscaleRuns,
    /// An all-`0x00`/`0xFF` layer is stored as grayscale REE (strict).
    ReeGrayscaleAllBinary,
    /// An all-`0x00`/`0xFF` layer is stored as split REE, with no anti-aliasing
    /// to overlay (strict).
    ReeSplitAllBinary,
    /// The split binary component does not threshold at `v >= 128` (strict).
    ReeSplitThreshold,
    /// Split overlay positions are not strictly increasing, or out of range.
    ReeSplitPositions,
    /// Attached-form escapes are not strictly increasing, fall outside the layer,
    /// or do not sit strictly inside the core run they belong to.
    ReeAttachPositions,
    /// `aa_pixel_count` disagrees with the pixels the attachment bits and escapes
    /// describe.
    ReeAttachCount,
    /// An attachment bit addresses a run that does not exist, or a run of one
    /// pixel claims both of its ends, or padding bits past the last run are set
    /// (strict).
    ReeAttachBits,
    /// An attached-form overlay value is `0x00`/`0xFF`, or does not threshold to
    /// the core run it sits in (strict).
    ReeAttachThreshold,
    /// The decoded stream does not end exactly at `total_pixels`.
    ReeEndPositions,
    /// Bytes follow the end of a layer's REE stream.
    ReeNoTrailingBytes,
    /// A layer's `data_size` does not match its decoded stream.
    ReeDataSize,

    // -- group: sector (section 7) -----------------------------------------
    /// Sector masks of one layer overlap (strict mode).
    SectorPartition,

    // -- group: crypt (section 9) ------------------------------------------
    /// `AUTH.mode` has no bit set.
    CryptModeEmpty,
    /// The password section is shorter than its fixed 65 bytes.
    CryptPasswordSectionLen,
    /// The machine section holds no complete recipient entry.
    CryptMachineSectionLen,
    /// Argon2id parameters exceed the reader's budget.
    CryptArgon2Budget,
    /// A chunk's encrypted flag disagrees with the file's `ENCRYPTED` flag.
    CryptChunkFlags,
    /// An AEAD tag did not verify.
    CryptTagVerify,
    /// A `LAYR` frame's associated data did not bind it to its directory index.
    CryptUnitIndexBinding,
    /// A wrapped session key did not unwrap.
    CryptKeyUnwrap,
    /// A machine recipient entry is malformed.
    CryptRecipientEntry,
    /// An X25519 exchange produced the all-zero shared secret.
    CryptLowOrderPoint,
    /// No key could be recovered: wrong password, or no matching recipient.
    CryptNoKey,
}

impl Check {
    /// The `<group>.<rule>` name, verbatim as the conformance corpus spells it.
    pub fn name(self) -> &'static str {
        use Check::*;
        match self {
            TrailerMagic => "trailer.magic",
            TrailerCrc32c => "trailer.crc32c",
            HeaderMagic => "header.magic",
            HeaderVersion => "header.version",
            HeaderDirOffset => "header.dir_offset",
            HeaderFlagsReserved => "header.flags_reserved",
            HeaderTotalUncompressedSize => "header.total_uncompressed_size",
            DirDescriptor => "dir.descriptor",
            DirOverlap => "dir.overlap",
            DirChunkExtent => "dir.chunk_extent",
            DirHeadFirst => "dir.head_first",
            PresenceHead => "presence.head",
            PresenceMeta => "presence.meta",
            PresenceLtbl => "presence.ltbl",
            PresenceLayr => "presence.layr",
            PresenceAuth => "presence.auth",
            PresenceZdic => "presence.zdic",
            HeadVersion => "head.version",
            HeadFrame => "head.frame",
            HeadTotalLayers => "head.total_layers",
            HeadLayerHeight => "head.layer_height",
            HeadBuildDims => "head.build_dims",
            HeadDisplayPixels => "head.display_pixels",
            HeadMultiSectorFlag => "head.multi_sector_flag",
            AuthVersion => "auth.version",
            AuthCipherKnown => "auth.cipher_known",
            AuthFrame => "auth.frame",
            MetaJson => "meta.json",
            MetaVersion => "meta.version",
            MetaRequiredFields => "meta.required_fields",
            MetaTimeInteger => "meta.time_integer",
            MetaExposure => "meta.exposure",
            MetaLayerHeight => "meta.layer_height",
            MetaMaterialsShape => "meta.materials_shape",
            MetaSectorsShape => "meta.sectors_shape",
            MetaSectorMaterialIndex => "meta.sector_material_index",
            MetaCureCurve => "meta.cure_curve",
            MetaTemperatureRange => "meta.temperature_range",
            PwmRange => "pwm.range",
            ProfProfileType => "prof.profile_type",
            ProfProfileIdentity => "prof.profile_identity",
            ProfSettingsTimeInteger => "prof.settings_time_integer",
            ProfSettingsExposure => "prof.settings_exposure",
            ProfSettingsLayerHeight => "prof.settings_layer_height",
            ProfCureCurve => "prof.cure_curve",
            ProfProfileUuid => "prof.profile_uuid",
            ProfMaterialsShape => "prof.materials_shape",
            LrovJson => "lrov.json",
            LrovTimeInteger => "lrov.time_integer",
            LrovOrphan => "lrov.orphan",
            PrevFlags => "prev.flags",
            PrevPngSignature => "prev.png_signature",
            VoxlSignature => "voxl.signature",
            ExtdFrame => "extd.frame",
            ExtdExtType => "extd.ext_type",
            ExtdFlags => "extd.flags",
            ExtdCritical => "extd.critical",
            LtblVersion => "ltbl.version",
            LtblLayerCount => "ltbl.layer_count",
            LtblEntrySize => "ltbl.entry_size",
            LtblEntryCount => "ltbl.entry_count",
            LtblLayerIndexRange => "ltbl.layer_index_range",
            LtblSectorIdsAscending => "ltbl.sector_ids_ascending",
            LtblSectorIdUnique => "ltbl.sector_id_unique",
            LtblFirstEntryIsSectorZero => "ltbl.first_entry_is_sector_zero",
            LtblFirstLayrInRange => "ltbl.first_layr_in_range",
            LtblFirstLrovNull => "ltbl.first_lrov_null",
            LtblFirstLrovInRange => "ltbl.first_lrov_in_range",
            LtblOffsetWithinChunk => "ltbl.offset_within_chunk",
            LtblSlicesDisjoint => "ltbl.slices_disjoint",
            LayrVersion => "layr.version",
            LayrContentSizePresent => "layr.content_size_present",
            LayrFrameDecompressedSize => "layr.frame_decompressed_size",
            FrameAllocationBound => "frame.allocation_bound",
            LayrDictIdMatch => "layr.dict_id_match",
            LayrDictIdAbsent => "layr.dict_id_absent",
            ZdicVersion => "zdic.version",
            ZdicDictSize => "zdic.dict_size",
            ZdicDictIdMatch => "zdic.dict_id_match",
            ZdicSingle => "zdic.single",
            LhasFrame => "lhas.frame",
            LhasHashAlgorithm => "lhas.hash_algorithm",
            LhasLayerCount => "lhas.layer_count",
            LhasRootRecompute => "lhas.root_recompute",
            LhasLeafMatch => "lhas.leaf_match",
            ReeTag => "ree.tag",
            ReeVarint => "ree.varint",
            ReePlanes => "ree.planes",
            ReeFirstValue => "ree.first_value",
            ReeNoRunCountZero => "ree.no_run_count_zero",
            ReeRunLengths => "ree.run_lengths",
            ReeGrayscaleRuns => "ree.grayscale_runs",
            ReeGrayscaleAllBinary => "ree.grayscale_all_binary",
            ReeSplitAllBinary => "ree.split_all_binary",
            ReeSplitThreshold => "ree.split_threshold",
            ReeSplitPositions => "ree.split_positions",
            ReeAttachPositions => "ree.attach_positions",
            ReeAttachCount => "ree.attach_count",
            ReeAttachBits => "ree.attach_bits",
            ReeAttachThreshold => "ree.attach_threshold",
            ReeEndPositions => "ree.end_positions",
            ReeNoTrailingBytes => "ree.no_trailing_bytes",
            ReeDataSize => "ree.data_size",
            SectorPartition => "sector.partition",
            CryptModeEmpty => "crypt.mode_empty",
            CryptPasswordSectionLen => "crypt.password_section_len",
            CryptMachineSectionLen => "crypt.machine_section_len",
            CryptArgon2Budget => "crypt.argon2_budget",
            CryptChunkFlags => "crypt.chunk_flags",
            CryptTagVerify => "crypt.tag_verify",
            CryptUnitIndexBinding => "crypt.unit_index_binding",
            CryptKeyUnwrap => "crypt.key_unwrap",
            CryptRecipientEntry => "crypt.recipient_entry",
            CryptLowOrderPoint => "crypt.low_order_point",
            CryptNoKey => "crypt.no_key",
        }
    }

    /// Whether the specification marks this check strict-mode only ([section 11.5]).
    ///
    /// A loose-mode read must not fail these; a strict validator must.
    pub fn is_strict_only(self) -> bool {
        use Check::*;
        matches!(
            self,
            ReeNoRunCountZero
                | ReePlanes
                | ReeRunLengths
                | ReeGrayscaleRuns
                | ReeGrayscaleAllBinary
                | ReeSplitAllBinary
                | ReeSplitThreshold
                | ReeAttachBits
                | ReeAttachThreshold
                | SectorPartition
                | LhasLeafMatch
                | PrevPngSignature
                | VoxlSignature
        )
    }
}

impl core::fmt::Display for Check {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.write_str(self.name())
    }
}