diff options
-rw-r--r-- | tests/date.test | 25 | ||||
-rw-r--r-- | toys/posix/date.c | 157 |
2 files changed, 106 insertions, 76 deletions
diff --git a/tests/date.test b/tests/date.test index fd45773f..285d3dec 100644 --- a/tests/date.test +++ b/tests/date.test @@ -6,29 +6,29 @@ # Use a consistent TZ for these tests, but not GMT/UTC because that # makes mistakes harder to spot. -tz=Europe/London +tz=Europe/Berlin # Unix date parsing. -testing "-d @0" "TZ=$tz date -d @0 2>&1" "Thu Jan 1 01:00:00 BST 1970\n" "" "" +testing "-d @0" "TZ=$tz date -d @0 2>&1" "Thu Jan 1 01:00:00 CET 1970\n" "" "" testing "-d @0x123 invalid" "TZ=$tz date -d @0x123 2>/dev/null || echo expected error" "expected error\n" "" "" # POSIX format with 2- and 4-digit years. # All SKIP_HOST=1 because coreutils rejects POSIX format dates supplied to -d. -# TODO: busybox thinks this should use the current year, not 1900, which would make more sense? +# These expected values are from running on the host without -d (not as root!). SKIP_HOST=1 testing "-d MMDDhhmm" \ - "TZ=$tz date -d 06021234 2>&1" "Sun Jun 2 12:34:00 UTC 1900\n" "" "" + "TZ=$tz date -d 06021234 2>&1" "Sun Jun 2 12:34:00 CEST 2019\n" "" "" SKIP_HOST=1 testing "-d MMDDhhmmYY.SS" \ - "TZ=$tz date -d 1110143115.30 2>&1" "Sun Nov 10 14:31:30 UTC 1915\n" "" "" + "TZ=$tz date -d 1110143115.30 2>&1" "Tue Nov 10 14:31:30 CET 2015\n" "" "" # busybox thinks this is the year 603 (ISO time 0602-12-34 19:82 with out of range fields normalized). SKIP_HOST=1 testing "-d MMDDhhmmCCYY" \ - "TZ=$tz date -d 060212341982 2>&1" "Sun Jun 2 12:34:00 UTC 1982\n" "" "" + "TZ=$tz date -d 060212341982 2>&1" "Wed Jun 2 12:34:00 CEST 1982\n" "" "" SKIP_HOST=1 testing "-d MMDDhhmmCCYY.SS" \ - "TZ=$tz date -d 111014312015.30 2>&1" "Sun Nov 10 14:31:30 UTC 2015\n" "" "" + "TZ=$tz date -d 111014312015.30 2>&1" "Tue Nov 10 14:31:30 CET 2015\n" "" "" # ISO date format. -testing "-d 1980-01-02" "TZ=$tz date -d 1980-01-02 2>&1" "Wed Jan 2 00:00:00 GMT 1980\n" "" "" -testing "-d 1980-01-02 12:34" "TZ=$tz date -d '1980-01-02 12:34' 2>&1" "Wed Jan 2 12:34:00 GMT 1980\n" "" "" -testing "-d 1980-01-02 12:34:56" "TZ=$tz date -d '1980-01-02 12:34:56' 2>&1" "Wed Jan 2 12:34:56 GMT 1980\n" "" "" +testing "-d 1980-01-02" "TZ=$tz date -d 1980-01-02 2>&1" "Wed Jan 2 00:00:00 CET 1980\n" "" "" +testing "-d 1980-01-02 12:34" "TZ=$tz date -d '1980-01-02 12:34' 2>&1" "Wed Jan 2 12:34:00 CET 1980\n" "" "" +testing "-d 1980-01-02 12:34:56" "TZ=$tz date -d '1980-01-02 12:34:56' 2>&1" "Wed Jan 2 12:34:56 CET 1980\n" "" "" # Reject Unix times without a leading @. testing "Unix time missing @" "TZ=$tz date 1438053157 2>/dev/null || echo no" \ @@ -51,7 +51,6 @@ testing "just %" "touch -d 2012-01-23T12:34:56.123456789 f && date -r f +%" "%\n rm -f f # Test embedded TZ to take a date in one time zone and display it in another. -# TODO: not yet working correctly in toybox. -testing "TZ=" "TZ='America/Los_Angeles' date -d 'TZ=\"Europe/London\" 2018-01-04 08:00'" "Thu Jan 4 00:00:00 PST 2018\n" "" "" -testing "TZ=" "TZ='America/Los_Angeles' date -d 'TZ=\"Europe/London\" 2018-10-04 08:00'" "Thu Oct 4 00:00:00 PDT 2018\n" "" "" +testing "TZ=" "TZ='America/Los_Angeles' date -d 'TZ=\"Europe/Berlin\" 2018-01-04 08:00'" "Wed Jan 3 23:00:00 PST 2018\n" "" "" +testing "TZ=" "TZ='America/Los_Angeles' date -d 'TZ=\"Europe/Berlin\" 2018-10-04 08:00'" "Wed Oct 3 23:00:00 PDT 2018\n" "" "" testing "TZ= @" "TZ='America/Los_Angeles' date -d 'TZ=\"GMT\" @1533427200'" "Sat Aug 4 17:00:00 PDT 2018\n" "" "" 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: |