Commit Graph
196 Commits
Author SHA1 Message Date
aiongg 3bc47b4cc0 refactor(formats): split sheet.rs into sheet/ 2026-10-26 21:16:49 +00:00
aiongg 2e61c73cb7 refactor(formats): split markdown.rs into markdown/ 2026-10-26 21:16:49 +00:00
aiongg 75bc3eb906 refactor(formats): split model.rs into model/ 2026-10-26 21:16:49 +00:00
aiongg 1bf2735604 refactor(formats): split html.rs into html/ 2026-10-26 21:16:49 +00:00
aiongg 59cec9d8bc refactor(formats): split odt.rs into odt/ 2026-10-26 21:16:49 +00:00
aiongg a209d1b268 refactor(formats): split rtf.rs into rtf/ 2026-10-26 21:16:49 +00:00
aiongg f0c66c81df refactor(formats): split latex.rs into latex/ 2026-10-26 21:16:49 +00:00
aiongg 0ad6f34c49 refactor(formats): move latex tests to latex/tests.rs 2026-10-26 21:16:49 +00:00
aiongg 6990384d9d refactor(formats): move html tests to html/tests.rs 2026-10-26 20:54:02 +00:00
aiongg 1b4c340879 refactor(formats): move markdown tests to markdown/tests.rs 2026-10-26 20:54:02 +00:00
aiongg 0cc8533624 refactor(formats): move odt tests to odt/tests.rs 2026-10-26 20:54:02 +00:00
aiongg 78ff598282 refactor(formats): move rtf tests to rtf/tests.rs 2026-10-26 20:54:01 +00: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
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 22df7caf4d #3692: review — formats timing tests get a margin a busy CI machine can't break
many_formatted_leading_breaks_trim_quickly failed at its 2 s bound on a machine at load ~50
(it takes about a second there). Bounds now sit at least 10x over the fixed code's time on a
loaded machine and far under the old code's on the same input (measured by running the
pre-fix code, capped at 400 s):

- HTML leading breaks: 2 s -> 20 s; ~1 s now, old trim ran past 6 minutes.
- LaTeX nested multicolumn: 2 s -> 10 s; under 1 s now, old unwrapping took 213 s.
- Markdown delimiters: 2 s -> 10 s; under 1 s now, old backwards search past 6 minutes.
- Para::trim blank spans: 1 s -> 10 s; 0.16 s now, old Vec::remove(0) past 6 minutes.
- repeated sheet headers: 2 s -> 10 s; 20 ms now, old numbering past 6 minutes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
2026-10-25 21:00:54 -04:00
Martin BallandClaude Opus 5.5 2c4f4669c6 #3713: review — importer limits that drop fonts, links, style names or sheet rows log a warning once per document
Every limit this branch added that leaves data out now says so:
- font names over 256 bytes and links over 8 KiB (HTML, ODT, RTF,
  LaTeX, Markdown), and RTF style names cut to 256 bytes;
- runs' links and fonts past the 64 MiB run-string budget;
- spreadsheet rows refused for being over the text budget (ODS and
  Grid::push), once per sheet.

model::dropped logs each kind once per imported document; each
importer's entry point starts a new document with begin_import. Tests
capture the warnings (crate::test_log) and check each is logged once,
again for the next document, and not at all when nothing is dropped.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
2026-10-25 21:00:20 -04:00
Martin BallandClaude Opus 5.5 41de47398a #3713: review — ODS repeated cells are budgeted only for the copies the grid can still keep
With one cell slot left, a long value repeated a thousand times was refused for all
its copies' text although Grid::push keeps only the first, which fits.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
2026-10-25 21:00:20 -04:00
Martin BallandClaude Opus 5.5 582f411876 #3692: Markdown export wraps link destinations with any Unicode whitespace in <…>
CodeRabbit review: only an ASCII space triggered the angle brackets, but the reader ends a bare
destination at any whitespace (U+00A0, U+3000), so such a target read back as plain text.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:00:20 -04:00
Martin BallandClaude Opus 5.5 d8501a5aeb #3713: rustfmt
Finding 58 follow-up: formatting only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:00:20 -04:00
Martin BallandClaude Opus 5.5 79e3610cbb #3713: importers drop overlong font names and links; spans share them; runs' copies budgeted
Findings 58 and 65. Every span deep-copied its inherited link and font
strings, so one long name or Markdown definition times many spans or
references expanded a small file into gigabytes. Fmt now holds both as
Arc<str> (spans and Markdown references share one copy), names over
256 bytes and links over 8 KiB are dropped at the importers (RTF, HTML,
Markdown, ODT, LaTeX), and the document builder stops giving runs link
and font copies past 64 MiB.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 20:59:48 -04:00
Martin BallandClaude Opus 5.5 faa8221bca #3692: LaTeX — verbatim delimiter check after control characters go; multicolumn/multirow unwrapping bounded
Finding 57: Writer::code replaced `\end{verbatim}` in a code line and then removed control
characters, so `\end{verb<U+000E>atim}` became a real closing delimiter and the rest of
the line was compiled as TeX. Control characters are now removed first and the
replacement runs on the final text.

