* security(compose): bound dense working sets and repeated group evaluation
Smart-filter mixing and stroke fitting work tile by tile instead of over the
union of sparse surfaces; render_layer admits its rect like render; hidden
layers no longer disable the proxy and uniform-area shortcuts; Blend If
groups nested deeper than two are judged by their result, so nesting costs
polynomial instead of exponential work; effect builds past an absolute byte
cap are skipped.
* CodeRabbit #8949: exact tile skips in filter_mask, allocated-tile scan in fit_stroke, contrasting backdrop in the nested-group test
- filter_mask mix/unmix: bare tiles (no tiles on either side) skip for
non-alpha formats too when the two surfaces' default pixels are equal;
differing defaults still get processed, so the clone's default pixel is
never left wrong under a non-unit mask
- shape_split fit_stroke: scan the union of the three surfaces' allocated
tiles (exact skip when all three defaults are transparent) instead of
every tile of a 300k-canvas document; non-transparent defaults fall
back to the full scan
- tests: the nested-groups assertion now uses a black backdrop so the
asserted white can only come from rendering through the group chain
fixes CodeRabbit review comments (major/minor) on filter_mask.rs:27,29,
shape_split.rs:126, tests.rs:2354
* compose: skip a BTreeSet in fit_stroke — a sorted Vec compiles under the wasm toolchain (E0599 on ::drain there)
* security(algo): bound working sets sized by caller-controlled extents
Zero-width inpaint grids, content-aware fill mosaics of turned thin images,
Liquify fields over huge canvases, tiny-scale reduction weights, warp tile
bookkeeping and per-tile source footprints, and windowed filters whose halo
follows an unclamped radius or a stacked Filter Gallery reach all asked for
memory sized by file or parameter input. Each is now bounded; halo
arithmetic saturates instead of overflowing.
* security(algo): budget variants by dimensions, size-check footprints first, widen the tile-halo maths
CodeRabbit follow-up:
- fill_cow settles the mosaic budget on variant dimensions only, so a
variant's image and mask buffers are not built when the mosaic would
exceed MAX_MOSAIC_PIXELS (complete falls back to the image itself).
- warp validates the footprint's pixel area against MAX_FOOTPRINT_PIXELS
before src.has_tiles_in, so an oversized footprint returns without the
tile-grid scan.
- auto_tile widens the input-derived halo arithmetic to i64 and clamps
before the cast, so an i32::MAX radius cannot overflow in debug builds
and the 2048 tile cap stands.
* fix(algo): drop the duplicate mosaic_size call left behind in fill_cow
The variant-budget rework kept both the new dimensions-based budget and
the old variants-based one; the old call no longer compiled after
mosaic_size started taking dimensions. Keep the comment that explains
the fallback.
Delete the coverage tests, the per-language parity checks and the tests
of one language's exact wording. Catalog integrity, placeholder and
plural-form checks stay, as do the engine tests (locale resolution,
plural rules, fallback, language scoping, drawing in a language); those
compare against tr() or the plural rule rather than catalog wording.
Docs describe the report instead of a failing test.
The report prints, per language, the source keys its catalog lacks (one
line for a language with none). `cargo xtask ci` runs it as its last step
and never fails on it: a missing translation shows in English. A report
that can't run prints a warning.
* packaging: use [[ for bash conditional tests (SonarCloud S7688)
SonarCloud flags every bare [ conditional in bash scripts under packaging/ as a reliability smell (rule S7688). Convert the flagged [ ... ] tests to [[ ... ]]: safer word-splitting and no glob surprises, same semantics for these string and file tests.
* packaging/web: parse size overrides as base 10 under [[
CodeRabbit follow-up:
- data_end counts the scan-data bytes it crosses, and decode's
truncated-input bound is measured on those, not on bytes.len(): APP
padding padded before the scan used to charge the budget without
describing one more pixel.
- a regression test builds a truncated JPEG with ~256 KiB of complete
COMMENT padding whose patched 20000x20000 frame was admissible under
the file-length bound, and verifies padding alone cannot admit it
(while the same file at its real size still recovers).
- Limits::max_alloc docs state the animated WebP charging, next to the
PNG text/XMP sentence.
Rect::tile_count counts a rect's tiles in u64; try_take_tiles uses it (no usize overflow on 32-bit). Surface::translated reuses one rebuilt tile for equal source-tile windows. auto_slices merges vertically adjacent runs through a hash index instead of searching the output.
Reject zero-width fixed-record arrays, borrow pixel state arrays instead of
collecting them per occurrence, charge FiEf slots, curve records and layer
name bytes across the document, bound mixed-text expansion before appending,
and cap distinct allocation-table ids (deletion records now hold no Entry).
A truncated JPEG scan under a large declared frame was filled out by the
backend to the full admitted size; reject it when the file cannot plausibly
account for the pixels. Animated WebP frames each keep a copy of the ICC
profile, so the aggregate admission now counts it.
A source replacement, a patch or a path override in either cargo config never shows up in
Cargo.lock, so the check refuses them. Both files are parsed as TOML, as Cargo reads them.
The release playbook runs the slow platform checks nightly and on every
push to release, so a regression shows up before a release is published.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The generated droplet shim only needs to run as its creator, so 0700 keeps
other local accounts from reading the CLI path it invokes. The reversed
range in the arrow-key test is the case under test, not an accident.
🤖 Generated with Codebuff
Co-authored-by: Codebuff <noreply@codebuff.com>
* fix(vector): bound hostile geometry raster work
Guard geometry and tracing operations against malformed dimensions and degenerate inputs with bounded work and safe failures.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
* fix(vector): address review (knot cap on split, relative identity tol, non-vacuous test)
* revert(geom): splitting past the quilt limit is intentional (smart_map test)
* fix(geom): cap identity tolerance at 1e-6 px
---------
Co-authored-by: Codebuff <noreply@codebuff.com>
Profile::content_hash is a fixed-key 64-bit SipHash of the ICC bytes, so a
crafted profile can share another profile's hash (about 2^32 work). Two
places took an equal hash as proof of an identical profile:
- transform::cached keyed its process-wide cache on the two hashes and the
options, so a colliding profile was handed the other profile's cached
transform and converted with the wrong colours.
- Profile::same_colors returned true on equal hashes, so a colliding
profile was taken for the other one and a needed conversion was skipped.
Add Profile::same_bytes, which compares the ICC bytes (an Arc pointer check
first, then the hash, then the bytes). same_colors uses it instead of the
hash. Each transform cache entry now keeps the ICC bytes of its two
profiles; a hit counts only when they match, and a colliding pair gets its
own transform without replacing the cached entry. The transform is built
after the cache lock is released, as before.
The regression tests give Display P3 the content hash of sRGB (a stand-in
for a found collision): before the fix, same_colors called them the same
and the cache returned sRGB's transform for P3; now both see P3's colours.
Fixes#8613
Co-authored-by: John Turnbach <jturnbach@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(text): bound font paths and imported text processing
Reject encoded path tricks and cap resource-heavy font and PSD text processing to keep untrusted inputs bounded.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
* fix(text): reject percent-encoded DEL in served paths; satisfy clippy chunks_exact lint
---------
Co-authored-by: Codebuff <noreply@codebuff.com>
* fix(algo): avoid copying duplicate EXIF text payloads
Keep the first valid text value for each EXIF tag, preventing duplicate entries from repeatedly decoding and copying large attacker-controlled strings.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
* test(exif): count text decodes so the duplicate-tag test observes skipped payloads
---------
Co-authored-by: Codebuff <noreply@codebuff.com>
refine_mask_with builds its result densely over the selection's bounds
(grown by the spread, plus Refine Edge Brush additions). After a thin
edit across a huge canvas, or with a whole-canvas selection there, that
is billions of pixels. matting::refine_area names that area and
select.refineEdge refuses one over MAX_REFINE_AREA (2^30 px) with an
error before allocating, leaving the selection unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FszacAJDjznVimaxQktoJT
The Brush edit, the Lasso edit and EdgeBrush::new allocated a dense buffer
over their bounding box, which the swept-area cap does not bound: a 1 px
diagonal across a 100,000 px canvas sweeps ~141k px but asked for ~40 GB.
- matting::brush_surface (replaces brush_region) and EdgeBrush paint only
the tiles a stroke touches (painted_tiles), capped at MAX_PAINTED_TILES.
- selection::polygon_surface rasterises only the tiles the Lasso's edges
cross, byte-identical to selection::polygon; interior tiles share one.
- selection::combine_sparse adds/subtracts visiting only the new shape's
tiles, keeping the rest of the selection shared.
- select.refineEdge refuses edits and refineStrokes that would cross too
many tiles before painting anything, leaving the selection unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FszacAJDjznVimaxQktoJT
The tilt-shift reference tests and tiled_clear_and_fill_match_one_pass_over_the_area cross pixel
formats or channel counts with independent geometry and fill settings. Each run takes a quarter of
the combinations through Sweep, a different quarter each UTC day.
precheck_agrees_with_execute built all five document states for every call and kept one; it now
builds only the state the call needs.
swept_blocks_match_the_pixel_search skips the cases where the colour mode reaches extrude only
through a channel count and ink setting an earlier mode already covered. It and
damaged_layer_data_opens_flattened run a quarter of their cases per run, a different quarter each
UTC day (photocraft-testkit's Sweep). PHOTOCRAFT_FULL_SWEEP=1 runs every case, and
PHOTOCRAFT_SWEEP_PART=<n> replays the part a failing run printed.
Review of #8783:
- The Invert edit no longer builds a canvas-sized buffer: the canvas
takes the inverse of the selection's untouched value (one shared tile
for every fully covered tile, as fill_rect stores it), then only the
tiles the selection holds are inverted. Byte-for-byte equal to the
dense inversion; a 100k x 100k canvas inverts in well under a second.
- A run of Inverts is applied as one or none (pairs cancel exactly), so
4096 of them cost at most one pass between other edits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>