- pcraft_layer_rgba builds a name-to-entry map of the bundle once per
call instead of walking the central directory for every tile.
- A tile that several references name is read, decoded and counted
against max_total once per call. A hash referenced at two pixel sizes
is malformed.
- Surfaces stored as RGB or grayscale are read in documents of every
mode, so bitmap, duotone, indexed and multichannel documents read. A
CMYK or Lab layer or mask gives Unsupported, and an RGB mask gives its
first channel.
- A canvas of zero width or height is malformed.
- The docs name video layers, the masks of layers without pixels, and
tile references counting against max_items.
- doc.yml runs the tests without features again, next to
--all-features.
An <include> naming a folder listed, collected and sorted the folder's
entries before any budget applied, and again for every repetition, so a
configuration repeating one include made a single discovery call walk
the same folder thousands of times. Include folders are now checked
against the depth and file budgets first, listed at most once (by
canonical path), and all listings share a cap on entries.
A relative link next to a document whose path is not Unicode was joined
to the document's folder natively, skipping the parsing that refuses
device and object-manager namespaces. On Windows a document path with an
unpaired surrogate under \\.\, \\?\GLOBALROOT or \??\ therefore let
the link resolve inside that namespace. The native join now refuses those
roots as resolve does for a Unicode path; drives and shares are kept.
Inverting a sampled curve scanned the whole table for its flat start and
end runs whenever the value fell at or beyond an end. A constant table
falls there for every input, so each pixel the exact pipeline evaluated
read the whole curve again. inverse64_many already cached the ends for
output tables; a Pipeline now caches them for every inverse-curve stage,
recomputed on extend and optimize.
Indexed, Separation, DeviceN, Pattern and an ICCBased /Alternate each build
their base space with a recursive ColorSpace::new call, and each level can be
a fresh indirect object. A long chain of indirect /Indexed arrays therefore
recursed once per object and could exhaust the stack.
The depth is now passed down through every child construction and a space
nested 8 or more levels deep is refused (the deepest legitimate nesting,
Pattern over Indexed over ICCBased with an /Alternate, is four).
An xref-stream entry can be a single decoded byte, so one xref stream at the
256 MiB decode limit could expand into hundreds of millions of map entries
(several gigabytes) while the document was opened. Every insert into the xref
map now goes through insert_entry, which refuses a new object past 8,388,607
entries (the PDF 32000-1 Annex C limit on indirect objects). This covers
xref tables, xref streams, merges of Prev/XRefStm sections and the repair
scan. Replacing an object already in the map is still allowed.
The JBIG2 filter ignored the limit every other filter gets, so a stream
decoded with decoded_within (an output intent's 16 MiB ICC profile in
craft-prepress) could still build a page up to 2^28 one-byte pixels. The
page header's size is now checked against the limit before any output is
allocated: the returned bytes when the stream has no image dimensions,
the 1-bit packed size for an image of known size.
The Ghostscript work directory had a predictable name under the shared
temporary directory, and a failed removal of an existing one was ignored.
Another local user could create it first with an in.pdf symlink, and
writing the input would follow it. The directory is now created with
tempfile (random name, owner-only permissions, fails if it exists), and
in.pdf is created with create_new.
A cmap can list 65535 encoding records that all name the same subtable,
and Cmap::ranges expanded that subtable's spans once per record before
merging them. Records naming a subtable already read are now skipped,
and at most 16 distinct Unicode subtables are read from one face.
A manifest file or licence_file that is absolute, climbs out with .., or
resolves through a symlink to a file outside the checkout is now an
Error::OutsideCheckout instead of being embedded with include_bytes! or
copied (and written) outside the directory given to Embed::serve.
An integration test builds the library without cfg(test), so it checks the release configuration: the remote agents refuse plain HTTP (an upper-case scheme too) without connecting, loopback links still work, and with local-mirror the remote agents take plain HTTP.
A refused hop's message names only the host. ureq's message holds the whole link, and a redirect to the file host carries a signed query. host() also drops a query and any user:password@.
pcraft_layer_rgba(bytes, layer, &Limits) decodes one layer of a .pcraft
bundle over the canvas: a raster layer's surface, or the rendered copy
saved with a text, shape or smart object layer, as RGBA at the layer's
8, 16 or 32-bit sample type, with the layer mask as its own plane.
Grayscale repeats the gray value, missing tiles hold the default pixel,
and tiles outside the canvas are not read. Other color modes give
Unsupported; groups, adjustment and fill layers give None.
Tiles are read with craft-pcraft-format's TileRef and tile paths,
decoded with ruzstd and checked against their BLAKE3 hash, as PhotoCraft
reads them. The decoded layer and mask count against max_entry and
max_total before they are allocated; a frame is decoded only up to its
tile's size, and a frame larger than the tile plus an eighth and 4 KiB,
or one that declares a window over 8 MiB, is refused.
The feature adds ruzstd and blake3, both pure Rust. The doc workflow
runs clippy and the tests with --all-features.
Part of #59.
A 4 MiB list holds some 60,000 names, and searching the earlier entries
for each one made parsing quadratic: about 8 s for a full-size list in
a release build. A map from name to position and a set of conflicting
names make it linear again, with the same entries in the same order.
- renditions checks where the last VectorCraft member after the JSON
ends against max_total before it inflates anything past the marker,
and keeps only the members' bytes, not the data before them.
- A ZIP archive whose manifest.json has no format_version gives
Unrecognized instead of Malformed.
- A PhotoCraft manifest that is a JSON array is refused, as
read_manifest refuses it.
- renditions and outline refuse a PhotoCraft manifest nested past
PhotoCraft's limit (manifest_depth(MAX_GROUP_DEPTH), 367 levels).
- The TooNew check reads only the format's own version member, so a
newer file that also has another format's member is TooNew, not
Malformed.
- A generator that is not a string reads as None. A repeated VectorCraft
generator, preview or pdf reads as its last value, as File::read
reads it.
- The VectorCraft marker must follow the JSON's closing }, ] or ",
through craft_vectorcraft_format::marker_at.
- sniff reads the names in a whole archive's central directory, which
info reads too; a head too short to reach it still uses the local
headers.
read_code_num read the leading zeros one read_bit() at a time. It now takes
peek(32) and, when the code has fewer than 16 leading zeros and lies wholly
inside the data, decodes it from that one word. Longer codes, codes cut off
by the end of the data and invalid codes take the previous bit-by-bit loop,
moved to a cold function, so errors and the position after an error are
unchanged.
write_code_num wrote the zero prefix as up to 32 one-bit writes. A code of
up to 32 bits is now one write_bits call; longer codes write the zero prefix
in one call.
Tests compare read_ue, read_se (several codes in a row, every start
position, zero runs of 28 to 40 bits, buffers of 0 to 16 bytes) and write_ue
with verbatim copies of the previous code, plus the 31/32/33-bit code
boundaries and u32::MAX.
Closes#517
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
unescape_rbsp pushed every byte into the output Vec one at a time. It now
scans for 00 00 03 with the usual skip rule (a nonzero byte at i rules out
a match ending at i + 1 or i + 2, so the scan advances by 3) and copies the
bytes between matches with extend_from_slice. No unsafe, no indexing that
can panic.
Output is identical: a removed 03 still restarts the zero count. Tests
compare it with a verbatim copy of the old function over 20000 random
buffers (fixed-seed PRNG) and list the edge cases (empty input, 00 00 03 at
the end, 00 00 03 03, runs of zeros).
Closes#518
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The shared suggestion code already edits across the whole lowercased word, so no code change is needed; the test keeps it that way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>