From e0ca1922dc8d08fd34b61e52e58c72540ade662f Mon Sep 17 00:00:00 2001 From: Nicolas Williams Date: Sun, 27 Aug 2023 22:56:33 -0500 Subject: [PATCH] Broken down time needs tm_{isdst,gmtoff,zone} too (fix #1912) --- configure.ac | 7 +++- docs/content/manual/manual.yml | 12 ++++-- jq.1.prebuilt | 4 +- src/builtin.c | 73 ++++++++++++++++++++++++++++++---- src/util.c | 72 ++++++++++++++++----------------- tests/jq.test | 4 +- tests/man.test | 2 +- tests/optional.test | 4 +- 8 files changed, 122 insertions(+), 56 deletions(-) diff --git a/configure.ac b/configure.ac index 9e8830bf41..f91fdac816 100644 --- a/configure.ac +++ b/configure.ac @@ -145,9 +145,12 @@ AC_FIND_FUNC([gmtime], [c], [#include ], [0]) AC_FIND_FUNC([localtime_r], [c], [#include ], [0, 0]) AC_FIND_FUNC([localtime], [c], [#include ], [0]) AC_FIND_FUNC([gettimeofday], [c], [#include ], [0, 0]) -AC_CHECK_MEMBER([struct tm.tm_gmtoff], [AC_DEFINE([HAVE_TM_TM_GMT_OFF],1,[Define to 1 if the system has the tm_gmt_off field in struct tm])], +AC_STRUCT_TIMEZONE +AC_CHECK_MEMBER([struct tm.tm_gmtoff], [AC_DEFINE([HAVE_TM_TM_GMTOFF],1,[Define to 1 if the system has the tm_gmtoff field in struct tm])], [], [[#include ]]) -AC_CHECK_MEMBER([struct tm.__tm_gmtoff], [AC_DEFINE([HAVE_TM___TM_GMT_OFF],1,[Define to 1 if the system has the __tm_gmt_off field in struct tm])], +AC_CHECK_MEMBER([struct tm.__tm_gmtoff], [AC_DEFINE([HAVE_TM___TM_GMTOFF],1,[Define to 1 if the system has the __tm_gmtoff field in struct tm])], + [], [[#include ]]) +AC_CHECK_MEMBER([struct tm.tm_zone], [AC_DEFINE([HAVE_TM_TM_ZONE],1,[Define to 1 if the system has the tm_zone field in struct tm])], [], [[#include ]]) dnl Figure out if we have the pthread functions we actually need diff --git a/docs/content/manual/manual.yml b/docs/content/manual/manual.yml index f28d9a89f6..e1533043bf 100644 --- a/docs/content/manual/manual.yml +++ b/docs/content/manual/manual.yml @@ -2199,9 +2199,13 @@ sections: (in this order): the year, the month (zero-based), the day of the month (one-based), the hour of the day, the minute of the hour, the second of the minute, the day of the week, and the - day of the year -- all one-based unless otherwise stated. The - day of the week number may be wrong on some systems for dates - before March 1st 1900, or after December 31 2099. + day of the year -all one-based unless otherwise stated- + followed by (depending on platform support) a boolean + indicating whether the time is in a daylight savings + timezone, followed by the offset to UTC, followed by the + name of the timezone. The day of the week number may be + wrong on some systems for dates before March 1st 1900, or + after December 31 2099. The `localtime` builtin works like the `gmtime` builtin, but using the local timezone setting. @@ -2230,7 +2234,7 @@ sections: input: '"2015-03-05T23:51:47Z"' output: ['1425599507'] - - program: 'strptime("%Y-%m-%dT%H:%M:%SZ")' + - program: 'strptime("%Y-%m-%dT%H:%M:%SZ")[0:8]' input: '"2015-03-05T23:51:47Z"' output: ['[2015,2,5,23,51,47,4,63]'] diff --git a/jq.1.prebuilt b/jq.1.prebuilt index 3c65674849..05aa5020ec 100644 --- a/jq.1.prebuilt +++ b/jq.1.prebuilt @@ -2388,7 +2388,7 @@ The \fBnow\fR builtin outputs the current time, in seconds since the Unix epoch\ Low\-level jq interfaces to the C\-library time functions are also provided: \fBstrptime\fR, \fBstrftime\fR, \fBstrflocaltime\fR, \fBmktime\fR, \fBgmtime\fR, and \fBlocaltime\fR\. Refer to your host operating system\'s documentation for the format strings used by \fBstrptime\fR and \fBstrftime\fR\. Note: these are not necessarily stable interfaces in jq, particularly as to their localization functionality\. . .P -The \fBgmtime\fR builtin consumes a number of seconds since the Unix epoch and outputs a "broken down time" representation of Greenwich Mean Time as an array of numbers representing (in this order): the year, the month (zero\-based), the day of the month (one\-based), the hour of the day, the minute of the hour, the second of the minute, the day of the week, and the day of the year \-\- all one\-based unless otherwise stated\. The day of the week number may be wrong on some systems for dates before March 1st 1900, or after December 31 2099\. +The \fBgmtime\fR builtin consumes a number of seconds since the Unix epoch and outputs a "broken down time" representation of Greenwich Mean Time as an array of numbers representing (in this order): the year, the month (zero\-based), the day of the month (one\-based), the hour of the day, the minute of the hour, the second of the minute, the day of the week, and the day of the year \-all one\-based unless otherwise stated\- followed by (depending on platform support) a boolean indicating whether the time is in a daylight savings timezone, followed by the offset to UTC, followed by the name of the timezone\. The day of the week number may be wrong on some systems for dates before March 1st 1900, or after December 31 2099\. . .P The \fBlocaltime\fR builtin works like the \fBgmtime\fR builtin, but using the local timezone setting\. @@ -2416,7 +2416,7 @@ jq \'fromdate\' "2015\-03\-05T23:51:47Z" => 1425599507 -jq \'strptime("%Y\-%m\-%dT%H:%M:%SZ")\' +jq \'strptime("%Y\-%m\-%dT%H:%M:%SZ")[0:8]\' "2015\-03\-05T23:51:47Z" => [2015,2,5,23,51,47,4,63] diff --git a/src/builtin.c b/src/builtin.c index 2ea40dc54f..25f3b4e00e 100644 --- a/src/builtin.c +++ b/src/builtin.c @@ -1287,14 +1287,28 @@ static jv f_stderr(jq_state *jq, jv input) { } static jv tm2jv(struct tm *tm) { - return JV_ARRAY(jv_number(tm->tm_year + 1900), + jv a = JV_ARRAY(jv_number(tm->tm_year + 1900), jv_number(tm->tm_mon), jv_number(tm->tm_mday), jv_number(tm->tm_hour), jv_number(tm->tm_min), jv_number(tm->tm_sec), jv_number(tm->tm_wday), - jv_number(tm->tm_yday)); + jv_number(tm->tm_yday), + tm->tm_isdst ? jv_true() : jv_false()); + // JV_ARRAY() only handles up to 9 arguments +#ifdef HAVE_TM_TM_GMTOFF + a = jv_array_append(a, jv_number(tm->tm_gmtoff)); +#else + a = jv_array_append(a, jv_null()); +#endif +#ifdef HAVE_STRUCT_TM_TM_ZONE + if (tm->tm_zone) + return jv_array_append(a, jv_string(tm->tm_zone)); +#else + a = jv_array_append(a, jv_null()); +#endif + return a; } #if defined(WIN32) && !defined(HAVE_SETENV) @@ -1488,7 +1502,10 @@ static jv f_strptime(jq_state *jq, jv a, jv b) { jv_free(n); \ } while (0) -static int jv2tm(jv a, struct tm *tm) { +static int jv2tm(jv a, struct tm *tm, char **freeme) { + int ret = 1; + + *freeme = NULL; memset(tm, 0, sizeof(*tm)); TO_TM_FIELD(tm->tm_year, a, 0); tm->tm_year -= 1900; @@ -1499,6 +1516,34 @@ static int jv2tm(jv a, struct tm *tm) { TO_TM_FIELD(tm->tm_sec, a, 5); TO_TM_FIELD(tm->tm_wday, a, 6); TO_TM_FIELD(tm->tm_yday, a, 7); + jv v = jv_array_get(jv_copy(a), 8); + switch (jv_get_kind(v)) { + case JV_KIND_INVALID: break; + case JV_KIND_FALSE: break; + case JV_KIND_TRUE: tm->tm_isdst = 1; break; + case JV_KIND_NUMBER: tm->tm_isdst = !!(int)jv_number_value(v); break; + default: ret = 0; break; + } + jv_free(v); +#ifdef HAVE_TM_TM_GMTOFF + v = jv_array_get(jv_copy(a), 9); + if (jv_get_kind(v) == JV_KIND_NUMBER) + tm->tm_gmtoff = jv_number_value(v); + else if (jv_is_valid(v)) + ret = 0; + jv_free(v); +#endif +#ifdef HAVE_STRUCT_TM_TM_ZONE + v = jv_array_get(jv_copy(a), 10); + if (jv_get_kind(v) == JV_KIND_STRING) { + tm->tm_zone = *freeme = strdup(jv_string_value(v)); + if (tm->tm_zone == NULL) + ret = 0; + } else if (jv_is_valid(v)) { + ret = 0; + } +#endif + jv_free(v); jv_free(a); // We use UTC everywhere (gettimeofday, gmtime) and UTC does not do DST. @@ -1509,7 +1554,11 @@ static int jv2tm(jv a, struct tm *tm) { // hope it is okay to initialize them to zero, because the standard does not // provide an alternative. - return 1; + if (!ret) { + free(*freeme); + *freeme = NULL; + } + return ret; } #undef TO_TM_FIELD @@ -1520,9 +1569,11 @@ static jv f_mktime(jq_state *jq, jv a) { if (jv_array_length(jv_copy(a)) < 6) return ret_error(a, jv_string("mktime requires parsed datetime inputs")); struct tm tm; - if (!jv2tm(a, &tm)) + char *freeme; + if (!jv2tm(a, &tm, &freeme)) return jv_invalid_with_msg(jv_string("mktime requires parsed datetime inputs")); time_t t = my_mktime(&tm); + free(freeme); if (t == (time_t)-1) return jv_invalid_with_msg(jv_string("invalid gmtime representation")); if (t == (time_t)-2) @@ -1618,7 +1669,8 @@ static jv f_strftime(jq_state *jq, jv a, jv b) { return ret_error2(a, b, jv_string("strftime/1 requires a string format")); } struct tm tm; - if (!jv2tm(a, &tm)) + char *freeme; + if (!jv2tm(a, &tm, &freeme)) return ret_error(b, jv_string("strftime/1 requires parsed datetime inputs")); const char *fmt = jv_string_value(b); @@ -1626,6 +1678,7 @@ static jv f_strftime(jq_state *jq, jv a, jv b) { char *buf = alloca(alloced); size_t n = strftime(buf, alloced, fmt, &tm); jv_free(b); + free(freeme); /* POSIX doesn't provide errno values for strftime() failures; weird */ if (n == 0 || n > alloced) return jv_invalid_with_msg(jv_string("strftime/1: unknown system failure")); @@ -1643,19 +1696,25 @@ static jv f_strftime(jq_state *jq, jv a, jv b) { static jv f_strflocaltime(jq_state *jq, jv a, jv b) { if (jv_get_kind(a) == JV_KIND_NUMBER) { a = f_localtime(jq, a); + if (!jv_is_valid(a)) { + jv_free(b); + return a; + } } else if (jv_get_kind(a) != JV_KIND_ARRAY) { return ret_error2(a, b, jv_string("strflocaltime/1 requires parsed datetime inputs")); } else if (jv_get_kind(b) != JV_KIND_STRING) { return ret_error2(a, b, jv_string("strflocaltime/1 requires a string format")); } struct tm tm; - if (!jv2tm(a, &tm)) + char *freeme; + if (!jv2tm(a, &tm, &freeme)) return ret_error(b, jv_string("strflocaltime/1 requires parsed datetime inputs")); const char *fmt = jv_string_value(b); size_t alloced = strlen(fmt) + 100; char *buf = alloca(alloced); size_t n = strftime(buf, alloced, fmt, &tm); jv_free(b); + free(freeme); /* POSIX doesn't provide errno values for strftime() failures; weird */ if (n == 0 || n > alloced) return jv_invalid_with_msg(jv_string("strflocaltime/1: unknown system failure")); diff --git a/src/util.c b/src/util.c index e35eb9067d..e2edd97622 100644 --- a/src/util.c +++ b/src/util.c @@ -550,7 +550,7 @@ static const unsigned char *find_string(const unsigned char *, int *, const char #define strncasecmp _strnicmp #endif -#ifdef TM_ZONE +#ifdef HAVE_STRUCT_TM_TM_ZONE static char* utc = "UTC"; #endif /* RFC-822/RFC-2822 */ @@ -630,12 +630,12 @@ fromzone(const unsigned char **bp, struct tm *tm, int mandatory) *bp = rp; tm->tm_isdst = 0; /* XXX */ -#ifdef TM_GMTOFF - tm->TM_GMTOFF = tzgetgmtoff(tz, tm->tm_isdst); +#ifdef HAVE_TM_TM_GMTOFF + tm->tm_gmtoff = tzgetgmtoff(tz, tm->tm_isdst); #endif -#ifdef TM_ZONE +#ifdef HAVE_STRUCT_TM_TM_ZONE // Can't use tzgetname() here because we are going to free() - tm->TM_ZONE = NULL; /* XXX */ + tm->tm_zone = NULL; /* XXX */ #endif // tzfree(tz); return 1; @@ -993,11 +993,11 @@ again: switch (c = *fmt++) { if (!delim(*bp)) goto namedzone; tm->tm_isdst = 0; -#ifdef TM_GMTOFF - tm->TM_GMTOFF = 0; +#ifdef HAVE_TM_TM_GMTOFF + tm->tm_gmtoff = 0; #endif -#ifdef TM_ZONE - tm->TM_ZONE = utc; +#ifdef HAVE_STRUCT_TM_TM_ZONE + tm->tm_zone = utc; #endif continue; case '+': @@ -1014,30 +1014,30 @@ again: switch (c = *fmt++) { if (delim(bp[1]) && ((*bp >= 'A' && *bp <= 'I') || (*bp >= 'L' && *bp <= 'Y'))) { -#ifdef TM_GMTOFF +#ifdef HAVE_TM_TM_GMTOFF /* Argh! No 'J'! */ if (*bp >= 'A' && *bp <= 'I') - tm->TM_GMTOFF = + tm->tm_gmtoff = (int)*bp - ('A' - 1); else if (*bp >= 'L' && *bp <= 'M') - tm->TM_GMTOFF = (int)*bp - 'A'; + tm->tm_gmtoff = (int)*bp - 'A'; else if (*bp >= 'N' && *bp <= 'Y') - tm->TM_GMTOFF = 'M' - (int)*bp; - tm->TM_GMTOFF *= SECSPERHOUR; + tm->tm_gmtoff = 'M' - (int)*bp; + tm->tm_gmtoff *= SECSPERHOUR; #endif -#ifdef TM_ZONE - tm->TM_ZONE = NULL; /* XXX */ +#ifdef HAVE_STRUCT_TM_TM_ZONE + tm->tm_zone = NULL; /* XXX */ #endif bp++; continue; } /* 'J' is local time */ if (delim(bp[1]) && *bp == 'J') { -#ifdef TM_GMTOFF - tm->TM_GMTOFF = -timezone; +#ifdef HAVE_TM_TM_GMTOFF + tm->tm_gmtoff = -timezone; #endif -#ifdef TM_ZONE - tm->TM_ZONE = NULL; /* XXX */ +#ifdef HAVE_STRUCT_TM_TM_ZONE + tm->tm_zone = NULL; /* XXX */ #endif bp++; continue; @@ -1052,11 +1052,11 @@ again: switch (c = *fmt++) { goto loadzone; ep = find_string(bp, &i, nast, NULL, 4); if (ep != NULL) { -#ifdef TM_GMTOFF - tm->TM_GMTOFF = (-5 - i) * SECSPERHOUR; +#ifdef HAVE_TM_TM_GMTOFF + tm->tm_gmtoff = (-5 - i) * SECSPERHOUR; #endif -#ifdef TM_ZONE - tm->TM_ZONE = __UNCONST(nast[i]); +#ifdef HAVE_STRUCT_TM_TM_ZONE + tm->tm_zone = __UNCONST(nast[i]); #endif bp = ep; continue; @@ -1064,11 +1064,11 @@ again: switch (c = *fmt++) { ep = find_string(bp, &i, nadt, NULL, 4); if (ep != NULL) { tm->tm_isdst = 1; -#ifdef TM_GMTOFF - tm->TM_GMTOFF = (-4 - i) * SECSPERHOUR; +#ifdef HAVE_TM_TM_GMTOFF + tm->tm_gmtoff = (-4 - i) * SECSPERHOUR; #endif -#ifdef TM_ZONE - tm->TM_ZONE = __UNCONST(nadt[i]); +#ifdef HAVE_STRUCT_TM_TM_ZONE + tm->tm_zone = __UNCONST(nadt[i]); #endif bp = ep; continue; @@ -1081,11 +1081,11 @@ again: switch (c = *fmt++) { NULL, 2); if (ep != NULL) { tm->tm_isdst = i; -#ifdef TM_GMTOFF - tm->TM_GMTOFF = -timezone; +#ifdef HAVE_TM_TM_GMTOFF + tm->tm_gmtoff = -timezone; #endif -#ifdef TM_ZONE - tm->TM_ZONE = tzname[i]; +#ifdef HAVE_STRUCT_TM_TM_ZONE + tm->tm_zone = tzname[i]; #endif bp = ep; continue; @@ -1138,11 +1138,11 @@ again: switch (c = *fmt++) { if (neg) offs = -offs; tm->tm_isdst = 0; /* XXX */ -#ifdef TM_GMTOFF - tm->TM_GMTOFF = offs; +#ifdef HAVE_TM_TM_GMTOFF + tm->tm_gmtoff = offs; #endif -#ifdef TM_ZONE - tm->TM_ZONE = NULL; /* XXX */ +#ifdef HAVE_STRUCT_TM_TM_ZONE + tm->tm_zone = NULL; /* XXX */ #endif continue; diff --git a/tests/jq.test b/tests/jq.test index 9d0b59285b..3738158374 100644 --- a/tests/jq.test +++ b/tests/jq.test @@ -1563,9 +1563,9 @@ strftime("%A, %B %d, %Y") 1435677542.822351 "Tuesday, June 30, 2015" -gmtime +gmtime|(.[9] |= if .==null then 0 else . end)|(.[10] |= if (.=="UTC") or (.==null) then "GMT" else . end) 1425599507 -[2015,2,5,23,51,47,4,63] +[2015,2,5,23,51,47,4,63,false,0,"GMT"] # test invalid tm input try strftime("%Y-%m-%dT%H:%M:%SZ") catch . diff --git a/tests/man.test b/tests/man.test index 822de5ab9a..b11474d959 100644 --- a/tests/man.test +++ b/tests/man.test @@ -718,7 +718,7 @@ fromdate "2015-03-05T23:51:47Z" 1425599507 -strptime("%Y-%m-%dT%H:%M:%SZ") +strptime("%Y-%m-%dT%H:%M:%SZ")[0:8] "2015-03-05T23:51:47Z" [2015,2,5,23,51,47,4,63] diff --git a/tests/optional.test b/tests/optional.test index 85bc9e9941..f1a00d0b35 100644 --- a/tests/optional.test +++ b/tests/optional.test @@ -3,14 +3,14 @@ # strptime() is not available on mingw/WIN32 [strptime("%Y-%m-%dT%H:%M:%SZ")|(.,mktime)] "2015-03-05T23:51:47Z" -[[2015,2,5,23,51,47,4,63],1425599507] +[[2015,2,5,23,51,47,4,63,false,0],1425599507] # Check day-of-week and day of year computations # (should trip an assert if this fails) # This date range last(range(365 * 67)|("1970-03-01T01:02:03Z"|strptime("%Y-%m-%dT%H:%M:%SZ")|mktime) + (86400 * .)|strftime("%Y-%m-%dT%H:%M:%SZ")|strptime("%Y-%m-%dT%H:%M:%SZ")) null -[2037,1,11,1,2,3,3,41] +[2037,1,11,1,2,3,3,41,false,0] # %e is not available on mingw/WIN32 strftime("%A, %B %e, %Y")