* fix(hayro-syntax): read full /W[0] width for xref stream entry type (follow-up to #239)
* hayro-syntax: loader-path test for oversized /W[0]; document patch; drop committed build output (CodeRabbit)
* security: bound vendored hayro xref, object-stream, JBIG2 and graphics-state work; krilla PNG size
hayro-syntax:
- xref-stream subsections must be backed by decoded bytes: zero /W widths and counts
longer than the data are refused before inserting (CodeRabbit CWE-400, xref.rs:962)
- object-stream offset tables are parsed once per document and cached in Data
(CodeRabbit CWE-407, xref.rs:1079)
- embedded JBIG2 pages over 2^28 pixels are refused before allocating, with checked,
fallible reservation (CodeRabbit CWE-789, filter/jbig2.rs:34)
- a JBIG2Globals stream with an image filter is ignored, so globals cannot recurse
(CodeRabbit CWE-674, filter/jbig2.rs:20)
hayro-interpret:
- at most 4096 saved graphics states / 64 MiB retained; dash arrays over 256 entries stroke
solid (CodeRabbit CWE-400, context.rs:226)
krilla:
- PNGs implying more than 1 GiB of decoded output are refused at Image::from_png and in
the deferred decoder; the buffer is reserved fallibly (CodeRabbit CWE-789,
graphics/image.rs:565)
SOURCE.json local hashes updated; patches recorded in vendor/README.craft-reader.md and
vendor/krilla/README.craft.md.
* vendor: fix SOURCE.json local hashes; reserve krilla's colour/alpha split buffers fallibly
- SOURCE.json: only the local sha256 changes; the source/upstream hashes of the patched
files are restored (CodeRabbit review)
- krilla handle_u8_image/handle_u16_image reserve their split buffers with try_reserve_exact
and return an error, so a valid image under the 1 GiB decode cap cannot abort the process
on the second allocation (CodeRabbit review)
Drags the resize grip of sample()'s floating group: a movable group emits MoveFloating,
one with a protected member emits none. Fails without the surface.rs check (CodeRabbit
review nitpick).
A NaN input gives a NaN fraction, which the active-dimension check dropped, returning a
finite node value. Keep any nonzero fraction, NaN included (CodeRabbit review).
Follow-up to the hostile-input fixes in #196, covering findings that PR does not:
- ISO BMFF metadata: all udta/meta entries share a 4 MiB budget charged before a keys name
is copied, so many ilst items naming one long key cannot multiply it (CodeRabbit CWE-400,
isobmff/demux.rs:939)
- ISO BMFF stbl: reserve only the samples the chunk map can place, fallibly, instead of the
declared stsz count (CodeRabbit CWE-789, isobmff/demux.rs:767)
- Matroska: a header-stripping prefix over 256 bytes is treated as unsupported compression
rather than copied in front of every frame; Track::sample_size no longer allocates the
prefix (CodeRabbit CWE-409, matroska/track.rs:257)
- Matroska muxer: per-track cue flags use a cluster number, so starting a cluster no longer
clears every declared track (CodeRabbit CWE-407, matroska/mux.rs:402)
- craft-ui shown_names resolves label collisions with hash-map counts and a worklist, so a
crafted cascade is linear instead of cubic (CodeRabbit CWE-407, ui/src/printing.rs:32)
- parse_ppd caps group, option and choice labels at 256 characters before each option copies
its group (CodeRabbit CWE-400, print/src/lib.rs:232)
- printers_parsed tracks seen destinations in a HashSet (CodeRabbit CWE-407, lib.rs:417)
- sheets::Reader::rect checks an inline array's length before reading it and borrows direct
objects instead of deep-cloning the inherited MediaBox/CropBox/Rotate for every page
(CodeRabbit CWE-400, print/src/sheets.rs:774)
MoveFloating checked only the panel it named, but changes the rectangle shared by that
panel's whole floating group, so naming a movable sibling moved a panel whose policy forbids
it. The model now refuses the action unless every member is movable, and the floating
window's resize grip no longer emits it for such a group.
CodeRabbit CWE-863, crates/ui/src/docking/model.rs:392
deserialize_sections tried each top-level key against a document holding every key accepted
before it, cloning and deserializing the growing document per key. A config.toml with many
unknown keys took quadratic time (20k keys: >2 minutes, now <1 s). Each key is now checked
against the defaults plus that key, and accepted keys are merged once.
CodeRabbit CWE-407, crates/settings/src/document.rs:47
- Clut::multilinear enumerates corners only for dimensions between two grid points, so
singleton dimensions no longer multiply per-pixel work (CodeRabbit CWE-407, clut.rs:238)
- Transform::from_pipeline finds a sampled curve's flat ends once per curve via
Curve::inverse64_many instead of rescanning the table for each of the 4096 output entries
(CodeRabbit CWE-407, curve.rs:276)
- redact matches secrets in the original text only, so markers are never searched again
(CodeRabbit CWE-400, secret.rs:47)
- render_text/render_value share one MAX_RENDERED_BYTES budget across repeated slots
(CodeRabbit CWE-400, spec/template.rs:122)
- Connection::validate checks model-ID uniqueness with a set instead of a pairwise scan
(CodeRabbit CWE-407, connection.rs:205)
- Disclosure::kinds counts parameters as text, so a request carrying only params still needs
an agreement (CodeRabbit CWE-863, consent.rs:123)
Add opt-in browser download helpers that own detached links and object URLs, clean up failed or abandoned preparation, and register delayed URL cleanup before issuing a click. Applications retain filename, MIME type, pacing, and user-activation policy.
Include browser lifecycle and failure-path tests, public API documentation, and Sonar test-source classification. All 32 PR checks passed; targeted WASM Clippy and formatting passed in local validation.
* hayro-jbig2: cap long-form referred-to-segment count
A crafted JBIG2 segment header can claim a 29-bit referred-to-segment count;
Vec::with_capacity reserved ~2 GiB from that field before a single
referred-to number was read, and the resulting allocation failure aborts
the process in a way catch_unwind cannot contain (fatal on wasm32, where
memory is capped well below 2 GiB). A ~100 KB Flate-compressed PDF was
enough to reach it inside the 256 MiB stream budget.
Real files refer to a handful of segments, so the count is now capped at
u16::MAX; larger claims fail as InvalidReferredCount before any
retention-flag bytes are consumed. Regression test included.
Updates SOURCE.json's local hash for src/segment.rs and records the patch
in vendor/README.craft-reader.md, following the vendored tree's provenance
practice.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* hayro-jbig2: record git-blob sha256 for patched segment.rs
The previous entry hashed the Windows working-tree checkout (CRLF) of
src/segment.rs instead of the LF content git stores, so every other
manifest entry's convention (git-blob content hash) was broken by this
one file. Re-hash against `git show HEAD:vendor/hayro-jbig2/src/segment.rs`
as the vendored manifests' other 30 entries do.
Co-Authored-By: Claude Code <noreply@anthropic.com>
---------
Co-authored-by: Claude Code <noreply@anthropic.com>
Pin third-party actions to full-length commit SHAs (with version comments)
instead of mutable tags/branches to prevent supply-chain tampering via
retagged releases. Resolves SonarCloud rule githubactions:S7637.
Resolve and validate command-line path arguments before use, enforce HTTPS at
download sinks, and confirm path containment. Hardens local codegen/fetch tools
against path-traversal and command-argument-injection findings.
Calls storytold/craft-common-actions/.github/workflows/sonarcloud.yml@main
with secrets: inherit. Adds SonarCloud badge to README where missing and
removes the redundant sonar-project.properties (config now passed by the
reusable workflow).
* feat(window): shared crate deciding who draws the window frame
Add craft-window, which decides whether the desktop or the app draws the
window frame (titlebar, borders, window buttons) and applies it to an
egui ViewportBuilder. This fixes the drift where some Craft apps drew
their own titlebar on KDE while others kept KWin's.
The rule: macOS, Windows and X11 keep the desktop frame; a Wayland
compositor with zxdg_decoration_manager_v1 (KDE, Sway, Hyprland, COSMIC)
keeps its frame; one without it (GNOME) leaves the frame to the app.
<PREFIX>_TITLEBAR=app|system|desktop overrides. When the app draws the
frame, the decision carries the window-button Layout from GNOME's
button-layout so the app's header bar matches the desktop's order.
Decision logic only; drawing the buttons stays in each app. Extracted and
generalized from pdfcraft's decorations.rs. forbid(unsafe_code); default
build has no egui dependency (egui feature adds decorate()).
* style(window): rustfmt the Wayland probe and Dispatch impl
cargo fmt --check failed CI: collapse the thread-spawn builder chain and
the Dispatch::event signature onto the lines rustfmt wants (verified with
rustfmt 1.98 against the workspace rustfmt.toml).
* fix(window): address review — WAYLAND_SOCKET, bounded gsettings, checkout creds
- Skip the Wayland probe when WAYLAND_SOCKET is set: connect_to_env would
take ownership of the inherited fd and unset the variable, closing the
descriptor winit needs. Keep the desktop frame instead.
- Run the gsettings button-layout lookup behind the same 500 ms bounded
worker as the Wayland probe, so a stalled gsettings can't block startup.
Factored the worker into a shared bounded() helper.
- window.yml: set persist-credentials: false on checkout so the token
isn't left in .git/config while cargo runs dependency build scripts.
- README: document the socket guard and the bounded gsettings lookup.
Verified with rustfmt 1.98 and clippy -D warnings (both feature sets).
* fix(window): reap the gsettings child on timeout; document probe tradeoffs
Follow-up to review: the button-layout lookup now spawns gsettings with
piped output, reads it on a worker, and kills+reaps the child whether it
finishes or times out — so a stalled gsettings leaves neither a blocked
process nor a parked worker (it is a one-shot startup call).
Also document, on the Wayland probe, that an inherited WAYLAND_SOCKET
(rare; normal launches get WAYLAND_DISPLAY) deliberately falls back to the
desktop frame to protect winit's connection — the same frame an app that
doesn't probe would get, a safe default rather than a regression.
Verified: rustfmt 1.98 clean, clippy -D warnings clean (default + egui),
tests pass.
Adds .coderabbit.yaml so CodeRabbit no longer reviews or comments on PRs opened by dependabot[bot].
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* print, ui: a printer is shown by its name, not its CUPS queue name
CUPS queue names can't hold dots or spaces, so macOS adds the printer at
192.0.2.15 as the queue `_192_0_2_15`, and the Print dialog showed that
underscored name. The name the user knows is the queue's description
(CUPS `printer-info`).
craft-print now lists queues with `lpstat -l -p -d` (same lines, plus
each queue's indented details) and keeps the first `Description:` as
`Printer::description`, capped at 256 characters. Windows names are
already human, so they leave it empty.
craft-ui's `Printer` gains `label`; the dialog shows it, or the name when
it's empty, and `shown_names` adds the queue name in brackets when two
printers would show the same text. Selection and jobs still use `name`.
Both structs gain a field, so apps that build them by hand add it
(`..Default::default()` works for craft-ui's) when they update the pin.
* print, ui: an empty first description stays first, and shown names never collide
parse_lpstat tracks whether a queue has had its Description line, so an
empty first one isn't replaced by a later one. shown_names checks the
final texts: printers whose bracketed text another label already reads
show their queue names instead.
* media: add craft-media with bitstream and isobmff from FilmCraft
Moves filmcraft-bitstream and filmcraft-isobmff (storytold/filmcraft@6c3948a)
into one crate with a feature per module. The public API matches FilmCraft's so
its crates can become re-exports. Tests, oracle tests and fuzzing come along;
the ffmpeg discovery helpers replace filmcraft-testkit.
Out-of-contract inputs no longer panic: bit widths above 32, a zero NAL length
prefix width, and PCM frame sizes that overflow u32. Adds
BitReader::read_ue_plus from DeckCraft's copy.
* media: public ISO BMFF box reader (isobmff::boxes)
Make the demuxer's box iterator and big-endian reader public as
isobmff::boxes::{BoxIter, BoxRef, BoxHeader, Reader}, for walkers of
boxes the demuxer does not model (CR3 uuid boxes, HEIF meta items,
JPEG 2000). BoxIter::within keeps offsets relative to the outer slice,
and allow_truncated yields a cut last box. The fuzz test walks every
box of each mutated file.
* media: matroska feature from FilmCraft's filmcraft-matroska
Port storytold/filmcraft@6c3948a crates/matroska as craft_media::matroska
behind the opt-in feature matroska, with the same public API so FilmCraft
can make filmcraft-matroska a re-export. Its unit and ffprobe oracle tests
come with it, on craft-media's test helpers.
From DeckCraft's copy: cluster and block timestamps no longer overflow;
they saturate at i64::MAX. MuxingApp and the default WritingApp name
craft-media. New tests: block additions and mutation fuzzing. The oracle
skips the duration of a track's last packet when ffmpeg 8 shortened it.
* media: MOV PCM uses lpcm v2 above 16.16 rates and for unsigned 16-bit; signed 8-bit as twos
Version 0 sound descriptions store the rate as 16.16 fixed point, so 88.2 kHz
and higher were written as 0 Hz. 8-bit signed PCM was labelled raw (unsigned)
and 16-bit unsigned as twos/sowt (signed). The reader also took raw under a
sound handler for uncompressed video.
* media: stop the top-level box scan at a box that runs past the end of the file
A 64-bit box size near u64::MAX saturated the scan offset, and the next
offset + 8 overflowed.
* media: one sample budget for the whole ISO BMFF file
The sample cap and the file-length bound on samples without table entries
(constant-size stsz, trun without per-sample fields) applied per track or
per trun, so repeating a small trak or trun multiplied the allocation.
Both are now shared by all tracks and fragments.
* media: set_sample_entry rejects entries that change the track's media type or PCM frame size
The handler, stsz constant size and PCM timescale check are fixed at add_track;
a replacement that changed them produced a corrupt file. A bad track index
returns NoSuchTrack like the other writer methods.
* media: saturate the sample end in MkvFile::duration_ns
Cluster timestamps saturate at i64::MAX, so pts + duration could overflow
for files without Info/Duration.
* media: checked add for the Matroska cue cluster position
CueClusterPosition comes from the file; an add past u64::MAX panicked in
debug builds and wrapped in release. Such a cue now falls back to the index.
* media: cap the Matroska TimestampScale at 2^32 and saturate tick conversions
A scale near 2^64 overflowed u128/i128 in the per-lace split and in
Track::ticks_to_ns. A BlockDuration near u64::MAX also overflowed the tick
difference at the default scale. Larger scales are ignored like a zero scale,
and tick and nanosecond values outside i64 saturate.
* media: Matroska fixture generation no longer truncates the shared subtitle inputs
Parallel tests generating subs.mkv or webvtt.webm could read in.srt or in.vtt
while another thread rewrote it, and cache a fixture with missing packets.
* media: rustfmt parse_moof signature
* ci: add craft-media workflow covering feature sets, WASM and ffmpeg oracles
The lint job runs fmt, clippy for each feature set (none, bitstream,
isobmff, matroska, all) and a wasm32 check. The test job runs the
all-features tests with Ubuntu's ffmpeg and CRAFT_MEDIA_REQUIRE_ORACLES=1,
installing ffmpeg while the tests compile.
* media: Matroska lacing keeps timestamp gaps
A laced frame's timestamp is implied by the block's and the track's
DefaultDuration, so lacing collapsed gaps in the input. A frame now joins
a lace only when that implied tick equals its own, and tracks without a
DefaultDuration do not lace. The laced remux oracle compares timestamps.
* media: required oracles fail on fixture generator and ffprobe failures
Every path that gave up on an oracle check (missing tool, failed fixture
generator, failed ffprobe run) goes through one helper that prints
SKIPPED, or fails the test with CRAFT_MEDIA_REQUIRE_ORACLES=1. Fixture
generators of both containers share run_generator, which takes the
required flag explicitly so tests can exercise it without changing the
process environment.
* media: use US spelling
* media: a failed ffprobe probe skips the optional oracle check
* media: the Matroska oracle compares the last subtitle cue's duration
Skylar's review of 886a0b3:
1. tc/tid go locale by locale: in each locale the context or @id row,
then the plain row, so pt-br's plain row beats pt's context row.
2. pt (and pt-* other than pt-br) uses pt-PT plural rules: 0 is plural.
3. A script subtag (sr-Latn-RS, sr_RS@latin, be_BY@latin) never picks a
code of another script; sr-Cyrl-RS still picks sr.
4. A row defined twice is reported and the first kept, as in the checker.
5. The pseudo locale is debug-only via from_code, get marks too, and
looked_up() lists what was looked up for an app's audit.
6. intl_pluralrules pinned to =7.0.2; plural_form_counts copied from the
checker, plus pt.
7. I18n::new takes the app's offered codes: languages(), from_code,
match_tag and system_lang only return those; others stay fallbacks.
Smaller: en/pseudo/repeated codes are skipped and reported,
system_lang is cached, zh-hant-hk falls back to zh-hant, rtl adds ckb,
dv, ps, sd, ug and yi.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review fixes (CodeRabbit): a glibc @latin/@cyrillic modifier becomes the
script subtag and only codes with that script are tried. sys-locale
drops the modifier, so on desktop Unix system_lang reads LANGUAGE,
LC_ALL, LC_MESSAGES and LANG raw, in its order. README: tl! qualifies
CURRENT with $crate; get follows the locale chain but never the plain
row or English.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Final review fixes: a European Portuguese locale wins over pt-br when both
are embedded; sr-latn no longer falls back to Cyrillic sr; get() looks up
one context with no plain or English fallback (key-cap names).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F9fM3mv1PUyKpk5rdwUQrW
resolve_absolute_refuses_results_that_are_not_absolute resolved links against a
document at /home/u/docs/a.doc outside its cfg!(windows) branch. On Windows that
path is rooted but has no drive, so it is not absolute, resolve_absolute refuses
the result as documented, and the unwrap panicked. The Windows arm already covers
the same three cases with a drive. Found by the new docpath CI job on its first run.
The per-crate workflows name their crates with -p, so the new crate had no job
and its tests, including the Windows-only colon and stream assertions, never ran
in CI.
A test is not instant and the address field stays editable while it runs,
so its answer can arrive after the address has changed. `set_models` then
recorded the models against whatever the field held by the time it
returned, filing one endpoint's models under another — the mislabelling
`models_fit` exists to prevent, reached the asynchronous way rather than
the synchronous ones already closed.
`set_models` now takes the address the test was started against and
returns whether the models were taken, so an answer that outlived its
address is dropped and the caller can say the result is out of date. The
parameter replaces the old signature rather than joining it: there was no
second legitimate caller, and leaving an address-free version beside it
would have left the racy call the shorter, easier one to reach — against
this module's rule that the rules live once so two screens cannot
disagree.
Also fixes a pre-existing mismatch found while testing this:
`from_connection` recorded the loaded address untrimmed while `prepare`
compares it trimmed, so a stored endpoint with stray whitespace dropped
its models on the first save.