Exports: SOURCE_DATE_EPOCH stands in for the clock, so output repeats

PNG, PDF, EPS, legacy `.ai`, packaged zips and saved `.vectorcraft` files
record the wall-clock time they were made in, so the same commands run twice
gave different bytes although the pixels matched (#2880):

  PDF   /CreationDate and /ModDate   D:20261018155816 vs D:20261018155817
  PNG   the `Creation Time` tEXt entry
  .vectorcraft  one byte of the stored date
  SVG   carries no time, and already repeated

`metadata::output_time` is the time a file records: `SOURCE_DATE_EPOCH` when it
holds a non-negative whole number of seconds, else the clock, as the
reproducible-builds specification defines it. A date a command is given
(`file.new {created}`, a save's `modified`) still wins over the variable, and a
blank, negative or non-numeric value is ignored so that an export is never
refused over the environment.

`metadata::now_unix` keeps the real clock and now has exactly two callers, both
of which must not be pinnable from the environment: the recovery heartbeat, and
the seed PDF encryption draws on.
This commit is contained in:
Hamza
2026-10-19 00:37:55 +07:00
committed by bflatastic
parent c9a2491ef7
commit 1c80b23745
16 changed files with 88 additions and 15 deletions
+17
View File
@@ -160,3 +160,20 @@ fn newer_keys_and_legacy_profiles_survive_open_and_save() {
let v2 = json_of(&saved(&mut s, json!({"version": 2})));
assert_eq!(v2["document"]["unknown"]["colorProfiles"]["rgb"], "sRGB IEC61966-2.1");
}
/// A save records the date the document was made in, so the same commands twice gave different
/// bytes (#2880). A pinned date makes them identical — `file.new {created}` here, and
/// `SOURCE_DATE_EPOCH` for an export that takes no date param.
#[test]
fn a_pinned_date_makes_saves_byte_identical() {
let once = |t: i64| {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 400, "height": 200, "created": t})).unwrap();
s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 100, "height": 50})).unwrap();
saved(&mut s, json!({}))
};
let at = 1_791_208_980;
assert_eq!(once(at), once(at), "the same document and date give the same bytes");
// The date really is in the bytes, so the comparison above is not vacuous.
assert_ne!(once(at), once(at + 1), "another date shows in the bytes");
}