Commit Graph
3025 Commits
Author SHA1 Message Date
Dylan M. Taylor dc57def3d1 address review: escape image attributes within the page's room 2026-11-11 14:32:16 +00:00
Dylan M. Taylor 4a2006cf9d address review: bound the merge index, escape cell text within the page's room, size images before encoding 2026-11-11 13:57:59 +00:00
Dylan M. Taylor 0d94a4fb7e Merge branch 'main' into fix/sec-html-export-merge-index 2026-11-11 08:39:31 -05:00
Dylan M. Taylor 7b825d00be Save as Web Page: a byte budget for the charts' SVG (an error past it) (#3209)
* 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
2026-11-11 08:39:25 -05:00
Dylan M. Taylor 7726e9c6ef xls: charge expanded shared-formula text to the import's text allowance (#3256)
* 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
2026-11-11 08:28:28 -05:00
Dylan M. Taylor 9a025205bd pivot: Rank and Running Total computed once per sibling set; accumulator, caption and chart-series bounds (#3306)
* 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
2026-11-11 08:16:57 -05:00
Dylan M. Taylor 01abd95420 xlsx: resolve cell pictures through a per-structure layout and a relationship index (#3255)
* 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
2026-11-11 08:02:00 -05:00
Dylan M. Taylor e5ac9474df engine: Scenario Summary size limit, bounded solver report names, undo history counts comment and link text (#3308)
* 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
2026-11-11 07:40:55 -05:00
Dylan M. Taylor b21a8541e7 Row auto-fit: sweep merges once per sheet instead of per row (#3195)
* 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
2026-11-11 07:26:18 -05:00
Dylan M. Taylor a6c331b63f calc: one cumulative work budget per formula, and INDEX decides multi-cell picks from their shape (#3188)
* 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
2026-11-11 07:17:04 -05:00
Dylan M. Taylor 12d26d53cc xlsx: analyse each kept chart sheet's graph once per save (#3261)
* xlsx: analyse each kept chart sheet's graph once per save

chart_sheet_usable walked a chart sheet's relationship graph and parsed
its charts for every workbook entry, so many entries naming one shared
graph multiplied the work. The referenced sheet names are now cached by
relationship id.

* address review: cache the chart-sheet usability verdict per relationship id instead of every reference

* address review: cache chart-sheet analysis by the graph root, not the relationship id
2026-11-11 07:06:18 -05:00
Dylan M. Taylor 9dc7e3826a numfmt: TEXT returns #VALUE! when its result passes a cell's 32,767 characters (#3264)
* numfmt: TEXT returns #VALUE! when its result passes a cell's 32,767 characters

A text section repeats its input once per @ placeholder, and TEXT handed
back whatever that built, so nested TEXT calls could multiply a long
text until memory ran out. Results past the cell text limit are now
#VALUE!.

* address review: stop TEXT rendering past the cell limit and count UTF-16 units
2026-11-11 06:44:34 -05:00
Dylan M. Taylor f79a425ca9 xlsx: find a reused picture through a content hash instead of comparing every media part (#3214)
* xlsx: find a reused picture through a content hash instead of comparing every media part

Out::image_part compared each new picture byte by byte with every
media part written so far, so a workbook of many distinct pictures
sharing a long prefix was exported in quadratic time. Media parts are
now indexed by extension and SHA-256.

* address review: index preserved media parts when they are added

* address review: do not hash an image twice when it is first added
2026-11-11 06:32:44 -05:00
Dylan M. Taylor 76e53085e3 xlsx: keep the cached value of a follower whose shared-formula copy is refused (#3212)
* xlsx: one hyperlink allowance per sheet, and a copy limit for unparsable shared masters

Every <hyperlinks> element of a worksheet started a fresh cell and byte
allowance although all of them fill the same map. The sheet now keeps
one allowance across its elements.

Followers of a shared master the parser refuses each copied its full
text; a sheet full of followers of a long master multiplied that text
without bound. The copies are now charged against a 64 MiB per-sheet
limit, past which the followers keep their cached values.

* address review: keep the cached value of a follower dropped for the shared-formula copy limit inside array ranges
2026-11-11 06:15:08 -05:00
Dylan M. Taylor bc2dabc7ea FORMULATEXT and the in-cell editor keep Hidden formulas hidden (#3298)
* FORMULATEXT and the cell editor keep a hidden formula's source hidden

* address review: recalculate formulas when sheet protection changes

* address review: skip the full recalculation for a no-op unprotect

* address review: recalculate on protect/unprotect without marking the edit structural
2026-11-11 05:48:31 -05:00
Dylan M. Taylor e14f783503 Analysis ToolPak: stop building a report once it outgrows the output limit (#3207)
* Analysis ToolPak: stop building a report once it outgrows the output limit

Report builders filled their whole output map before write_report
checked its size, so Descriptive Statistics grouped by rows over a
tall, nearly empty column allocated about 16 entries for each of up
to a million variables before refusing the report.

Grid now tracks its extent as values are stored and keeps nothing
once the report's rows x columns pass MAX_OUTPUT_CELLS; write_report
refuses such a report. extent() no longer rescans every entry.

* address review: stop the descriptive builder once the grid is too large

* address review: descriptive statistics reads variables one at a time
2026-11-11 05:40:31 -05:00
Dylan M. Taylor 542aa176ce Wildcards: match * and ? patterns in one pass over the text (#3205)
* Wildcards: match * and ? patterns in one pass over the text

After a mismatch the matcher restarted the pattern after the last
star at the next text position, so a long repetitive cell against a
near-matching pattern cost text x pattern comparisons. AutoFilter
custom criteria, Find and Replace all call it once per cell.

wildcard() now runs the pattern as a bit-parallel automaton: one pass
over the text, one step per 64 pattern positions for each character.
Results are unchanged; a test compares it with the old matcher on
every short text and pattern over a small alphabet.

* address review: build wildcard literal masks lazily with a bounded cache

* address review: keep masks of the most frequent wildcard literals past the cache cap
2026-11-11 05:19:24 -05:00
Dylan M. Taylor 67a0b7f325 Security: chart rasterising, spline points and category labels stay bounded (#3204)
* chart: bound scan conversion, spline points and category label bytes

- The rasteriser tested every edge of a path at every sub-sample of every
  covered row. It now keeps an active-edge list (edges sorted by top, joining
  and leaving as the scan passes them), and one rasterize call has a total
  scan-work allowance after which remaining fills are skipped.
- Smoothed lines budgeted interpolated points per run, so gap-separated short
  runs escaped both decimation and the cap. The budget is now shared by all
  runs of a line; once spent, runs are drawn through their data points.
- Category labels copied every text value in full. Labels are clipped to 255
  characters before copying, within an 8 MiB budget per series.

* address review: chart raster and label budget fixes

* address review: bound polyline point count before stroke_paths
2026-11-11 05:07:04 -05:00
Dylan M. Taylor 6c3c51829a Merge branch 'main' into fix/sec-html-export-merge-index 2026-11-11 04:56:10 -05:00
Dylan M. Taylor bbe0825404 Security: TEXTJOIN builds its result piece by piece within the text cap (#3202)
* functions: TEXTJOIN builds its result piece by piece within the text cap

TEXTJOIN converted every input and delimiter into an owned String before
joining, so many references to one long shared text copied gigabytes before
the joined length was ever checked. Errors are now found first without
building text (so they still win in argument order), then each piece and
delimiter is converted only as it is appended, stopping at the cap.

* address review: check the cap before copying each text piece
2026-11-11 04:55:21 -05:00
Dylan M. Taylor aa4951fdf4 Security: WORKDAY, PIVOTBY, SORT and the D-functions stop rescanning their input (#3200)
* functions: stop WORKDAY, PIVOTBY, SORT and the D-functions rescanning their input

- WORKDAY/WORKDAY.INTL counted the holidays each step passed by scanning every
  holiday, once per step; a run of consecutive holidays made that quadratic.
  The working-day holidays are kept sorted and counted by binary search.
- PIVOTBY totals rescanned the whole source (rebuilding keys) once per total
  row and column. The grouping pass now also indexes rows by row key and by
  column key, and totals read from those indexes in the same order.
- SORT copied one key per sort_index entry for every row, so a long
  sort_index with repeated columns multiplied the source. Repeats can't break
  a tie their first use left, so only first uses are kept.
- DSUM and the other database functions tested every record against every
  criteria row. A selection whose records x conditions exceed 5e8 is #NUM!,
  and each criteria label's database column is found once, not per cell.

* address review: bounds-check SORT indices before narrowing; empty criteria rows are not charged
2026-11-11 04:36:48 -05:00
Dylan M. Taylor 79fe7b8eb6 Forecast Sheet: count hindcast rows against the work limit (#3199)
* Forecast Sheet: count hindcast rows against the work limit

MAX_WORK bounded history rows x forecast rows, but a Forecast Start
inside the history adds a hindcast row for every history point after
it, each holding the same ETS formulas that refit the model over the
history before the start. Those rows were not counted, so a start in
the middle of 10,000 rows of history built millions of uncharged
model fits per recalculation.

The forecast row cap is now computed from the rows the model is fit
to, after subtracting the hindcast rows; a start so early that the
hindcast alone passes the limit is refused, and the default number of
periods stays within the cap.

* address review: base the in-history forecast cap on the slots the fit range spans

* address review: document the hindcast-aware forecast work limit

* address review: document the fitted-row minimum in the forecast work limit

* address review: say the forecast start row counts toward the hindcast work limit
2026-11-11 04:22:54 -05:00
Dylan M. Taylor ff9f4b8ace Find and Replace: bound the text a replacement builds (#3194)
* Find and Replace: bound the text a replacement builds

Each occurrence copied the whole replacement string, so a cell full of
short matches and a long replacement expanded tens of kilobytes of
input into about a gigabyte, in both the case-sensitive and the
case-insensitive paths.

Replacement now builds the new text only while it stays within
Excel's 32,767 characters per cell (counted as UTF-8 bytes, 4 each at
most) and within 256 MiB for the whole command, checking before each
copy. Past either limit the command fails and changes nothing.

* address review: enforce the 32,767-character limit on replacements and borrow the replacement text
2026-11-11 04:02:22 -05:00
Dylan M. Taylor 5ce63e5fc2 Get Data: refuse an import whose dense rows would be too large (#3193)
* Get Data: refuse an import whose dense rows would be too large

data.getData and data.fromTextCsv built every row densely before
writing it: JSON records were padded to the union of all keys (found
with a quadratic Vec::contains scan), and CSV cells were expanded over
the whole used rectangle. A few sparse records or one far-away CSV
cell therefore allocated billions of slots.

The importer now collects JSON keys with a HashSet, refuses more keys
than a sheet has columns, and checks the projected rows x columns
against MAX_IMPORT_CELLS (10,000,000) before building any row.

* address review: stop collecting JSON keys at the column limit and reject oversized row counts before building rows

* address review: enforce the CSV import cell limit while parsing
2026-11-11 03:48:32 -05:00
Dylan M. Taylor b7fd198e06 Conditional formatting: one scan budget per rule, no thresholds for unsupported colour scales (#3201)
* Conditional formatting: one scan budget per rule, and no thresholds for unsupported colour scales

numbers_in() scanned each of a rule's ranges separately with its own
2,000,000-cell limit, so a rule listing the same (or overlapping)
large range many times multiplied the scan while painting a single
cell. Colour scales also evaluated every stop's threshold, formulas
included, before discarding scales that are not 2- or 3-colour.

- A rule's statistics now share STATS_SCAN_LIMIT across all its ranges,
  and an exact repeat of a range is scanned once.
- A colour scale with a stop count other than 2 or 3 paints nothing
  and evaluates none of its thresholds.

* address review: count stored and spilled cells of sparse ranges, skip statistics for unsupported colour scales

* address review: skip unrelated spills a row at a time in the statistics scan
2026-11-11 03:18:38 -05:00
Valentin KleinandClaude Sonnet 5.5 55f3004cb2 Numfmt: one built-in format table, fewer allocations rendering numbers and General (#817) (#820)
* Numfmt: one built-in format table, no dead render arms, fewer allocations (#817)

S1: the built-in format table lives only in gridcraft-xlsx (file formats do not
depend on numfmt); numfmt's unused copy and its exports are removed, xlsx gets
tests pinning its lookup rules.
S3: drop the render_number arm that printed what the fallback prints; literal
text is appended to the output instead of built as a String per call.
O3: render_number keeps pieces, text-before and literal replacements in one
buffer (Parts), fill_int writes digits left to right, Section stores the date
flags (sub-second digits, calendar, AM/PM) computed at parse time, render_date
writes into one buffer.
O4: significant_digits formats into a stack buffer; format_general_fit re-rounds
the 15 digits per width into reused strings.

An equality test compares format_value, TEXT() and format_general_fit with
digests recorded from the previous implementation.

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

* xlsx test: builtin ids 42 and 44 are written as custom formats like 5-8

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

* numfmt: push_str for short month/day names (clippy)

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-11 08:57:06 +01:00
FredApps 022074344b Merge pull request #2196 from Higgs32584/fix/2170-shape-font-attr-escape
xlsx: escape shape font typeface attribute (#2170)
2026-11-11 08:12:56 +01:00
FredApps ac4fb45c3f Merge pull request #2326 from nihilist-narwhal/fix/2164-mcp-stdio-line-bound
MCP: the stdio server keeps at most 16 MB of a message (#2164)
2026-11-11 08:12:47 +01:00
FredApps 3d52a20dbd Merge pull request #379 from vangel76/fix/lookup-blank-result
Functions: lookups keep an empty target cell blank (#374)
2026-11-11 08:12:38 +01:00
FredApps 4cc2e21d7c Merge pull request #3382 from vangel76/fix/3321-manage-rules
Manage Rules: scroll the rule list instead of laying out every rule
2026-11-11 08:11:07 +01:00
FredApps 088b43be28 Merge branch 'main' into fix/2170-shape-font-attr-escape 2026-11-11 07:58:02 +01:00
FredApps 942f639fbf Merge branch 'main' into fix/2164-mcp-stdio-line-bound 2026-11-11 07:57:52 +01:00
FredApps a01ff3e7b3 Merge branch 'main' into fix/lookup-blank-result 2026-11-11 07:57:42 +01:00
FredApps b79e992576 Merge branch 'main' into fix/3321-manage-rules 2026-11-11 07:57:35 +01:00
Pranav 0fed26a14f Sandbox: a crate that runs a helper program confined (Landlock and seccomp on Linux, sandbox-exec on macOS)
## Stack

The Python cells work is split by layer. It merges in this order, each with a merge commit rather than a squash. Nobody can switch Python on before #3481.

1. #3475 depends on `main`
2. #3476 depends on `main` **(this pull request)**
3. #3477 depends on [#3475](https://github.com/storytold/gridcraft/pull/3475) and [#3476](https://github.com/storytold/gridcraft/pull/3476)
4. #3478 depends on [#3477](https://github.com/storytold/gridcraft/pull/3477)
5. #3479 depends on [#3478](https://github.com/storytold/gridcraft/pull/3478)
6. #3480 depends on [#3479](https://github.com/storytold/gridcraft/pull/3479)
7. #3481 depends on [#3480](https://github.com/storytold/gridcraft/pull/3480)

## Summary

`crates/sandbox` runs a program that can read only the paths its policy lists, write nothing except `/dev/null`, start no process, and use no network. An application re-executes itself with a launcher flag, closes every inherited descriptor above standard error, applies the confinement, checks that it holds, and only then runs the program. If any of that fails, the program does not run.

## Changes

- **`crates/sandbox`:** the policy, the Linux backend (Landlock, and seccomp that denies all sockets, with an x32 filter), the macOS backend (`/usr/bin/sandbox-exec` with a deny-default profile), the probes and a canary.
- **`apps/gridcraft`, `apps/gridcraft-cli`:** the launcher flag comes first in `main`, before any window or log.
- **`gridcraft-cli sandbox-check`:** says whether this system confines programs, and can run a confined self-test.
- **`docs/sandbox.md`:** what is refused and how.
- **`nix`:** a new dependency on Linux and macOS, for closing descriptors without `unsafe`.

## Compatibility

Windows and FreeBSD have no backend here. There the crate reports that it can't confine, and nothing that needs it runs. On Linux a launch needs Landlock ABI 3, which is Linux 6.2 and later, so an older kernel is treated the same way: this excludes, for example, Debian 12's kernel (6.1) and Ubuntu 22.04's original kernel (5.15). Nothing changes for a process started without the launcher flag.

## Review fixes

- **File metadata on Linux.** Landlock has no right for a file's permissions, owner, extended attributes, times or flags, and the seccomp filter allows what it doesn't name, so a confined program could change them on any file its user owns, outside the policy too. The filter now refuses the `chmod`, `chown`, `setxattr`, `removexattr` and timestamp families with their `*at` variants, by path and by descriptor, and any `ioctl` whose number says it hands the kernel data, as `FS_IOC_SETFLAGS` and `FS_IOC_FSSETXATTR` do on a descriptor opened only to read. A filter allows what it doesn't name, so a call a later kernel adds is allowed until it is named; the newest named is `file_setattr` (Linux 6.17).
- **Truncation on Linux.** Landlock covers truncation only from ABI 3, so on ABI 1 or 2 `os.truncate(path, 0)` emptied any file its user can write, and `open` with `O_RDONLY | O_TRUNC` emptied one it may read. A launch now requires that right. A filter can't take its place: `openat2` hides its flags from seccomp, and refusing `O_TRUNC` would refuse opening `/dev/null` to write, as `open(os.devnull, "w")` does.
- **Standard streams.** They stay open with the rights they were opened with, so a stream that is a file can be written and truncated, and one that is a socket can still send. The page and `launch_command` say so, and that a parent gives the program pipes.
- **The enforcement check, again.** It now also tries to change the canary's permissions and to truncate it, asking for what it already has, so nothing changes if one works.
- **Other processes on Linux.** On a kernel without Landlock's signal scope, a confined program could signal any process of the same user, and `kill(-1, 9)` would reach them all. The seccomp filter now refuses signals to any process but the program itself, whose ID `exec` keeps; changes to the limits, priority or scheduling of any other process; `pidfd_open`, `pidfd_send_signal`, `kcmp`, `get_robust_list`, `migrate_pages` and `move_pages`; every System V shared memory, message queue and semaphore call and every POSIX message queue call, which no path rule covers; and `syslog`.
- **The terminal on Linux.** A program started from a shell could type into its terminal. `TIOCSTI` and `TIOCLINUX` are refused, compared in 32 bits as the kernel reads them, so high bits set on the argument don't get past the rule.
- **The enforcement check.** It now also tries signal 0 on the launcher's parent, which delivers nothing, so `sandbox-check` reports a gap rather than assuming none. The launcher tries the same signal before it confines itself and refuses if that fails, and the check refuses if its parent has changed since, since a parent that refuses anyone would prove nothing. A launcher's arguments must include the current executable, which that check starts; otherwise the file rules refused the start and the check proved nothing about new processes.
- **A refused launch.** The launcher read its arguments only after closing every inherited descriptor, so a refused launch had already closed them. It reads them first.
- **A kernel without Landlock.** The refusal says so in that case too, which the tests that skip on such a machine look for. Only the first ABI's rights being refused, or `ENOSYS` or `EOPNOTSUPP` from creating the ruleset, says so. Any other failure, such as `EINVAL` or too many layers, is reported as what it is and doesn't skip the tests.
- **Folders in the exec list on macOS.** A folder got a rule that matches only the folder, so a launcher under it failed to start where the same policy works on Linux. It now covers what is in it, in both rules.
- **The sandbox page** names only what this layer has: only `gridcraft-cli sandbox-check` starts the launcher here.

## Testing

All 10 steps of `cargo xtask ci` pass on macOS (arm64), and `cargo clippy` passes for this crate for both Linux architectures. The gate on ubuntu-24.04 is the hosted one on this pull request, and it executes the Linux backend through the real binary, including `a_confined_program_leaves_other_processes_alone` and `a_confined_program_changes_no_file`, in which a confined Python tries each refused call on a file inside the policy, one outside it and a descriptor, and the test then compares contents, permissions and modification time.

A separate run on the fork, on Linux 7.0 with Landlock ABI 8, covered the same commit's code with parts of the fix taken out. Without the metadata rules, the launch check refuses with `change the canary's permissions: it worked`. Without those rules or the new checks, `a_confined_program_changes_no_file` fails with `permissions of ...: it worked`. With the Landlock rights limited to ABI 2, as on an older kernel, it fails with `truncate ...: it worked`. A kernel that is actually older than ABI 3, or that has no Landlock at all, has not been run. On macOS, terminal input was checked by hand on a pseudo-terminal: `TIOCSTI` is refused when confined and works when not.

The commits group the change by area for reading. Only the last commit of the branch is expected to build.


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **New Features**
  * Added sandbox support for running commands with policy-limited file access and restrictions on network and process operations. The sandbox is available on supported Linux and macOS systems; launches are refused if confinement cannot be established or verified.
  * Added the `sandbox-check` command to check sandbox support and enforcement, or run a specified program in the sandbox. It reports when sandboxing is unsupported.
* **Documentation**
  * Added guidance on sandbox behavior, platform support, access restrictions, launch checks, and `sandbox-check` usage.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-11 07:31:42 +01:00
Dylan M. Taylor 4484157cbe merge main into fix/sec-html-export-merge-index 2026-11-11 01:13:05 +00:00
Dylan M. Taylor 6a08240eb6 io: refuse a FIFO before opening it; keep the replaced file's group on save (#3313)
* io: refuse a FIFO before opening it; keep the replaced file's group on save

* address review: create the replacement owner-only, then restore group and mode; never widen it when the group cannot be restored

* address review: keep the old owner and other bits when the group cannot be restored
2026-11-10 18:05:44 -05:00
Dylan M. Taylor a3a98cf745 functions: REGEXREPLACE keeps only the requested occurrence's captures (#3288)
* REGEXREPLACE keeps only the requested occurrence's captures

* address review: do not truncate REGEXREPLACE occurrence on 32-bit targets
2026-11-10 18:05:25 -05:00
Dylan M. Taylor fe39331ac4 engine: Consolidate finds keys through hash maps and refuses an oversized table (#3285)
* engine: Consolidate finds keys through hash maps and refuses an oversized table

data.consolidate searched the keys collected so far for every row and
column key, so 100,000 positional rows cost about five billion
comparisons. It also built every row × column combination without a
size check. Keys are now indexed, and a table over 5 million cells is
refused before it is built.

* address review: count label cells in the consolidate output limit
2026-11-10 18:05:08 -05:00
Dylan M. Taylor a84259d58c ui-egui: embed craft-fonts files only from inside the checkout (#3281)
* ui-egui: embed craft-fonts files only from inside the checkout

The build hook canonicalised each manifest row's file and embedded it
with include_bytes! wherever it led, so an absolute path, a .. path or
a symlink in a craft-fonts checkout could put any readable file of the
build machine into the binary. Manifests and font files must now
resolve inside the canonical checkout directory.

* address review: register the manifest path before validation and keep the Chinese manifest fallback optional
2026-11-10 18:04:49 -05:00
Dylan M. Taylor 85d32c20be engine: show overlong format codes as General without hashing them per cell (#3273)
* engine: show overlong format codes as General without hashing them per cell

The display cache hashed the full format code on every lookup, so one
huge code shared by many cells cost its length for every cell drawn.
A code with more than 4 × 255 bytes can't be within Excel's 255
characters and is now formatted as General straight away.

* address review: keep the original code in the public number_format for overlong formats
2026-11-10 18:04:31 -05:00
Dylan M. Taylor 1deb108a7c xtask: probe keytip commands without file access, and refuse text that could escape generated code (#3269)
* xtask: probe keytip commands without file access, and refuse text that could escape generated code

cargo xtask keytips ran every command keytips.json names, with its
params, under the user's file permissions (file.exportPdf could replace
any file, also during --check). It also wrote target, excel and note
strings into generated Rust without escaping. Probes now run inside the
engine's ScriptSandbox, and those fields may not hold quotes,
backslashes, braces or control characters.

* address review: make ScriptSandbox !Send so it drops on the thread that entered it
2026-11-10 18:04:13 -05:00
Dylan M. Taylor 2c9e2d1eec Go To: index defined names when resolving and listing them (#3206)
Go To resolved every visible name, and each alias inside a name was
looked up with Workbook::name, a linear scan of all names. A workbook
of many short aliases made the Go To list quadratic (or worse along
alias chains) in the number of names.

A Resolver now indexes the names by scope and folded spelling once
per command and remembers, per name, sheet and depth, whether a name
resolves and to how many areas, so listing names resolves each alias
once. Lookups keep the old precedence and matching rules.
2026-11-10 18:03:53 -05:00
Dylan M. Taylor 9c8b4a37ea PDF export: index merged areas when drawing a block (#3203)
draw_block checked every cell against every merge meeting the block,
and the fill, border and text passes searched the merge list again
for each merge start, so an imported sheet with many merge ranges in
a print area cost cells x merges (and merges x merges) comparisons.

The merges meeting a block are now gathered once and swept down the
rows: whether a cell is covered is a BTreeMap lookup among the merges
active on its row, and merge starts are found through a HashMap. A
merge overlapping one kept before it (only possible in a damaged file)
is left out, as Excel does when it repairs the file. Hyperlink
rectangles use the same index instead of scanning all sheet merges.
2026-11-10 18:03:35 -05:00
FredAppsandFredApps a36e4987c1 i18n(uk): translate Chart as Діаграма (#3703)
Use the shared craft-i18n Ukrainian term, distinct from Diagram (Схема).
Also translate the remaining Chart-labelled strings that were still English.

Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-10 17:07:10 +01:00
FredAppsandFredApps 457bc7e374 Naming: use Diagram and Decorative Text in the feature catalog (#3622)
* Naming: use Diagram and Decorative Text in the feature catalog

* Naming: align the Chinese decorative text term

* test(naming): guard neutral display labels

* Naming: Diagram no longer repeats the chart word in de, uk and ro

German Schema and Ukrainian Схема match craft-i18n#211; Romanian Schemă
avoids GridCraft's own chart word Diagramă. A catalog test checks every
shipped language translates both labels and that Diagram differs from
Chart/Charts. The display-name guard now handles escaped char literals
and any #[cfg(test)] item, asserts it scanned catalog.rs and a minimum
of files and literals, and tests word boundaries. The catalog test
asserts each ID appears once. Docs carry timestamps and revision history.

---------

Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-10 16:13:13 +01:00
Harrison Booth 50a9e9ede4 XLS: formulas open as legacy formulas with Excel's implicit @ (#410) (#1367)
.xls formulas are read as legacy formulas with Excel's implicit @ (apply_legacy via read_xls_with), one-cell Ctrl+Shift+Enter arrays keep their range, @ of a 2-D range meets the formula's row and column, and defined-name analyses are shared across the formulas of a pass and replayed exactly.

Closes #410
2026-11-10 15:05:02 +01:00
74ab5cc969 M12: regional format data (Region, 20 regions) (#3048)
* M12: regional format data (Region, 15 regions, Workbook::region)

Adds the data model for Excel-style regional formats, with no behaviour
change: nothing reads it yet.

- core: region.rs with `Region` (separators, grouping, date order, short and
  long date and time codes, month and day names, AM/PM words, currency) and
  the 15 regions en-US, en-GB, en-IN, es-ES, de-DE, fr-FR, it-IT, pt-BR,
  pt-PT, ru-RU, cs-CZ, ja-JP, ko-KR, zh-CN, zh-TW. Values follow Unicode CLDR;
  the deliberate departures (four-digit short-date year, Windows grouping
  spaces) are listed in the module doc. `all`, `find` (strict tag),
  `lookup` (system or browser tag, best effort) and `default_region` (en-US)
  select one; `EN_US` reproduces today's behaviour exactly. Unit tests check
  the tables and the lookups.
- model: `Workbook::region`, a `#[serde(skip)]` runtime field that is never
  saved and defaults to en-US. It is here so that the following changes can
  be reviewed independently of each other.

The region is independent of the interface language and of the formula
language. Builds on this, in order: typed input (core `parse_input_in`),
display (numfmt and engine display), the engine session and `app.region`
command, then the Options UI and the app defaults.

* M12: region data: review fixes (tag extensions, zh territory, month periods)

- lookup stops at an extension or private-use singleton:
  en-GB-u-ca-gregory is en-GB (ca was read as Canada) and
  zh-Hant-u-nu-latn is zh-TW.
- For Chinese an explicit territory wins over the script, so
  zh-Hant-CN is China's conventions, consistent with zh-Hans-TW.
- In regions whose month abbreviations end in a period (ru-RU, fr-FR),
  month_from_name accepts any abbreviation typed with one (мар., июн.);
  elsewhere a period stays outside the name, so en-US is unchanged.

* Region: document where dates and month names follow Windows, not CLDR

* M12: Swiss regions (en-CH, de-CH, fr-CH, it-CH) and nl-NL

The Swiss regions group digits with an apostrophe (CLDR 48) and accept the
typographic one glibc uses; lookup maps CH and LI tags to them. Dutch
(Netherlands) follows CLDR 48.

* Region: table-driven lookup test, under main's cognitive-complexity limit

* Region: French and Italian Liechtenstein tags resolve to the Swiss regions

* Region: credit the Unicode CLDR data under the Unicode License v3 (NOTICE, ATTRIBUTION.md)

* Region: month_from_name takes one word; the currency format has the currency's own decimals

---------

Co-authored-by: quantmind-br <dsrodrigues20@gmail.com>
Co-authored-by: quantforge-br <218010578+quantforge-br@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-10 13:54:53 +01:00
RockerHXandFredApps ec12eba83e UI.1265: align CJK fallback text with the Latin baseline (#3081)
* UI.1265: align CJK fallback text with the Latin baseline

Use epaint's existing baseline alignment for system CJK fallback faces
and embedded faces tagged with CJK scripts. Mixed CJK, Latin and digits
then share a baseline in cells, editors and window titles.

Add regression coverage for zoom, font styles, wrapping, fixed-height
clipping, vertical alignment, editor height and title truncation.

Refs #1265, #2621.

* UI.1265: assert title baselines and document visual validation

* UI.1265: add baseline comparison and review validation

* UI.1265: adapt tooltip regression after upstream merge

The upstream shortcut update uses the runtime OS to choose tooltip text.
Use the harness OS for the existing split-button test expectation too.

Validation: cargo xtask ci; all 10 steps pass; 3,002 tests passed, 1 ignored, no exclusions.

* UI.1265: propagate CJK fallback to installed fonts

* UI.1265: align fallback baselines across cells

* UI.1265: align wrapped and unwrapped cell text

* UI.1265: preserve blank lines in wrapped cell alignment

* UI.1265: preserve centered fallback placement

* UI.1265: find CJK cell text by its visible bounds

Use the overlap of text ink, clip rect and cell rect to locate painted text
instead of testing a fixed offset from the galley origin. Baseline-aligned
fallbacks can move that origin above the cell on Ubuntu.

Keep the existing baseline and wrapping assertions unchanged.

Validation: cargo xtask ci on macOS; all 3 cjk_baseline tests on Ubuntu 24.04 x86_64.

---------

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-10 12:44:19 +01:00
ca39b5ab9f XLSX: legacy formulas (no dynamic-array cm) open with Excel's implicit @ and calculate like Excel (#417)
* XLSX: legacy formulas (no dynamic-array cm) open with Excel's implicit @ and calculate like Excel; implied @ saved back without cm

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

* XLSX: long operator chains analysed in full and unfinished legacy analysis leaves the formula unchanged (never saved as SINGLE); @ of a union keeps its parentheses; IF's condition intersected inside SUMPRODUCT

* XLSX: a legacy union already in parentheses saves back without a second pair (never as SINGLE); a name's definition analysed as an array throughout, inserting no @

* XLSX: drop the unreachable keep-unions retry; an @ of a bare union saves as a dynamic formula

* XLSX: an @ keeps parentheses the reader would not put back (redundant ones, a union's)

* XLSX: a redundant @ keeps a legacy formula legacy; IFERROR and IFNA intersected inside SUMPRODUCT; file parentheses and unfinished analyses saved as read

An edit could leave an implied @ on a single cell: deleting rows 2:3 under the legacy SQRT(A1:A3), read as SQRT(@A1:A3), gives SQRT(@A1:A1), and a name redefined to one cell does the same to SQRT(@Rng). The writer kept an @ only when re-analysing the formula without it put the same @ back, which it no longer does for one cell, so the formula was saved as SQRT(_xlfn.SINGLE(A1:A1)) with cm, readable only by dynamic-array Excel. The writer now analyses the formula with its own @ in place: an @ is accepted when the analysis implies it at that position or when its operand is a single value there (such an @ changes nothing), and any @ inserted elsewhere or any other @ still makes the formula dynamic. SQRT(@A1:A1) saves as SQRT(A1:A1), SQRT(@A1) as SQRT(A1) and @SUM(A1:A3) as SUM(A1:A3), all without cm; SUMPRODUCT(@SQRT(A1:A3)) stays dynamic because SQRT(A1:A3) is an array there.

Legacy Excel intersects IFERROR's and IFNA's arguments inside SUMPRODUCT unless the formula is entered with Ctrl+Shift+Enter, like IF's condition: =SUMPRODUCT(IFERROR(VALUE(C19:C21),0)) returns one row's value or #VALUE!, and the accepted fix is CSE or N(+range) (https://stackoverflow.com/questions/59228707). They now get the same single-value parameter class as IF's condition (Param::Single), so the legacy SUMPRODUCT(IFERROR(A1:A3/A1:A3,0)) reads as SUMPRODUCT(IFERROR(@A1:A3/@A1:A3,0)), and a typed SUMPRODUCT(IFERROR(VALUE(A1:A3),0)), which dynamic-array Excel evaluates as an array, is saved with cm. IF's result arguments are left as arrays: in https://stackoverflow.com/questions/29682328, a non-CSE =SUMPRODUCT(--(B4=IF(NOT($C$4:$C$11),$B$4:$B$11))) in row 4 returns 2 (both "apple" rows of B4:B11 counted, so the result branch was not intersected), 0 in a row where C is TRUE and #VALUE! outside rows 4:11 (the condition was intersected).

The reader's @ in front of a whole-formula array result took no extra parentheses when the file already had them, so ({1,2}+1) and {1,2}+1 both read as @({1,2}+1), and the writer dropped the file's parentheses. An operand already in parentheses now gets a second pair, so ({1,2}+1) saves as written.

A legacy formula the analysis could not finish (node budget, nesting) was read unchanged, evaluated as a dynamic-array formula and saved with cm, so Excel spilled it. Names were analysed again at every use, so a 399-term name used six times exhausted the 4096-node budget; their kinds are now remembered per formula, and the budget is 16384 nodes. The nesting limit is 128, so Excel's 64 levels of nested functions are analysed. A formula that still exceeds the limits is saved as it was read, without cm.

* XLSX: an implied @ is dropped and any other @ of a single value kept as _xlfn.SINGLE in a legacy formula (as Excel writes it); a legacy formula the analysis cannot finish or whose semantics it does not know opens with a whole-formula @ and never spills; analysis depth follows the parser's limit

* XLSX: a legacy union given to a function's single-value parameter keeps its implied @, so it calculates to #VALUE! instead of spilling and saves as read; checked on a calculated workbook

* XLSX: only a one-cell range an @ applies to counts as one an edit shrank; a reading past the parser's nesting limit leaves the formula as read, and the writer saves the text a formula it cannot analyse was read from

* XLSX: a formula nested past the parser's limit for its reading is left as read (documented: it may spill); test covers it

* XLSX: a name's definition counts its one-cell ranges as collapsed only when it is itself a reference, so CHOOSE's index or IF's condition in it keeps the name legacy

* XLSX: an @ on a name or one-cell range counts as implied when ranges an edit may have shrunk, anywhere the name returns them (CHOOSE's choices, parentheses), make it so; deleting rows keeps such formulas legacy

* XLSX: the collapsed-range retry of an @ runs only when the first pass reached a one-cell range written as a range (tracked through cached names)

* docs: describe unfinished formula export fallback

* docs: the unfinished-analysis fallback differs for formulas with an explicit @

* Preserve main formula analyzer fixes

* Order file-format revision history

---------

Co-authored-by: neilcawse <8085959+neilcawse@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Local contributor <contributor@localhost>
2026-11-10 12:43:07 +01:00