Hello,

Working on gettext I came up with the module named on the subject, which
provides an interface for the translation of the environment variable
SOURCE_DATE_EPOCH to a time_t object, based on the current standard[1].

As suggested by Bruno, I moved this work to the frame of gnulib, so I
added documentation for the gnulib manual and extended the configuration
macros a bit.  The macros for displacement and scaling may be
over-engineered so I didn't advance much on their implementation, I
couldn't find any implementation where time_t depends on the timezone
and the only implementation I found with a different epoch is __time32_t
for TI processors, which uses 1900.  Any comment or pointer about these
issues and related portability quirks will be gladly welcome.

There is another issue I should point out in advance:

Bruno Haible <[email protected]> writes:

> You will need to file a copyright assignment with the FSF for gnulib,
> like you have one already for gettext.

I'd sign the copyright assignment for gnulib without any fuss, but the
disclaimer signed by my employer doesn't include it explicitly, unlike
gettext.  This document was signed after a long negotiation which ended
with a closed list of projects as a compromise, as they saw it
irrelevant regarding Spanish law---to which I could agree, as there is
no exclusivity clause on my contract, unlike when I was employed on
Germany---because I don't/must not work on any Free Software project
during my working hours.  I've already started the negotiation for its
modification though, but I'm currently on holiday leave, as almost
everybody else on the company, therefore the new disclaimer won't be
available before January.

Could you confirm with the legal people what should be the way forward?

Best regards,
Miguel

[1] https://reproducible-builds.org/specs/source-date-epoch/
From e3906c72a9fe298b21a9147ebb765600ceecc593 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Miguel=20=C3=81ngel=20Arruga=20Vivas?=
 <[email protected]>
Date: Mon, 21 Dec 2020 12:06:21 +0100
Subject: [PATCH] source-date-epoch: New module.

* ChangeLog: Update changes.
* doc/gnulib.texi: Add Reproducible timestamps node.
* 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                  |  10 +++
 doc/gnulib.texi            |   2 +
 doc/source-date-epoch.texi | 110 +++++++++++++++++++++++++++
 lib/source-date-epoch.c    | 148 +++++++++++++++++++++++++++++++++++++
 lib/source-date-epoch.h    |  52 +++++++++++++
 m4/source-date-epoch.m4    |  94 +++++++++++++++++++++++
 modules/source-date-epoch  |  27 +++++++
 7 files changed, 443 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 8bf1adf93..453ad7011 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,13 @@
+2020-12-22  Miguel Ángel Arruga Vivas <[email protected]>
+
+	source-date-epoch: New module.
+	* doc/gnulib.texi: Add Reproducible timestamps node.
+	* 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-20  Bruno Haible  <[email protected]>
 
 	Remove support for broken <wchar.h> in AIX 3.
diff --git a/doc/gnulib.texi b/doc/gnulib.texi
index d646d6de1..6452ccfe1 100644
--- a/doc/gnulib.texi
+++ b/doc/gnulib.texi
@@ -6769,6 +6769,7 @@ to POSIX that it can be treated like any other Unix-like platform.
 * Supporting Relocation::
 * func::
 * stat-size::
+* Reproducible timestamps::
 @end menu
 
 @node alloca
@@ -6819,6 +6820,7 @@ to POSIX that it can be treated like any other Unix-like platform.
 
 @include stat-size.texi
 
+@include source-date-epoch.texi
 
 @node Regular expressions
 @chapter Regular expressions
