From 1f48670fcef2e5b9f5294650cb7dc51cdf5a3cff Mon Sep 17 00:00:00 2001 From: jonathanwxh-cell Date: Mon, 9 Nov 2026 08:32:08 +0800 Subject: [PATCH] refactor(cos): consolidate duplicate civil_from_days onto pdfcraft_cos::civil_from_days (#6540) Fixes #6466 --- apps/pdfcraft/src/logging.rs | 23 ++++-------------- crates/cos/src/lib.rs | 2 +- crates/cos/src/writer.rs | 20 ++++++++++------ crates/engine/src/lib.rs | 2 +- crates/sign/src/der.rs | 14 ++--------- .../ui-egui/src/workflows/combine_ui/parts.rs | 23 ++++-------------- xtask/src/demo_pdf/pdf.rs | 24 ++----------------- 7 files changed, 29 insertions(+), 79 deletions(-) diff --git a/apps/pdfcraft/src/logging.rs b/apps/pdfcraft/src/logging.rs index bef27de169..4c0f39e85c 100644 --- a/apps/pdfcraft/src/logging.rs +++ b/apps/pdfcraft/src/logging.rs @@ -102,25 +102,12 @@ pub fn timestamp(t: SystemTime) -> String { format!("{y:04}-{m:02}-{d:02}T{:02}:{:02}:{:02}.{:03}Z", rem / 3600, rem % 3600 / 60, rem % 60, since.subsec_millis()) } -/// Days since 1970-01-01 → proleptic Gregorian (year, month, day). -/// -/// `timestamp` clamps a pre-1970 clock to the epoch, so `days` is an unsigned count that can run -/// past `NaiveDate`'s ±262143-year range. The Gregorian calendar repeats every 400 years -/// (146_097 days), so chrono converts the day within one cycle and whole cycles go back into the -/// year (an `i128`, then saturated into `i64`), keeping chrono as the only calendar logic and -/// reproducing the old `civil_from_days` result exactly without hand-rolled day math. 1970-01-01 -/// is `num_days_from_ce` 719_163. +/// Days since 1970-01-01 → proleptic Gregorian (year, month, day): `pdfcraft_engine::civil_from_days`, +/// which keeps the date past chrono's range by whole 400-year cycles. `days` is a `u64` second +/// count over 86_400, at most about 2.1e14, so it always fits an `i64`. fn civil_from_days(days: u64) -> (i64, u32, u32) { - use chrono::Datelike; - let (cycles, rest) = (days / 146_097, days % 146_097); - // `rest + 719_163` is 719_163..865_259, always a valid CE day. - match i32::try_from(rest + 719_163).ok().and_then(chrono::NaiveDate::from_num_days_from_ce_opt) { - Some(d) => { - let y = i128::from(d.year()) + i128::from(cycles) * 400; - (i64::try_from(y).unwrap_or(i64::MAX), d.month(), d.day()) - } - None => (1970, 1, 1), // unreachable - } + // The fallback is unreachable: `days` is at most `u64::MAX / 86_400`, far below `i64::MAX`. + pdfcraft_engine::civil_from_days(i64::try_from(days).unwrap_or(i64::MAX)) } fn numbered(dir: &Path, n: usize) -> PathBuf { diff --git a/crates/cos/src/lib.rs b/crates/cos/src/lib.rs index 32a6f6d8b3..0d0fa33f61 100644 --- a/crates/cos/src/lib.rs +++ b/crates/cos/src/lib.rs @@ -30,7 +30,7 @@ pub use parser::{Lexer, is_whitespace, parse_indirect}; pub use pdfcraft_crypt::{ Algorithm, Auth, Method as CryptMethod, NewEncryption, Permissions, SecurityHandler, effective_password, pdfdoc_byte, pdfdoc_char, }; -pub use writer::{SaveOptions, pdf_date, reachable_objects, serialize, write_full, write_incremental}; +pub use writer::{SaveOptions, civil_from_days, pdf_date, reachable_objects, serialize, write_full, write_incremental}; #[derive(Debug, thiserror::Error, Clone, PartialEq)] pub enum CosError { diff --git a/crates/cos/src/writer.rs b/crates/cos/src/writer.rs index 75d49dbd76..3037669aea 100644 --- a/crates/cos/src/writer.rs +++ b/crates/cos/src/writer.rs @@ -919,14 +919,14 @@ pub fn pdf_date(unix_secs: i64) -> String { format!("D:{y:04}{m:02}{d:02}{:02}{:02}{:02}Z", secs / 3600, secs / 60 % 60, secs % 60) } -/// Days since 1970-01-01 → proleptic Gregorian (year, month, day). +/// Days since 1970-01-01 → proleptic Gregorian (year, month, day). Total for any `i64` count: +/// dates past `NaiveDate`'s ±262143-year range keep their proleptic Gregorian year instead of +/// falling back to the epoch. /// -/// A `from_timestamp` epoch fallback would change dates past `NaiveDate`'s ±262143-year range, -/// which the old hand-rolled converter formatted directly. The Gregorian calendar repeats every -/// 400 years (146_097 days), so chrono converts the day within one cycle and whole cycles go back -/// into the year (an `i128` so the extremes can't overflow), keeping chrono as the only calendar -/// logic and reproducing the old result exactly. 1970-01-01 is `num_days_from_ce` 719_163. -fn civil_from_days(days: i64) -> (i64, u32, u32) { +/// The Gregorian calendar repeats every 400 years (146_097 days), so chrono converts the day +/// within one cycle and whole cycles go back into the year (an `i128` so the extremes can't +/// overflow), keeping chrono as the only calendar logic. 1970-01-01 is `num_days_from_ce` 719_163. +pub fn civil_from_days(days: i64) -> (i64, u32, u32) { use chrono::Datelike; let (cycles, rest) = (days.div_euclid(146_097), days.rem_euclid(146_097)); // `rest + 719_163` is 719_163..865_259, always a valid CE day. @@ -1110,6 +1110,12 @@ mod tests { // its sign, exactly as before). assert_eq!(pdf_date(10_000_000_000_000), "D:3188570520174640Z"); assert_eq!(pdf_date(-10_000_000_000_000), "D:-3149180813061320Z"); + assert_eq!(crate::civil_from_days(0), (1970, 1, 1)); + assert_eq!(crate::civil_from_days(-1), (1969, 12, 31)); + assert_eq!(crate::civil_from_days(11_016), (2000, 2, 29)); + assert_eq!(crate::civil_from_days(19_782), (2024, 2, 29)); + assert_eq!(crate::civil_from_days(i64::MAX), (25_252_734_927_768_524, 7, 27)); + assert_eq!(crate::civil_from_days(i64::MIN), (-25_252_734_927_764_585, 6, 7)); } #[test] diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index baa06b02fd..e6e21cb1eb 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -86,7 +86,7 @@ pub use pdfcraft_organize::Standard; pub use pdfcraft_organize::check_custom_key; pub use pdfcraft_organize::view::{InitialView, Layout as InitialLayout, Magnification, Navigation}; -pub use pdfcraft_cos::Algorithm; +pub use pdfcraft_cos::{Algorithm, civil_from_days}; pub use pdfcraft_create::{CONVERTIBLE, CreateError, ImageResolution, SourceKind, decode_text, source_kind}; pub use pdfcraft_edit::added::{ CHAR_SPACING_LIMIT as ADDED_CHAR_SPACING_LIMIT, paste_place as added_paste_place, paste_rect as added_paste_rect, text_rect as added_text_rect, diff --git a/crates/sign/src/der.rs b/crates/sign/src/der.rs index 0cac252c33..f9f2f3e6d9 100644 --- a/crates/sign/src/der.rs +++ b/crates/sign/src/der.rs @@ -349,19 +349,9 @@ impl Time { /// year is cast exactly as the old code did (`as u32`, wrapping) — no well-formed signing time /// is anywhere near that range, and this preserves byte-for-byte the old output everywhere. pub fn from_unix(t: i64) -> Time { - use chrono::Datelike; let (days, secs) = (t.div_euclid(86_400), t.rem_euclid(86_400)); - let (cycles, rest) = (days.div_euclid(146_097), days.rem_euclid(146_097)); - // `rest + 719_163` is 719_163..865_259, always a valid CE day. - let (year, month, day) = match i32::try_from(rest + 719_163).ok().and_then(chrono::NaiveDate::from_num_days_from_ce_opt) { - Some(d) => { - let y = i128::from(d.year()) + i128::from(cycles) * 400; - // Match the old `as u32` cast: saturate i128→i64 first (the old year was i64), then wrap into u32. - (i64::try_from(y).unwrap_or(if y < 0 { i64::MIN } else { i64::MAX }) as u32, d.month(), d.day()) - } - None => (1970, 1, 1), // unreachable - }; - Time { year, month, day, hour: (secs / 3600) as u32, minute: (secs % 3600 / 60) as u32, second: (secs % 60) as u32 } + let (year, month, day) = pdfcraft_cos::civil_from_days(days); + Time { year: year as u32, month, day, hour: (secs / 3600) as u32, minute: (secs % 3600 / 60) as u32, second: (secs % 60) as u32 } } pub fn add_seconds(&self, s: i64) -> Time { diff --git a/crates/ui-egui/src/workflows/combine_ui/parts.rs b/crates/ui-egui/src/workflows/combine_ui/parts.rs index a64d0eeb34..4527ec061d 100644 --- a/crates/ui-egui/src/workflows/combine_ui/parts.rs +++ b/crates/ui-egui/src/workflows/combine_ui/parts.rs @@ -274,23 +274,10 @@ pub(super) fn utc_stamp(time: SystemTime) -> String { format!("{y:04}-{m:02}-{d:02} {:02}:{:02} UTC", rem / 3600, rem % 3600 / 60) } -/// Days since 1970-01-01 → proleptic Gregorian (year, month, day). -/// -/// A pre-1970 time already clamps to the epoch (`unwrap_or(0)`), so `days` is an unsigned count -/// that can run past `NaiveDate`'s ±262143-year range, where the old hand-rolled converter still -/// produced a date. The Gregorian calendar repeats every 400 years (146_097 days), so chrono -/// converts the day within one cycle and whole cycles go back into the year (an `i128`, then -/// saturated into `i64`), keeping chrono as the only calendar logic and reproducing the old -/// result exactly. 1970-01-01 is `num_days_from_ce` 719_163. +/// Days since 1970-01-01 → proleptic Gregorian (year, month, day): `pdfcraft_cos::civil_from_days`, +/// which keeps the date past chrono's range by whole 400-year cycles. `days` is a `u64` second +/// count over 86_400, at most about 2.1e14, so it always fits an `i64`. fn civil_from_days(days: u64) -> (i64, u32, u32) { - use chrono::Datelike; - let (cycles, rest) = (days / 146_097, days % 146_097); - // `rest + 719_163` is 719_163..865_259, always a valid CE day. - match i32::try_from(rest + 719_163).ok().and_then(chrono::NaiveDate::from_num_days_from_ce_opt) { - Some(d) => { - let y = i128::from(d.year()) + i128::from(cycles) * 400; - (i64::try_from(y).unwrap_or(i64::MAX), d.month(), d.day()) - } - None => (1970, 1, 1), // unreachable - } + // The fallback is unreachable: `days` is at most `u64::MAX / 86_400`, far below `i64::MAX`. + pdfcraft_cos::civil_from_days(i64::try_from(days).unwrap_or(i64::MAX)) } diff --git a/xtask/src/demo_pdf/pdf.rs b/xtask/src/demo_pdf/pdf.rs index b51ea1a643..dcdc156683 100644 --- a/xtask/src/demo_pdf/pdf.rs +++ b/xtask/src/demo_pdf/pdf.rs @@ -77,29 +77,9 @@ impl Stamp { pub fn from_unix(secs: i64) -> Self { let (days, rem) = (secs.div_euclid(86_400), secs.rem_euclid(86_400)); let (h, mi, s) = (rem / 3600, rem % 3600 / 60, rem % 60); - let (y, m, d) = Self::civil_from_days(days); + let (y, m, d) = pdfcraft_cos::civil_from_days(days); // `{y:04}` rather than `%Y`, which writes a `+` before years past 9999. - Stamp { unix: secs, pdf: format!("D:{y:04}{m:02}{d:02}{h:02}{mi:02}{s:02}Z"), iso: format!("{y:04}-{m:02}-{d:02}T{h:02}:{mi:02}:{s:02}Z") } - } - - /// Days since 1970-01-01 → proleptic Gregorian (year, month, day). - /// - /// Past `NaiveDate`'s ±262143-year range the old hand-rolled converter still produced a date, - /// which both stamps formatted directly; a `from_timestamp` epoch fallback would change it. - /// The Gregorian calendar repeats every 400 years (146_097 days), so chrono converts the day - /// within one cycle and whole cycles go back into the year (an `i128` so the extremes can't - /// overflow), keeping chrono as the only calendar logic. 1970-01-01 is `num_days_from_ce` 719_163. - fn civil_from_days(days: i64) -> (i64, u32, u32) { - use chrono::Datelike; - let (cycles, rest) = (days.div_euclid(146_097), days.rem_euclid(146_097)); - // `rest + 719_163` is 719_163..865_259, always a valid CE day. - match i32::try_from(rest + 719_163).ok().and_then(chrono::NaiveDate::from_num_days_from_ce_opt) { - Some(d) => { - let y = i128::from(d.year()) + i128::from(cycles) * 400; - (i64::try_from(y).unwrap_or(if y < 0 { i64::MIN } else { i64::MAX }), d.month(), d.day()) - } - None => (1970, 1, 1), // unreachable - } + Stamp { unix: secs, pdf: pdfcraft_cos::pdf_date(secs), iso: format!("{y:04}-{m:02}-{d:02}T{h:02}:{mi:02}:{s:02}Z") } } /// The same moment shifted by `minutes` (used to order threaded replies).