From 9da1c5bed32310c31873e4b2c236af887013de74 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Miguel=20=C3=81ngel=20Arruga=20Vivas?=
 <rosen644835@gmail.com>
Date: Mon, 14 Dec 2020 22:21:16 +0100
Subject: [PATCH] source-date-epoch: New module.

* doc/source-date-epoch.texi: New file.
* lib/source-date-epoch.c: Likewise.
* lib/source-date-epoch.h: Likewise.
* m4/source-date-epoch.m4: Likewise.
* modules/source-date-epoch: Likewise.
---
 ChangeLog                  |   9 +++
 doc/source-date-epoch.texi |  69 ++++++++++++++++++
 lib/source-date-epoch.c    | 142 +++++++++++++++++++++++++++++++++++++
 lib/source-date-epoch.h    |  52 ++++++++++++++
 m4/source-date-epoch.m4    |  94 ++++++++++++++++++++++++
 modules/source-date-epoch  |  27 +++++++
 6 files changed, 393 insertions(+)
 create mode 100644 doc/source-date-epoch.texi
 create mode 100644 lib/source-date-epoch.c
 create mode 100644 lib/source-date-epoch.h
 create mode 100644 m4/source-date-epoch.m4
 create mode 100644 modules/source-date-epoch

diff --git a/ChangeLog b/ChangeLog
index 9f9a02160..140ccb463 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,12 @@
+2020-12-14  Miguel Ángel Arruga Vivas <rosen644835@gmail.com>
+
+	source-date-epoch: New module.
+	* doc/source-date-epoch.texi: New file.
+	* lib/source-date-epoch.c: Likewise.
+	* lib/source-date-epoch.h: Likewise.
+	* m4/source-date-epoch.m4: Likewise.
+	* modules/source-date-epoch: Likewise.
+
 2020-12-13  Paul Eggert  <eggert@cs.ucla.edu>
 
 	string: port memchr macro to AIX 7.2 XLC
