iso_parse.c (3613B)
- // 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 */
- // 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)
- {
- // 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;
- }
- nsec = 0;
- gmtime_r(&now, time);
- return endptr;
- }
- // No %F in POSIX
- 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;
- }
- }
- if(s != NULL && s[0] != '\0')
- {
- if(s[0] == 'Z' && s[1] == '\0')
- {
- time->tm_gmtoff = 0;
- time->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]))
- {
- time->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]))
- {
- time->tm_gmtoff += (o[0] - '0') * 600 + (o[1] - '0') * 60;
- o += 2;
- }
- else
- {
- *errstr = "Invalid timezone offset, no digits after [+|-]";
- return NULL;
- }
- if(neg) time->tm_gmtoff = -time->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);
- time->tm_zone = offname;
- }
- }
- return s;
- }
- // 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;
- }