commit: 7f10d23fd7b1ab10be7a36ad26ec02bdc942b4b8
parent 49fc0e9fd47588e9922a91e8938977fafa0a3437
Author: Haelwenn (lanodan) Monnier <contact@hacktivis.me>
Date: Sun, 11 Jan 2026 12:47:15 +0100
libutils/datetime_parse: fix dealing with timezone offsets
Fixes: https://todo.sr.ht/~lanodan/utils-std/7
Diffstat:
5 files changed, 106 insertions(+), 140 deletions(-)
diff --git a/cmd/date.c b/cmd/date.c
@@ -2,7 +2,7 @@
// SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
// SPDX-License-Identifier: MPL-2.0
-#define _DEFAULT_SOURCE // tm_gmtoff/tm_zone
+#define _DEFAULT_SOURCE // tm_gmtoff/tm_zone (POSIX.1-2024), timegm (mktime() future directions)
#define _POSIX_C_SOURCE 200809L
#define _XOPEN_SOURCE 700 // strptime is in XSI
@@ -137,17 +137,6 @@ int
main(int argc, char *argv[])
{
char outstr[BUFSIZ] = "";
- struct tm tm = {
- .tm_year = 0,
- .tm_mon = 0,
- .tm_mday = 0,
- .tm_hour = 0,
- .tm_min = 0,
- .tm_sec = 0,
- .tm_isdst = -1, // unknown if DST is in effect
- .tm_gmtoff = 0,
- .tm_zone = NULL,
- };
struct timespec tp = {
.tv_sec = 0,
.tv_nsec = 0,
@@ -210,22 +199,13 @@ main(int argc, char *argv[])
return 1;
}
- datetime_parse(optarg, &tm, &tp.tv_nsec, &errstr);
+ datetime_parse(optarg, &tp.tv_sec, &tp.tv_nsec, &errstr);
dflag = 1;
if(errstr != NULL)
{
fprintf(stderr, "%s: error: datetime_parse(\"%s\", …): %s\n", argv0, optarg, errstr);
return 1;
}
-
- tp.tv_sec = mktime_tz(&tm);
- if(tp.tv_sec == (time_t)-1)
- {
- fprintf(stderr, "%s: error: mktime: %s\n", argv0, strerror(errno));
- return 1;
- }
- errno = 0;
-
break;
case 'f': /* input datetime format */
if(dflag == 1)
@@ -322,26 +302,10 @@ main(int argc, char *argv[])
argc -= optind;
argv += optind;
- if(uflag)
- {
- if(gmtime_r(&tp.tv_sec, &tm) == NULL)
- {
- fprintf(stderr, "%s: error: gmtime_r: %s\n", argv0, strerror(errno));
- return 1;
- }
- }
- else
- {
- if(localtime_r(&tp.tv_sec, &tm) == NULL)
- {
- fprintf(stderr, "%s: error: localtime_r: %s\n", argv0, strerror(errno));
- return 1;
- }
- }
-
if(argc > 0 && input_format != NULL)
{
- char *res = strptime(argv[0], input_format, &tm);
+ struct tm input_tm;
+ char *res = strptime(argv[0], input_format, &input_tm);
if(res == NULL)
{
@@ -353,11 +317,12 @@ main(int argc, char *argv[])
return 1;
}
- tp.tv_sec = mktime_tz(&tm);
+ /* TODO: %N (nanoseconds) support */
+ tp.tv_sec = timegm(&input_tm);
tp.tv_nsec = 0;
if(tp.tv_sec == (time_t)-1)
{
- fprintf(stderr, "%s: error: mktime: %s\n", argv0, strerror(errno));
+ fprintf(stderr, "%s: error: timegm: %s\n", argv0, strerror(errno));
return 1;
}
errno = 0;
@@ -368,8 +333,17 @@ main(int argc, char *argv[])
if(input_format == NULL && argc > 0 && *argv && **argv != '+')
{
+ struct tm arg_tm;
const char *fmt = "%m%d%H%M";
- char *s = strptime(argv[0], fmt, &tm);
+
+ int year = 0;
+ int eyear = 0;
+ if(gmtime_r(&tp.tv_sec, &arg_tm) == NULL)
+ eyear = errno;
+ else
+ year = arg_tm.tm_year;
+
+ char *s = strptime(argv[0], fmt, &arg_tm);
if(s == NULL)
{
fprintf(stderr, "%s: error: strptime(\"%s\", \"%s\", …) returned NULL\n", argv0, *argv, fmt);
@@ -396,7 +370,7 @@ main(int argc, char *argv[])
return 1;
}
- s = strptime(s, fmt, &tm);
+ s = strptime(s, fmt, &arg_tm);
if(s == NULL)
{
fprintf(
@@ -404,12 +378,22 @@ main(int argc, char *argv[])
return 1;
}
}
+ else if(eyear != 0)
+ {
+ fprintf(stderr,
+ "%s: error: Year wasn't specified in '%s' and failed to get current time: %s\n",
+ argv0,
+ *argv,
+ strerror(eyear));
+ return 1;
+ }
- tp.tv_sec = mktime(&tm);
+ /* TODO: %N (nanoseconds) support */
+ tp.tv_sec = timegm(&arg_tm);
tp.tv_nsec = 0;
if(tp.tv_sec == (time_t)-1)
{
- fprintf(stderr, "%s: error: mktime: %s\n", argv0, strerror(errno));
+ fprintf(stderr, "%s: error: timegm: %s\n", argv0, strerror(errno));
return 1;
}
errno = 0;
@@ -419,6 +403,24 @@ main(int argc, char *argv[])
settime = true;
}
+ struct tm tm;
+ if(uflag)
+ {
+ if(gmtime_r(&tp.tv_sec, &tm) == NULL)
+ {
+ fprintf(stderr, "%s: error: gmtime_r: %s\n", argv0, strerror(errno));
+ return 1;
+ }
+ }
+ else
+ {
+ if(localtime_r(&tp.tv_sec, &tm) == NULL)
+ {
+ fprintf(stderr, "%s: error: localtime_r: %s\n", argv0, strerror(errno));
+ return 1;
+ }
+ }
+
if(settime && !jflag)
{
if(clock_settime(CLOCK_REALTIME, &tp) != 0)
diff --git a/cmd/touch.c b/cmd/touch.c
@@ -195,19 +195,8 @@ main(int argc, char *argv[])
break;
case 'd':
{
- long nsec = 0;
- struct tm iso_res = {
- .tm_year = 0,
- .tm_mon = 0,
- .tm_mday = 0,
- .tm_hour = 0,
- .tm_min = 0,
- .tm_sec = 0,
- .tm_isdst = -1, // unknown if DST is in effect
- .tm_gmtoff = 0,
- .tm_zone = NULL,
- };
- char *s = datetime_parse(optarg, &iso_res, &nsec, &errstr);
+
+ char *s = datetime_parse(optarg, &target.tv_sec, &target.tv_nsec, &errstr);
if(errstr != NULL)
{
fprintf(stderr, "touch: error: datetime_parse(\"%s\", …): %s\n", optarg, errstr);
@@ -218,15 +207,6 @@ main(int argc, char *argv[])
fprintf(stderr, "touch: error: datetime_parse(\"%s\", …) returned NULL\n", optarg);
return 1;
}
-
- target.tv_sec = mktime_tz(&iso_res);
- target.tv_nsec = nsec;
- if(target.tv_sec == (time_t)-1)
- {
- fprintf(stderr, "touch: error: mktime: %s\n", strerror(errno));
- return 1;
- }
- errno = 0;
break;
}
case ':':
diff --git a/libutils/datetime_parse.c b/libutils/datetime_parse.c
@@ -2,8 +2,8 @@
// SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
// SPDX-License-Identifier: MPL-2.0
-#define _DEFAULT_SOURCE // tm_gmtoff/tm_zone
-#define _XOPEN_SOURCE 700 // strptime (NetBSD)
+#define _DEFAULT_SOURCE // tm_gmtoff/tm_zone, timegm (POSIX.1-2024 mktime() future directions)
+#define _XOPEN_SOURCE 700 // strptime (NetBSD)
#define _POSIX_C_SOURCE 200809L // st_atim/st_mtim
#include "./datetime_parse.h"
@@ -27,14 +27,6 @@ static const char short_month_name[12][3] = {
static char *
tzoffset_parse(char *s, struct tm *time, const char **errstr)
{
-#ifndef TZNAME_MAX
-#define TZNAME_MAX _POSIX_TZNAME_MAX
-#endif
-#if TZNAME_MAX < 5
-#error TZNAME_MAX is too small
-#endif
- static char offname[TZNAME_MAX + 1] = "";
-
int neg;
if(s[0] == '+')
neg = 0;
@@ -46,14 +38,12 @@ tzoffset_parse(char *s, struct tm *time, const char **errstr)
return NULL;
}
- size_t offname_i = 0;
- offname[offname_i++] = *s++;
+ s++;
if(isdigit(s[0]) && isdigit(s[1]))
{
time->tm_gmtoff = (s[0] - '0') * 36000 + (s[1] - '0') * 3600;
- offname[offname_i++] = *s++;
- offname[offname_i++] = *s++;
+ s += 2;
}
else
{
@@ -66,8 +56,6 @@ tzoffset_parse(char *s, struct tm *time, const char **errstr)
if(isdigit(s[0]) && isdigit(s[1]))
{
time->tm_gmtoff += (s[0] - '0') * 600 + (s[1] - '0') * 60;
- offname[offname_i++] = *s++;
- offname[offname_i++] = *s++;
}
else
{
@@ -77,14 +65,25 @@ tzoffset_parse(char *s, struct tm *time, const char **errstr)
if(neg) time->tm_gmtoff = -time->tm_gmtoff;
- offname[offname_i++] = '\0';
-
time->tm_isdst = 0;
- time->tm_zone = offname;
+ time->tm_zone = NULL;
return s;
}
+// Wrap timegm() to still get portable behavior while setting tm_gmtoff directly
+static time_t
+utils_timegm(struct tm *tm)
+{
+ long gmtoff = tm->tm_gmtoff;
+ tm->tm_isdst = 0;
+ tm->tm_gmtoff = 0;
+ tm->tm_zone = "UTC";
+
+ time_t ret = timegm(tm);
+ return ret - gmtoff;
+}
+
// Sets *errstr to NULL when it isn't an email date-time
//
// Check if it could be Email / Internet Message Format datetime
@@ -93,7 +92,7 @@ tzoffset_parse(char *s, struct tm *time, const char **errstr)
//
// RFC5322 and RFC2822 (no obs): "([ ]*Day,)[ ]*DD[ ]+Mon[ ]+YYYY[ ]+HH:MM(:SS)?[ ]+[+/-]hhmm"
static char *
-email_datetime_parse(char *arg, struct tm *time, const char **errstr)
+email_datetime_parse(char *arg, time_t *epoch, const char **errstr)
{
// Kept free of strptime() due to update/overriding being undefined and
// requiring custom parsing, notably locale-free, which strptime() can't handle
@@ -102,9 +101,10 @@ email_datetime_parse(char *arg, struct tm *time, const char **errstr)
;
// Change `time` only right before returning in case datetime is invalid
- struct tm tmp_time = *time;
- tmp_time.tm_isdst = -1;
- tmp_time.tm_wday = -1;
+ struct tm tmp_time = {
+ .tm_isdst = -1,
+ .tm_wday = -1,
+ };
if(arg[3] == ',')
{
@@ -222,7 +222,7 @@ email_datetime_parse(char *arg, struct tm *time, const char **errstr)
return NULL;
}
- memcpy(time, &tmp_time, sizeof(tmp_time));
+ *epoch = utils_timegm(&tmp_time);
return arg;
}
@@ -230,15 +230,15 @@ email_datetime_parse(char *arg, struct tm *time, const char **errstr)
//
// Check if it could be asctime() format: Thu Nov 24 18:22:48 1986
static char *
-asctime_datetime_parse(char *arg, struct tm *time, const char **errstr)
+asctime_datetime_parse(char *arg, time_t *epoch, const char **errstr)
{
// Kept free of strptime() due to update/overriding being undefined and
// requiring custom parsing, notably locale-free, which strptime() can't handle
- // Change `time` only right before returning in case datetime is invalid
- struct tm tmp_time = *time;
- tmp_time.tm_isdst = -1;
- tmp_time.tm_wday = -1;
+ struct tm tmp_time = {
+ .tm_isdst = -1,
+ .tm_wday = -1,
+ };
// asctime() doesn't gives any timezone information, assume UTC
tmp_time.tm_isdst = 0;
@@ -313,22 +313,26 @@ asctime_datetime_parse(char *arg, struct tm *time, const char **errstr)
for(; isspace(arg[0]); arg++)
;
- memcpy(time, &tmp_time, sizeof(tmp_time));
+ *epoch = utils_timegm(&tmp_time);
return arg;
}
// Sets errstr on failure
// YYYY-MM-DD[T ]hh:mm:SS([,\.]frac)?(Z|[+\-]hh:?mm)?
static char *
-iso_datetime_parse(char *arg, struct tm *time, long *nsec, const char **errstr)
+iso_datetime_parse(char *arg, time_t *epoch, long *nsec, const char **errstr)
{
// Try parsing as RFC3339 subset of ISO 8601:1988
+ struct tm tmp_time = {
+ .tm_isdst = -1,
+ .tm_wday = -1,
+ };
// FIXME?: Calling strptime() multiple times is explicitly unspecified in POSIX.1-2024
// instead a single strptime() call should be done
// No %F in POSIX prior to POSIX.1-2024 (<https://www.austingroupbugs.net/view.php?id=920>)
- char *s = strptime(arg, "%Y-%m-%d", time);
+ char *s = strptime(arg, "%Y-%m-%d", &tmp_time);
if(s == NULL)
{
@@ -347,7 +351,7 @@ iso_datetime_parse(char *arg, struct tm *time, long *nsec, const char **errstr)
for(; isspace(s[0]); s++)
;
- s = strptime(s, "%H:%M:%S", time);
+ s = strptime(s, "%H:%M:%S", &tmp_time);
if(s == NULL)
{
*errstr = "strptime(…, \"%H:%M:%S\", …) returned NULL";
@@ -394,23 +398,24 @@ iso_datetime_parse(char *arg, struct tm *time, long *nsec, const char **errstr)
{
if(s[0] == 'Z' && s[1] == '\0')
{
- time->tm_isdst = 0;
- time->tm_gmtoff = 0;
- time->tm_zone = "UTC";
+ tmp_time.tm_isdst = 0;
+ tmp_time.tm_gmtoff = 0;
+ tmp_time.tm_zone = "UTC";
}
else
{
- s = tzoffset_parse(s, time, errstr);
+ s = tzoffset_parse(s, &tmp_time, errstr);
if(s == NULL) return NULL;
}
}
+ *epoch = utils_timegm(&tmp_time);
return s;
}
// Sets errstr on failure
char *
-datetime_parse(char *arg, struct tm *time, long *nsec, const char **errstr)
+datetime_parse(char *arg, time_t *epoch, long *nsec, const char **errstr)
{
*nsec = 0;
@@ -420,7 +425,7 @@ datetime_parse(char *arg, struct tm *time, long *nsec, const char **errstr)
arg++;
char *endptr = NULL;
- time_t now = strtol(arg, &endptr, 10);
+ *epoch = strtol(arg, &endptr, 10);
if(errno != 0)
{
*errstr = strerror(errno);
@@ -428,45 +433,22 @@ datetime_parse(char *arg, struct tm *time, long *nsec, const char **errstr)
return NULL;
}
- gmtime_r(&now, time);
-
return endptr;
}
char *ret = NULL;
- ret = email_datetime_parse(arg, time, errstr);
+ ret = email_datetime_parse(arg, epoch, errstr);
if(ret != NULL || *errstr != NULL)
{
return ret;
}
- ret = asctime_datetime_parse(arg, time, errstr);
+ ret = asctime_datetime_parse(arg, epoch, errstr);
if(ret != NULL || *errstr != NULL)
{
return ret;
}
- return iso_datetime_parse(arg, time, nsec, errstr);
-}
-
-// Because mktime() messes with tm_gmtoff yet doesn't applies it, even in POSIX.1-2024
-// Returns (time_t)-1 on failure
-// Maybe should be replaced by mktime_z once <https://www.austingroupbugs.net/view.php?id=1794> gets accepted and implemented
-time_t
-mktime_tz(struct tm *tm)
-{
- long gmtoff = tm->tm_gmtoff;
- const char *zone = tm->tm_zone;
-
- time_t res = mktime(tm);
- tm->tm_gmtoff = gmtoff;
- tm->tm_zone = zone;
-
- if(res == (time_t)-1) return res;
-
- // 12:00+02:00 corresponds to 10:00Z so needs to be reversed
- res += -gmtoff;
-
- return res;
+ return iso_datetime_parse(arg, epoch, nsec, errstr);
}
diff --git a/libutils/datetime_parse.h b/libutils/datetime_parse.h
@@ -2,11 +2,7 @@
// SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
// SPDX-License-Identifier: MPL-2.0
-#include <time.h> /* struct tm */
+#include <time.h> /* time_t */
// Sets errstr on failure
-extern char *datetime_parse(char *arg, struct tm *time, long *nsec, const char **errstr);
-
-// Because mktime() messes with tm_gmtoff yet doesn't applies the offset
-// Returns (time_t)-1 on failure
-extern time_t mktime_tz(struct tm *tm);
+extern char *datetime_parse(char *arg, time_t *epoch, long *nsec, const char **errstr);
diff --git a/test-cmd/date.sh b/test-cmd/date.sh
@@ -2,8 +2,8 @@
# SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
# SPDX-License-Identifier: MPL-2.0
+plans=35
target="$(dirname "$0")/../cmd/date"
-plans=33
. "$(dirname "$0")/tap.sh"
. "$(dirname "$0")/init_env.sh"
@@ -100,6 +100,12 @@ t_args 'email Y2K' '2017-11-21 09:55:06+0000
t_args 'asctime' '1973-09-16 01:03:52+0000
' -u -d 'Sun Sep 16 01:03:52 1973' '+%F %T%z'
+TZ=ABB1:2 t_args '%:z' '2006-08-14 07:32:56 -01:02 ABB @1155544496
+' -d @1155544496 '+%F %T %:z %Z @%s'
+
+TZ=ABC1:2:3 t_args '%::z' '2006-08-14 07:32:53 -01:02:03 ABC @1155544496
+' -d @1155544496 '+%F %T %::z %Z @%s'
+
#usage="\
#date [-uR] [-d datetime] [+format]
#date [-uR] -f now_format now [+format]