diff --git a/doc/source-date-epoch.texi b/doc/source-date-epoch.texi
new file mode 100644
index 000000000..ceea3ac16
--- /dev/null
+++ b/doc/source-date-epoch.texi
@@ -0,0 +1,69 @@
+@c GNU SOURCE_DATE_EPOCH documentation
+
+@c Copyright (C) 2020 Free Software Foundation, Inc.
+
+@c Permission is granted to copy, distribute and/or modify this document
+@c under the terms of the GNU Free Documentation License, Version 1.3 or
+@c any later version published by the Free Software Foundation; with no
+@c Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.  A
+@c copy of the license is at <https://www.gnu.org/licenses/fdl-1.3.en.html>.
+
+@node Reproducible timestamps
+@section SOURCE_DATE_EPOCH
+
+The module @samp{source-date-epoch} provides an interface to the
+enironment variable
+@url{https://reproducible-builds.org/specs/source-date-epoch/,SOURCE_DATE_EPOCH}
+which allows the user to control the timestamps introduced by the
+application on the generated output, thus giving first-class status as
+an input to these timestamps.  Examples of such output are binary
+objects compiled from the source code, but also parsers generated from a
+grammar description, or files of symbols or strings extracted from the
+source code.
+
+To enable it, just include the header and substitute the appropiate time
+calls with the function source_date_epoch_time handling the error return
+at the application level.
+@smallexample
+#include "source-date-epoch.h"
+...
+void
+foo (void)
+{
+  time_t now;
+  switch (source_date_epoch_time (&now))
+    {
+    case 0:
+      return now;
+    case 1:
+      /* Oops, SOURCE_DATE_EPOCH is defined but not with a correct
+         value, we should notify the user.  */
+    case -1:
+      /* SOURCE_DATE_EPOCH isn't defined, call time.  */
+      now = time (&now);
+      break;
+    }
+  return time (now);
+}
+@end smallexample
+
+This functionality shouldn't be used for log or interactive messages
+shown to the user, therefore the wrapper for the time function isn't
+provided by default.  Instead, calls to time can be replaced with
+source_date_epoch_time_rpl and any error related to SOURCE_DATE_EPOCH
+will be ignored.
+
+The macro @code{REPLACE_TIME_WITH_SOURCE_DATE_EPOCH} enables the
+replacement of time.  The header must be included after <time.h> or
+substitute it completely to avoid any issue.
+@smallexample
+#define REPLACE_TIME_WITH_SOURCE_DATE_EPOCH 1
+#include "source-date-epoch.h"
+...
+   time_t now = time (NULL);
+@end smallexample
+
+Please, note that this may affect other calls not used for the output,
+so this isn't compatible neither with source units where the time
+function is used for other means as logging or profiling, nor when the
+application wants to notify the user of the different behavior in place.
diff --git a/lib/source-date-epoch.c b/lib/source-date-epoch.c
new file mode 100644
index 000000000..642804e82
--- /dev/null
+++ b/lib/source-date-epoch.c
@@ -0,0 +1,142 @@
+/* Support for SOURCE_DATE_EPOCH environment variable.
+
+   Copyright (C) 2020 Free Software Foundation, Inc.
+
+   Written by Miguel Ángel Arruga Vivas <rosen644835@gmail.com>.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "source-date-epoch.h"
+
+#include <errno.h>
+#include <string.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+/* time_t has to be an arithmetic (real since C11) type, therefore it
+   must be assignable, with an optional conversion factor, from a 64
+   bit value.  */
+#ifdef SCALE_FACTOR_SECONDS_TO_TIME_T
+#define SCALE_TO_TIME_T(x) (x * SCALE_FACTOR_SECONDS_TO_TIME_T)
+#define SCALE_FROM_TIME_T(x) (x / SCALE_FACTOR_SECONDS_TO_TIME_T)
+#else
+#define SCALE_TO_TIME_T(x) x
+#define SCALE_FROM_TIME_T(x) x
+#endif
+
+#ifndef TIME_T_IS_INTEGRAL_TYPE
+/* time_t can be a floating point type; assume +/- 2^64 seconds in
+   that case.  Only scale the value, as the displacement may involve a
+   function call if the difference with UTC may change depending on
+   the timezone.  */
+#define TIME_T_MAX ((time_t) SCALE_TO_TIME_T (((1ULL << 63) - 1) * 2 + 1))
+#define TIME_T_MIN (- TIME_T_MAX)
+#else /* TIME_T_IS_INTEGRAL_TYPE */
+#ifdef TIME_T_IS_SIGNED
+#include <limits.h>
+/* From mktime.m4.  */
+#define TIME_T_MAX \
+  ((((time_t) 1 << (sizeof (time_t) * CHAR_BIT - 2)) - 1) * 2 + 1)
+#define TIME_T_MIN \
+  (((time_t) ~ (time_t) 0 < (time_t) -1) ? ~ (time_t) 0 : ~ TIME_T_MAX)
+#else /* !defined (TIME_T_IS_SIGNED) */
+#define TIME_T_MAX ((time_t) -1)
+#define TIME_T_MIN ((time_t) 0)
+#endif /* !defined (TIME_T_IS_SIGNED) */
+#endif /* TIME_T_IS_INTEGRAL_TYPE */
+
+#ifdef DISPLACEMENT_UNIX_EPOCH_TO_TIME_T
+#ifdef TIME_T_DEPENDS_ON_TZ
+
+/* Calculate at runtime the displacement from UTC.  */
+static int_least64_t
+calculate_displacement_from_utc (void)
+{
+  struct tm zero_time;
+  time_t zero = 0;
+
+  zero_time = gmtime (&zero);
+  /* TODO: Too much right now, steal difftm later.  */
+  if (zero_time.tm_year == 70 || zero_time.tm_year == 69)
+    {
+      return ((365 * 24 * 60 * 60 * (70 - zero_time.tm_year))
+	      + ((69 - zero_time.tm_year)
+		 * (zero_time.tm_sec
+		    + (60 * zero_time.tm_min
+		       + (60 * zero_time.tm_hour
+			  + (24 * zero_time.tm_yday))))));
+    }
+  abort ();
+}
+#define DISPLACE_TO_UNIX_EPOCH(x) (x + calculate_displacement_from_utc ())
+#define DISPLACE_FROM_UNIX_EPOCH(x) (x - calculate_displacement_from_utc ())
+#else /* The displacement can be statically calculated.  */
+#define DISPLACE_TO_UNIX_EPOCH(x) (x + DISPLACEMENT_UNIX_EPOCH_TO_TIME_T)
+#define DISPLACE_FROM_UNIX_EPOCH(x) (x - DISPLACEMENT_UNIX_EPOCH_TO_TIME_T)
+#endif
+#else
+#define DISPLACE_TO_UNIX_EPOCH(x) x
+#define DISPLACE_FROM_UNIX_EPOCH(x) x
+#endif
+
+/* XXX: Perhaps strtoll is always ok?  */
+#ifdef LONG_HAS_64_BITS
+#define STRING_TO_INT_LEAST64_T strtol
+#else
+#define STRING_TO_INT_LEAST64_T strtoll
+#endif
+#define SECONDS_FROM_UNIX_EPOCH_TO_TIME_T(s) \
+  ((time_t) DISPLACE_TO_UNIX_EPOCH (SCALE_TO_TIME_T (s)))
+#define TIME_T_TO_SECONDS_FROM_UNIX_EPOCH(s) \
+  ((int_least64_t) (SCALE_FROM_TIME_T (DISPLACE_FROM_UNIX_EPOCH (s))))
+
+int
+source_date_epoch_time (time_t *tp)
+{
+  char *end;
+  int_least64_t value;
+  const char *source_date_epoch = getenv ("SOURCE_DATE_EPOCH");
+
+  if (!source_date_epoch)
+    return -1;
+
+  errno = 0;
+  value = STRING_TO_INT_LEAST64_T (source_date_epoch, &end, 10);
+
+  if (errno != 0 || end == source_date_epoch || *end != '\0'
+      || value > TIME_T_TO_SECONDS_FROM_UNIX_EPOCH (TIME_T_MAX)
+      || value < TIME_T_TO_SECONDS_FROM_UNIX_EPOCH (TIME_T_MIN))
+    return 1;
+
+  *tp = SECONDS_FROM_UNIX_EPOCH_TO_TIME_T (value);
+  return 0;
+}
+
+#ifdef time
+#undef time
+#endif
+
+time_t
+source_date_epoch_time_rpl (time_t *tp)
+{
+  time_t stack;
+  time_t *ptr = tp ? tp : &stack;
+  if (source_date_epoch_time (ptr) == 0)
+    return *ptr;
+  return time (ptr);
+}
diff --git a/lib/source-date-epoch.h b/lib/source-date-epoch.h
new file mode 100644
index 000000000..1f0056cd9
--- /dev/null
+++ b/lib/source-date-epoch.h
@@ -0,0 +1,52 @@
+/* Support for SOURCE_DATE_EPOCH environment variable.
+
+   Copyright (C) 2020 Free Software Foundation, Inc.
+
+   Written by Miguel Ángel Arruga Vivas <rosen644835@gmail.com>.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
+
+#ifndef SOURCE_DATE_EPOCH_H
+#define SOURCE_DATE_EPOCH_H
+
+#include <time.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Read the environment variable SOURCE_DATE_EPOCH into the provided
+   time_t object.  Return 0 when the value has been read successfully,
+   -1 when the variable isn't defined or 1 when the variable is
+   defined with an invalid value.
+
+   See https://reproducible-builds.org/specs/source-date-epoch/  */
+int source_date_epoch_time (time_t *);
+
+#ifdef REPLACE_TIME_WITH_SOURCE_DATE_EPOCH
+# ifdef time /* If it's already a macro, remove it.  */
+#   undef time
+# endif
+# define time source_date_epoch_time_rpl
+#endif
+
+/* Fallback to calling time when SOURCE_DATE_EPOCH isn't defined or
+   its value isn't valid.  */
+time_t source_date_epoch_time_rpl (time_t *);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/m4/source-date-epoch.m4 b/m4/source-date-epoch.m4
new file mode 100644
index 000000000..131b6aec4
--- /dev/null
+++ b/m4/source-date-epoch.m4
@@ -0,0 +1,94 @@
+# source-date-epoch.m4 serial 1
+dnl Copyright (C) 2020 Free Software Foundation, Inc.
+dnl This file is free software; the Free Software Foundation
+dnl gives unlimited permission to copy and/or distribute it,
+dnl with or without modifications, as long as this notice is preserved.
+
+AC_DEFUN([gl_LONG_HAS_64_BITS],
+[
+  AC_CACHE_CHECK([whether long has 64 bits],
+    [gl_cv_long_has_64_bits],
+    [AC_COMPILE_IFELSE(
+       [AC_LANG_PROGRAM([[#include <limits.h> /* For CHAR_BIT.  */
+char time_t_conv[((sizeof (long) * CHAR_BIT) >= 64) ? 1 : -1];]])],
+       [gl_cv_long_has_64_bits=yes],
+       [gl_cv_long_has_64_bits=no])])
+  if test $gl_cv_long_has_64_bits = yes; then
+    AC_DEFINE([LONG_HAS_64_BITS], [1],
+              [Whether long has, at least, 64 bits of size])
+  fi
+])
+
+AC_DEFUN([gl_TIME_T_IS_INTEGRAL_TYPE],
+[
+  AC_CACHE_CHECK([whether time_t is an integral type],
+    [gl_cv_time_t_is_integral_type],
+    [AC_COMPILE_IFELSE(
+       [AC_LANG_PROGRAM([[#include <time.h> /* For time_t.  */
+char time_t_integral[((time_t) 1.0 == (time_t) 1.25) ? 1 : -1];]])],
+       [gl_cv_time_t_is_integral_type=yes],
+       [gl_cv_time_t_is_integral_type=no])])
+  if test $gl_cv_time_t_is_integral_type = yes; then
+    AC_DEFINE([TIME_T_IS_INTEGRAL_TYPE], [1],
+              [Whether time_t is an integral type])
+  fi
+])
+
+AC_DEFUN([gl_UNIX_EPOCH_COMPATIBILITY_WITH_TIME_T],
+[
+  AC_REQUIRE([AC_CANONICAL_HOST]) dnl for cross-compiles
+  AC_REQUIRE([gl_MULTIARCH])
+  AC_CACHE_CHECK([whether time_t has unix epoch],
+    [gl_cv_time_t_has_unix_epoch],
+    [if test "x$gl_cv_func_working_mktime" != xyes; then
+       # XXX: Perhaps this check could be worked around.
+       gl_cv_time_t_has_unix_epoch=no
+     else
+       AC_RUN_IFELSE(
+         [AC_LANG_SOURCE(
+[[/* Test program from Miguel Ángel Arruga Vivas.  */
+#include <stdlib.h> /* putenv, EXIT_SUCCESS, EXIT_FAILURE  */
+#include <string.h> /* memset  */
+#include <time.h>   /* time_t, mktime  */
+]GL_MDA_DEFINES[
+int
+main (int argc, char **argv)
+{
+  time_t utc_time;
+  struct tm utc_epoch;
+
+  /* Ensure we are using the right timezone.  */
+  putenv ("TZ=GMT0");
+
+  /* Desired epoch: t0 = 1970-01-01 00:00:00+0000.  */
+  memset (&utc_epoch, 0, sizeof (utc_epoch));
+  utc_epoch.tm_year = 70; /* Displacement is 1900.  */
+  utc_epoch.tm_mday = 1; /* First DOM is 1 unlike other values.  */
+
+  utc_time = mktime (&utc_epoch);
+  return (utc_time == 0 ? EXIT_SUCCESS : EXIT_FAILURE);
+}]])],
+         [gl_cv_time_t_has_unix_epoch=yes],
+         [gl_cv_time_t_has_unix_epoch=no]
+         [gl_cv_time_t_has_unix_epoch="$gl_cross_guess_normal"])
+     fi
+  ])
+
+  if test "x$gl_cv_time_t_has_unix_epoch" != xyes; then
+    # TODO: Extract the actual conversion factors
+    AC_MSG_WARN([time_t doesn't have unix epoch!])
+    AC_DEFINE([SCALE_FACTOR_SECONDS_TO_TIME_T], [1],
+              [TODO: Scale factor from seconds to time_t units.])
+    AC_DEFINE([DISPLACEMENT_UNIX_EPOCH_TO_TIME_T], [0],
+              [TODO: Displacement from unix epoch to time_t.])
+    AC_DEFINE([TIME_T_DEPENDS_ON_TZ], [1],
+              [TODO: Whether the displacement must be calculated at runtime.])
+  fi
+])
+
+AC_DEFUN([gl_SOURCE_DATE_EPOCH],
+[
+  AC_REQUIRE([gl_LONG_HAS_64_BITS])
+  AC_REQUIRE([gl_TIME_T_IS_INTEGRAL_TYPE])
+  AC_REQUIRE([gl_UNIX_EPOCH_COMPATIBILITY_WITH_TIME_T])
+])
diff --git a/modules/source-date-epoch b/modules/source-date-epoch
new file mode 100644
index 000000000..1e9e96fa4
--- /dev/null
+++ b/modules/source-date-epoch
@@ -0,0 +1,27 @@
+Description:
+Support for SOURCE_DATE_EPOCH environment variable.
+
+Files:
+lib/source-date-epoch.c
+lib/source-date-epoch.h
+m4/source-date-epoch.m4
+
+Depends-on:
+c99
+errno
+mktime
+
+configure.ac:
+gl_SOURCE_DATE_EPOCH
+
+Makefile.am:
+lib_SOURCES += source-date-epoch.c
+
+Include:
+"source-date-epoch.h"
+
+License:
+GPL
+
+Maintainer:
+Miguel Ángel Arruga Vivas, gettext
-- 
2.29.2

