Finding 49. drawing::anchors built a fresh object for every PlcSpa entry,
so one group of 1000 members named by 5000 entries built 5M children
(1.4 s, ~1 GB) with the per-group child cap resetting each time. Word
anchors each shape once, so a repeated spid is now skipped: every node of
the shape tree is built at most once, bounded by the shared OfficeArt
record budget. walk also cloned each floating object every time a story
holding its anchor was walked; each copy is now charged by size (a group
with all its members) to the document-wide MAX_WORK budget.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Finding 68. Pictures::get cloned the cached Inline (alt text up to 4096
UTF-16 units) for every '\x01' anchor, and the char budget allows ~64M
anchors, so one picture referenced repeatedly could copy hundreds of GB.
get now returns a reference; walk copies the alt text per anchor only
while the document-wide work budget lasts, then leaves it empty (the
picture itself is still placed).
The Prm work budget from #3692 becomes one shared MAX_WORK (64 MiB) for
repeated work beyond the text: Prm grpprls interpreted, hyperlink targets
(#3708, moved off the char budget so long links in small files are never
dropped) and alt text copied.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Finding 64. push_formatted re-ran char_props and apply_prm for every
character, so a single piece whose Prm names a 63 KB Prc over 100k
characters took 15 s. walk now keeps the last resolved CharProps and
reuses it while the CHPX slice, the piece's raw Prm and the paragraph
style are unchanged. Prm1 grpprl bytes interpreted are charged to a
document-wide MAX_PRM_WORK (64 MiB) budget, so files alternating pieces
per character can't multiply the work; past it the Prm is skipped.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Finding 75. finish_field cloned the HYPERLINK target into every result
character's CharProps and ParaBuild::push then compared it per character,
so a long target over a long result was quadratic (250k x 250k: 2.6 s).
Equal-formatted result characters now extend the current run; each run
start copies the target once and charges its length to the document's
character budget, past which the rest of the result is kept unlinked.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Finding 63. cfb 0.15 appends a whole sector of FAT entries for every FAT
sector the DIFAT lists and validates the FAT only afterwards, so a small
file whose DIFAT chain names one sector thousands of times made it
allocate ~128x the file size (2048 DIFAT sectors: 133 MB, 370 ms) before
failing. docbin now walks the header DIFAT and chain the way cfb does
(same trimming, one step per sector) and refuses a file listing more FAT
sectors than it has sectors, so the FAT is never larger than the file.
Both is_encrypted and read_with_password go through it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Picks up the startup prompt without the glyphs a font may lack (storytold/craft-libs#343, #344): the close mark is painted and the version pill is ASCII, so apps that panic on a missing glyph no longer take the prompt down.
Part of tracking issue storytold/craft-libs#319
Picks up the update prompt fix that draws its close mark and version pill
without glyphs a font may lack, so the prompt no longer takes the app down
when a bundled face has no ✕ or →.
Part of tracking issue storytold/craft-libs#319🤖 Generated with Codebuff
Co-authored-by: Codebuff <noreply@codebuff.com>
Wake the UI when a manual check is queued from a menu callback; the update prompt owns the keyboard while it is open (LightCraft); Download opens through the desktop URL hook (FilmCraft); the Help menu dispatches through menus::invoke (FigCraft); print jobs and comment drafts hold an install back (WordCraft); Check for Updates is a registered command (GridCraft); an install holds back while documents are unsaved.
Results are read independent of the prompt, a dropped worker is an error, failures return to the prompt, and no update starts over unsaved work. A manual check during the startup check is honored.
Startup check is opt-in for the desktop app, the Windows update helper is dispatched before argument parsing, and a successful update closes through the app's own close path.
Keeps the worker's failure visible instead of leaving the prompt installing, repaints while a check runs, and no longer calls process::exit past the unsaved-work prompt.
Integrate `craft-update` pinned to storytold/craft-libs fe61243 to check for
updates on startup, display the standard suite update modal with changelog notes,
and support one-click in-app updating and restart.
Ref: storytold/craft-libs#319
* #3680: Read Aloud tidies in one linear pass and cuts space-free sentences
Finding 56. tidy() repeated whole-string replacements until nothing
changed; each pass removed one level of nested empty brackets, so 40,000
pairs took ~50 s in a debug build. It is now one pass that drops a closing
bracket's empty pair as it goes, checked against the old algorithm on
20,000 generated strings. Sentences with no space to split at are cut at
MAX_UTTERANCE (1,200) characters, and each sentence is sliced from a char
vector instead of skipping from the paragraph's start.
Fixes#3680
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* #3692: Paste Special reads only a regular picture file, bounded
Finding 24. A copied path naming a device such as /dev/zero reports a
length of 0, so the size check passed and std::fs::read read forever.
The path and the opened handle must both be regular files, and at most
the picture limit + 1 bytes are read. A named pipe swapped in between the
check and the open can still block the open (no portable non-blocking
open without platform flags); noted in the code.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* #3692: macOS signing import decodes the certificate into a private temp folder
Finding 25. The .p12 bundle was written to a predictable name in the
shared temp folder, where a planted link could redirect it. It now goes
into a mktemp -d folder under the same temp root, removed on exit. The
keychain is unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* #3666: engine file reads open once and check the handle; a FIFO is refused without blocking
read_file_capped checked the path's metadata and then read the path
again, so a file swapped or grown after the check was read whole;
read_file_head opened the path with no type check, so saving to a FIFO
.xml path (the Word 2003 sniff) blocked until a writer appeared. Both
now go through open_regular_file (path checked before opening, handle
checked after, opened once) and a bounded read; read_regular_file uses
the same pieces. Tests: both helpers refuse a FIFO without blocking
and a folder; read_file_capped refuses content over the cap.
Fixes#3666
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* #3692: Paste Special reads picture files through the engine's bounded read
Ultrareview finding 9: paste_picture kept a third copy of the regular-file
bounded read; it now calls wordcraft_engine::io::read_regular_file (public,
with a wasm stub).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* #3666: open a checked file non-blocking so a swap to a named pipe can't hang
CodeRabbit: a path replaced by a FIFO between the regular-file check and
File::open blocked in open, before the handle check could refuse it. On Unix the
open now uses O_NONBLOCK (no effect on regular files; libc is already in the
lockfile) and the handle check refuses the pipe. The FIFO tests' cleanup writers
open non-blocking too, so a regression fails the test instead of hanging it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* #3666: review — Paste Special refuses a picture file whose handle reports more than the cap before reading it
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
* #3692: review — macOS signing keychain lives in the private mktemp folder too; removed if the import fails
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
* #3666: review — libc comes from the workspace dependencies
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
* #3680: review — Read Aloud logs a warning when its sentence cap leaves text unread
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
* #3692: review — engine speech timing test gets a margin a busy CI machine can't break
tidy_is_linear_on_nested_brackets: 2 s -> 10 s. The one-pass tidy takes ~13 ms on a loaded
machine; tidy_by_replacing (the old tidy, kept in the tests) took 293 s on the same input.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Please enter the commit message for your changes. Lines starting
# with '#' will be kept; you may remove them yourself if you want to.
# An empty message aborts the commit.
#
# interactive rebase in progress; onto d588e0b8
# Last command done (1 command done):
# pick 6a1268ea # #1845: filter-as-you-type command search in Customize Ribbon, QAT and Keyboard pickers
# No commands remaining.
# You are currently rebasing branch 'feat/1845-command-filter' on 'd588e0b8'.
#
# Changes to be committed:
# modified: crates/ui-egui/src/dialogs_keyboard.rs
# modified: crates/ui-egui/src/dialogs_ribbon.rs
# modified: docs/ui-parity.md
#
# Conflicts:
# docs/ui-parity.md
* Bound MCP stdio request and bridge reply line reads
Fixes#3685
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* #3685: an unreadable reply from the app is final, not retried
A reply that is too long (or not UTF-8) means the app already ran the
request; retrying on a new connection could run a state-changing request
twice.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* AutoFormat As You Type: optional period with double-space, off by default (#2848)
* Test the double-space option through the AutoCorrect dialog (#2848)
* File > Options > Proofing: an AutoCorrect Options button opens the dialog (#476)
* #2848: report doubleSpacePeriod in tools.autocorrect's params doc and result
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* With Regular expressions on, Replace with reads $1…$n, ${n}, ${name}, $name,
$0/${0}, $$, \1…\9, \n (paragraph mark, splits the paragraph through the ^p
path), \t and \\, alongside the ^ codes. Named groups resolve through
meta::Regex::group_info(); missing, unknown and out-of-range groups expand to
nothing. Plain and wildcard Replace are unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* #4177: review — special:false keeps regex group references
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* #4177: review — \$ and \^ in regex Replace keep the character as typed
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>