diff --git a/doc/source-date-epoch.texi b/doc/source-date-epoch.texi
new file mode 100644
index 000000000..e4921ebef
--- /dev/null
+++ b/doc/source-date-epoch.texi
@@ -0,0 +1,110 @@
+@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 handling
+
+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 source code, but also parsers generated from
+grammar descriptions, or files of symbols and/or strings extracted from
+source code.
+
+The header @file{source-date-epoch.h} provides the following
+functionality:
+
+@menu
+* source_date_epoch_time::      Interface for SOURCE_DATE_EPOCH.
+* source_date_epoch_time_rpl::  Replacement for time function.
+@end menu
+
+@node source_date_epoch_time
+@subsection source_date_epoch_time
+@deftypefun {int} source_date_epoch_time (time_t@tie{}*@var{tp})
+
+This function parses @env{SOURCE_DATE_EPOCH} and uses its value to
+generate a valid @code{time_t} object.  It's undefined behavior to
+provide an invalid pointer through @var{tp}.
+
+The function may signal these three options through its return code:
+@itemize
+@item @code{0}:
+The environment variable @env{SOURCE_DATE_EPOCH} is defined and contain
+a number of seconds since January 1st, 1970 GMT0 representable by the
+domain of @code{time_t} type.  This value is stored into *@var{tp}.
+@item @code{1}:
+The environment variable @env{SOURCE_DATE_EPOCH} is defined but its
+value isn't valid.  The errno value from @code{strtol}-like functions is
+preserved through the return, but no value is written to *@var{tp}.
+@item @code{-1}:
+The environment variable @env{SOURCE_DATE_EPOCH} isn't defined.  No
+value is written to *@var{tp}.
+@end itemize
+
+The following example shows the basic error handling proposed by the
+standard:
+
+@example
+  time_t now;
+  int result = source_date_epoch_time (&now);
+
+  /* A switch can be used too.  */
+  if (result == 0)
+    /* SOURCE_DATE_EPOCH contains a valid value.  */
+  else if (result == 1)
+    /* Oops, SOURCE_DATE_EPOCH is defined but not with a correct
+       value, we should notify the user.  */
+    exit (EXIT_FAILURE);
+  else if (result == 1)
+    /* SOURCE_DATE_EPOCH isn't defined, call time.  */
+    now = time (&now);
+
+  /* The value of now can be used as usual.  */
+@end example
+@end deftypefun
+
+@node source_date_epoch_time_rpl
+@subsection source_date_epoch_time_rpl
+@deftypefun {time_t} source_date_epoch_time_rpl (time_t@tie{}*@var{tp})
+
+This function is a replacement for the function @code{time}.  It uses
+the value provided by @code{source_date_epoch_time}
+(@pxref{source_date_epoch_time}) or falls back to the actual @code{time}
+function when the returned value isn't @code{0}, ignoring any error
+related to the variable value.  As mandated by the standard, the
+behavior of this function is defined when a @code{NULL} pointer is
+provided by @var{tp}.
+
+@cindex REPLACE_TIME_WITH_SOURCE_DATE_EPOCH
+The following example uses the macro
+@code{REPLACE_TIME_WITH_SOURCE_DATE_EPOCH} to replace @code{time}
+function calls with calls to @code{source_date_epoch_time_rpl} through a
+macro.  For this reason, the inclusion of @file{source-date-epoch.h}
+must be included after any @file{time.h} standard header replacement to
+avoid conflicting declarations.
+
+@example
+#define REPLACE_TIME_WITH_SOURCE_DATE_EPOCH
+#include "source-date-epoch.h"
+
+/* No change needed on the calling site.  */
+time_t now = time (NULL);
+@end example
+
+Be aware that this affects every call on the same source file, so this
+isn't compatible neither when that function is used on that source unit
+for other means, such as logging or profiling, nor when the application
+wants to notify the user of the different behavior taking place and/or
+errors of the environment variable value.
+@end deftypefun
diff --git a/lib/source-date-epoch.c b/lib/source-date-epoch.c
new file mode 100644
index 000000000..473195ea3
--- /dev/null
+++ b/lib/source-date-epoch.c
@@ -0,0 +1,148 @@
+/* Support for SOURCE_DATE_EPOCH environment variable.
+
+   Copyright (C) 2020 Free Software Foundation, Inc.
+
+   Written by Miguel Ángel Arruga Vivas <[email protected]>.
+
+   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 /* !DISPLACEMENT_UNIX_EPOCH_TO_TIME_T  */
+#define DISPLACE_TO_UNIX_EPOCH(x) x
+#define DISPLACE_FROM_UNIX_EPOCH(x) x
+#endif
+
+#ifdef TIME_T_IS_SIGNED
+typedef int_least64_t seconds_type;
+#ifdef LONG_HAS_64_BITS
+#define STRING_TO_NUMBER strtol
+#else
+#define STRING_TO_NUMBER strtoll
+#endif
+#else /* !TIME_T_IS_SIGNED  */
+typedef uint_least64_t seconds_type;
+#ifdef LONG_HAS_64_BITS
+#define STRING_TO_NUMBER strtoul
+#else
+#define STRING_TO_NUMBER strtoull
+#endif
+#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) \
+  ((seconds_type) (SCALE_FROM_TIME_T (DISPLACE_FROM_UNIX_EPOCH (s))))
+
+int
+source_date_epoch_time (time_t *tp)
+{
+  char *end;
+  seconds_type value;
+  const char *source_date_epoch = getenv ("SOURCE_DATE_EPOCH");
+
+  if (!source_date_epoch)
+    return -1;
+
+  errno = 0;
+  value = STRING_TO_NUMBER (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;
+}
+
+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 <[email protected]>.
+
+   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

base-commit: 87dc278345db394227f281c831a3fafb0b7854fb
-- 
2.29.2

Attachment: signature.asc
Description: PGP signature

Reply via email to