Finding 52: the table reader unwrapped every nested \multicolumn / \multirow in a cell,
rescanning the remaining braced content each time (quadratic; the nesting limit didn't
apply). At most MAX_NEST wrappers are unwrapped now; real tables use one or two, which
read as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 20:59:47 -04:00
Martin BallandClaude Opus 5.5 e9fe3d29b6 #3692: Markdown export escapes link destinations instead of deleting characters
Finding 53: link_target removed `<`, `>` and newlines from a destination after
is_safe_href had accepted it, so an accepted relative target such as
`java<script:alert(1)` was written as `<javascript:alert(1)>` (or `/</host` as
`//host`). The exporter now backslash-escapes `<`, `>`, a backslash that a reader would
treat as an escape, and `&` that starts an entity reference, so a CommonMark reader gets
back exactly the checked target; control characters, which the check ignores, are still
left out. The importer now ends a `<...>` destination at the first unescaped `>`, so these
round-trip. Ordinary destinations are written as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 20:59:47 -04:00
Martin BallandClaude Opus 5.5 f6398e29a9 #3714: Markdown import tracks the text before each delimiter instead of searching back for it
Finding 67: when emphasis was flattened, every delimiter run searched backwards for the
nearest text node to borrow its formatting, reaching the start of the paragraph when the
paragraph was all delimiters (or links): quadratic. The last text node's formatting is now
carried forward through the loop, and nothing is looked up for a delimiter with no literal
left. The result is the same.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 20:59:47 -04:00
Martin BallandClaude Opus 5.5 42c254c87a #3705: Para::trim_with drops the leading blank spans with one drain
Finding 74: trimming removed each leading whitespace-only inline with Vec::remove(0),
shifting the rest every time. A paragraph opening with many separately formatted line
breaks (HTML <b><br></b><i><br></i>…, LaTeX \\) took quadratic time on open or paste.
The removable prefix is now counted and drained once; the first remaining span is
trimmed as before. 200,000 pairs ran past 60 s before, now milliseconds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 20:59:47 -04:00
Martin BallandClaude Opus 5.5 89a64d1ef5 #3692: sheet import — linear duplicate-header naming; ODS string attributes capped and over-budget rows refused before copying
Finding 40: field_names restarted every repeated header at "Name 2" and compared each
candidate with every name so far (cubic). It now keeps the names in an ASCII-lowercased
set and remembers each base name's next number; existing headers like "Name 2" stay
collision-free and the output is unchanged.

Finding 47: an ODS office:string-value bypassed MAX_CELL_CHARS and was copied for each
repeated column before the text budget was checked. The attribute is now capped like
<text:p> text, a row stops being built (and is refused) once its text can't fit the
remaining budget, and Grid::push measures what fits before cloning.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 20:59:47 -04:00
Martin BallandClaude Opus 5.5 17d19b42c5 #3642: review — the DIFAT sample is test-only, valid .doc/.xls files pass the guard, and budget drops are logged
- repeating_difat_sample is built only for tests: this crate's, and the
  readers' through a `test-support` feature they turn on as a
  dev-dependency; it is no longer in the production API.
- Tests write valid compound files with the cfb crate in Word's and Excel's
  shapes (WordDocument + table stream with Data, summary and ObjectPool
  streams; Workbook with summary streams), in 512- and 4096-byte sectors,
  plus an 8 MB one whose FAT sectors spill into a DIFAT chain; each passes
  cfb_guard::check and opens (no Word- or Excel-made files are added).
