diff options
| author | Ralph Amissah <ralph.amissah@gmail.com> | 2026-09-21 00:02:21 -0400 |
|---|---|---|
| committer | Ralph Amissah <ralph.amissah@gmail.com> | 2026-09-22 12:50:33 -0400 |
| commit | 67d27829788d49eab208d871cf4dfc95c5f3ba6e (patch) | |
| tree | 0d19f03b18c1dbf393e5c429de8e45e4397ab1e2 /org | |
| parent | conf: read conf/document_make, drop the dr_ prefix (diff) | |
epub: dcterms:modified timestamp, reproducibility
timestamp dcterms:modified from the document, not the clock
EPUB3 requires exactly one dcterms:modified on the package
It is now read in order: SOURCE_DATE_EPOCH where the environment sets
it, which is the reproducible-builds convention and lets a build pin
every artefact of one run; then the document's own date.modified; then
date.published. The clock remains the last resort, for a document that
says nothing about when it is from.
A document date is loose markup. "1991", "2006-03" and "2021-12-00" all
occur in the sample set, and a month or day of 00 is not a date. A
missing or zero part becomes 01, and a day out of range for its month
falls back to the first, so what is written is always a valid
CCYY-MM-DDThh:mm:ssZ.
Over the sample set two consecutive runs now produce all 36 epubs byte
identical, and epubcheck reports no change: 0 fatal, 0 errors.
(previously taken from the clock at build time. Two runs over unchanged
markup therefore produced different epub bytes, so no comparison of epub
output could tell a real change from the seconds between two builds. It
also meant an epub was a function of when it was built rather than of
what it was built from.)
(assisted by Claude-Code)
Diffstat (limited to 'org')
| -rw-r--r-- | org/out_xmls.org | 65 |
1 files changed, 62 insertions, 3 deletions
diff --git a/org/out_xmls.org b/org/out_xmls.org index 90218bd..db031e2 100644 --- a/org/out_xmls.org +++ b/org/out_xmls.org @@ -2853,9 +2853,68 @@ string _epub_publication_uuid(string _doc_uid) { #+BEGIN_SRC d /+ ↓ the last modified time, as EPUB3 requires it: exactly one dcterms:modified on the package, in UTC to the second, CCYY-MM-DDThh:mm:ssZ + . + Taken from the document rather than from the clock, so that an epub is + a function of its source and not of the moment it was built. Read in + order: SOURCE_DATE_EPOCH where the environment sets it, which is the + reproducible-builds convention and lets a packager pin every artefact + of one run; then the document's own date.modified; then + date.published. The clock is the last resort, for a document that says + nothing about when it is from. + . + Before this, two runs over unchanged markup produced different epub + bytes, so no comparison of epub output could tell a real change from + the seconds between two builds. + . + A document date is loose markup: "1991", "2006-03" and "2021-12-00" + all occur in the sample set, and a month or day of 00 is not a date. + A missing or zero part becomes 01, and a day out of range for its + month falls back to the first, so what is written is always a valid + timestamp. +/ -string _epub_modified_utc() { - import std.datetime : Clock, UTC; +string _epub_modified_utc(M)(M _meta) { + import std.datetime : Clock, UTC, SysTime, Date, unixTimeToStdTime; + import std.process : environment; + import std.regex : ctRegex, matchFirst; + string _stamp(int _y, int _mo, int _d) { + return format("%04d-%02d-%02dT00:00:00Z", _y, _mo, _d); + } + /+ ↓ a build may pin every artefact of the run +/ + string _sde = environment.get("SOURCE_DATE_EPOCH", ""); + if (_sde.length > 0) { + try { + auto _t = SysTime(unixTimeToStdTime(_sde.to!long), UTC()); + return format("%04d-%02d-%02dT%02d:%02d:%02dZ", + _t.year, _t.month.to!int, _t.day, _t.hour, _t.minute, _t.second); + } catch (Exception) { + } + } + /+ ↓ what the document itself says +/ + static auto _rgx_date + = ctRegex!(`^\s*(?P<y>\d{4})(?:[-/](?P<m>\d{1,2}))?(?:[-/](?P<d>\d{1,2}))?`); + foreach (_said; [_meta.date_modified, _meta.date_published]) { + if (_said.length == 0) { + continue; + } + if (auto _m = _said.matchFirst(_rgx_date)) { + int _y = _m["y"].to!int; + int _mo = (_m["m"].length > 0) ? _m["m"].to!int : 1; + int _d = (_m["d"].length > 0) ? _m["d"].to!int : 1; + if (_mo < 1 || _mo > 12) { + _mo = 1; + } + if (_d < 1) { + _d = 1; + } + try { + cast(void) Date(_y, _mo, _d); + } catch (Exception) { + _d = 1; + } + return _stamp(_y, _mo, _d); + } + } + /+ ↓ the document says nothing: the run's own time, as before +/ auto _t = Clock.currTime(UTC()); return format("%04d-%02d-%02dT%02d:%02d:%02dZ", _t.year, _t.month.to!int, _t.day, _t.hour, _t.minute, _t.second); @@ -2954,7 +3013,7 @@ string epub3_oebps_content(D,P)(D doc, P parts) { ? "" : xhtml_format.special_characters_plain_date(doc.matters.conf_make_meta.meta.date_published), (doc.matters.conf_make_meta.meta.rights_copyright.empty) ? "" : xhtml_format.special_characters_plain(doc.matters.conf_make_meta.meta.rights_copyright), - _epub_modified_utc(), + _epub_modified_utc(doc.matters.conf_make_meta.meta), _epub_a11y_metadata(doc), (pth_epub3.fn_oebps_css).chompPrefix("OEBPS/"), ); |
