* Sonar: explicit imports, fn paths for closures, extract helpers for cognitive complexity
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Fix imports
* Fix serde imports
* Fix engine imports
* Fix clippy gate and a type error from the explicit-import change
Remove the unused imports the review flagged (cf.rs, pivot.rs, tables.rs, cmd/sheet.rs) so clippy -D warnings passes, and point the backstage parent-folder lookup at Path::to_str, which the fn-path rewrite had broken.
Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
* Apply rustfmt to the explicit-import cleanup so the fmt gate passes
* Share the per-directory font path expansion in theme.rs
The candidate font lists repeated the same loop over the Linux font
directories in every role. A single helper expands them in the same order,
so the lookup order is unchanged and the duplicated blocks go away.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
* Read the Windows fonts folder in one place in theme.rs
Every script-font list repeated the same WINDIR lookup. A single helper
returns the same path, so the duplicated lines go away without changing
which font files are tried.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
* Build the light and dark UI palettes from one table in theme.rs
Tokens::light() and Tokens::dark() listed the same 40 colour fields in the
same order, so every change had to be made twice. A single palette
constructor takes the mode and picks each colour's value, so each field is
written once and both modes keep their exact colours.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
* Share the Noto CJK font list between the CJK script lists in theme.rs
The Japanese, Simplified and Korean lists each repeated the same loop over
the Linux CJK directories with a different locale file name. One helper
builds that loop, and each list keeps its own face index, so the lookup
order is unchanged.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
* Share Linux Noto directory list across Thai and Hebrew font lookups
Removes the duplicated directory arrays flagged by SonarCloud new-code duplication.
* Reuse in_dirs for Thai and Hebrew Noto font candidates
Removes the remaining duplicated per-directory loops flagged by SonarCloud.
* Share one font-list builder for the Thai and Hebrew faces
The two per-script functions repeated the same Windows, macOS, Linux and
variable-font setup. A single script_fonts helper now builds both lists,
keeping the candidate order unchanged, which clears the Sonar duplication gate.
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
save_fails_upfront_when_the_temp_name_is_taken,
save_keeps_the_destination_access_restrictions and
open_refuses_directories_and_unknowable_lengths in crates/engine/src/io.rs
all worked in temp_dir()/gridcraft-fs-safety and removed it at the start
and the end. cargo test runs them on parallel threads, so one test could
delete the folder while another was writing to it, and the other test
failed. Each test now uses its own folder, named with a short label and
the process id, as the gridcraft-write and gridcraft-sandbox tests in the
same file do.
- read_file: verify the opened file is regular and cap the bytes actually
taken at the 2 GB limit — a workbook-named symlink to an endless device
reported length 0 but streamed forever (finding 22).
- write_file: create the temporary file exclusively, so a planted file or
symlink at the predictable `book.xlsx.sctmp` path makes the save fail
instead of truncating the link's target (finding 25).
- write_file: inherit the destination's permissions, so saving an
owner-only workbook doesn't become world-readable through the rename
(finding 15); new files note restrictive defaults.
Tests: exclusive temp creation vs planted symlink, permissions preserved,
directory refused on open. Engine: 460 tests pass; clippy clean.
- clip_html: cap wildcard/glob lookback to a 12-byte window instead of scanning
outgoing text arbitrarily far for a semicolon (finding 18).
- print: wrap_lines binary-searches its break instead of rescanning the line
from the start for every band (finding 21).
- autofit: per-row merge index instead of scanning every merge per row during
autofit (finding 27) — behaviour covered by the existing autofit merge test.
- Save as Web Page: the used rectangle bounds the HTML (finding 28, #2387) —
a tall/wide or sparse far-right sheet now errors instead of growing
gigabytes of <td></td>.
- print: decode each embedded picture once per document instead of once per
band that shows it (finding 30).
Engine: 459 tests pass; clippy clean.
- The workbook's active sheet is an atomic set in place (Workbook::active_sheet /
set_active_sheet), so a window swap or sheet switch never clones the
Arc<Workbook> or marks it dirty. Selections already live in DocState and the
window views. Save records the sheet of the most recently active window.
- A dialog taken out of its window while it runs goes back to that window when
a command run from it switched windows (dialogs::show, ui.dialog.confirm).
- view.arrangeAll {arrange?: tiled|horizontal|vertical|cascade, activeWorkbook?}
returns the visible windows, active first; the desktop moves and sizes the
native windows over the main window's monitor. New Window and Arrange All
buttons on the View ribbon; Arrange Windows dialog.
- Docs: ui-parity, hardware-parity, gaps #20, target-app-parity, ROADMAP.
- automate.trustScripts: only a missing `trusted` defaults to true; a
non-boolean value is an error and leaves the workbook untrusted.
- The refusal message points at Trust Scripts in View > Macros (Alt+F8)
and at automate.trustScripts.
- Catalog row for automate.trustScripts; parity checklist regenerated.
- Script steps run inside an io::ScriptSandbox (thread-local) so
read_file/write_file refuse whatever the parameter is called, and a
step that raises OpenUrl is dropped and stops the script. The id/param
deny list stays as the up-front refusal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Save as Web Page wrote only the cell table, so pictures and charts on a sheet
were dropped. A sheet that has either now keeps its own geometry and draws them:
- The table gets the sheet's column widths and row heights (`table-layout:fixed`,
a `<colgroup>`, a height per row); hidden columns and rows take no room and
merged-cell spans count only visible lines.
- A picture is an `<img>` with a `data:` URI, a chart is inline SVG built from
the chart crate's drawing primitives (titles, axes, legends and data labels
included), each absolutely positioned at its anchor cell plus offset.
- Sheets without pictures or charts write byte-identical HTML.
Adds contributors/people.toml entry for bilalmughal.
It returned an empty string for every workbook, saved or not, so the classic
sheet-name formula `MID(CELL("filename",A1),FIND("]",…)+1,31)` gave #VALUE!:
with no `]` in the string there is nothing for FIND to find. Excel returns
`<folder>\[<file>]<sheet>`, and empty only before the first save.
The model had no file to report. `source_dir` could not serve: it names the
folder relative links to other workbooks are relative to, and must keep naming
it after a Save As into another folder, because the links held in memory are
still written for the old one — only the saved copy is rebased. `source_path`
is therefore its own field, with that distinction written down.
It is set when a workbook is opened, from the path made absolute so the folder
is the one the user sees, and when a full-fidelity save makes a file the
document's own — before the `saved` snapshot, or the workbook would read as
dirty the moment it was saved, and only when it changes, so an ordinary Save
does not clone the workbook for nothing. A workbook that arrived as bytes knows
its name but never a folder and reports `[name]Sheet`, which still splits on the
`]`; an import reports the file it came from, as Excel does. The sheet is the one
the reference is on, not the active one.
CELL is volatile and already in the volatile sets, so a cell holding it answers
from the next recalculation; after a Save As that means F9 rather than the save
itself, which the engine test asserts rather than papers over. Making a save
recalculate is a larger decision than this issue.
`check` still skips CELL("filename") as volatile, as docs/cli.md says: the
cached value in a file is the folder of the machine that saved it.
file.open takes {csvAsText: true}: a CSV, TSV or TXT file opens with every field as the text
it holds, with no number, date or boolean conversion, so identifiers such as GTINs, SKUs and
ZIP codes keep their digits and leading zeros and save back unchanged. The default is
unchanged. The CsvOptions.parse_values switch already existed in the reader.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Every edit copies the workbook for its undo step (copy-on-write: sheets
and 64-row bands of cells are shared). A floating picture's bytes
(Image.data) and the VBA project (Workbook.vba_project) were plain
Vec<u8>, so each edit on a sheet with a picture, and every edit in a
workbook with macros, kept another full copy of them, up to the 100
steps of the history. They are Arc<Vec<u8>> now, as cell pictures
already are, with base64 helpers for serde (b64::shared, b64::opt). JSON
and XLSX are unchanged.
Measured on macOS 27 (Apple M3 Max), gridcraft-cli, kernel peak
footprint: a 19 MB picture and 50 cell edits on its sheet, 992 -> 22 MB;
10 edits, 215 -> 21 MB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
write_csv visited every position of the used range and appended to one growing
String with no limit, so two cells far apart (A1 and XFD1048576) asked for
about 16 GiB of commas. The output is now capped at 512 MiB: a lower bound
(delimiters and line ends of every row) is checked before anything is
allocated, and the text written so far is checked after each row. Over the
cap, write_csv returns IoError::TooLarge naming the rows, columns and limit,
and Save As / Export CSV and TSV report it without writing a file. Accepted
exports are unchanged. write_csv now returns a Result.
Excel stores the path of a linked workbook relative to the file that uses it, and rewrites it when the file is saved into another folder. The path was kept as text, so after Save As it named a file next to the new copy: a missing link, or another workbook with the same name. Relative externalLinkPath targets are now resolved against the folder the workbook was read from and written relative to the folder it is saved into (absolute when there is no relative path, on another drive).
write_file writes `<path>.sctmp` and renames it over the workbook. When the
write or the rename failed (a full disk, or the target locked by another
app such as Excel or a sync client), the temporary file was left beside the
user's workbook. Remove it on either failure and report the same error.
Adds io::tests::failed_save_leaves_no_temporary_file, which fails before
this change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A `;`-separated CSV (the usual CSV where `,` is the decimal separator) was
rewritten with `,` on save: read_csv sniffed the delimiter but the result was
dropped, and save_bytes passed a hard-coded b','. file.save, Save As .csv,
file.exportCsv, file.saveBytes and gridcraft-cli convert all went through it.
The workbook now remembers the separator it was read with
(Workbook::csv_delimiter, set by read_csv) and a CSV save writes it back.
Workbooks not read from CSV, and .tsv/.txt sources saved as .csv, keep `,`.
Regression tests: `;`, `,` and tab .csv round trips byte for byte, new and
.xlsx-origin workbooks save with `,`, and open-by-path + edit + file.save on a
`;` file keeps `;` (also through exportCsv and saveBytes).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gridcraft_xlsx::check_encryption parses EncryptionInfo without a
password, so RC4, Extensible, certificate-only and rights-management
(IRM) files, and damaged ones, get their real message instead of a
password prompt first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without a password, file.open returns an error carrying
io::PASSWORD_REQUIRED (never a dialog). A decrypted workbook gets no save
path, so Save never replaces the encrypted original.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>