- The .doc work budget notes what it leaves out (floating objects, link
  targets, alt text, Prm formatting) and logs one warning per kind once the
  document is read (#3692).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
2026-10-25 23:47:39 +00:00
Martin BallandClaude Opus 5.5 bbeb7e2253 #3642: compound-file precheck shared in wordcraft-doc; .docx encryption sniff and .xls reader call it too
Finding 63 follow-up. #3642 names three cfb call sites: the .doc reader, the encrypted-package sniff and decrypt in docx/crypt.rs (reached for any .docx name), and the .xls reader. The DIFAT precheck moves from docbin to wordcraft_doc::cfb_guard::check (bytes only, no cfb dependency) and runs before every CompoundFile::open in the three crates. The crypt.rs change is only the two precheck calls (open PR #2125 also edits that file).

Regression tests use the shared hostile sample (cfb_guard::repeating_difat_sample): docbin a_difat_repeating_one_fat_sector_is_refused_before_cfb_builds_the_fat, docx tests/malformed.rs and formats sheet::xls a_repeating_difat_is_refused_before_cfb_opens_it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 23:47:30 +00:00
Dylan M. Taylor e5cffe7641 Merge pull request #4233 from VoiceLessQ/fix/small-cleanups
Small fixes: MERGEFIELD in any case, 18 SonarCloud closures, one unused constructor
2026-10-25 16:45:39 -04:00
divyanshu2074 b37a732455 fix(formats): support negative timezone offsets and author fallback in ODT metadata (#2189) 2026-10-25 10:39:59 -04:00
SimoneandLucenx9 b0b7e6b5ea Fix Markdown code span line endings (#3804)
Co-authored-by: Lucenx9 <185146821+Lucenx9@users.noreply.github.com>
2026-10-25 10:32:31 -04:00
divyanshu2074 1b0faddfa8 fix(formats): ignore block and inline HTML comments in markdown import (#2225) 2026-10-25 10:31:01 -04:00
divyanshu2074 0d2d1776dd fix(formats): support backslash space, soft breaks and zero-width spaces in RTF (#2250) 2026-10-25 10:29:09 -04:00
VoiceLessQ 65a0dc8dc6 Method paths for 18 closures that only call a method (SonarCloud S1612)
SonarCloud's analysis of main at b4da59f0c lists 23 closures such as `|v| v.as_slice()` that hand their argument straight to one method. This passes the method itself: `Vec::as_slice`, `str::to_ascii_lowercase`, `f64::total_cmp` and the like, all forms the repo already uses elsewhere. Behaviour is unchanged.

Five stay. The four in crates/proof/src/es/mod.rs sit in code that #4191 replaces, and its replacement already uses `PoisonError::into_inner`. In crates/engine/src/io/safe_save.rs the path would be `<dyn std::error::Error + Send + Sync>::is::<Guided>`, which reads worse than `|inner| inner.is::<Guided>()`.
2026-10-25 01:19:28 -02:00
aiongg db8efbe593 LaTeX: \'{} and \"{} import as the spacing acute and diaeresis, not an apostrophe and a quotation mark (#4170) 2026-10-24 21:05:19 +00:00
aiongg 26c4d4b869 Merge PR #4170: Import LaTeX \^{} and \~{} as plain ^ and ~ 2026-10-24 20:38:55 +00:00
VoiceLessQ d7429d217a Pictures: one decoder checks the header against the budget, progressive JPEG coefficients included (#3644)
Document pictures now decode through wordcraft_geom::picture (behind a
new `image` feature of the geom crate). It reads the header, refuses the
picture before anything is allocated when the decode's peak would pass
the budget, then decodes with the header's size and the budget as
limits.

The peak is the larger of three moments: the decoder's output beside
what it holds while decoding, the output beside its RGBA8 conversion,
and that beside a premultiplied copy. A progressive JPEG's decoder
(zune-jpeg) holds an i16 for every coefficient of every component, in
whole MCUs, for the whole picture; the SOF header gives that size. A
7900 x 7900 progressive JPEG without subsampling needs 9 bytes a pixel
(561 MB), past the 512 MB budget, although two RGBA buffers (499 MB)
fit. With the fix reverted, the test's header-only file of that size
was decoded and the test process peaked at 542 MiB resident.

- Render and PDF export ask the probe in their wrappers (decode_pixmap,
  load_image), so a picture past the budget gets the grey box or is
  left out, as before.
- The crop dialog and every picture edit decoded with load_from_memory
  and looked at the size afterwards. They decode through the probe now,
  and so do wrap outlines and the RTF re-encode.
- A picture edit has the drawing budget instead of an 80 M pixel cap
  checked after decoding. A picture past that budget (about 67 M RGBA
  pixels) can't be drawn either, so no edit that could be seen is lost.
2026-10-24 18:01:45 -01:00
NetterSilverandClaude Haiku 5.5 ee558313b0 Import LaTeX \^{} and \~{} as plain ^ and ~
Found while testing crates/formats/src/latex.rs:2212-2216 (accent command handling, fallback for an empty argument)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 18:45:44 +00:00
NetterSilverandClaude Haiku 5.5 97a6a26ca9 Format negative zero as 0 in spreadsheet import (#4040)
Found while testing crates/formats/src/sheet.rs:726-727 (fn number)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-24 10:02:47 -04:00
Dylan M. TaylorandClaude Sonnet 5.5 01d0c63451 Sonar S1612: use fn paths for simple method closures (#4103)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 04:33:08 -04:00
47edcd2ff2 chore: resolve SonarCloud maintainability issues (#4083)
* chore: resolve SonarCloud maintainability issues

* fix: restore compiling refactors, revert behaviour changes, run cargo fmt

- fontdb::norm: keep the closure for filter (char::is_alphanumeric takes
  self, not &self, so it does not fit Iterator::filter).
- story.rs: write_mark_rpr needs &mut self (rev_attrs, rpr_change).
- Revert the viewport meta change and the return 0 added to
  workspace_version/sha256, which changed behaviour or masked failures.
- cargo fmt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Import Document in odt page_parts after dropping glob re-export

* fix: apply_kind_properties binds ContentControl; cell_ref keeps char closures

* fix(engine): pass case helpers by value (clippy needless borrows)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dylan Taylor <dylan@craftmaker.dev>
2026-10-24 03:54:19 -04:00
NetterSilverandClaude Haiku 5.5 957b1c3e6e Drop U+FFFE and U+FFFF from ODT export text (#3817)
Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-23 10:31:21 -04:00
aiongg eb12fc6ee4 Integration: cargo fmt 2026-10-22 01:59:03 +00:00
aiongg 64f98d824a Merge PR #2280: fix(formats): support xs:time, string-value, and case-insensitive cell types in spreadsheets 2026-10-22 01:54:24 +00:00
aiongg 04b3881e3e Integration: track t06
docs/gaps.md row 4: one row (t06 had kept a second, older copy of it); t05's #1840 and t06's #2964 clauses both in, main's #2963 clause kept. docs/recovery.md: t06's retry sentence with t01's base64 size text. Test files: both tracks' tests kept whole.
2026-10-22 01:47:34 +00:00
aiongg 16d1c8a933 Integration: track t05
crates/pdf/src/lib.rs: t01's vector_text kept before t05's watermark (box area signature). Test files and canvas.rs tests: both tracks' tests kept whole. Docs: status lines combined, rows unioned or word-merged.
2026-10-22 01:46:47 +00:00
aiongg 0d3b8eb0cb Integration: track t04
crates/formats/src/odt.rs: t01's Body::new/take_blocks kept; take_blocks finishes tables with t04's TableB::finish (column widths). Test files: both tracks' tests kept whole. Docs: status lines combined, rows unioned or word-merged.
2026-10-22 01:46:17 +00:00
aiongg 86df1e70ce Integration: track t03
# Conflicts:
#	ROADMAP.md
#	crates/formats/src/html.rs
#	docs/gaps.md
#	docs/ui-parity.md
2026-10-22 01:44:54 +00:00
aiongg 245b5bd8d0 Integration: track t02
Docs: status lines combined (t02's entries first); log rows unioned; file-format-parity .rtf row from t02 (#1653, duplicate .rtf row dropped as t02 did), .odt row word-merged.
2026-10-22 01:44:01 +00:00
aiongg 520f54cb81 Integration: cargo fmt 2026-10-22 01:34:26 +00:00
aiongg 5c260a16fd Integration: cargo fmt 2026-10-22 01:31:48 +00:00