Audit finding 79 (CWE-117): the CLI printed Zotero's alert text (and, with
--trace, the method name and call/reply JSON) to stderr as received, so a peer
on Zotero's port could end the "Zotero:" line, forge following output or send
terminal escape sequences. escape_peer shows backslashes, control characters
(C0, DEL, C1), line separators and bidi overrides as visible escapes (\\, \n,
\u{1b}), keeping each record on its own attributable line.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Startup check is opt-in for the desktop app, the Windows update helper is dispatched before argument parsing, and a successful update closes through the app's own close path.
SonarCloud's analysis of main at b4da59f0c lists 23 closures such as `|v| v.as_slice()` that hand their argument straight to one method. This passes the method itself: `Vec::as_slice`, `str::to_ascii_lowercase`, `f64::total_cmp` and the like, all forms the repo already uses elsewhere. Behaviour is unchanged.
Five stay. The four in crates/proof/src/es/mod.rs sit in code that #4191 replaces, and its replacement already uses `PoisonError::into_inner`. In crates/engine/src/io/safe_save.rs the path would be `<dyn std::error::Error + Send + Sync>::is::<Guided>`, which reads worse than `|inner| inner.is::<Guided>()`.
* Bump craft-print, craft-pdf, krilla and craft-i18n to craft-libs 1774443
* PDF export writes link annotations only for safe targets (craft_pdf::write::uri_target)
* Chart default category and series names use fmt_plain, so RTL interfaces put no isolate marks into the document
On Windows the paste path asked arboard (3.6.1) for decoded pixels.
arboard decodes the registered "PNG" format and CF_DIBV5 with no image
limits, so a clipboard item of a few dozen bytes whose header declares
100,000 x 100,000 pixels made the decoder allocate tens of GB. The
allocation failure aborted the app outside Session::run's guard, before
paste_picture's size checks ever saw the picture.
On Windows the app now reads the clipboard itself (clipboard-win, the
raw read capped at 200 MB):
- "PNG" goes to Insert Picture as encoded bytes, once its header shows
a size a paste takes (16,000 a side, 50 M pixels).
- Otherwise CF_DIBV5 or CF_DIB is probed through
wordcraft_geom::picture::probe_dib, checked the same way, then decoded
within the decode budget into pixels.
probe_dib reads a DIB in the layout the image crate expects: a 32-bit
BI_RGB DIB with an alpha mask becomes bit fields, so its alpha is kept
as arboard did. A V4 or V5 bit-field DIB without the copy of its colour
masks after the header (arboard writes one) gets that copy, because the
decoder skips 12 bytes there and would otherwise read into the pixels.
Linux keeps using arboard, which decodes the clipboard's PNG within the
image crate's default 512 MB limit. macOS keeps it too: arboard decodes
the pasteboard's TIFF there, which can still reach the JPEG strip
decoder of #3645; that path is untouched here because it can't be built
or run on this machine.
cargo check and clippy for x86_64-pc-windows-gnu pass on the
Windows-only code; it hasn't been run on Windows.
Connecting eagerly made the server fail when the app wasn't open yet, and a
client marks a failed MCP server failed for its whole session: an unavailable
app now leaves a lazy remote that connects on the first call, warns about a
missing control key only once connected, and says which state it is in.
--connect addresses are checked before the server starts, so 127.0.0.1:notaport
reports a numeric port instead of retrying on every tool call.
- wordcraft:ready is posted once WebRunner::start has succeeded, so a parent
never hears ready and then failed.
- ?author= signs the visit but the browser keeps the user's own name in its
stored preferences; a name chosen in File › Options is stored as usual.
- The saveAs flag is documented case by case: Save false, Save As and
File › Export to a document format true, PDF/PNG exports false (copies).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* CI: line-table debug info for dev builds, none for dependencies
* CI: one integration-test binary per crate
* CI: nextest with doctests, test build without examples, one wasm check
* CI: the interface audit reports missing catalog entries instead of failing
* Docs: the interface audit, not CI as a whole, passes with missing catalog entries
The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.
🤖 Generated with Codebuff
Co-authored-by: Codebuff <noreply@codebuff.com>
Resolves the conflicts with the changes that landed since the branch was cut:
- crates/engine/src/cmd/protect.rs: the forms-mode refusals stay behind
upstream's `greyed_out()` (so the ribbon still greys the Developer tab's
control buttons out), and the refusal of the document's properties and its
privacy options is added on top of it.
- ROADMAP.md, docs/file-format-parity.md, docs/gaps.md: both sides' entries
are kept; the status lines carry the merge's date and this change at the
front.
Co-Authored-By: opencode <noreply@opencode.ai>
A same-origin page that frames wordcraft-web (a Nextcloud app, a personal
file server) can now hand it a document and store what it saves, with the
same postMessage protocol as GridCraft's and PdfCraft's bridges:
wordcraft:ready / open / save {name, saveAs, bytes} / dirty / failed.
In host mode Save, Save As and Export go to the parent instead of the save
picker or a download (Services::host_save, with whether it was Save As).
Save writes the opened file under its own name and format, as the desktop
app does (an import-only .doc still becomes a .docx), and a Save or Save
As that keeps everything makes that name the document's file, so the next
Save goes to it. The parent guards leaving the page; ?author= names the
user for comments and tracked changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/gaps.md row 4: one row (t06 had kept a second, older copy of it); t05's #1840 and t06's #2964 clauses both in, main's #2963 clause kept. docs/recovery.md: t06's retry sentence with t01's base64 size text. Test files: both tracks' tests kept whole.
* #2852: personal templates in File › New, Save as Word Template
File › New lists the .dotx/.dotm files in the user templates folder under
Personal (a subfolder per category, search covers them); a click makes an
untitled document attached to the template. Save As › Save as Word Template
starts in that folder (<settings>/Templates, made on first use; File ›
Options › Advanced › File locations › User templates overrides it).
Engine: file.listTemplates (bounded, network folders refused, read root
applies), file.newFromTemplate sets the attached template, file.options
userTemplatesFolder, file.saveTemplate keeps a .dotm name. UI:
ui.saveAsTemplate; Save dialog requests carry a start folder.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WEhZXSxzS8orX7f9J3qpr
* #2852: review — a personal template asks about unsaved changes; network templates folder refused before it's looked at; New lists the folder once per visit with Refresh; .dotx/.dotm labels alike
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WEhZXSxzS8orX7f9J3qpr
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
* Use chrono for UTC/calendar conversion instead of hand-rolled copies
Replaces the hand-rolled civil_from_days / days_from_civil copies and the
Excel serial-date, FILETIME and logging/PDF date conversions with chrono as
a workspace dependency (default-features = false, std only, so wasm is
unaffected). Output formats are unchanged and pinned by the existing (and one
new) test. Excel's 1900 leap-year bug and the 1900/1904 systems keep their
explicit special cases; a small proleptic-Gregorian fallback covers years
outside chrono's NaiveDate range so no input changes behaviour or panics.
AGENTS.md now says time goes through chrono.
Follow-up to storytold/cadcraft#1785
* Never panic or reshape date output for clocks outside chrono's range
chrono's From<SystemTime> unwraps, so a clock past year 262143 panicked
inside the logger. logging::timestamp now uses the fallible
DateTime::from_timestamp and reads such a clock as the epoch, as the old
saturating code did.
iso_from_unix_secs fell back to the Unix epoch for timestamps chrono
rejects; it now keeps the proleptic Gregorian date civil_from_days gives,
restoring the pre-chrono result for supported out-of-range u64 inputs.
Years past 9999 are written with {:04} rather than %Y (which prepends a
'+') in logging::timestamp, iso_from_unix_secs, docbin::filetime_iso and
odt::now_iso, so byte-for-byte output is preserved.
filetime_iso gained a docstring; AGENTS.md's chrono bullet now notes the
narrow out-of-range fallback. Tests cover each boundary.
* Don't build an unrepresentable SystemTime in the far-future test
UNIX_EPOCH + Duration::from_secs(u64::MAX / 2) overflows Windows'
SystemTime and panicked before timestamp() ran, failing the Windows
test job. Build it with checked_add and only assert where it exists.
* Route out-of-range dates through chrono's 400-year cycle, not hand-rolled copies
The PR kept hand-rolled Hinnant `civil_fallback`/`days_from_civil_fallback`
copies for dates outside chrono's ±262143-year range, and AGENTS.md carved a
"narrow fallback" exception for them. That left calendar arithmetic outside
chrono after all.
Replace every copy with the Gregorian 400-year cycle (146_097 days): chrono
converts the day/year within one cycle and whole cycles are added back via i128
(so i64::MIN/MAX can't overflow). This keeps chrono as the only calendar logic
and reproduces the pre-chrono proleptic Gregorian result for every value:
- engine cmd/mod.rs `civil_from_days` (reached by the system clock and by
`iso_from_unix_secs` for a u64 seconds count past chrono's range).
- engine cmd/controls.rs `days_from_civil` and `days_in_month` (`days_in_month`
now uses `rem_euclid(400)` since month lengths repeat every 400 years).
- formats sheet.rs `civil` and docx chart.rs `civil` (Excel serials).
Outputs are unchanged: verified the cycle result equals the old formula over a
wide range and at the extremes with a scratch chrono program. New tests pin
accepted inputs past chrono's range on both sides of the epoch, through the
public/lib entry points and the private converters, with values from the old
formula. Removed the AGENTS.md "narrow fallback" exception.
pdf/lib.rs `civil` is left as-is: its only caller is the live system clock and
its year is clamped to 0..9999 by the PDF DateTime, so the epoch fallback past
year 262143 is the accepted live-clock behavior, not a hand-rolled copy.
Tested: cargo test -p wordcraft-engine -p wordcraft-formats -p wordcraft-docx;
fmt --check; clippy --all-targets -D warnings.
* logging/docbin/odt/pdf: keep extended-year dates, no epoch fallback
FredApps (CHANGES_REQUESTED) showed `DateTime::from_timestamp(..).unwrap_or_default()`
changes output: a timestamp the old code converted past chrono's year 262143 range now
collapsed to 1970-01-01 instead of its real proleptic-Gregorian date. The same regression
exists wherever the PR swapped a hand-rolled converter for the epoch fallback. Fix every
such site to keep chrono as the only calendar logic via the 400-year Gregorian cycle
(146_097 days): chrono converts the day within one cycle, whole cycles carry in the year
(i128, saturated into i64). Verified the cycle result equals the old formula across a wide
range and at the extremes.
- apps/wordcraft/src/logging.rs `timestamp`: added `civil_from_days(u64)` (cycle), kept the
pre-1970 epoch clamp (`duration_since`) and the milliseconds/time-of-day format exactly.
Pins: UNIX_EPOCH + 10_000_000_000_000 s -> 318857-05-20T17:46:40.000Z (FredApps' example,
was 1970-...); the checked_add u64::MAX/2 s fixture now asserts 292277026596-12-04T15:30:07.000Z
instead of the epoch.
- crates/docbin/src/metadata.rs `filetime_iso`: restored a cycle-based `civil(i64)` instead
of `from_timestamp(..)?` (which dropped the field past chrono's range). A u64 FILETIME caps
at ~1.83e12 unix s (year 60056), inside chrono's range, so no reachable value changes; pinned
the largest FILETIME (u64::MAX) -> 60056-05-28T05:36:10Z to lock the extended-year path.
- crates/formats/src/odt.rs `now_iso`: reuse the crate's existing cycle converter
`sheet::civil` (made pub(crate)) rather than the epoch fallback.
- crates/pdf/src/lib.rs `civil`: cycle converter; year returned unclamped and the caller's
existing PDF DateTime 0..=9999 year clamp is kept exactly, so month/day/time of a far-future
clock survive. Pins: civil(10_000_000_000_000)=(318857,5,20,17,46,40),
civil(-10_000_000_000_000)=(-314918,8,13,6,13,20), civil(253_402_300_800)=(10000,1,1,0,0,0).
engine `cmd/mod.rs::iso_from_unix_secs` already uses the cycle fallback (last round); unchanged.
* Don't build a year-318857 SystemTime unchecked in the logging test
Windows' SystemTime only reaches about year 30828, so
UNIX_EPOCH + Duration::from_secs(10_000_000_000_000) panicked before
timestamp() ran and failed the Windows test job. Build it with
checked_add and assert where the platform can hold it. The year-10000
case fits everywhere and stays unconditional.
* Name the 400-year range extension as the one exception to the time rule
The rule said never to hand-roll calendar math, but the range fix
(shifting by whole 400-year Gregorian cycles so dates past chrono's year
range keep their old output) is cycle arithmetic, so the rule and the code
disagreed. The rule now names that exception: the library still computes
month, day and time, and only whole cycles are carried in the year.
* Say the library computes month and day; time-of-day is plain arithmetic
Narrows the range-extension wording: the date library computes the
calendar fields, and splitting seconds into hours, minutes and seconds
is ordinary arithmetic, which is what the code does.
`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.
The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.
Co-Authored-By: opencode <noreply@opencode.ai>
A save makes the document clean, which queues discarding this
session's now-redundant recovery copy — but that's only queued, on a
background worker thread, the same as a print job. Closing the app
right after Save (the very next frame, with no regular recovery_tick
in between to notice and queue the discard, or with the discard
queued but not yet finished by the worker) could race the process
exiting before the copy was actually removed, so the next launch
wrongly offered to recover a document that was in fact saved cleanly.
Added Recovery::finish and Session::finish_recovery, mirroring the
print spooler's own finish_pending_jobs, and call it from the same
places: on_exit and the macOS hard-quit path.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A bare `run` built its document with Document::new() (Letter) and never went
through file.new, so it skipped the system region's paper. It now runs
file.new, as `run --template` already did.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #2999: a file dialog that "cancels" at once asks again without a parent, then shows the built-in browser
rfd folds the xdg-desktop-portal's failure response into "cancelled", so on desktops whose
FileChooser portal fails to show a dialog, Save As, Export and Open did nothing and logged
nothing. A cancel quicker than anyone can make one (300 ms) is now taken as that failure: the
dialog is asked for once more without the parent window (GridCraft, which passes none, works on
the reporter's machine), and if that too comes back at once the app shows its built-in browser.
Each step logs a warning.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WEhZXSxzS8orX7f9J3qpr
* #2999: review — the quick-cancel retry runs only on Linux and the BSDs; Windows and macOS answer a quick cancel as a cancel
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WEhZXSxzS8orX7f9J3qpr
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`graphics.rs` allowed dead code only on macOS (where the Windows items are unused). Since #2845
the two indirect-validation functions are called only on macOS, so on Windows and Linux
`cargo clippy --workspace --all-targets -- -D warnings` stopped with "function is never used".
They now carry `allow(dead_code)` off macOS.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* cli: keep the non-UTF-8 file name test off macOS
APFS refuses file names that aren't valid UTF-8, so the fixture can't be
made there (as in tests/non_utf8_args.rs); the unit test runs on the
other unixes.
* chore: add @maxm11 to About-window contributors with their own name
wgpu compiles its indirect-call validation compute shaders inside request_device.
On Intel HD Graphics 5000 (Big Sur) Metal's Intel compiler aborts on them, the
device is lost and eframe exits before the window opens. WordCraft issues no
indirect draws or dispatches, so x86_64 macOS builds leave
VALIDATION_INDIRECT_CALL out of the instance flags (WGPU_VALIDATION_INDIRECT_CALL=1
still re-enables it). Same fix as storytold/PhotoCraft#3171.
Fixesstorytold/WordCraft#2652
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
info, text, convert, render and inspect took no password, though file.open
understood one, so an agent driving the CLI could not open a protected
document at all. --password, --password-file and WORDCRAFT_PASSWORD, in
that order.
The file form exists because a password on a command line is readable by
every other process on the machine; a trailing newline is dropped so a
file written by echo works. Never logged, never named in an error.
Rebased onto current main, which moved `open` from &str to &Path so that a
file name that isn't valid Unicode no longer crashes the CLI (#1440). Both
changes touch the same function, so the resolution keeps their shape: the
closure that substitutes the real path still stands, and it takes the
password as a second argument.
Co-authored-by: Bra1nZ <bra1nz@astraoffice.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Theme: default to system appearance
Sets Appearance::System as the default variant for ThemePreference,
so fresh installs and unconfigured preference files follow the host
OS appearance dynamically.
* contributors: add the missing blank line
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* contributors: drop the duplicate entry (already listed on main)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Web prefs test: junk storage keeps the new default (System) appearance
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>
The web shell never loaded or saved the UiState the desktop app keeps in
ui.json, so a reload reset the theme and every other preference. It now
restores it from localStorage at startup and writes it back whenever it
changes (a reload runs no exit code).
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
A copy or cut of one equation on its own now also puts its MathML in the
platform's MathML clipboard formats, beside the HTML (with MathML, #816) and
the linear text: "MathML" and "MathML Presentation" (UTF-16LE) on Windows,
public.mathml.presentation and public.mathml on macOS. These are the names
W3C MathML 3 section 6.2.4 registers, and Microsoft's "MathML Support in
Microsoft 365" doc says Office checks them first when it pastes math. No
public clipboard format carries OMML. A copy with text around the equation
doesn't offer them, so Word keeps the text.
Not yet verified in Word: the parity docs say MathML clipboard formats, not
an editable paste.
Claude-Session: https://claude.ai/code/session_01P8veLveuUsJJm6UEcQPEuW
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>