Review fix from #6087. tess_3d charged one element per triangle, but each
triangle also holds its normal-slot corners and one corner triple per
texture set any face of the mesh uses, up to eight. Faces reusing one
index range could so allocate well past the 128 MiB the element ceiling
documents. Each face's triangles are now charged in 8-byte elements for
everything they hold (corners, normal slots, texture triples, style)
before the face is triangulated.
Tests, each shown failing with the fix reverted:
reused_textured_faces_are_charged_for_every_corner_array,
faces_reusing_one_index_range_are_charged_for_each_triangle_they_make.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
Review fixes from #6087:
- Picture and uncompressed blocks are charged to the parse ceiling before
they are copied. File-structure headers may overlap, so each structure's
copy counts. The ceiling test now includes the 3-byte picture.
- Schema expressions use checked multiply, add and subtract; an overflow is
Malformed rather than a wrapped loop count.
- Each face's triangles are counted from its entity sizes and charged before
it is triangulated.
- Matrix::mirrors divides each row by its largest entry before taking the
determinants, so a mirror whose determinant underflows still reverses the
winding.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
A negative w (uniform or projective) turns mapped triangles inside out
though the 3x3 block does not mirror; the flip and the normal sign now
follow the 4x4 determinant, falling back to the 3x3 block when the last
row is zero.
The tree walk read each structure's globals schema with its own full
allowance, then again for the globals. It now reads it once, charged to
the walk, and continues from there for the globals.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
- Huffman leaf tables where one code begins another are malformed; the
shorter leaf would end the longer code early and misalign the payload.
- A model-file end past the input is malformed rather than clipped.
- Each schema entry costs one element, so empty programs are not free.
- tessellations() carries the globals schema's remaining allowance into
the tessellation reader: one stream, one budget.
- A face behaviour wider than 16 bits is malformed, not zeroed.
- Coordinate arrays that are not whole points are malformed; a partial
last normal drops the stored normals, as an unusable normal slot does.
- Triangles a TESS_3D's faces emit are charged to the element budget as
they are made, since faces may reuse one index range.
- TriangleMesh::transformed drops stored normals under a projective
matrix (last row not [0, 0, 0, w]).
- Placement::tessellation's doc says placements() does not check it
against the tessellation section.
Each fix has a test, seen failing with the fix reverted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
- Schema programs draw on one step allowance per reading pass:
MAX_SCHEMA_STEPS (2^22) moves into the BitReader's budget next to the
decoded-element allowance, and the tree walk carries both from reader
to reader. A per-call ceiling let a cheap entity repeated many times
run ~2^22 steps each.
- The vertex-colour pre-check charges the minimum encoding (one full
colour, then one reuse bit each), so a run of reused colours may end a
section.
- placements() reports unreadable globals when an entity reference, not
only an item, names a reference coordinate system.
- chunks_exact -> as_chunks for clippy 1.99 (chunks_exact_to_as_chunks).
Tests: programs_on_one_stream_share_the_step_allowance,
one_step_allowance_covers_the_whole_walk,
reused_vertex_colours_may_be_the_last_data,
unreadable_globals_are_an_error_when_a_reference_names_a_coordinate_system;
each fails with its fix reverted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
Disabling each input guard in turn left several with no failing test.
Each now has one that pins the guard's own refusal, not a later error:
- a zero Huffman code-length width, and a zero code length;
- a boolean array longer than the data, refused before any bit is read;
- a character array count past the data;
- no file structures, or more than MAX_FILE_STRUCTURES;
- a schema entry count past the data;
- normal angles wider than 16 bits;
- more texture sets than a face may carry (now by message);
- representation sets nested past the ceiling (and at it, accepted);
- a prototype chain past the ceiling;
- the element allowance, at each place it is charged: an entity's
counted array, uncompressed character and short arrays, a schema
program, and across one walk's readers (walk_within takes the
allowance so a test can set it; an allowance that fits one file
structure does not fit two).
Each fails with its guard disabled (the texture-set one by aborting on
the allocation the guard prevents).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
The inflation cap bounds bytes, but one bit can decode to a 4-8 byte
element, so a 512 MiB section could ask for about 16 GiB. Every array
count is now charged against MAX_DECODED_ELEMENTS (2^24), shared by
all readers of one tree walk; over the allowance is TooManyElements.
Huffman pre-sizing is capped at 1024.
decoded_elements_are_charged_against_one_allowance fails with the
charge in BitReader::take_elements removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
CAD 3D files carrying PMI failed model_tree() and placements() with
"tree markups (PMI) is not supported": the tree section has no length
prefix, so MarkupData and views had to be parsed to be skipped.
Now read past, not modelled (new tree/markup.rs):
- MarkupData [WD 7.3.10.4]: markup linked items (204), leaders (503),
markups (502), annotation items/sets/references (504-506); sets
nest under the MAX_DEPTH ceiling.
- Views (501) [WD 7.7.3], with their annotation plane (SURF_Plane 86),
optional scene parameters, and from 8137 their flags, linked items
and filters.
- Scene lights (731-734) and clipping planes, so
SceneDisplayParameters no longer refuses them.
ReferenceData and ReferenceOnPRCBase are shared readers now, and skip
their schema-added fields.
Field orders the working draft leaves open follow prc-rs's format
description (MIT); each such reader carries a `prc-rs:` marker.
Tests: one synthetic test per entity kind (each reads to a sentinel
and every truncation errors), nesting ceiling, wrong annotation type,
and a tree with markups and views on a part and an occurrence that
still places its mesh. Each shown failing with its reader reverted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
Walk the PRC product tree into placed, coloured tessellations.
- PrcFile::placements / placements_with(StyleAlpha, EntityOverrides):
every drawable tessellation reached from the model file's roots, placed
by its occurrence path's locations and then the representation item's
local coordinate systems (a 4x4 Matrix) [WD 7.3.10.1, 7.6.3.2].
- Colours: styles and materials from the file structure globals, the
father/son inheritance bits [WD 7.2.4.2], style transparency combined
with material alpha by StyleAlpha, per-face styles and topology
references via Placement::triangle_colours. A prototype's part is
coloured by its own structure's styles. A colour index counts the colour
array's doubles (pdf-issues #816).
- Occurrences' entity references: colour, hide or relocate the entity
they name, in the occurrence's subtree, everywhere, or not at all.
- PrcFile::model_tree: the assembly tree as a model-tree panel lists it,
with name sources, visibility and each node's placement range.
- TriangleMesh::transformed applies a Matrix: positions, cofactor-mapped
normals, winding flipped under a mirror.
Never panics: occurrence and prototype cycles are Malformed; depth 64,
2^20 visits, MAX_PLACEMENTS and a face-override ceiling bound the walk;
counts are checked against the remaining data before allocating.
Markups, views, scene lights and clipping planes are Unsupported;
textures are read past.
Field orders the working draft leaves open follow prc-rs (MIT); each is
marked `// prc-rs:`, as are the step-1 container, Double and schema
choices that did.
Adapted from pdfcer (MIT, same author), crates/pdfcer-3d/src/tree and
vec3.rs at 0c9742b5936232af89cd707138ef149dc1f4a660.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
Read the tessellation section of a PRC file structure:
FileStructure::tessellations() returns each entity as a Tessellation.
- TESS_3D becomes a TriangleMesh: positions, triangles from the
triangle/fan/strip blocks (one-normal and textured variants included),
one triangle range per face, stored normals and texture coordinates
with per-corner indices, and per-triangle line-attribute styles.
- TESS_3D_Wire becomes polylines (closing and continuous flags); vertex
colours are read past, sized by points (closing point included).
- TESS_Markup is read past.
- TESS_3D_Compressed is read field by field to its end so the entities
after it decode; its triangles are not rebuilt (later step).
- The compressed Character/Short/Integer/Indice arrays and the Huffman
container they use (WD 11.6-11.9, 12.2) live in arrays.rs; the base
fields every entity shares (attributes, name, user data) in entity.rs.
Never-panic hardening beyond the source this is adapted from:
- Huffman symbol width must be 1..=16; the leaf table and element count
are bounded by the payload before anything is allocated.
- A face may carry at most 8 texture-coordinate sets (an unbounded
count allocated per set and padded per triangle).
- Every count is checked against the remaining bits; indice sums and
deltas use checked arithmetic.
A few reading choices (Huffman bit order and code-length width, indice
running sums, 64-bit attribute order, three version gates, the
is_segment_color bit, the compressed field order) follow prc-rs (MIT);
each is marked `// prc-rs:` in the source.
Tests are synthetic, written with the crate's test bit writer. Each
fix was reverted and its test seen failing (17 reversions).
Adapted from pdfcer (MIT, same author), pdfcer-3d at
0c9742b5936232af89cd707138ef149dc1f4a660.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
Every PRC Double decodes through a fixed 2077-row prefix-code table
(working draft 11.17.2). The rows come from prc-rs src/double.rs at
287b671bb5b31b05efb5ec1b98f7c359b9e4dd7d under its MIT licence, whose
notice stays in the file header; ATTRIBUTION.toml records it.
The table is a complete prefix code, so the empty-table stub test is
replaced by a truncation test. The exponent lookup masks the 11 exponent
bits, because the last exponent row's upper word is 0x7ff80000.
Revert proof: with the table emptied, eight Double tests fail.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
New standalone L0 crate pdfcraft-prc: the PRC container walk (file
header, file-structure descriptions, per-section zlib inflation under a
512 MiB ceiling), the bit-level primitives of the compressed sections,
and the schema interpreter a newer producer uses to describe fields it
added (step and nesting ceilings).
The Double prefix-code table is not in this commit: Double reads
return UnknownDoubleCode, and the tests that need the table are
ignored with that reason.
Adapted from pdfcer (MIT, same author), crates/pdfcer-3d at
0c9742b5936232af89cd707138ef149dc1f4a660.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9