PrintCraft asks GitHub for the latest release and, when it is newer, offers
its release page to download from. Nothing is downloaded or installed
automatically (installing in place needs signed release artifacts).
- ui-egui: updates module (version comparison, background check, dialog);
no network code: the desktop app supplies the source, and without one
(web build, tests) the command opens the releases page.
- desktop: one HTTPS request (ureq over rustls with the OS's trusted roots,
10 s timeout, 1 MiB cap). Only a page under the releases list is offered.
No bundled root list: webpki-roots/webpki-root-certs are CDLA-licensed.
- The check at start is opt-in and silent unless there is a newer version.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- On hybrid-graphics laptops wgpu picked the discrete GPU (egui-wgpu defaults
to HighPerformance). NVIDIA's can lose or corrupt its memory across suspend
and screen lock, which matches the illegible window in #8. PrintCraft now
asks for the low-power adapter unless WGPU_POWER_PREF is set; one-GPU
machines are unaffected, and it saves battery.
- "Save changes?" had no keyboard: Enter saves, Escape cancels, and Don't save
takes Cmd/Ctrl+D (macOS) or Alt+D / Alt+N (the mnemonics the reporter tried).
Its keys are read before the canvas (which took Escape), and no other
shortcut runs underneath it (Cmd+D opened Document properties).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The second full nightly fuzz run on main found two more:
- hayro-syntax computed a Flate/LZW predictor row as columns × bits in
wrapping arithmetic: /Columns 9223372036854775807 asked for 2^61 bytes and
aborted on any machine. Row widths are now saturating usize (colors × bits
also overflowed u8), a zero width is refused (it divided by zero), and a
row longer than the data is not allocated (empty output, as before).
- hayro stroked /LW 9223372036854775807 as given, and stroke expansion
allocated 10 GB. Widths are clamped to 4 × (canvas width + height) device
pixels, which covers the same pixels.
Both findings now run in 8 MB. Synthetic regression test in render.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first full nightly fuzz run on main (now that the runner survives) found
two 4 GiB allocations, caught by the 4 GiB child cap:
- an inline stencil mask claiming /W 4294967295 was decoded in hayro-interpret
before the renderer's MAX_IMAGE_PIXELS check could skip it (locally 9.6 GB,
42 s). Vendored patch image_size_ok: images and masks over 2^28 pixels are
not decoded.
- a CCITT image whose /DecodeParms claimed /Columns 4294967295 (with a small
/Width) sized the decoder's line buffers (locally 4.3 GB). Vendored patch
ccitt_size_ok in hayro-syntax: at most 2^20 columns and 2^28 pixels.
Both findings now run in 7 MB. Synthetic regression test in render.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fuzzed inputs drove one check-one process to 2-6 GB (the 16 GB CI runner
running two of them was killed every night). Three causes, each with a test:
- lopdf (bootstrap inspector) sized PNG predictor rows from /Columns before
reading data: /Columns 4294967295 allocated two 4 GiB rows. Vendored lopdf
0.45.0 refuses rows longer than the data (and uses saturating row widths);
its load-time stream decoding also gets a 256 MiB limit (it had none).
- hayro sized a tiling pattern's cell pixmap from /XStep x scale, bounded only
by u16: a step far beyond /BBox allocated 2 GB. The vendored patch keeps
the cell within 3000 px a side, lowering the resolution instead.
- cos decoded cross-reference streams up to the general 1 GiB limit; they now
stop at what 8,388,607 objects' rows can hold (~200 MB at most), and the
section is reconstructed (logged) instead.
On the nightly's seed (41) the worst captured input fell from 6.2 GB to 6 MB,
and a 300 s fuzz run had no process above 300 MB (before: many at 2-5 GB).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- xtask looked for target/release/printcraft-cli even under CARGO_TARGET_DIR
(which parallel agents use), and a child that failed to spawn counted as a
pass: such runs fuzzed nothing and reported 0 crashes. The binary now comes
from CARGO_TARGET_DIR (gates' corpus check and text oracle too), and fuzz
stops if it is missing.
- On Linux each child runs under a 4 GiB address-space limit, so an input that
allocates without bound becomes a crash finding instead of exhausting the
machine: the nightly fuzz job's runner was killed this way every night,
losing the findings. Allocation failures are kept as the finding's message.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Windows runners check out with core.autocrlf=true, which turned the SVG icons'
LF into CRLF, so their SHA-256 no longer matched ATTRIBUTION.toml and
xtask's the_real_manifest_passes failed there (hidden until now behind the
perf-test failure, since cargo test stops at the first failing binary).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The two budget tests ran in parallel, each with its own render workers, and
a single preempted stretch on a shared CI runner pushed the average over the
40 ms budget (41-52 ms on Ubuntu and Windows). They now hold a lock while
measuring, and a round over budget is measured again (up to three rounds):
noise only adds time, so the best round is the UI's own cost and a real
regression still fails every round. The budget is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- edit: the substitute font name is a `&str` constant, so there's no `from_utf8(..).expect`.
Paragraph grouping uses `let … else` / `if let` chains, and the reuse-font path takes the
metrics with `filter` instead of `expect("checked")`.
- xfdf: a comment key that vanished between listing and lookup is skipped.
Both crates now deny the panic lints. Adds the root clippy.toml (identical to #11) so tests may
still unwrap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Edit text: a rewritten paragraph keeps its place in the text object it shares
A rewritten paragraph was drawn again just before the BT of its text object. When earlier
paragraphs shared that BT … ET (as in text-only pages), the paragraph moved ahead of them in
the content: reading order changed (screen readers, copy, `pdftotext -raw`) and text_paragraphs
renumbered, so after text_edit paragraph 2 a following text_edit paragraph 1 rewrote the
already edited paragraph instead of the first one. The text object is now split where the
paragraph was, and the second half resumes from the same line matrix and text state.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Edit text: read the text before a paragraph without slicing by input positions (never-panic rule)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Edit text: test a recoloured paragraph in a shared text object (split + q/Q together)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: storybored379 <storybored379@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Export data writes a multi-select list's values into one cell joined by ", " ("A, C"), but
import passed the cell on as a single choice, which the list refused, so the selection was
silently lost on an export → import round trip (the field was only counted as rejected).
A multi-select list now splits such a cell into its options, unless the whole cell is itself
one of them.
Co-authored-by: storybored379 <storybored379@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The same file as the steering PR (#11), so this PR stands on its own. The crate-level
`#![deny(clippy::unwrap_used, …)]` applies to production code only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The same file as the steering PR (#11), so this PR stands on its own. The crate-level
`#![deny(clippy::unwrap_used, …)]` applies to production code only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The same file as the steering PR (#11), so this PR stands on its own. The crate-level
`#![deny(clippy::unwrap_used, …)]` applies to production code only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The same file as the steering PR (#11), so this PR stands on its own. The crate-level
`#![deny(clippy::unwrap_used, …)]` applies to production code only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The same file as the steering PR (#11), so this PR stands on its own. The crate-level
`#![deny(clippy::unwrap_used, …)]` applies to production code only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The same file as the steering PR (#11), so this PR stands on its own. The crate-level
`#![deny(clippy::unwrap_used, …)]` applies to production code only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The same file as the steering PR (#11), so this PR stands on its own. The crate-level
`#![deny(clippy::unwrap_used, …)]` applies to production code only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The same file as the steering PR (#11), so this PR stands on its own. The crate-level
`#![deny(clippy::unwrap_used, …)]` applies to production code only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The same file as the steering PR (#11), so this PR stands on its own. The crate-level
`#![deny(clippy::unwrap_used, …)]` applies to production code only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The same file as the steering PR (#11), so this PR stands on its own. The crate-level
`#![deny(clippy::unwrap_used, …)]` applies to production code only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The same file as the steering PR (#11), so this PR stands on its own. The crate-level
`#![deny(clippy::unwrap_used, …)]` applies to production code only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rewriting a paragraph (text_edit, Format text) draws it in its own text object, then sets the
text state back for what follows, but not the fill colour. Changing one paragraph's colour
therefore also coloured every paragraph after it in the same content stream (recolour the
first paragraph of a page red and the rest of the page turns red). The rewritten paragraph is
now wrapped in q … Q, so its colour and other graphics state stay with it.
Co-authored-by: storybored379 <storybored379@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Forms: CSV import keeps values that span lines
A quoted CSV value may hold line breaks, and export_data writes multiline text fields that
way. The importer split the file into lines before reading quotes, so such a value was cut
at its first line break and every later column was lost (an export, then an import, of a form
with a multiline field silently dropped data). CSV records now end only at unquoted line
breaks; tab-delimited text is read as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Forms: build CSV records without unwrap (never-panic rule)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: storybored379 <storybored379@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Content added with Edit ▸ Add content records its image in the content stream's dictionary
(/PCAdded /Image). The audit claimed a page's content streams before its resources and
followed that reference, so an added image's bytes were reported as Content Streams and
Images showed 0 % (a 1.9 MB picture read as 99.9 % content). Resources are now claimed
first, so whatever a content stream's dictionary points at keeps its own category.
Co-authored-by: storybored379 <storybored379@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Follows the owner's AGENTS.md §4 / CLAUDE.md rules with what the shared standard
(craftrules/standards/never-crash.md) adds: why (lost work) and that it outranks feature work,
the `unsafe` ban (workspace `unsafe_code = "forbid"`; no unsafe exists outside vendor/),
the last-resort guard around commands and file import/export, the clippy ratchet attribute,
and a root clippy.toml that lets tests use unwrap/expect/panic/indexing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Untrusted input (PDFs, scripts, MCP calls, settings) must yield errors, not crashes: Result
over unwrap/expect, no input-derived indexing, checked arithmetic, capped allocations, bounded
recursion, lock-poison handling, and a regression test for every crash fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
annot, compare, content, cos, export, geom, model, ocr, preflight, print and redact have no
production `unwrap`/`expect`/`panic!`/`unreachable!`/`todo!`/`unimplemented!`. The attribute
keeps it that way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`add_page` returns the crate's error when the new document has no page tree, and text pages are
filled with `if let`. The crate now denies the panic lints.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each "checked" `expect` becomes `let … else`, `if let` or `?`, with the obvious fallback (the
dialog closes, the command is ignored, the view refresh is skipped). ui-egui and the three apps
now deny the panic lints. The Windows `build.rs` reports a failed resource step with
`cargo::error` instead of panicking.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Renderer cache: built with the entry API instead of insert-then-`expect`.
- Page-text cache: filled and read with `get`/`get_mut`.
- Schemas and form JSON: extended through `if let` instead of `as_object_mut().expect`.
- A replace-text mark of the wrong shape is an invalid-argument error.
The crate now denies the panic lints.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The text reading order uses `let … else`, `while let` and `get_mut` instead of `expect`, and
named destinations bind their key with an or-pattern instead of `unreachable!`. The crate now
denies the panic lints.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The outline import, bookmark lookup and page-label code use `first`/`last`, a final `Err` and a
default range instead of `expect`/`unreachable!`. The crate now denies the panic lints.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>