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:
bonfim6P
2026-10-15 02:53:57 -04:00
committed by GitHub
co-authored by Paulo Junior Claude Sonnet 5.5
parent 41d4c15849
commit 6a78a9ce99
3 changed files with 56 additions and 3 deletions
+6
View File
@@ -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
}
+20
View File
@@ -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");
+30 -3
View File
@@ -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 {