mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-24 02:17:24 -06:00
* 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.
109 lines
3.8 KiB
TOML
109 lines
3.8 KiB
TOML
[workspace]
|
|
resolver = "3"
|
|
members = ["crates/*", "apps/*", "xtask"]
|
|
default-members = ["crates/*", "apps/wordcraft", "apps/wordcraft-cli", "xtask"]
|
|
|
|
[workspace.package]
|
|
version = "0.9.0"
|
|
edition = "2024"
|
|
license = "MIT OR Apache-2.0"
|
|
rust-version = "1.95"
|
|
repository = "https://github.com/storytold/wordcraft"
|
|
|
|
[workspace.dependencies]
|
|
# internal
|
|
wordcraft-geom = { path = "crates/geom" }
|
|
wordcraft-doc = { path = "crates/doc" }
|
|
wordcraft-fonts = { path = "crates/fonts" }
|
|
wordcraft-layout = { path = "crates/layout" }
|
|
wordcraft-docx = { path = "crates/docx" }
|
|
wordcraft-docbin = { path = "crates/docbin" }
|
|
wordcraft-formats = { path = "crates/formats" }
|
|
wordcraft-render = { path = "crates/render" }
|
|
wordcraft-pdf = { path = "crates/pdf" }
|
|
wordcraft-proof = { path = "crates/proof" }
|
|
wordcraft-engine = { path = "crates/engine" }
|
|
wordcraft-mcp = { path = "crates/mcp" }
|
|
wordcraft-zotero = { path = "crates/zotero" }
|
|
wordcraft-control-key = { path = "crates/control-key" }
|
|
wordcraft-ui-egui = { path = "crates/ui-egui" }
|
|
wordcraft-metafile = { path = "crates/metafile" }
|
|
|
|
# foundation
|
|
kurbo = { version = "0.13", features = ["serde"] }
|
|
thiserror = "2"
|
|
serde = { version = "1", features = ["derive", "rc"] }
|
|
serde_json = { version = "1", features = ["float_roundtrip"] }
|
|
log = "0.4"
|
|
# All date/time conversion goes through chrono (no hand-rolled calendar code beyond the 400-year range extension; AGENTS.md).
|
|
chrono = { version = "0.4.45", default-features = false, features = ["std"] }
|
|
proptest = "1"
|
|
rayon = "1"
|
|
unicode-bidi = "0.3"
|
|
unicode-segmentation = "1"
|
|
unicode-linebreak = "0.1"
|
|
unicode-script = "0.5"
|
|
regex = { version = "1", default-features = false, features = ["std", "unicode", "perf"] }
|
|
regex-automata = { version = "0.4", default-features = false, features = ["std", "syntax", "meta", "nfa-pikevm", "unicode"] }
|
|
|
|
# text / rendering / formats
|
|
skrifa = "0.47"
|
|
harfrust = "0.13"
|
|
# WOFF / WOFF2 web fonts added by the user (#582): pure Rust (MIT); its brotli and zlib decoders
|
|
# (brotli-decompressor, flate2) are already in the tree.
|
|
wuff = "0.2.9"
|
|
vello_cpu = "0.2"
|
|
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif", "bmp", "tiff"] }
|
|
craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "c3487320b2d1de657b860b3c8abb44e1f2b32bd8", version = "=0.1.0", default-features = false, features = ["write"] }
|
|
quick-xml = "0.41"
|
|
zip = { version = "3", default-features = false, features = ["deflate"] }
|
|
# zlib streams (compressed metafile pictures in `.doc` drawings); already in the lock as a dependency of other crates.
|
|
miniz_oxide = "0.8"
|
|
cfb = "0.15"
|
|
# Legacy text encodings (GBK/GB18030, Big5, Shift_JIS, EUC-KR, single-byte code pages) for plain
|
|
# text and CSV recipient lists (#778): encoding_rs ((Apache-2.0 OR MIT) AND BSD-3-Clause) and its
|
|
# detector chardetng (Apache-2.0 OR MIT), both pure Rust (Firefox's).
|
|
encoding_rs = "0.8"
|
|
chardetng = "1"
|
|
# Password-protected (agile-encrypted) Word documents: pure-Rust RustCrypto primitives.
|
|
aes = "0.9"
|
|
sha1 = "0.11"
|
|
sha2 = "0.11"
|
|
hmac = "0.13"
|
|
base64 = "0.22"
|
|
getrandom = "0.3"
|
|
zeroize = "1"
|
|
|
|
# ui
|
|
egui = "0.36"
|
|
egui_extras = { version = "0.36", default-features = false }
|
|
eframe = { version = "0.36", default-features = false }
|
|
egui_kittest = "0.36"
|
|
rfd = "0.17"
|
|
arboard = { version = "3.6", default-features = false, features = ["wayland-data-control"] }
|
|
|
|
[workspace.lints.rust]
|
|
unsafe_code = "forbid"
|
|
|
|
[workspace.lints.clippy]
|
|
dbg_macro = "warn"
|
|
todo = "warn"
|
|
too_many_arguments = "allow"
|
|
type_complexity = "allow"
|
|
|
|
[profile.dev.package."*"]
|
|
opt-level = 2
|
|
|
|
[profile.dev]
|
|
opt-level = 1
|
|
|
|
[profile.release]
|
|
opt-level = 3
|
|
lto = "thin"
|
|
codegen-units = 16
|
|
debug = 0
|
|
|
|
# Required by craft-pdf: the backend rejects writers that failed during unwinding.
|
|
[patch.crates-io]
|
|
krilla = { git = "https://github.com/storytold/craft-libs", rev = "c3487320b2d1de657b860b3c8abb44e1f2b32bd8" }
|