logo

utils-std

Collection of commonly available Unix tools git clone https://anongit.hacktivis.me/git/utils-std.git/
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:

Mcmd/date.c94++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mcmd/touch.c24++----------------------
Mlibutils/datetime_parse.c112+++++++++++++++++++++++++++++++++----------------------------------------------
Mlibutils/datetime_parse.h8++------
Mtest-cmd/date.sh8+++++++-
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]