aboutsummaryrefslogtreecommitdiff
path: root/toys/posix
diff options
context:
space:
mode:
authorElliott Hughes <enh@google.com>2019-03-12 18:00:26 -0700
committerRob Landley <rob@landley.net>2019-03-13 18:03:41 -0500
commit7aa276db3c0f6e335914401f961d6e76587b141b (patch)
tree64c23c1765052d2e0bb09fad7a8ac9878523f0af /toys/posix
parent1c17ba88c7f12166035f158e8733c053441e6413 (diff)
downloadtoybox-7aa276db3c0f6e335914401f961d6e76587b141b.tar.gz
date: fix various time zone/daylight time issues.
Sunday's transition in the US broke a bunch of the tests. Worse, it broke some of the QA folks' scripts. Finally, the boil that is date's handling of time zones and daylight time has come to a head... This patch fixes the newly-failing tests *and* the other tests that were checked in failing to serve as TODOs. I've resolved the test TODOs about whether implied year/century in POSIX format should mean the current year or 1900 in favor of the current year. Both busybox and coreutils agree, and Rob fixed the code recently so toybox agrees too, but without fixing the tests. I've switched tests from Europe/London to Europe/Berlin to avoid disagreements between C libraries about whether to say "GMT" or "UTC" when daylight savings is not in force. The majority of this patch implements what I'd been unsuccessfully trying to explain on the list: that to correctly implement the distinct input and output time zones (as demonstrated in the three failing tests we've been carrying around for a while), we should switch to working with time_t internally rather than struct tm. I've also added the code to temporarily switch to the input time zone (and back again). All the tests now pass.
Diffstat (limited to 'toys/posix')
-rw-r--r--toys/posix/date.c157
1 files changed, 94 insertions, 63 deletions
diff --git a/toys/posix/date.c b/toys/posix/date.c
index 09a548f5..43b558a5 100644
--- a/toys/posix/date.c
+++ b/toys/posix/date.c
@@ -29,6 +29,9 @@ config DATE
YYYY-MM-DD [hh:mm[:ss]] ISO 8601
hh:mm[:ss] 24-hour time today
+ All input formats can be preceded by TZ="id" to set the input time zone
+ separately from the output time zone. Otherwise $TZ sets both.
+
+FORMAT specifies display format string using strftime(3) syntax:
%% literal % %n newline %t tab
@@ -41,7 +44,7 @@ config DATE
%N nanosec (output only)
%U Week of year (0-53 start sunday) %W Week of year (0-53 start monday)
- %V Week of year (1-53 start monday, week < 4 days not part of this year)
+ %V Week of year (1-53 start monday, week < 4 days not part of this year)
%D = "%m/%d/%y" %r = "%I : %M : %S %p" %T = "%H:%M:%S" %h = "%b"
%x locale date %X locale time %c locale date/time
@@ -56,10 +59,25 @@ GLOBALS(
unsigned nano;
)
-// Handle default posix date format (mmddhhmm[[cc]yy]) or @UNIX[.FRAC]
-// returns 0 success, nonzero for error
-static int parse_default(char *str, struct tm *tm)
+static void check_range(int a, int low, int high)
+{
+ if (a<low) error_exit("%d<%d", a, low);
+ if (a>high) error_exit("%d>%d", a, high);
+}
+
+static void check_tm(struct tm *tm)
{
+ check_range(tm->tm_sec, 0, 60);
+ check_range(tm->tm_min, 0, 59);
+ check_range(tm->tm_hour, 0, 23);
+ check_range(tm->tm_mday, 1, 31);
+ check_range(tm->tm_mon, 0, 11);
+}
+
+// Returns 0 success, nonzero for error.
+static int parse_formats(char *str, time_t *t)
+{
+ struct tm tm;
time_t now;
int len = 0, i;
char *formats[] = {
@@ -71,12 +89,9 @@ static int parse_default(char *str, struct tm *tm)
// Parse @UNIXTIME[.FRACTION]
if (*str == '@') {
long long ll;
- time_t tt;
- // Collect seconds and nanoseconds
- // Note: struct tm hasn't got a fractional seconds field, thus strptime()
- // doesn't support it, so store nanoseconds out of band (in globals).
- // tt and ll are separate because we can't guarantee time_t is 64 bit (yet).
+ // Collect seconds and nanoseconds.
+ // &ll is not just t because we can't guarantee time_t is 64 bit (yet).
sscanf(str, "@%lld%n", &ll, &len);
if (str[len]=='.') {
str += len+1;
@@ -86,9 +101,7 @@ static int parse_default(char *str, struct tm *tm)
}
}
if (str[len]) return 1;
- tt = ll;
- localtime_r(&tt, tm);
-
+ *t = ll;
return 0;
}
@@ -97,25 +110,28 @@ static int parse_default(char *str, struct tm *tm)
char *p;
now = time(0);
- localtime_r(&now, tm);
- tm->tm_hour = tm->tm_min = tm->tm_sec = 0;
- if ((p = strptime(str, formats[i], tm)) && !*p) return 0;
+ localtime_r(&now, &tm);
+ tm.tm_hour = tm.tm_min = tm.tm_sec = 0;
+ tm.tm_isdst = -1;
+ if ((p = strptime(str,formats[i],&tm)) && !*p) {
+ if ((*t = mktime(&tm)) != -1) return 0;
+ }
}
// Posix format?
- sscanf(str, "%2u%2u%2u%2u%n", &tm->tm_mon, &tm->tm_mday, &tm->tm_hour,
- &tm->tm_min, &len);
+ sscanf(str, "%2u%2u%2u%2u%n", &tm.tm_mon, &tm.tm_mday, &tm.tm_hour,
+ &tm.tm_min, &len);
if (len != 8) return 1;
str += len;
- tm->tm_mon--;
+ tm.tm_mon--;
- // If year specified, overwrite one we fetched earlier
+ // If year specified, overwrite one we fetched earlier.
if (*str && *str != '.') {
unsigned year;
len = 0;
sscanf(str, "%u%n", &year, &len);
- if (len == 4) tm->tm_year = year - 1900;
+ if (len == 4) tm.tm_year = year - 1900;
else if (len != 2) return 1;
str += len;
@@ -123,33 +139,55 @@ static int parse_default(char *str, struct tm *tm)
// A "future" date in past is a century ahead.
// A non-future date in the future is a century behind.
if (len == 2) {
- unsigned r1 = tm->tm_year % 100, r2 = (tm->tm_year + 50) % 100,
- century = tm->tm_year - r1;
+ unsigned r1 = tm.tm_year % 100, r2 = (tm.tm_year + 50) % 100,
+ century = tm.tm_year - r1;
if ((r1 < r2) ? (r1 < year && year < r2) : (year < r1 || year > r2)) {
if (year < r1) year += 100;
} else if (year > r1) year -= 100;
- tm->tm_year = year + century;
+ tm.tm_year = year + century;
}
}
+ // Fractional part?
if (*str == '.') {
len = 0;
- sscanf(str, ".%u%n", &tm->tm_sec, &len);
+ sscanf(str, ".%u%n", &tm.tm_sec, &len);
str += len;
- } else tm->tm_sec = 0;
+ } else tm.tm_sec = 0;
- // Fix up weekday
- now = mktime(tm);
- localtime_r(&now, tm);
+ // Does that look like a valid date?
+ check_tm(&tm);
+ if ((*t = mktime(&tm)) == -1) return 1;
- // shouldn't be any trailing garbage
+ // Shouldn't be any trailing garbage.
return *str;
}
-static void check_range(int a, int low, int high)
+// Handles any leading `TZ="blah" ` in the input string.
+static int parse_date(char *str, time_t *t)
{
- if (a<low) error_exit("%d<%d", a, low);
- if (a>high) error_exit("%d>%d", a, high);
+ char *new_tz = NULL, *old_tz;
+ int result;
+
+ if (!strncmp(str, "TZ=\"", 4)) {
+ // Extract the time zone and skip any whitespace.
+ new_tz = str+4;
+ str = strchr(new_tz, '"');
+ if (!str) return 1;
+ *str++ = '\0';
+ while (isspace(*str)) ++str;
+
+ // Switch $TZ.
+ old_tz = getenv("TZ");
+ setenv("TZ", new_tz, 1);
+ tzset();
+ }
+ result = parse_formats(str, t);
+ if (new_tz) {
+ if (old_tz) setenv("TZ", old_tz, 1);
+ else unsetenv("TZ");
+ }
+ return result;
}
// Print strftime plus %N escape(s). note: modifies fmt for %N
@@ -183,17 +221,25 @@ static void puts_time(char *fmt, struct tm *tm)
void date_main(void)
{
- char *setdate = *toys.optargs, *format_string = "%a %b %e %H:%M:%S %Z %Y";
- struct tm tm;
-
- memset(&tm, 0, sizeof(struct tm));
+ char *setdate = *toys.optargs, *format_string = "%a %b %e %H:%M:%S %Z %Y",
+ *tz = NULL;
+ time_t t;
+
+ if (FLAG(u)) {
+ tz = getenv("TZ");
+ setenv("TZ", "UTC", 1);
+ tzset();
+ }
if (TT.d) {
if (TT.D) {
+ struct tm tm = {};
char *s = strptime(TT.d, TT.D+(*TT.D=='+'), &tm);
if (!s || *s) goto bad_showdate;
- } else if (parse_default(TT.d, &tm)) goto bad_showdate;
+ check_tm(&tm);
+ if ((t = mktime(&tm)) == -1) goto bad_showdate;
+ } else if (parse_date(TT.d, &t)) goto bad_showdate;
} else {
struct timespec ts;
struct stat st;
@@ -203,7 +249,7 @@ void date_main(void)
ts = st.st_mtim;
} else clock_gettime(CLOCK_REALTIME, &ts);
- ((toys.optflags & FLAG_u) ? gmtime_r : localtime_r)(&ts.tv_sec, &tm);
+ t = ts.tv_sec;
TT.nano = ts.tv_nsec;
}
@@ -216,37 +262,22 @@ void date_main(void)
// Set the date
} else if (setdate) {
- char *tz;
struct timeval tv;
- int u = toys.optflags & FLAG_u;
-
- if (parse_default(setdate, &tm)) goto bad_setdate;
-
- check_range(tm.tm_sec, 0, 60);
- check_range(tm.tm_min, 0, 59);
- check_range(tm.tm_hour, 0, 23);
- check_range(tm.tm_mday, 1, 31);
- check_range(tm.tm_mon, 0, 11);
-
- if (u) {
- tz = getenv("TZ");
- setenv("TZ", "UTC", 1);
- tzset();
- }
- errno = 0;
- tv.tv_sec = mktime(&tm);
- if (errno) goto bad_setdate;
- if (u) {
- if (tz) setenv("TZ", tz, 1);
- else unsetenv("TZ");
- tzset();
- }
+ if (parse_date(setdate, &t)) goto bad_setdate;
+ tv.tv_sec = t;
tv.tv_usec = TT.nano/1000;
if (settimeofday(&tv, NULL) < 0) perror_msg("cannot set date");
}
- puts_time(format_string, &tm);
+ puts_time(format_string, localtime(&t));
+
+ if (FLAG(u)) {
+ if (tz) setenv("TZ", tz, 1);
+ else unsetenv("TZ");
+ tzset();
+ }
+
return;
bad_showdate: