* #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>
- 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
CodeRabbit review: the anchor copy cost counted only the struct size, so an aliased picture's
alt text (up to ~12 KB) was copied per walk uncharged. Recursion is bounded by MAX_GROUP_DEPTH.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
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>
A field begin marker flagged w:dirty by a producer that couldn't compute
its result (pandoc-style TOC placeholders) lost the flag on save, so Word
no longer refreshed the field on open. Mirror w:fldLock end to end:
FieldState, InlineObject::Field/FieldStart, the reader (fldChar and
fldSimple) and the writer (buffered, range and TOC-spanning begins).
Update Field/Update Table clear it on the fields they compute, like
Word's F9; untouched fields keep it. Round-trip and engine regression
tests, incl. a pin for #878 (imported TOC placeholder replaced in place,
fixed earlier by #617).
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M12: preserve document properties when importing legacy DOC and DOT
M12: preserve document properties when importing legacy DOC and DOT
Integrate current upstream while retaining import behavior and embedded-font loading.
* docbin: as_chunks for fixed-size records (clippy::chunks_exact_to_as_chunks on Rust 1.99); rustfmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: LabDennis <341377186+LabDennis@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Harden against hostile-document DoS and non-web link export
A security pass found several small files that make the app allocate or
compute without bound when opened (GUI, wordcraft-cli, or the MCP server),
and link targets leaving on export without the scheme check the HTML and
Markdown writers already apply. None lets a document run code or read files;
all are denial-of-service or credential-leak-on-click. Each fix bounds an
attacker-controlled size and lands with a regression test, per the
never-crash standard.
- layout: a table repeats its header rows on every page it spills onto, so
many tall header rows paginate to O(headers x pages) placed items. Cap the
total header-row repeats per table (lib.rs). 2 KB docx: 1.1 GB -> 124 MB.
- formats/model, html: an HTML table of many <tr> with large rowspan expanded
to rows x 64 covered cells, and `model::table` pre-allocated a full
rows x cols grid only to clear it. Cap rows at MAX_ROWS before
insert_covered, bound its total work, and build the grid template with one
row. 640 KB html: >1.5 GB -> 214 MB (a plain large table is faster too).
- docx/read/freeform: a custom-geometry Bezier segment took any number of
points, making de Casteljau flattening quadratic. Cap points per segment.
3 KB docx: 12 s -> 0.15 s.
- docbin: footnote/header/text-box story tables and the piece table can name
the same backing text many times. Hold the total characters materialised to
twice the WordDocument stream size (a ceiling no real file reaches, since
each char costs >=1 stream byte). 45 KB doc: >1.2 GB -> 14 MB.
- render, pdf, formats/rtf: document pictures were decoded with no dimension
limit, so a tiny file declaring a huge image could allocate before the size
check. Decode through image::Limits with a side cap first.
- doc/link, formats/rtf, odt, latex: route exported hyperlink targets through
is_safe_href (as HTML/Markdown already do), and extend it to reject
protocol-relative (//host) and UNC (\host) references. Keeps file:,
javascript:, ms-msdt: and UNC targets from being written into exported RTF,
ODT, LaTeX and PDF where a reader would follow them (UNC leaks NTLM on
Windows).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Align picture-decode limits with the bounds already in force
Review of the previous commit: `image::load_from_memory` and
`load_from_memory_with_format` already applied `Limits::default()`, whose
512 MB allocation cap bounded every picture decode. The PDF change had set
`max_alloc = None`, loosening that; restore the default. The render and RTF
side caps (16 384 px) newly rejected wide-but-short pictures that fit in
memory, so render now uses the pixmap's own `u16` side limit (what
`premul_pixmap` enforced after decoding, applied before it instead) and the
RTF re-encode keeps only the explicit allocation cap. Net effect: rejection
moves to header time with no change to the sizes accepted. Also fix a
comment: the header-row repeat cap is per table, not per document.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* Word forms keep their legacy form fields and rows' skipped grid columns when saved (part of #1174)
* Tests: field initializers name the new form field
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>
docbin decrypts XOR-obfuscated, RC4 and RC4 CryptoAPI files after verifying the
password against the stored verifier; the engine reuses the .docx password
prompt errors. Written from [MS-DOC] and [MS-OFFCRYPTO]; synthetic tests only.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Found by comparing EPA's .doc templates with Word for Windows.
Before: custom tab stops in .doc files were skipped, so centred titles,
right-aligned dates and dot leaders set with tabs lost their alignment;
and a section mark (0x0C) at a section's end became a page break inside
a paragraph, so the file's sections (continuous breaks, footers) were
lost and every section break started a new page.
After: sprmPChgTabsPapx and sprmPChgTabs are read in styles and
paragraphs (removed stops clear inherited ones; each added stop keeps
its alignment and leader), and sprmPChgTabs' one-byte size is decoded
per [MS-DOC] (it was read as two bytes, swallowing the sprms after it).
A section mark at a section's end now ends that section's last
paragraph, so the section's properties and footers are kept. Both EPA
.doc templates in the comparison set now have Word's page count.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* .doc import: inline pictures, floating pictures and basic shapes (#551)
Word 97-2003 files kept their text but dropped every picture and drawing.
Pictures and drawings are now read from the OfficeArt records ([MS-DOC],
[MS-ODRAW]):
- Inline pictures: the PICF's OfficeArtInlineSpContainer (MM_SHAPE /
MM_SHAPEFILE) and its FBSE/BLIP, with crop and alt text from the shape
properties; the older raw-image path stays as a fallback.
- Floating pictures and shapes: PlcSpaMom/PlcSpaHdr anchors joined with
the shapes of the OfficeArtContent drawings (fcDggInfo), placed by the
SPA bounds, bx/by or posrelh/posrelv/posh/posv, wrap type and
fBelowText; rotation and flips; pictures from the BLIP store.
- Shapes: rectangle, rounded rectangle, ellipse, diamond, triangles, star,
block arrows, heart, lines/connectors, with fill/line colour and width;
groups flattened into one group space; text boxes linked to their
PlcftxbxTxt story parts by spid.
- BLIPs: PNG, JPEG, TIFF as-is, DIB converted to PNG, EMF/WMF inflated
(the metafile renderer draws them), PICT kept as a sized placeholder.
- Hostile input: record lengths clamped to their parent, nesting depth
and record counts capped, decompression size-limited, picture bytes
capped per document as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Review fixes for #558: miniz_oxide as a workspace dependency
Restores the change @echelon pushed in 04b59287, which a rebase dropped: miniz_oxide is declared once in the root [workspace.dependencies] and docbin uses it with workspace = true.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Show field codes instead of results with View > Field Codes (Alt+F9) or
per field with Toggle Field Codes (Shift+F9); a field showing its code is
drawn as { CODE } on light grey. Edit a field's code in a small popup
(double-click a field showing its code, right-click > Edit Field..., or
fields.setCode), and Ctrl+F9 asks for a new field's code. F9 updates
fields and shows the selected ones' results again.
New commands: fields.unlink (Ctrl+Shift+F9) replaces fields with their
text, fields.lock / fields.unlock (Ctrl+F11 / Ctrl+Shift+F11) keep a
result through updates (already saved as w:fldLock), fields.next /
fields.previous (F11 / Shift+F11) select the next or previous field, and
fields.selected reports the fields in the selection. Range fields
(citations) can be locked, unlinked, edited and stepped through too.
The right-click menu gains Update Field, Edit Field... and Toggle Field
Codes when the selection holds a field. Strings are translated in every
catalog; parity catalog, ui-parity, gaps and roadmap updated.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Follow [MS-DOC] in the Word 97-2003 reader:
- Prm: bit 0 is fComplex; Prm0 isprm in bits 1-7, Prm1 Prc index in bits 1-15 (#458)
- sprmSBkc operands 0-4 map to continuous/new column/new page/even/odd (#459)
- sprmSBOrientation 1 = portrait, 2 = landscape (#460)
- negative sprmPIlfo (0xF802-0xFFFF) references the negated LFO index (#461)
- StdfBase: istdBase shares the word with stk and is zero-based; the
whole base chain is applied, bounded and cycle-safe (#462)
- SttbfFfn entries advance by cchData, covering the optional xszAlt (#463)
One regression test per issue, built in code from the spec.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Keep charts, SmartArt and OLE objects when saving DOCX (#319)
Charts and SmartArt diagrams were drawn since #292 but the writer skipped
them, and OLE objects kept only their picture, so opening and saving a
document silently dropped them.
The reader now keeps each object's own markup (the a:graphic of a chart or
diagram, the whole w:object of an OLE object) on the model object, and every
part it reaches through relationships (chart part, embedded workbook, colour
and style parts; diagram data, layout, quick style, colours and drawing; OLE
embedding and picture) as opaque bytes in Document::passthrough, with their
content types and relationships in a manifest, as the VBA project already is.
The writer emits the object with fresh relationship ids pointing at the
re-added parts, keeping each part's own relationship ids and content types.
A chart or diagram gets its frame from the object's current size and
position; an OLE object gets its new size written into its VML shape. A part
whose name the writer uses itself is renamed, and an object pasted more than
once gets its own copies of every part (sharing past a size budget). Part
counts, relationship counts, markup size and copy bytes are all capped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep charts, SmartArt and OLE objects when saving DOCX (#319)
Charts and SmartArt diagrams were drawn since #292 but the writer skipped
them, and OLE objects kept only their picture, so opening and saving a
document silently dropped them.
The reader now keeps each object's own markup (the a:graphic of a chart or
diagram, the whole w:object of an OLE object) on the model object, and every
part it reaches through relationships (chart part, embedded workbook, colour
and style parts; diagram data, layout, quick style, colours and drawing; OLE
embedding and picture) as opaque bytes in Document::passthrough, with their
content types and relationships in a manifest, as the VBA project already is.
The writer emits the object with fresh relationship ids pointing at the
re-added parts, keeping each part's own relationship ids and content types.
A chart or diagram gets its frame from the object's current size and
position; an OLE object gets its new size written into its VML shape. A part
whose name the writer uses itself is renamed, and an object pasted more than
once gets its own copies of every part (sharing past a size budget). Part
counts, relationship counts, markup size and copy bytes are all capped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Merge fix: #340's rotation round-trip test builds an Image with #333's ole field
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* D0+D1: open Word 97-2003 .doc — container, FIB, piece table text (#28)
New wordcraft-docbin crate (L2) reads the OLE2 container via the cfb
crate (MIT) and parses the FIB and piece table from the [MS-DOC]
specification: text pieces decode as UTF-16LE or Windows-1252 (table
moved to wordcraft-doc::encoding), subdocument CP ranges, and Word's
special characters map to the model's (tabs, breaks, hyphens). .doc/.dot
registered in OPEN_EXTS and the I/O dispatch. Encrypted and Word 6/95
files give clear errors; every length is checked and proptest
truncation/flips never panic.
* D2: .doc formatting — stylesheet, fonts, CHPX/PAPX FKPs, sprms (#28)
Sprm decoder with operand-size rules (incl. sprmTDefTable/sprmPChgTabs
exceptions and the Prm0 isprm table), bin tables + ChpxFkp/PapxFkp page
lookup, STSH styles (StdfBase layout validated against real Word files:
bchUpe == cbStd, sti 0 = Normal) and the SttbfFfn font table in both
extended and self-delimiting flavours. The CP walk now splits runs on
formatting changes and resolves paragraph styles: bold/italic/strike/
underline/caps/hidden, colours (Ico palette + COLORREF), half-point
sizes, super/subscript, fonts, highlights; alignment, indents, spacing,
keep options, outline levels. Unknown sprms are skipped leniently;
an env-gated corpus test opens real files (styles + text asserted).
* D3: .doc structure — sections, headers/footers, tables, lists (#28)
- Sections: PlcfSed/Sepx with 16-bit measurement sprms (sprmSXaPage & co),
explicit sprmSBOrientation with a width/height fallback; even/odd/first
header and footer stories from Plcfhdd become document parts.
- Tables: sprmTDefTable grids (operand length fixed to cb+1 per the spec),
horizontal merges (sprmTMerge), vertical merges (sprmTVertMerge), cell
shading (sprmTSetShd / sprmTDefTableShd80), cell widths, valign, row
height/header/cant-split, and table borders (sprmTTableBorders80 Brc80s).
- Lists: PlfLst (LSTF + appended LVLs) and PlfLfo (LFO + LFOLVL overrides)
resolved into the numbering model; sprmPIlvl/sprmPIlfo mark paragraphs.
- Combined fixture (intro + table + list + 2 sections + header) round-trips
through Document JSON; hostile proptests now mutate the structured fixture.
* D4: .doc notes, fields, bookmarks and inline pictures (#28)
- Footnotes/endnotes: PlcffndRef/PlcfEndRef reference CPs paired with the
PlcffndTxt/PlcfEndTxt story ranges; each story walks into a Footnote or
Endnote part and the 0x02 anchor becomes a NoteRef object.
- Fields: 0x13/0x14/0x15 state machine in the walker — instruction text is
hidden, the cached result kept; HYPERLINK fields keep their result as
linked runs, everything else becomes one Field object.
- Bookmarks: SttbfBkmk names + Plcfbkf/Plcfbkl CPs become BookmarkStart/
BookmarkEnd objects at their anchors.
- Pictures: sprmCPicLocation points into the Data stream; the PICF header
gives the display size (dxaGoal/dyaGoal scaled by mx/my) and the payload
is format-sniffed (PNG/JPEG/GIF/BMP; metafiles are dropped with a warn).
- Hostile proptests now truncate/flip a fixture with all of the above.
* D5: .doc import hardening, caps and roadmap (#28)
- Cap inline pictures at 5 000 per document.
- Engine test: .doc/.dot bytes dispatch through the Word 97-2003 reader
(garbage must error, never panic or open empty).
- ROADMAP: Word 97-2003 import row (60% — text, formatting, styles,
sections, headers/footers, tables, lists, notes, fields, bookmarks,
raster pictures).
Real-corpus validation stays opt-in: WORDCRAFT_DOCBIN_FIXTURES=<dir>
cargo test -p wordcraft-docbin corpus_files_open.
* Corpus fixes: compressed-piece FC space, PAPX query point, none-level labels (#28)
Validated against two real files (kept locally under plan/word/fixtures,
gitignored): a 1 MB Lorem-ipsum sample and a 2003 styles/lists/table
sample.
- FKP bin tables and pages address the WordDocument stream in true byte
offsets; a compressed piece stores its base fc doubled (byte = fc/2),
so fc_of_cp now unwinds the doubling. Before, every PAPX/CHPX lookup in
compressed (cp1252) files landed on the wrong entry — tables collapsed
to one row, lists and heading styles were lost.
- The paragraph-properties query now uses the paragraph mark's own FC
(inside the paragraph) instead of the FC after it, which picked up the
next paragraph's FKP entry.
- Fixture papx entries re-keyed to paragraph-start CPs, the real rgfc
convention.
- Counters::next_label returns nothing for NumFormat::None levels (Word's
numFmt none shows no label; also fixes a latent docx-reader case).
- Symbol/Wingdings bullet chars map to Unicode glyphs (• ▪ ○ ●).
With this, both corpus files render headings, styles, nested and numbered
lists, the tables and the PAGE field correctly; metafile-wrapped images
and OLE objects stay dropped by policy (logged).
* docbin: bound the text walk by real text; dedupe and cap pictures (#28)
Hostile CP numbers no longer drive the work:
- header/footnote/endnote story ranges from the Table stream are clamped to their
subdocument (ccpHdd/ccpFtn/ccpEdn) with checked base additions, and every walk stops at
the last piece's CP; a story spanning [0, 4e9) used to push billions of U+FFFD;
- characters are found by binary search over the pieces (PieceTable::char_at), and an
unreadable stretch (gap, truncated or out-of-stream piece) is skipped in one step rather
than one U+FFFD per CP; the old per-character scan was O(chars x pieces);
- the piece CPs are made non-decreasing at parse, so inverted pieces are empty and binary
search stays sound; surrogate pairs now decode;
- note references are looked up by binary search; the style's CharProps is borrowed, not
cloned, per character;
- pictures are interned by fc_pic (5000 anchors on one picture share one media entry and
one copy) and the bytes copied per document are capped at 256 MB.
Each fix has a crafted-bytes regression test; against the previous code the story,
piece-claim, last-piece and many-pieces tests hang (killed at 60 s) and the shared
picture test copies the picture once per anchor (80 GB).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docbin: checked offset arithmetic in lists, sections and table rows (#28)
- list.rs: PlfLst/PlfLfo/LVL offsets (at + 2 + cLst*28, at + 4 + lfoMac*16, off += len,
...) saturate, which matters on 32-bit targets (wasm32) where fc + n can overflow usize;
the LFO override path no longer indexes abstracts directly.
- sections.rs: the Sepx range (fc_sepx + 2 + cb) is checked; Plcfhdd is read only as far
as the stories a capped section count can use (it used to reserve lcb/4 entries).
- table.rs: sprmTDyaRowHeight = i16::MIN negated in i16 (overflow panic in debug builds);
now -(v as f32).
Regression tests: the row height test failed before (overflow panic with debug assertions, a 0 pt height without); the list test pins the saturating
offsets (it only overflowed on 32-bit targets).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Format the merged OPEN_EXTS list
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>