Commit Graph
2118 Commits
Author SHA1 Message Date
aiongg e900513104 Merge branch 'w3' into w6
# Conflicts:
#	ROADMAP.md
#	crates/engine/src/lib.rs
#	crates/engine/src/session.rs
#	crates/ui-egui/Cargo.toml
#	crates/ui-egui/src/lib.rs
#	docs/gaps.md
#	docs/parity-checklist.md
#	docs/ui-parity.md
2026-10-26 03:22:33 +00:00
aiongg 24986cdb23 W3: new app fields classified as app-wide; window ids and the test host on egui 0.37 2026-10-26 03:11:00 +00:00
aiongg 5632755be4 Merge branch 'w2' into w3
# Conflicts:
#	Cargo.lock
#	ROADMAP.md
#	apps/wordcraft/src/main.rs
#	crates/ui-egui/Cargo.toml
#	crates/ui-egui/src/backstage.rs
#	crates/ui-egui/src/control.rs
#	crates/ui-egui/src/dialogs.rs
#	crates/ui-egui/src/keys.rs
#	crates/ui-egui/src/lib.rs
#	crates/ui-egui/src/panes.rs
#	crates/ui-egui/src/previews.rs
#	docs/ui-parity.md
2026-10-26 03:09:46 +00:00
aiongg 645cbe52d4 Merge branch 'w1' into w2
# Conflicts:
#	ROADMAP.md
2026-10-26 03:04:40 +00:00
aiongg 4576b8e624 W1: a command that ends in another document counts as a state change for the input method 2026-10-26 03:04:24 +00:00
aiongg 5e7790ea85 W1: requests a command makes in another document keep their document id when the queue is full 2026-10-26 03:03:36 +00:00
aiongg 8634995c20 W1: control-channel doc tests in their own module beside the control tests 2026-10-26 03:03:36 +00:00
aiongg a44e3ec8f9 W1: pin craft-libs e8b4dde5 2026-10-26 03:02:07 +00:00
aiongg 4029a4e6fc Merge remote-tracking branch 'upstream/main' into w1
# Conflicts:
#	ROADMAP.md
#	crates/engine/src/lib.rs
#	crates/engine/src/session.rs
#	crates/mcp/src/tools.rs
#	crates/ui-egui/src/control.rs
#	crates/ui-egui/src/i18n/bg.tsv
#	crates/ui-egui/src/i18n/cs.tsv
#	crates/ui-egui/src/i18n/da.tsv
#	crates/ui-egui/src/i18n/de.tsv
#	crates/ui-egui/src/i18n/es.tsv
#	crates/ui-egui/src/i18n/et.tsv
#	crates/ui-egui/src/i18n/fr.tsv
#	crates/ui-egui/src/i18n/hr.tsv
#	crates/ui-egui/src/i18n/hu.tsv
#	crates/ui-egui/src/i18n/it.tsv
#	crates/ui-egui/src/i18n/ja.tsv
#	crates/ui-egui/src/i18n/ko.tsv
#	crates/ui-egui/src/i18n/nl.tsv
#	crates/ui-egui/src/i18n/pl.tsv
#	crates/ui-egui/src/i18n/pt-br.tsv
#	crates/ui-egui/src/i18n/ro.tsv
#	crates/ui-egui/src/i18n/ru.tsv
#	crates/ui-egui/src/i18n/sl.tsv
#	crates/ui-egui/src/i18n/sr-latn.tsv
#	crates/ui-egui/src/i18n/sr.tsv
#	crates/ui-egui/src/i18n/sv.tsv
#	crates/ui-egui/src/i18n/tr.tsv
#	crates/ui-egui/src/i18n/uk.tsv
#	crates/ui-egui/src/i18n/vi.tsv
#	crates/ui-egui/src/i18n/zh-hans.tsv
#	crates/ui-egui/src/i18n/zh-hant.tsv
#	crates/ui-egui/src/lib.rs
#	docs/gaps.md
#	docs/mcp.md
#	docs/parity-checklist.md
#	docs/ui-parity.md
2026-10-26 03:01:45 +00:00
Dylan M. Taylor 7583871d45 Merge pull request #3875 from benandez/sec/j
Security: engine and MCP authority checks (#3699, #3735–#3737, #3698)
2026-10-25 22:40:27 -04:00
Martin BallandClaude Sonnet 5.5 ed1296a1d4 #3871 follow-up: .doc picture decode charges zlib input and DIB copies; budget scales with the file (#4382)
* #3871: review — metafile input charged to the decode budget; inline BLIPs deduped; budget scales with input

- A compressed metafile is charged max(inflated size, zlib input), so a record claiming to inflate to nothing still costs its input.
- Inline pictures share one decode per BLIP record, cached by (stream, foDelay, record's own length) instead of the FBSE size field.
- The decode budget is max(512 MB, 64 x input bytes), capped at 2 GiB, so documents of many large legitimate DIBs decode; high-ratio bombs stay capped.
- Decode-budget refusals get their own log line.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HUodYU9r4mPHed4pPsjHC2

* #3871: review — a DIB's copy into a BMP is charged to the decode budget before it is made

The charge was w*h*4 after the copy, so a 1x1 header followed by megabytes of tail cost 4 bytes per picture. It is now max(pixels, DIB length), charged first; a bitmap whose copy doesn't fit is dropped rather than copied. The doc comment on the budget scale now says RLE8/RLE4 bitmaps (about 510x) rely on the 512 MB floor.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HUodYU9r4mPHed4pPsjHC2

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 22:37:10 -04:00
Martin BallandClaude Opus 5.5 b654049545 #3874: review — text box paste charges only what it copies; stories sharing Arc blocks (PDF imports) keep their text (#4381)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 22:30:32 -04:00
Dylan M. Taylor adf33b6ba7 Merge branch 'main' into sec/j 2026-10-25 22:01:43 -04:00
Dylan M. Taylor 66601038df tests(layout): build the nbsp test document without struct-update syntax (#4380) 2026-10-25 21:52:01 -04:00
Dylan M. Taylor ee7e98eafb hotfix: same_content must mention media_index (E0027 after #3872) 2026-10-25 21:22:09 -04:00
Dylan M. Taylor ba27ef13fb Merge pull request #3867 from benandez/sec/b
Security: bound layout memory (linked text boxes, repeated headers, crops, gridAfter)
2026-10-25 21:17:01 -04:00
Dylan M. Taylor c0627f2b0b Merge branch 'main' into sec/b 2026-10-25 21:16:51 -04:00
Dylan M. Taylor ebf27211cf Merge pull request #3872 from benandez/sec/g
Security: DOCX package/writer hardening — glossary privacy scrub, case-insensitive part names, chart and font limits, Flat OPC inlining (#3692, #3704, #3742, #3648)
2026-10-25 21:14:19 -04:00
Dylan M. Taylor ced8576f4c Merge branch 'main' into sec/g 2026-10-25 21:14:06 -04:00
Dylan M. Taylor 7740ea650e Merge pull request #3869 from benandez/sec/d
Security: font ingestion checks sizes and counts before allocating (#3703, #3716)
2026-10-25 21:10:28 -04:00
Dylan M. Taylor 8450531b0e Merge branch 'main' into sec/d 2026-10-25 21:10:17 -04:00
Dylan M. Taylor 25a087db43 Merge pull request #3874 from benandez/sec/i
Security: document model work bounds for media, math and paste (part of #3614)
2026-10-25 21:09:43 -04:00
Dylan M. Taylor 0ecfbb57f9 Merge pull request #3873 from benandez/sec/h
Security: import/export format fixes — LaTeX verbatim escape, Markdown link escaping, importer limits (#3713, #3714, #3705)
2026-10-25 21:09:20 -04:00
Dylan M. Taylor 6689d186f6 Merge pull request #3870 from benandez/sec/e
Security: render, PDF and metafile budgets
2026-10-25 21:08:40 -04:00
Martin BallandClaude Opus 5.5 def2b9bd31 #3692: review — doc timing tests get a margin a busy CI machine can't break
Wall-clock bounds of 2 s failed on loaded runners. Each bound is now 10 s, at least 10x the
fixed code's time and well under the old code's on the same input (measured by running the
pre-fix code under the same load):

- many distinct media: 0.5 s now; the old scan of every image took 131 s.
- object walks: under 0.3 s now; the old walks took 360 s (#3614).
- delete across lone control markers: 3,000 markers (was 1,500); 1 ms now, the old delete
  took 18 s at 1,500 and 114 s at 3,000.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
2026-10-25 21:03:06 -04:00
Martin BallandClaude Opus 5.5 bec112a4b8 #3692: review — pasted text boxes skip only the box over the budget, and say so; media_index private; kept markers' order pinned
- A text box whose story doesn't fit what is left of the paste's byte
  budget gets no story, but later boxes with smaller stories are still
  copied while the budget lasts (it used to zero the budget). Each
  source story is costed once per paste, so skipping a big story many
  times doesn't walk it again. The paste logs one warning with the
  number of boxes left empty. wordcraft-doc now depends on log.
- Document::media_index is a private field and MediaIndex a private
  type: a lookup cache, not API. The one struct-update literal outside
  the crate (a formats test) builds the document field by field.
- kept_control_markers documents that delete_range relies on its
  sorted, deduplicated output; delete_range sorts it if it ever isn't,
  and control_markers_kept_are_sorted pins it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
2026-10-25 21:03:06 -04:00
Martin BallandClaude Opus 5.5 26187afac2 #3692: review — pasted text box copies are charged for every payload their objects copy (group members, equations, kept XML), not only the object slots
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
2026-10-25 21:03:06 -04:00
Martin BallandClaude Opus 5.5 72cbbe096e #3692: review — the long-letter-run math test asserts results only, no wall-clock limit
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
2026-10-25 21:03:06 -04:00
Martin BallandClaude Opus 5.5 722a6ab1c0 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> 2026-10-25 21:03:06 -04:00
Martin BallandClaude Opus 5.5 f235208630 #3692: pasted text boxes share what they need not copy, within a byte budget
Finding 80 (CWE-400): adopting a fragment deep-copied a box's source story
once per text box showing it, including boxes inside the story being copied,
so 4096 boxes (the story budget) pointing at one large story copied it 4096
times. Now: a box showing a story it is inside gets no story (a cycle);
blocks without text boxes are shared with the source (copy-on-write Arcs)
instead of copied; and a paste adopts at most MAX_PASTED_BOX_BYTES (32 MiB,
estimated text + runs + objects) of box content, charged before copying,
after which later boxes keep no story, as when the story budget runs out.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:02:59 -04:00
Martin BallandClaude Opus 5.5 3a1c91f2d4 #3614: crates/doc walks a paragraph's objects in one pass
Finding 71 (CWE-407): loops of object_offsets + object_at recounted the
objects before every offset, so walking a paragraph's objects was quadratic
(40k markers: 360 s for the regression test before, 0.2 s after).
Paragraph::objects_with_offsets yields (offset, object) in one pass, and
every such walk in crates/doc uses it: the field scan (field_ranges, run on
every docx load), control events, bookmarks, perm ranges, text_box_anchor,
past_field_ends, typing_pos and the markers a range delete keeps. Also:
perm_ranges keeps open ranges per id instead of searching a list for each
end; kept_control_markers pairs each container with its table wraps once
(was once per marker) and probes a set instead of every range.

Part of #3614: the engine, docx reader and UI callers it lists live outside
crates/doc and still use object_at in loops; they can move to
objects_with_offsets separately.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:02:59 -04:00
Martin BallandClaude Opus 5.5 acfc3d5041 #3692: function-name lookahead in linear math stops at the longest name
Finding 48 (CWE-407): P::word scanned and copied the rest of an alphabetic run
for every letter that didn't start a function name, so a 20,000-letter
equation did ~200 million steps outside the parser's step budget (5 parses:
8.3 s before, well under 0.1 s after). The scan now stops one past the
longest name in FUNCTION_NAMES (computed at compile time); a longer run is
never a name, as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:02:59 -04:00
Martin BallandClaude Opus 5.5 01a75b99b1 #3692: add_media looks up duplicates by content instead of scanning every image
Finding 45 (CWE-407): Document::add_media compared each new image with every
stored one, so an import with many distinct small images did quadratic work
(40k images: 213 s before, 0.26 s after). A MediaIndex cache (not saved,
ignored by ==) maps bytes to their first key, is rebuilt when media changed
size behind its back, and every hit is checked against media. Generated
image{n} names also skip taken names once instead of on every add.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:02:59 -04:00
Martin BallandClaude Opus 5.5 364688a8c3 #3704: review — case-variant zip names keep the spelling a relationship names; chart, font and glossary limits log once per save
Folding zip entries whose names differ only in case now keeps the
spelling a relationship part targets, and the first in byte order only
when no relationship names any of them. The fold logs one warning per
package, not one per entry.

Limits that leave data out now say so, once per document or save:
- a chart past the chart limits on save (series or points dropped);
- embedded fonts past the byte budget (one warning with the count);
- a kept glossary that can't be cleaned of personal information and is
  written from the model instead.

Tests capture the warnings (crate::test_log) and check each is logged
once.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
2026-10-25 21:02:52 -04:00
Martin BallandClaude Opus 5.5 5819b1ca62 #3692: review — comment-id test checks the ids, not a wall-clock deadline
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
2026-10-25 21:02:52 -04:00
Martin BallandClaude Opus 5.5 c14e18cfa8 #3704: review — zip entries whose names differ only in case keep one stored spelling
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
2026-10-25 21:02:52 -04:00
Martin BallandClaude Opus 5.5 eb2f0dab46 #3742: review — a kept XML part with a malformed comment goes out in base64
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
2026-10-25 21:02:52 -04:00
Martin BallandClaude Opus 5.5 6dc062d132 #3692: a private save keeps the glossary's parts, without names and dates
Ultrareview finding 1. With "remove personal information" or "remove date
and time" set, the glossary was always written from the model (01c78899),
which holds only the entries the reader took: one without a name, past the
entry cap or with a bad content type was lost, and with none left the whole
glossary part went. The kept parts are now written with the same names and
dates removed that the writer leaves out of the main document (annotation
authors become "Author", comment initials "A", annotation and UTC dates
go; a people part's authors and presence too), rewritten with quick-xml and
matched by namespace. A part that can't be rewritten (malformed, UTF-16)
falls back to the model, so the privacy guarantee holds either way.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:02:52 -04:00
Martin BallandClaude Opus 5.5 a1f8be2a49 Progress log row and the file-format parity Hostile input line for findings 42, 43, 51, 55, 72, 73 and 76.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:02:52 -04:00
Martin BallandClaude Opus 5.5 60a64acec3 #3704: a part named in several casings is kept, cached and written once
Finding 73: Package lookup matched part names case-insensitively, but relationship targets kept the spelling they were written in, so every casing of one part became its own media entry, graphic parse and passthrough key, and a save wrote a full copy per spelling (64 spellings of a 64 KB picture: 64 media parts). Package now keeps a lowercase index, Package::canonical returns the stored spelling, and rels() resolves internal targets to it, so every downstream cache is keyed by the one part; get_shared uses the index instead of a linear scan.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:02:46 -04:00
Martin BallandClaude Opus 5.5 722afa4201 #3742: kept XML is inlined in Flat OPC only when it is one well-formed element
Finding 55: flat.rs xml_fragment inlined kept part bytes into pkg:xmlData after a prefix check only, so a part whose text closed the wrapper (</pkg:xmlData></pkg:part><pkg:part ...>) wrote parts of its own into the saved .xml - e.g. a second core properties part. Now the bytes are inlined only when quick-xml reads them as exactly one balanced element with nothing but whitespace, comments and PIs around it; anything else is written as pkg:binaryData and still round-trips.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:02:46 -04:00
Martin BallandClaude Opus 5.5 1955793aa6 #3648: a chart part is written within the chart limits whatever the model
Finding 76. chart_xml trusted its ChartSpec to be sanitized, but a .json
document is deserialised without ChartSpec::sanitize, and every series
repeats every category, so a chart past the limits multiplied into a
gigabyte-sized part on save. chart_xml now writes a bounded, sanitized copy:
only the series and points the limits keep are copied before sanitizing.
(Sanitizing on .json open, and SmartArt, are in the engine and not covered
here.)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:02:46 -04:00
Martin BallandClaude Opus 5.5 639964b0ed #3692: a glossary is written from the model when the privacy settings ask for changes
Finding 72. An unchanged glossary was carried byte for byte, judged by a
fingerprint of its entries that leaves out the document's privacy settings,
so a save with "remove personal information" or "remove date and time" kept
the authors and dates of the glossary's tracked changes. With either setting
on, the glossary is now written from the model, which applies them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:02:46 -04:00
Martin BallandClaude Opus 5.5 4c5930dac1 #3692: embedded fonts are copied once per file and within a byte budget
Finding 51. embedded_fonts copied a part's bytes for every font-table
reference, so the 64 references allowed to one 256 MB part could make 16 GB of
copies on open. A reference repeating one already taken (same part, key and
name) is skipped, and copies stop at MAX_FONT_BYTES (one part's limit),
checked before each copy is made.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:02:46 -04:00
Martin BallandClaude Opus 5.5 12afc75411 Finding 42. read_comments checked each candidate id for a comment whose
w:id isn't a number against every id mapped so far, so a comments part of
many non-numeric ids took time quadratic in their count (49,000 comments:
2.4 s, now 0.35 s in a debug build). The ids mapped before the part are
collected into a set once; ids given in the part are already in `entries`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:02:45 -04:00
Martin BallandClaude Opus 5.5 2337ff75fa #3723: zip entries that fail their CRC check are charged to the package total
Finding 43. Package::open_limited dropped an entry whose read failed before
adding the bytes it had already inflated to the running total, so many
entries with a wrong CRC (checked only after the whole stream inflates) could
each inflate up to the part limit for free. The bytes are now charged first
and the size check runs before the read result is looked at.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:02:28 -04:00
Martin BallandClaude Opus 5.5 2d9bcb6b1b #3692: review — fonts timing test gets a margin a busy CI machine can't break
named_instances_and_collection_members_are_capped: 2 s -> 10 s. The face counts in the test
(one face for a million identical members, MAX_FILE_FACES from add_font) are what catch the old
code, which built a face per member; the time (under 0.1 s on a loaded machine, so 10 s is over
100x) only backs up "read once".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
2026-10-25 21:02:21 -04:00
Martin BallandClaude Opus 5.5 b059da2d4e #3692: review — docs: why the hinting breaker is timed, and what brotli-decompressor brings
The hinting breaker stays wall-clock: skrifa's interpreter budget is fixed,
counts instructions rather than the points each one moves, and reports no
work count, so there is nothing deterministic to bound. The comment now says
the result can depend on the machine by design, and that only faces needing
the interpreter for their contours are hinted at all. Cargo.toml notes that
brotli-decompressor is the crate and version wuff already uses: pure Rust,
forbid(unsafe_code) by default, wasm-ready, BSD-3-Clause/MIT.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
2026-10-25 21:02:21 -04:00
Martin BallandClaude Opus 5.5 57e9e0f244 #3703: review — font store adds run one at a time, so racing adds never share a temporary or a file
CodeRabbit on #3869: two concurrent install_font calls could pick the same
destination and temporary, and the leftover-unlink retry could remove the
other call's open temporary. write_unique now holds a process-wide lock.
In-process only (noted in a ponytail comment).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
2026-10-25 21:02:21 -04:00
Martin BallandClaude Opus 5.5 87d3ea5363 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
2026-10-25 21:02:21 -04:00