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:
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