logo

utils-std

Collection of commonly available Unix tools git clone https://anongit.hacktivis.me/git/utils-std.git/
commit: 29a3265cc804f9d3e85b21ee51581ef43c60bc57
parent 78fcb7b90d11100be507ea8a9870a5a5cdc37920
Author: Haelwenn (lanodan) Monnier <contact@hacktivis.me>
Date:   Mon, 28 Apr 2025 17:23:58 +0200

lib/iso_parse -> lib/datetime_parse

Diffstat:

Mcmd/date.c6+++---
Mcmd/touch.c10+++++-----
Alib/datetime_parse.c469+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/datetime_parse.h12++++++++++++
Dlib/iso_parse.c464-------------------------------------------------------------------------------
Dlib/iso_parse.h13-------------
6 files changed, 489 insertions(+), 485 deletions(-)

diff --git a/cmd/date.c b/cmd/date.c @@ -6,8 +6,8 @@ #define _POSIX_C_SOURCE 200809L #define _XOPEN_SOURCE 700 // strptime is in XSI +#include "../lib/datetime_parse.h" /* datetime_parse */ #include "../lib/getopt_nolong.h" -#include "../lib/iso_parse.h" /* iso_parse */ #include <assert.h> #include <errno.h> @@ -169,11 +169,11 @@ main(int argc, char *argv[]) return 1; } - iso_parse(optarg, &tm, &tp.tv_nsec, &errstr); + datetime_parse(optarg, &tm, &tp.tv_nsec, &errstr); dflag = 1; if(errstr != NULL) { - fprintf(stderr, "%s: error: iso_parse(\"%s\", …): %s\n", argv0, optarg, errstr); + fprintf(stderr, "%s: error: datetime_parse(\"%s\", …): %s\n", argv0, optarg, errstr); return 1; } diff --git a/cmd/touch.c b/cmd/touch.c @@ -6,9 +6,9 @@ #define _XOPEN_SOURCE 700 // strptime (NetBSD) #define _POSIX_C_SOURCE 200809L // O_NOFOLLOW, st_atim/st_mtim -#include "../lib/bitmasks.h" /* FIELD_* */ +#include "../lib/bitmasks.h" /* FIELD_* */ +#include "../lib/datetime_parse.h" /* datetime_parse */ #include "../lib/getopt_nolong.h" -#include "../lib/iso_parse.h" /* iso_parse */ #include <assert.h> #include <errno.h> /* errno */ @@ -183,15 +183,15 @@ main(int argc, char *argv[]) .tm_gmtoff = 0, .tm_zone = NULL, }; - char *s = iso_parse(optarg, &iso_res, &nsec, &errstr); + char *s = datetime_parse(optarg, &iso_res, &nsec, &errstr); if(errstr != NULL) { - fprintf(stderr, "touch: error: iso_parse(\"%s\", …): %s\n", optarg, errstr); + fprintf(stderr, "touch: error: datetime_parse(\"%s\", …): %s\n", optarg, errstr); return 1; } if(s == NULL) { - fprintf(stderr, "touch: error: iso_parse(\"%s\", …) returned NULL\n", optarg); + fprintf(stderr, "touch: error: datetime_parse(\"%s\", …) returned NULL\n", optarg); return 1; } diff --git a/lib/datetime_parse.c b/lib/datetime_parse.c @@ -0,0 +1,469 @@ +// utils-std: Collection of commonly available Unix tools +// 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 _POSIX_C_SOURCE 200809L // st_atim/st_mtim + +#include "./datetime_parse.h" + +#include <assert.h> +#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 */ + +static const char *short_weekday_name[7] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; +static const char *short_month_name[12] = { + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + +// Parses [+|-]HH:?MM timezone offsets +// Would need tzalloc from <https://www.austingroupbugs.net/view.php?id=1794> to parse timezone names +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; + else if(s[0] == '-') + neg = 1; + else + { + *errstr = "Invalid timezone offset, must start with + or -"; + return NULL; + } + + size_t offname_i = 0; + offname[offname_i++] = *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++; + } + else + { + *errstr = "Invalid timezone offset, no digits after <+|->"; + return NULL; + } + + if(s[0] == ':') s++; + + 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 + { + *errstr = "Invalid timezone offset, no digits after <+|->HH[:]"; + return NULL; + } + + if(neg) time->tm_gmtoff = -time->tm_gmtoff; + + offname[offname_i++] = '\0'; + + time->tm_isdst = 0; + time->tm_zone = offname; + + return s; +} + +// Sets *errstr to NULL when it isn't an email date-time +// +// Check if it could be Email / Internet Message Format datetime +// - Ignores RFC822 (ARPA era, folding space, 2-digit year) +// - Uses RFC5322 / RFC2822 with ignoring RFC822 obsolete formats (aka obs) +// +// 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) +{ + // Kept free of strptime() due to update/overriding being undefined and + // requiring custom parsing, notably locale-free, which strptime() can't handle + + for(; isspace(arg[0]); arg++) + ; + + // 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; + + if(arg[3] == ',') + { + // Because %a/%A is locale-dependent, Sunday is tm_wday=0 + for(size_t i = 0; i < 7; i++) + { + if(memcmp(arg, short_weekday_name[i], 3) == 0) + { + tmp_time.tm_wday = i; + break; + } + } + + if(tmp_time.tm_wday == -1) + { + *errstr = "Failed parsing short weekday name"; + errno = 0; + return NULL; + } + + arg += 4; + + for(; isspace(arg[0]); arg++) + ; + } + + errno = 0; + int parsed = 0; + char month_name[4] = ""; + if(sscanf(arg, + "%2d %3s %d %2d:%2d%n", + &tmp_time.tm_mday, + month_name, + &tmp_time.tm_year, + &tmp_time.tm_hour, + &tmp_time.tm_min, + &parsed) < 5) + { + if(errno == 0 || errno == EINVAL) + { + if(tmp_time.tm_wday == -1) + { + *errstr = NULL; + } + else + { + *errstr = "Failed parsing Email-datetime"; + } + } + else + { + *errstr = strerror(errno); + errno = 0; + } + return NULL; + } + + if(tmp_time.tm_year < 49) + { + tmp_time.tm_year += 100; // 2000-2049 + } + else if(tmp_time.tm_year > 99) + { + tmp_time.tm_year -= 1900; + } + + arg += parsed; + + if(arg[0] == ':' && isdigit(arg[1])) + { + if(isdigit(arg[2])) + { + tmp_time.tm_sec = (arg[1] - '0') * 10 + (arg[2] - '0'); + arg += 3; + } + else + { + tmp_time.tm_sec = arg[1] - '0'; + arg += 2; + } + } + + for(; isspace(arg[0]); arg++) + ; + + // Consider that nobody is going to transmit a timezone name which isn't GMT + if(arg[0] == 'G' && arg[1] == 'M' && arg[2] == 'T' && (arg[3] == '\0' || isspace(arg[3]))) + { + tmp_time.tm_isdst = 0; + tmp_time.tm_gmtoff = 0; + tmp_time.tm_zone = "UTC"; + } + else + { + arg = tzoffset_parse(arg, &tmp_time, errstr); + if(arg == NULL) return NULL; + } + + // Done extracting directly from arg + + tmp_time.tm_mon = -1; + // Because %b/%B is locale-dependent + for(size_t i = 0; i < 12; i++) + { + if(memcmp(month_name, short_month_name[i], 3) == 0) + { + tmp_time.tm_mon = i; + break; + } + } + if(tmp_time.tm_mon < 0) + { + *errstr = "Failed parsing short month name"; + errno = 0; + return NULL; + } + + memcpy(time, &tmp_time, sizeof(tmp_time)); + return arg; +} + +// Sets *errstr to NULL when it isn't an email date-time +// +// 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) +{ + // 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; + + // asctime() doesn't gives any timezone information, assume UTC + tmp_time.tm_isdst = 0; + tmp_time.tm_gmtoff = 0; + tmp_time.tm_zone = "UTC"; + + errno = 0; + int parsed = 0; + char month_name[4] = ""; + char weekday_name[4] = ""; + if(sscanf(arg, + "%3s %3s %d %2d:%2d:%2d %d%n", + weekday_name, + month_name, + &tmp_time.tm_mday, + &tmp_time.tm_hour, + &tmp_time.tm_min, + &tmp_time.tm_sec, + &tmp_time.tm_year, + &parsed) < 7) + { + if(errno == 0 || errno == EINVAL) + { + *errstr = NULL; + } + else + { + *errstr = strerror(errno); + errno = 0; + } + return NULL; + } + + arg += parsed; + + tmp_time.tm_year -= 1900; + + tmp_time.tm_wday = -1; + // Because %a/%A is locale-dependent + for(size_t i = 0; i < 7; i++) + { + if(memcmp(weekday_name, short_weekday_name[i], 3) == 0) + { + tmp_time.tm_wday = i; + break; + } + } + if(tmp_time.tm_wday < 0) + { + *errstr = "Failed parsing short weekday name"; + errno = 0; + return NULL; + } + + tmp_time.tm_mon = -1; + // Because %b/%B is locale-dependent + for(size_t i = 0; i < 12; i++) + { + if(memcmp(month_name, short_month_name[i], 3) == 0) + { + tmp_time.tm_mon = i; + break; + } + } + if(tmp_time.tm_mon < 0) + { + *errstr = "Failed parsing short month name"; + errno = 0; + return NULL; + } + + for(; isspace(arg[0]); arg++) + ; + + memcpy(time, &tmp_time, sizeof(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) +{ + // Try parsing as RFC3339 subset of ISO 8601:1988 + + // 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); + + if(s == NULL) + { + *errstr = "strptime(…, \"%Y-%m-%d\", …) returned NULL"; + errno = 0; + 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 NULL; + } + s++; + + s = strptime(s, "%H:%M:%S", time); + if(s == NULL) + { + *errstr = "strptime(…, \"%H:%M:%S\", …) returned NULL"; + errno = 0; + return NULL; + } + + if(s[0] == ',' || s[0] == '.') + { + double fraction = 0.0; + int parsed = 0; + + if(s[0] == ',') s[0] = '.'; + + if(sscanf(s, "%10lf%n", &fraction, &parsed) < 1) + { + if(errno == 0) + { + *errstr = "Failed to parse fractional seconds"; + } + else + { + *errstr = strerror(errno); + errno = 0; + } + return NULL; + } + + *nsec = (long)(fraction * 1000000000); + s += parsed; + + // too many digits + if(isdigit(s[0])) + { + *errstr = "Too many digits (> 10) for fractional seconds"; + return NULL; + } + } + + for(; isspace(s[0]); s++) + ; + + if(s != NULL && s[0] != '\0') + { + if(s[0] == 'Z' && s[1] == '\0') + { + time->tm_isdst = 0; + time->tm_gmtoff = 0; + time->tm_zone = "UTC"; + } + else + { + s = tzoffset_parse(s, time, errstr); + if(s == NULL) return NULL; + } + } + + return s; +} + +// Sets errstr on failure +char * +datetime_parse(char *arg, struct tm *time, long *nsec, const char **errstr) +{ + *nsec = 0; + + // For Alpine's abuild compatibility + if(arg[0] == '@') + { + arg++; + + char *endptr = NULL; + time_t now = strtol(arg, &endptr, 10); + if(errno != 0) + { + *errstr = strerror(errno); + errno = 0; + return NULL; + } + + gmtime_r(&now, time); + + return endptr; + } + + char *ret = NULL; + + ret = email_datetime_parse(arg, time, errstr); + if(ret != NULL || *errstr != NULL) + { + return ret; + } + + ret = asctime_datetime_parse(arg, time, 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; +} diff --git a/lib/datetime_parse.h b/lib/datetime_parse.h @@ -0,0 +1,12 @@ +// utils-std: Collection of commonly available Unix tools +// SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me> +// SPDX-License-Identifier: MPL-2.0 + +#include <time.h> /* struct tm */ + +// 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); diff --git a/lib/iso_parse.c b/lib/iso_parse.c @@ -1,464 +0,0 @@ -// utils-std: Collection of commonly available Unix tools -// 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 _POSIX_C_SOURCE 200809L // st_atim/st_mtim - -#include "./iso_parse.h" - -#include <assert.h> -#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 */ - -static const char *short_weekday_name[7] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; -static const char *short_month_name[12] = { - "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; - -// Parses [+|-]HH:?MM timezone offsets -// Would need tzalloc from <https://www.austingroupbugs.net/view.php?id=1794> to parse timezone names -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; - else if(s[0] == '-') - neg = 1; - else - { - *errstr = "Invalid timezone offset, must start with + or -"; - return NULL; - } - - size_t offname_i = 0; - offname[offname_i++] = *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++; - } - else - { - *errstr = "Invalid timezone offset, no digits after <+|->"; - return NULL; - } - - if(s[0] == ':') s++; - - 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 - { - *errstr = "Invalid timezone offset, no digits after <+|->HH[:]"; - return NULL; - } - - if(neg) time->tm_gmtoff = -time->tm_gmtoff; - - offname[offname_i++] = '\0'; - - time->tm_isdst = 0; - time->tm_zone = offname; - - return s; -} - -// For iso_parse function -// Sets *errstr to NULL when it isn't an email date-time -// -// Check if it could be Email / Internet Message Format datetime -// - Ignores RFC822 (ARPA era, folding space, 2-digit year) -// - Uses RFC5322 / RFC2822 with ignoring RFC822 obsolete formats (aka obs) -// -// RFC5322 and RFC2822 (no obs): "([ ]*Day,)[ ]*DD[ ]+Mon[ ]+YYYY[ ]+HH:MM(:SS)?[ ]+[+/-]hhmm" -static char * -email_date_parse(char *arg, struct tm *time, 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 - - for(; isspace(arg[0]); arg++) - ; - - // 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; - - if(arg[3] == ',') - { - // Because %a/%A is locale-dependent, Sunday is tm_wday=0 - for(size_t i = 0; i < 7; i++) - { - if(memcmp(arg, short_weekday_name[i], 3) == 0) - { - tmp_time.tm_wday = i; - break; - } - } - - if(tmp_time.tm_wday == -1) - { - *errstr = "Failed parsing short weekday name"; - errno = 0; - return NULL; - } - - arg += 4; - - for(; isspace(arg[0]); arg++) - ; - } - - errno = 0; - int parsed = 0; - char month_name[4] = ""; - if(sscanf(arg, - "%2d %3s %d %2d:%2d%n", - &tmp_time.tm_mday, - month_name, - &tmp_time.tm_year, - &tmp_time.tm_hour, - &tmp_time.tm_min, - &parsed) < 5) - { - if(errno == 0 || errno == EINVAL) - { - if(tmp_time.tm_wday == -1) - { - *errstr = NULL; - } - else - { - *errstr = "Failed parsing Email-datetime"; - } - } - else - { - *errstr = strerror(errno); - errno = 0; - } - return NULL; - } - - if(tmp_time.tm_year < 49) - { - tmp_time.tm_year += 100; // 2000-2049 - } - else if(tmp_time.tm_year > 99) - { - tmp_time.tm_year -= 1900; - } - - arg += parsed; - - if(arg[0] == ':' && isdigit(arg[1])) - { - if(isdigit(arg[2])) - { - tmp_time.tm_sec = (arg[1] - '0') * 10 + (arg[2] - '0'); - arg += 3; - } - else - { - tmp_time.tm_sec = arg[1] - '0'; - arg += 2; - } - } - - for(; isspace(arg[0]); arg++) - ; - - // Consider that nobody is going to transmit a timezone name which isn't GMT - if(arg[0] == 'G' && arg[1] == 'M' && arg[2] == 'T' && (arg[3] == '\0' || isspace(arg[3]))) - { - tmp_time.tm_isdst = 0; - tmp_time.tm_gmtoff = 0; - tmp_time.tm_zone = "UTC"; - } - else - { - arg = tzoffset_parse(arg, &tmp_time, errstr); - if(arg == NULL) return NULL; - } - - // Done extracting directly from arg - - tmp_time.tm_mon = -1; - // Because %b/%B is locale-dependent - for(size_t i = 0; i < 12; i++) - { - if(memcmp(month_name, short_month_name[i], 3) == 0) - { - tmp_time.tm_mon = i; - break; - } - } - if(tmp_time.tm_mon < 0) - { - *errstr = "Failed parsing short month name"; - errno = 0; - return NULL; - } - - memcpy(time, &tmp_time, sizeof(tmp_time)); - return arg; -} - -// For iso_parse function -// Sets *errstr to NULL when it isn't an email date-time -// -// Check if it could be asctime() format: Thu Nov 24 18:22:48 1986 -static char * -asctime_date_parse(char *arg, struct tm *time, 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; - - // asctime() doesn't gives any timezone information, assume UTC - tmp_time.tm_isdst = 0; - tmp_time.tm_gmtoff = 0; - tmp_time.tm_zone = "UTC"; - - errno = 0; - int parsed = 0; - char month_name[4] = ""; - char weekday_name[4] = ""; - if(sscanf(arg, - "%3s %3s %d %2d:%2d:%2d %d%n", - weekday_name, - month_name, - &tmp_time.tm_mday, - &tmp_time.tm_hour, - &tmp_time.tm_min, - &tmp_time.tm_sec, - &tmp_time.tm_year, - &parsed) < 7) - { - if(errno == 0 || errno == EINVAL) - { - *errstr = NULL; - } - else - { - *errstr = strerror(errno); - errno = 0; - } - return NULL; - } - - arg += parsed; - - tmp_time.tm_year -= 1900; - - tmp_time.tm_wday = -1; - // Because %a/%A is locale-dependent - for(size_t i = 0; i < 7; i++) - { - if(memcmp(weekday_name, short_weekday_name[i], 3) == 0) - { - tmp_time.tm_wday = i; - break; - } - } - if(tmp_time.tm_wday < 0) - { - *errstr = "Failed parsing short weekday name"; - errno = 0; - return NULL; - } - - tmp_time.tm_mon = -1; - // Because %b/%B is locale-dependent - for(size_t i = 0; i < 12; i++) - { - if(memcmp(month_name, short_month_name[i], 3) == 0) - { - tmp_time.tm_mon = i; - break; - } - } - if(tmp_time.tm_mon < 0) - { - *errstr = "Failed parsing short month name"; - errno = 0; - return NULL; - } - - for(; isspace(arg[0]); arg++) - ; - - memcpy(time, &tmp_time, sizeof(tmp_time)); - return arg; -} - -// Sets errstr on failure -// YYYY-MM-DD[T ]hh:mm:SS([,\.]frac)?(Z|[+\-]hh:?mm)? -char * -iso_parse(char *arg, struct tm *time, long *nsec, const char **errstr) -{ - *nsec = 0; - - // For Alpine's abuild compatibility - if(arg[0] == '@') - { - arg++; - - char *endptr = NULL; - time_t now = strtol(arg, &endptr, 10); - if(errno != 0) - { - *errstr = strerror(errno); - errno = 0; - return NULL; - } - - gmtime_r(&now, time); - - return endptr; - } - - char *ret = NULL; - - ret = email_date_parse(arg, time, errstr); - if(ret != NULL || *errstr != NULL) - { - return ret; - } - - ret = asctime_date_parse(arg, time, errstr); - if(ret != NULL || *errstr != NULL) - { - return ret; - } - - // Try parsing as RFC3339 subset of ISO 8601:1988 - - // 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); - - if(s == NULL) - { - *errstr = "strptime(…, \"%Y-%m-%d\", …) returned NULL"; - errno = 0; - 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 NULL; - } - s++; - - s = strptime(s, "%H:%M:%S", time); - if(s == NULL) - { - *errstr = "strptime(…, \"%H:%M:%S\", …) returned NULL"; - errno = 0; - return NULL; - } - - if(s[0] == ',' || s[0] == '.') - { - double fraction = 0.0; - int parsed = 0; - - if(s[0] == ',') s[0] = '.'; - - if(sscanf(s, "%10lf%n", &fraction, &parsed) < 1) - { - if(errno == 0) - { - *errstr = "Failed to parse fractional seconds"; - } - else - { - *errstr = strerror(errno); - errno = 0; - } - return NULL; - } - - *nsec = (long)(fraction * 1000000000); - s += parsed; - - // too many digits - if(isdigit(s[0])) - { - *errstr = "Too many digits (> 10) for fractional seconds"; - return NULL; - } - } - - for(; isspace(s[0]); s++) - ; - - if(s != NULL && s[0] != '\0') - { - if(s[0] == 'Z' && s[1] == '\0') - { - time->tm_isdst = 0; - time->tm_gmtoff = 0; - time->tm_zone = "UTC"; - } - else - { - s = tzoffset_parse(s, time, errstr); - if(s == NULL) return NULL; - } - } - - return s; -} - -// 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; -} diff --git a/lib/iso_parse.h b/lib/iso_parse.h @@ -1,13 +0,0 @@ -// utils-std: Collection of commonly available Unix tools -// SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me> -// SPDX-License-Identifier: MPL-2.0 - -#include <time.h> /* struct tm */ - -// Sets errstr on failure -// YYYY-MM-DD[T ]hh:mm:SS([,\.]frac)?(Z|[+\-]hh:?mm)? -extern char *iso_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);