mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-29 04:28:20 -06:00
fix(engine): give a host with no clock or time zone a real one (Session::with_wall_clock) (#2317)
On wasm32-unknown-unknown the engine has neither a clock (`now()` is empty) nor a time zone (`local_utc_offset()` is 0), so in a browser Fill & Sign stamps 1 Jan 1970 and `/ModDate` is never written. The one way to inject a clock, `with_clock`, is a test shortcut: it takes the injected instant AS local time and zeroes the zone, so a host that injects only that gets either the local dates or the UTC ones wrong by its offset (21:30 in São Paulo, UTC-3, is already the next day in UTC). `Session::with_wall_clock(clock, utc_offset)` keeps the two separate: `clock` is the UTC instant (what the file records) and `utc_offset` is asked on every use, because the zone changes with daylight saving. `Automation::with_wall_clock` forwards it. Nothing changes for a native build, or for `with_clock`. The test pins the contrast: at 21:30 in São Paulo the stamped day is the 14th, `signing_date` carries `-03'00'`, the file's instant stays 00:30Z on the 15th, and the plain test clock still answers the 15th. Co-authored-by: Paulo Junior <195025778+bonfim6P@users.noreply.github.com> Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Paulo Junior
Claude Sonnet 5.5
parent
41d4c15849
commit
6a78a9ce99
@@ -140,6 +140,12 @@ impl Automation {
|
||||
self
|
||||
}
|
||||
|
||||
/// The clock and time zone of a host without its own. See [`Session::with_wall_clock`].
|
||||
pub fn with_wall_clock(mut self, clock: fn() -> i64, utc_offset: fn() -> i64) -> Self {
|
||||
self.session = std::mem::take(&mut self.session).with_wall_clock(clock, utc_offset);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn session(&self) -> &Session {
|
||||
&self.session
|
||||
}
|
||||
|
||||
@@ -669,6 +669,26 @@ mod tests {
|
||||
assert_eq!(s.date_format(), "dd.mm.yyyy", "a rejected pattern keeps the previous one");
|
||||
}
|
||||
|
||||
/// 21:30 on 14 Oct 2026 in São Paulo (UTC-3) is 00:30Z on the 15th. The date Fill & Sign
|
||||
/// stamps is the wall-clock day (14th); the time the file records (`/M`, `/ModDate`) is UTC.
|
||||
/// The test shortcut (`with_clock`) takes the injected time as local and says the 15th, which
|
||||
/// is why it cannot serve a host that knows both the UTC time and the zone.
|
||||
#[test]
|
||||
fn a_wall_clock_gives_the_local_day_and_keeps_utc_for_the_file() {
|
||||
let sao_paulo = Session::new().with_wall_clock(|| 1_792_024_200, || -3 * 3600);
|
||||
assert_eq!(sao_paulo.today(), (2026, 10, 14));
|
||||
assert_eq!(sao_paulo.today_text(None, None).as_deref(), Ok("10/14/2026"));
|
||||
assert_eq!(sao_paulo.signing_date(), "D:20261014213000-03'00'");
|
||||
assert_eq!(sao_paulo.stamp_by_line("Ada"), "By Ada at 9:30 pm, Oct 14, 2026");
|
||||
assert_eq!(sao_paulo.now_secs(), 1_792_024_200, "the file's instant stays in UTC");
|
||||
|
||||
let utc = Session::new().with_wall_clock(|| 1_792_024_200, || 0);
|
||||
assert_eq!(utc.today(), (2026, 10, 15));
|
||||
|
||||
let clock_only = Session::new().with_clock(|| 1_792_024_200);
|
||||
assert_eq!(clock_only.today(), (2026, 10, 15), "on its own, an injected clock counts as local time");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn names_follow_the_date_language() {
|
||||
assert_eq!(in_lang("dddd, d \\de mmmm \\de yyyy", "es"), "sábado, 7 de marzo de 2026");
|
||||
|
||||
@@ -2762,6 +2762,11 @@ pub struct Session {
|
||||
next_id: u64,
|
||||
/// Seconds since the Unix epoch, injected so saves are deterministic in tests.
|
||||
clock: Option<fn() -> i64>,
|
||||
/// Seconds to add to UTC for local time, read from whoever knows the zone (a browser, say).
|
||||
/// Separate from `clock` on purpose: `clock` alone is the test shortcut that takes the
|
||||
/// injected time AS local time and zeroes the zone, so injecting only it leaves either the
|
||||
/// local dates or the UTC ones wrong by the zone's offset.
|
||||
utc_offset: Option<fn() -> i64>,
|
||||
/// Certificates trusted for signing (Acrobat: Trusted Certificates).
|
||||
trust: Arc<TrustStore>,
|
||||
/// Preferences ▸ JavaScript ▸ Enable Acrobat JavaScript, inverted (on by default).
|
||||
@@ -3100,6 +3105,28 @@ impl Session {
|
||||
self
|
||||
}
|
||||
|
||||
/// A real clock for a host that has neither a clock nor a time zone of its own (a
|
||||
/// `wasm32-unknown-unknown` build: `now()` would be empty and `local_utc_offset()` is 0, so
|
||||
/// Fill & Sign stamps 1 Jan 1970 and `/ModDate` is never written). `clock` is the UTC time in
|
||||
/// seconds since the epoch; `utc_offset` is the local zone's seconds east of UTC, asked on every
|
||||
/// use because the zone changes with the date (daylight saving). Unlike
|
||||
/// [`Session::with_clock`], which is a test shortcut, nothing here is taken as local time.
|
||||
pub fn with_wall_clock(mut self, clock: fn() -> i64, utc_offset: fn() -> i64) -> Self {
|
||||
self.clock = Some(clock);
|
||||
self.utc_offset = Some(utc_offset);
|
||||
self
|
||||
}
|
||||
|
||||
/// Seconds to add to UTC for the local time of this session: the injected zone, else 0 under
|
||||
/// the test clock (which is already local), else the system's.
|
||||
fn local_offset(&self) -> i64 {
|
||||
match (self.utc_offset, self.clock) {
|
||||
(Some(zone), _) => zone(),
|
||||
(None, Some(_)) => 0,
|
||||
(None, None) => local_utc_offset(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Seconds since the Unix epoch from the session clock (0 when unknown).
|
||||
pub fn now_secs(&self) -> i64 {
|
||||
self.now().unwrap_or(0)
|
||||
@@ -3119,7 +3146,7 @@ impl Session {
|
||||
/// is taken as local time.
|
||||
/// A dynamic stamp's second line: "By Ada at 2:14 pm, Oct 02, 2026" (local time).
|
||||
pub fn stamp_by_line(&self, author: &str) -> String {
|
||||
let offset = if self.clock.is_some() { 0 } else { local_utc_offset() };
|
||||
let offset = self.local_offset();
|
||||
let d = self.now().map(|t| pdfcraft_cos::pdf_date(t + offset)).unwrap_or_default();
|
||||
let num = |a: usize, b: usize| d.get(a..b).and_then(|s| s.parse::<u32>().ok()).unwrap_or(0);
|
||||
let (y, mo, day, hh, mm) = (num(2, 6), num(6, 8), num(8, 10), num(10, 12), num(12, 14));
|
||||
@@ -3130,7 +3157,7 @@ impl Session {
|
||||
}
|
||||
|
||||
pub fn today(&self) -> (i64, u32, u32) {
|
||||
let offset = if self.clock.is_some() { 0 } else { local_utc_offset() };
|
||||
let offset = self.local_offset();
|
||||
self.now().map(|t| pdfcraft_cos::pdf_date(t + offset)).as_deref().and_then(parse_ymd).unwrap_or((1970, 1, 1))
|
||||
}
|
||||
|
||||
@@ -4580,7 +4607,7 @@ impl Session {
|
||||
|
||||
/// The signing time as a PDF date in local time with its offset (`D:…+02'00'`).
|
||||
pub fn signing_date(&self) -> String {
|
||||
let offset = if self.clock.is_some() { 0 } else { local_utc_offset() };
|
||||
let offset = self.local_offset();
|
||||
let local = pdfcraft_cos::pdf_date(self.now().unwrap_or(0) + offset);
|
||||
let stamp = local.trim_end_matches('Z');
|
||||
if offset == 0 {
|
||||
|
||||
Reference in New Issue
Block a user