logo

utils-std

Collection of commonly available Unix tools
commit: 9188780def233ee1ff4244f20691b97324aec934
parent 4e46eb9fa6d90ec000211cc191d81dcdf85e9ce4
Author: Haelwenn (lanodan) Monnier <contact@hacktivis.me>
Date:   Thu, 25 Jul 2024 12:46:42 +0200

lib/iso_parse: Handle UTC offsets

Diffstat:

Mcmd/date.c80+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mcmd/touch.c8+++++++-
Mlib/iso_parse.c161++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mlib/iso_parse.h11+++++++++--
Mlib/iso_parse.mdoc24++++++++++++++----------
Mtest-cmd/touch20+++++++++++++++++---
6 files changed, 210 insertions(+), 94 deletions(-)

diff --git a/cmd/date.c b/cmd/date.c @@ -2,6 +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 _POSIX_C_SOURCE 200809L #define _XOPEN_SOURCE 700 // strptime is in XSI @@ -33,7 +34,10 @@ main(int argc, char *argv[]) { char outstr[BUFSIZ]; struct tm *tm; - time_t now; + struct timespec tp = { + .tv_sec = 0, + .tv_nsec = 0, + }; char *format = "%c"; char *input_format = NULL; int uflag = 0; @@ -41,6 +45,7 @@ main(int argc, char *argv[]) int c; bool jflag = false; bool settime = false; + bool custom_datetime = false; errno = 0; setlocale(LC_ALL, ""); @@ -50,8 +55,8 @@ main(int argc, char *argv[]) errno = 0; } - now = time(NULL); - if(now == (time_t)-1) + tp.tv_sec = time(NULL); + if(tp.tv_sec == (time_t)-1) { perror("date: time"); exit(EXIT_FAILURE); @@ -68,13 +73,20 @@ main(int argc, char *argv[]) fprintf(stderr, "date: Cannot both use '-d' and '-f'\n"); exit(EXIT_FAILURE); } - now = iso_parse(optarg, &errstr).tv_sec; + + tm = iso_parse(optarg, &tp.tv_nsec, &errstr); + custom_datetime = true; dflag = 1; if(errstr != NULL) { fprintf(stderr, "date: iso_parse(\"%s\", …): %s\n", optarg, errstr); exit(EXIT_FAILURE); } + + assert(errno == 0); + tp.tv_sec = mktime_tz(tm); + errno = 0; + break; case 'f': /* input datetime format */ if(dflag == 1) @@ -90,6 +102,8 @@ main(int argc, char *argv[]) break; case 'u': /* UTC timezone */ uflag++; + setenv("TZ", "UTC", 1); + tzset(); break; case 'j': jflag = true; @@ -105,27 +119,6 @@ main(int argc, char *argv[]) } } - if(uflag) - { - tm = gmtime(&now); - - if(tm == NULL) - { - perror("date: gmtime"); - exit(EXIT_FAILURE); - } - } - else - { - tm = localtime(&now); - - if(tm == NULL) - { - perror("date: localtime"); - exit(EXIT_FAILURE); - } - } - argc -= optind; argv += optind; @@ -142,8 +135,11 @@ main(int argc, char *argv[]) exit(EXIT_FAILURE); } + custom_datetime = true; + assert(errno == 0); - now = mktime(tm); + tp.tv_sec = mktime_tz(tm); + tp.tv_nsec = 0; errno = 0; argv++; @@ -184,8 +180,11 @@ main(int argc, char *argv[]) } } + custom_datetime = true; + assert(errno == 0); - now = mktime(tm); + tp.tv_sec = mktime(tm); + tp.tv_nsec = 0; errno = 0; argv++; @@ -193,13 +192,30 @@ main(int argc, char *argv[]) settime = true; } - if(settime && !jflag) + if(!custom_datetime) { - struct timespec tp = { - .tv_sec = now, - .tv_nsec = 0, - }; + if(uflag) + { + tm = gmtime(&tp.tv_sec); + if(tm == NULL) + { + perror("date: gmtime"); + exit(EXIT_FAILURE); + } + } + else + { + tm = localtime(&tp.tv_sec); + if(tm == NULL) + { + perror("date: localtime"); + exit(EXIT_FAILURE); + } + } + } + if(settime && !jflag) + { if(clock_settime(CLOCK_REALTIME, &tp) != 0) { fprintf(stderr, diff --git a/cmd/touch.c b/cmd/touch.c @@ -156,13 +156,19 @@ main(int argc, char *argv[]) } break; case 'd': - target = iso_parse(optarg, &errstr); + { + long nsec = 0; + struct tm *iso_res = iso_parse(optarg, &nsec, &errstr); if(errstr != NULL) { fprintf(stderr, "touch: iso_parse(\"%s\", …): %s\n", optarg, errstr); exit(EXIT_FAILURE); } + assert(iso_res != NULL); + target.tv_sec = mktime_tz(iso_res); + target.tv_nsec = nsec; break; + } case ':': fprintf(stderr, "touch: Error: Missing operand for option: '-%c'\n", optopt); return 1; diff --git a/lib/iso_parse.c b/lib/iso_parse.c @@ -6,21 +6,37 @@ #define _XOPEN_SOURCE 700 // strptime (NetBSD) #define _POSIX_C_SOURCE 200809L // st_atim/st_mtim +#include "./iso_parse.h" + #include <assert.h> -#include <ctype.h> /* isdigit */ -#include <errno.h> /* errno */ -#include <stdio.h> /* perror, sscanf */ -#include <stdlib.h> /* strtol */ -#include <string.h> /* memset */ -#include <time.h> /* strptime, tm */ +#include <ctype.h> /* isdigit */ +#include <errno.h> /* errno */ +#include <inttypes.h> /* PRId16 */ +#include <limits.h> /* TZNAME_MAX */ +#include <stdio.h> /* perror, sscanf */ +#include <stdlib.h> /* strtol */ +#include <string.h> /* memset */ +#include <time.h> /* strptime, tm */ // Sets errstr on failure -struct timespec -iso_parse(char *arg, char **errstr) +// YYYY-MM-DD[T ]hh:mm:SS([,\.]frac)?(Z|[+\-]hh:?mm)? +struct tm * +iso_parse(char *arg, long *nsec, char **errstr) { - // YYYY-MM-DD[T ]hh:mm:SS([,\.]frac)?Z? - // Dammit Unixes why no nanoseconds in `struct tm` nor `strptime` - struct timespec time = {.tv_sec = 0, .tv_nsec = 0}; + // Need fractional seconds and GMT-offset + // struct timespec provides nanoseconds but not GMT-offset + // struct tm provides GMT-offset but not nanoseconds + static struct tm 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, + }; // For Alpine's abuild compatibility if(arg[0] == '@') @@ -28,49 +44,40 @@ iso_parse(char *arg, char **errstr) arg++; assert(errno == 0); - time.tv_sec = strtol(arg, NULL, 10); + time_t now = strtol(arg, NULL, 10); if(errno != 0) { *errstr = strerror(errno); errno = 0; - return time; + return NULL; } - return time; + nsec = 0; + return gmtime(&now); } - 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 - }; - // No %F in POSIX - char *s = strptime(arg, "%Y-%m-%d", &tm); + char *s = strptime(arg, "%Y-%m-%d", &res); if(s == NULL) { *errstr = "strptime(…, \"%Y-%m-%d\", …) returned NULL"; errno = 0; - return time; + return NULL; } if(s[0] != 'T' && s[0] != ' ') { *errstr = "Couldn't find time-separator (T or space) after date (Y-m-d)"; errno = 0; - return time; + return NULL; } s++; - s = strptime(s, "%H:%M:%S", &tm); + s = strptime(s, "%H:%M:%S", &res); if(s == NULL) { *errstr = "strptime(…, \"%H:%M:%S\", …) returned NULL"; errno = 0; - return time; + return NULL; } if(s[0] == ',' || s[0] == '.') @@ -92,37 +99,99 @@ iso_parse(char *arg, char **errstr) *errstr = strerror(errno); errno = 0; } - return time; + return NULL; } - time.tv_nsec = fraction * 1000000000; + *nsec = (long)(fraction * 1000000000); s += parsed; // too many digits if(isdigit(s[0])) { *errstr = "Too many digits (> 10) for fractional seconds"; - return time; + return NULL; } } - if(s != NULL && s[0] == 'Z') + if(s != NULL && s[0] != '\0') { - tm.tm_gmtoff = 0; - tm.tm_zone = "UTC"; + if(s[0] == 'Z' && s[1] == '\0') + { + res.tm_gmtoff = 0; + res.tm_zone = "UTC"; + } + else + { + int neg; + if(*s == '+') + neg = 0; + else if(*s == '-') + neg = 1; + else + { + *errstr = "Invalid timezone offset, must start with + or -"; + return NULL; + } + + char *o = s + 1; + + if(isdigit(o[0]) && isdigit(o[1])) + { + res.tm_gmtoff = (o[0] - '0') * 36000 + (o[1] - '0') * 3600; + o += 2; + } + else + { + *errstr = "Invalid timezone offset, no digits after [+|-]"; + return NULL; + } + + if(o[0] == ':') o++; + + if(isdigit(o[0]) && isdigit(o[1])) + { + res.tm_gmtoff += (o[0] - '0') * 600 + (o[1] - '0') * 60; + o += 2; + } + else + { + *errstr = "Invalid timezone offset, no digits after [+|-]"; + return NULL; + } + + if(neg) res.tm_gmtoff = -res.tm_gmtoff; + +#ifndef TZNAME_MAX +#define TZNAME_MAX _POSIX_TZNAME_MAX +#endif + static char offname[TZNAME_MAX + 1] = ""; + assert(o - s < TZNAME_MAX); + memcpy(offname, s, o - s); + res.tm_zone = offname; + } } assert(errno == 0); - time.tv_sec = mktime(&tm); - if(time.tv_sec == (time_t)-1) - { - *errstr = strerror(errno); - errno = 0; - return time; - } - // As observed on FreeBSD 14.0, non-errorneous mktime can still end up setting errno - // cf. https://builds.sr.ht/~lanodan/job/1181509 - errno = 0; - return time; + return &res; +} + +// Because mktime() messes with tm_gmtoff yet doesn't applies it, even in POSIX.1-2024 +// Returns (time_t)-1 on failure +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; } diff --git a/lib/iso_parse.h b/lib/iso_parse.h @@ -2,5 +2,12 @@ // SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me> // SPDX-License-Identifier: MPL-2.0 -#include <time.h> /* struct timespec */ -extern struct timespec iso_parse(char *arg, char **errstr); +#include <time.h> /* struct tm */ + +// Sets errstr on failure +// YYYY-MM-DD[T ]hh:mm:SS([,\.]frac)?(Z|[+\-]hh:?mm)? +extern struct tm *iso_parse(char *arg, long *nsec, 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); diff --git a/lib/iso_parse.mdoc b/lib/iso_parse.mdoc @@ -13,7 +13,7 @@ for example corresponds to 2023-10-31 23:30:20 UTC .Pp Or as -.Ql YYYY-MM-DDThh:mm:SS[frac][Z] , +.Ql YYYY-MM-DDThh:mm:SS[frac][tz] , where: .Bl -tag -width Ds .It Ql YYYY-MM-DD @@ -28,14 +28,19 @@ Is either empty, or fractional seconds starting with either a comma .Pq \&, or a period .Pq \&. . -.It Ql [Z] -Is either empty, signifying local time, or the letter -.Qq Z , +.It Ql [tz] +When empty it corresponds to local time. +Otherwise it can be an UTC offset in the format +.Ql [+-]HH:?MM +or the letter +.Qq Z , signifying UTC. -.Pp -This is the only part which disagrees with RFC3339 due to the lack of timezone-offset parsing in -.St -p1003.1-2008 .El .Pp -For example: -.Ql 2003-06-02T13:37:42.713Z -\ No newline at end of file +Some examples: +.Bl -bullet -compact +.It +.Ql 2003-06-02T13:37:42.713Z +.It +.Ql 1971-01-02T03:04:05.678+0900 +.El diff --git a/test-cmd/touch b/test-cmd/touch @@ -143,7 +143,7 @@ optd_body() { atime="$(./stat_atime ./foo)" mtime="$(./stat_mtime ./foo)" - unset TZ + export TZ=UTC atf_check ../cmd/touch -d 2003-06-02T13:37:42Z ./foo atf_check -o "not-inline:${atime}\n" ./stat_atime ./foo @@ -152,6 +152,19 @@ optd_body() { atf_check -o 'match:^2003-06-02[T ]13:37:42(\.0+)? ?(Z|[\+\-]00:?00)$' ./stat_mtime ./foo } +atf_test_case optd_tz +optd_tz_body() { + export TZ=UTC + + atf_check ../cmd/touch -d 2003-04-20T13:37:42+0000 ./foo + atf_check -o 'match:^2003-04-20[T ]13:37:42(\.0+)? ?(Z|[\+\-]00:?00)$' ./stat_atime ./foo + atf_check -o 'match:^2003-04-20[T ]13:37:42(\.0+)? ?(Z|[\+\-]00:?00)$' ./stat_mtime ./foo + + atf_check ../cmd/touch -d 2003-04-20T13:37:42+0666 ./foo + atf_check -o 'match:^2003-04-20[T ]06:31:42(\.0+)? ?(Z|[\+\-]00:?00)$' ./stat_atime ./foo + atf_check -o 'match:^2003-04-20[T ]06:31:42(\.0+)? ?(Z|[\+\-]00:?00)$' ./stat_mtime ./foo +} + atf_test_case optd_frac optd_frac_body() { atf_check touch -a ./foo @@ -161,7 +174,7 @@ optd_frac_body() { atime="$(./stat_atime ./foo)" mtime="$(./stat_mtime ./foo)" - unset TZ + export TZ=UTC atf_check ../cmd/touch -d 2003-06-02T13:37:42.713Z ./foo atf_check -o "not-inline:${atime}\n" ./stat_atime ./foo @@ -187,7 +200,7 @@ optt_body() { atime="$(./stat_atime ./foo)" mtime="$(./stat_mtime ./foo)" - unset TZ + export TZ=UTC atf_check ../cmd/touch -t 200306021337.42 ./foo atf_check -o "not-inline:${atime}\n" ./stat_atime ./foo @@ -212,6 +225,7 @@ atf_init_test_cases() { atf_add_test_case dir atf_add_test_case optd + atf_add_test_case optd_tz atf_add_test_case optt