aboutsummaryrefslogtreecommitdiffhomepage
path: root/org
diff options
context:
space:
mode:
authorRalph Amissah <ralph.amissah@gmail.com>2026-09-21 00:02:21 -0400
committerRalph Amissah <ralph.amissah@gmail.com>2026-09-22 12:50:33 -0400
commit67d27829788d49eab208d871cf4dfc95c5f3ba6e (patch)
tree0d19f03b18c1dbf393e5c429de8e45e4397ab1e2 /org
parentconf: 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.org65
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/"),
);