craft-cfb refuses a header without the 0xFFFE mark at offset 28
([MS-CFB] 2.2), so craft-xls's sniff called the fixture LegacyBinary.
The old hand-written parser never read that field, which is why the test
only broke with the switch. The fixture is a real workbook otherwise:
sniff now says Xls and read_xls parses it.
craft-cfb refuses a header without the 0xFFFE mark at offset 28
([MS-CFB] 2.2), so craft-xls's sniff called the fixture LegacyBinary.
The old hand-written parser never read that field, which is why the test
only broke with the switch. The fixture is a real workbook otherwise:
sniff now says Xls and read_xls parses it.
craft-cfb refuses a header without the 0xFFFE mark at offset 28
([MS-CFB] 2.2), so craft-xls's sniff called the fixture LegacyBinary.
The old hand-written parser never read that field, which is why the test
only broke with the switch. The fixture is a real workbook otherwise:
sniff now says Xls and read_xls parses it.
craft-xls reads Dv/DVal records (storytold/craft-libs#999, a port of #3636).
xls.rs maps each rule onto the model as the .xlsx reader maps a dataValidation:
a non-literal list source becomes =ref. The tests are #3636's, run through
read_xls; the cap test now expects craft-xls's skipped-rules warning.
Co-authored-by: kiwi4boy <kiwi4boy@gmail.com>
package.rs keeps GridCraft's Package, Rel and ZipOut, now on craft_opc::LazyPackage
and craft_opc::ZipOut (on craft-zip). Limits stay GridCraft's (200,000 entries,
512 MB a part, 1 GB a package) and so do the budget rules, which craft-opc took
from GridCraft. The budget tests moved with them. normalize, resolve and
split_dir are craft-opc's (the same code).
Every craft-libs crate moves to aa84415b. craft-xls now reads data validation;
until GridCraft maps it, a sheet with rules still warns that .xls data validation
is not imported. ods.rs matches entity names through a slice, since the new
dependencies make the old as_ref() call ambiguous.
Decrypting password-protected workbooks ([MS-OFFCRYPTO], Agile and Standard) moved
to storytold/craft-libs as craft-offcrypto (#931), on craft-cfb. GridCraft keeps
its DecryptError and the engine's messages; crypto.rs maps the crate's errors onto
it. The private compound-file reader and its tests are gone: sniffing looks for
the encryption streams with craft_cfb::has_entry_named (with the same fallback for
a damaged directory), and the test builders encrypt with craft-offcrypto and write
compound files with craft-cfb.
One difference, as craft-offcrypto has it: the dataIntegrity HMAC key is used at
the length it decrypts to (up to the hash size); GridCraft accepted only a
salt-size or hash-size key. Both verify what Office writes.
Text decoding (byte order marks, UTF-8, Windows-1256/1252), delimiter sniffing,
the record parser and field quoting moved to storytold/craft-libs as craft-csv
(#889), with the SYLK reader as craft_csv::sylk. GridCraft keeps typed values,
date-order inference, the used-range limit and the sheet writer in csv.rs, and in
io/sylk.rs the workbook a SYLK file becomes: formulas parsed (an unparsable one
is skipped and counted, as before), error values, number-format styles and column
widths.
The BIFF8 reader moved to storytold/craft-libs as craft-xls (#878). GridCraft
keeps the mapping onto its model in xls.rs: formulas parsed, Excel's implicit @
for legacy formulas, pixel widths and heights, styles interned per XF, sheet-name
checks, print areas and titles, calculation settings and GridCraft's warning text.
The workbook now opens on the sheet it was saved on when that sheet is visible,
else on the first visible sheet (from #3551 by @Morzilla, whose test is kept).
RString cells are read as text.
Every craft-libs crate moves to one revision, 119415f4.
release.yml also runs nightly on main with craft-common-actions' dry-run: every release build
except macOS, with nothing signed, pushed or drafted. Breakage that only the release build hits
(riscv64, Flatpak, FreeBSD, packaging) then shows up the next morning instead of on release
day, as it did on 11 Nov 2026.
* engine: bound Advanced Filter, Remove Duplicates, AutoFilter, Circle Invalid Data; one-pass AutoFit; saturating range dimensions
* address review: AutoFit scans each run of adjacent selected columns separately
* address review: report a truncated circle-invalid scan, no spill scan for plain cells in Replace, one pass for many AutoFit runs
* address review: warn in the app when Circle Invalid Data stopped early; Replace test requires success
* address review: the scan note joins an open-time notice; say cell visits
* engine: index merges and bound the page in HTML export and clipboard HTML
Save as Web Page and the clipboard's HTML scanned every merge of the
sheet for every cell, counted a merge's span over all the rows the file
gave it, placed each picture by summing every custom row before it, and
grew the page without a byte limit. Merges are now indexed once over
the exported rectangle (overlapping merges past a few visits per cell
are refused), spans use prefix counts clipped to the table, anchors use
LineIndex, and the page stops at 256 MiB with an error.
* address review: check the web page size after each chart and before returning it
* address review: bound the merge index, escape cell text within the page's room, size images before encoding
* address review: escape image attributes within the page's room
* Directory walkers key visited directories by (dev, inode); stats skips non-regular or oversized .rs targets
* address review: record a walked directory only within the depth limit; stats skips symlinked sources and bounds the read
* address review: rescan a directory reached from a shallower path, skipping files already collected
* xtask stats: test the directory walk (links, identity, size limit)
* address review: create the test directories atomically and privately
* xtask stats tests: no unreachable! in the directory helper
* gridcraft check: unit-test that each workbook is listed once whatever path reaches it
* functions: one work budget across every call of a lifted function
A function lifted over a computed array runs once per element, and only the
six lookups counted their repeated work against LIFTED_SCAN_BUDGET. Criteria
aggregates rescanned their ranges per criterion, CUMIPMT/CUMPRINC stepped
through up to 1e7 periods per element, and array-producing functions such as
MUNIT built a whole result per element before keeping its first cell.
The budget now covers all of it: COUNTIF/SUMIF/AVERAGEIF(S)/MAXIFS/MINIFS
charge their range cells per call, the period loops of every element are
added up before any runs, and array results charge their cells as they are
built. Past the budget the call is #NUM!, as lifted lookups already are.
* address review: charge numeric-text periods of lifted CUMIPMT/CUMPRINC to the work budget
* address review: charge only calls that reach the cumulative period loop
* address review: charge the numeric text a lifted period call parses
* address review: SUMIF/AVERAGEIF charge only the sum-range cells they read; weight cumulative period work
* print: hold the print area to the grid; render each chart once and budget picture and chart output across pages
* address review: budget chart path points and drop charts that do not fit the budget
Adds step-security/harden-runner (audit, pinned to v2.22.4 like
craft-common-actions) as the first step of each job that did not have it,
so every workflow in the org's public repos reports its egress to
StepSecurity. Audit, not block: block-mode allow lists come from each
job's observed traffic.
The seccomp list named libc::SYS_kexec_file_load, which libc (0.2.190) defines for x86_64 and
aarch64 but not riscv64, so the release's Linux riscv64 build stopped with E0425. riscv64 takes
the number from the kernel's generic table, 294, as aarch64 does; the test checks libc's aarch64
value against it.
* xlsx: share each custom number format across cell formats, cap font names at 31 characters
Every cell format record copied its font (name included) and built its
own copy of its number-format code, so many short records naming one
long font or code multiplied it in memory. Custom codes are now built
once and shared, and font names are cut to Excel's 31 characters.
* xlsx: drop stray worktree entries added by mistake
* address review: compare truncated theme font for the scheme link; intern inline dxf number formats
* Charts: one point budget per chart and a byte budget for web-page SVG
A chart resolved every series reference into its own vector of up to
100,000 points, so 255 short whole-column series retained and drew
25.5 million points, and Save as Web Page wrote one SVG element per
mark without any output limit.
- resolve() shares MAX_CHART_POINTS (1,000,000) across the values,
X values and bubble sizes of all series; a series past the budget
gets what is left.
- chart_svg() stops writing once a chart passes the bytes left of a
64 MiB per-page budget and leaves that chart out.
* address review: charge X values to the chart point budget only when they are kept
* address review: charge scatter Y and X values to the chart point budget as a pair
* xls: charge expanded shared-formula text to the import's text allowance
Shared-formula followers each kept their own printed formula text, but
none of it counted against the 256 MB text allowance, so compact PtgExp
records could expand into gigabytes of retained formulas. Each attach
now charges its text; past the allowance the formula is dropped and the
cell keeps its cached value.
* address review: keep resolving array formulas after the shared-formula text allowance is spent
* address review: keep the shared-formula text cut-off across worksheets
* address review: expand shared formulas only after every sheet's mandatory text is charged
* address review: refuse duplicate sheet names at import; adapt tests to read_xls_budgeted
* pivot: cached Rank and Running Total, accumulator-width limit, capped subtotal captions and PivotChart series
* address review: store running-total entries only for items with a number
* address review: running totals visit only the top-level items that hold the path
* xlsx: resolve cell pictures through a per-structure layout and a relationship index
Every rich value rebuilt its structure's whole key list before checking
its value count, and scanned the image relationship table for a
duplicate id. Structures are now laid out once and relationships are
indexed by id (a repeated id stays invalid), so reading cell-picture
metadata is linear in its records.
* address review: wide-structure fixture uses the right count and loads a valid picture
* engine: bound Scenario Summary size and solver report name searches; charge comment and link text to undo history
* address review: count result rows in the scenario summary limit and reply/date vector storage in the history estimate
* Row auto-fit: sweep merges once per sheet instead of per row
auto_row_height() picked the merges touching a row by scanning every
merge on the sheet, and opening a workbook (fit_imported_rows) or a
commit fitting many rows called it per row, so rows x merges
comparisons ran synchronously on open.
Rows are fitted in ascending order, so a MergeSweep sorts the sheet's
merges by first row once and keeps only those active on the current
row. All comparisons count against MERGE_FIT_WORK (50,000,000) per
pass; past it the remaining rows keep the heights they have.
auto_row_height keeps its signature for single rows.
* address review: share the merge-comparison budget across the whole auto-fit pass
* address review: build the merge sweep lazily, after the row fast paths
* address review: charge the merge sort to the fit budget, keep row heights once it is spent
* address review: a pass that fits one row scans the merges instead of sorting them
* calc: INDEX decides a multi-cell position from its shape before reading it
* calc: charge each formula's evaluation against one cumulative work budget
* address review: a database function whose computed criterion ran out of work is #CALC!
* address review: give the test's criteria cell its stored value
* address review: stop matching database records once a computed criterion exhausts the work budget
* address review: skip the criteria-row scan once work is exhausted
* address review: stop between conditions once the work budget is spent