From 60a33ca726518a3325e56d77f65bfdcbecf91444 Mon Sep 17 00:00:00 2001 From: Tavian Barnes Date: Tue, 11 Feb 2020 22:20:57 -0500 Subject: Implement explicit reference times (-newerXt) --- parse.c | 180 ++++++++++++++++++++++++++++++++++++++++++++++++- tests.sh | 6 ++ tests/test_newermt.out | 3 + util.c | 54 +++++++++++++++ util.h | 21 ++++++ 5 files changed, 261 insertions(+), 3 deletions(-) create mode 100644 tests/test_newermt.out diff --git a/parse.c b/parse.c index 9f9eabe..373f7c2 100644 --- a/parse.c +++ b/parse.c @@ -619,6 +619,10 @@ static const char *parse_int(const struct parser_state *state, const char *str, case IF_LONG_LONG: *(long long *)result = value; break; + + default: + assert(false); + break; } return endptr; @@ -1615,7 +1619,7 @@ static struct expr *parse_lname(struct parser_state *state, int casefold, int ar return parse_fnmatch(state, expr, casefold); } -/** Get the bfs_stat_field for X/Y in -newerXY */ +/** Get the bfs_stat_field for X/Y in -newerXY. */ static enum bfs_stat_field parse_newerxy_field(char c) { switch (c) { case 'a': @@ -1631,6 +1635,175 @@ static enum bfs_stat_field parse_newerxy_field(char c) { } } +/** Parse some digits from an explicit reference time. */ +static int parse_reftime_part(const struct parser_state *state, const char **str, size_t n, int *result, int delta) { + char buf[n + 1]; + for (size_t i = 0; i < n; ++i, ++*str) { + char c = **str; + if (c < '0' || c > '9') { + return -1; + } + buf[i] = c; + } + buf[n] = '\0'; + + if (!parse_int(state, buf, result, IF_INT | IF_QUIET)) { + return -1; + } + + *result += delta; + return 0; +} + +/** Parse an explicit reference time. */ +static int parse_reftime(const struct parser_state *state, struct expr *expr) { + const char *str = expr->sdata; + struct tm tm = {0}; + + int tz_hour = 0; + int tz_min = 0; + bool tz_negative = false; + bool local = true; + + // YYYY + if (parse_reftime_part(state, &str, 4, &tm.tm_year, -1900) != 0) { + goto invalid; + } + + // MM + if (*str == '-') { + ++str; + } + if (parse_reftime_part(state, &str, 2, &tm.tm_mon, -1) != 0) { + goto invalid; + } + + // DD + if (*str == '-') { + ++str; + } + if (parse_reftime_part(state, &str, 2, &tm.tm_mday, 0) != 0) { + goto invalid; + } + + if (!*str) { + goto end; + } else if (*str == 'T') { + ++str; + } + + // HH + if (parse_reftime_part(state, &str, 2, &tm.tm_hour, 0) != 0) { + goto invalid; + } + + // MM + if (!*str) { + goto end; + } else if (*str == ':') { + ++str; + } + if (parse_reftime_part(state, &str, 2, &tm.tm_min, 0) != 0) { + goto invalid; + } + + // SS + if (!*str) { + goto end; + } else if (*str == ':') { + ++str; + } + if (parse_reftime_part(state, &str, 2, &tm.tm_sec, 0) != 0) { + goto invalid; + } + + if (!*str) { + goto end; + } else if (*str == 'Z') { + local = false; + ++str; + } else if (*str == '+' || *str == '-') { + local = false; + tz_negative = *str == '-'; + ++str; + + // HH + if (parse_reftime_part(state, &str, 2, &tz_hour, 0) != 0) { + goto invalid; + } + + // MM + if (!*str) { + goto end; + } else if (*str == ':') { + ++str; + } + if (parse_reftime_part(state, &str, 2, &tz_min, 0) != 0) { + goto invalid; + } + } else { + goto invalid; + } + + if (*str) { + goto invalid; + } + +end: + if (local) { + expr->reftime.tv_sec = mktime(&tm); + if (expr->reftime.tv_sec == -1) { + perror("mktime()"); + return -1; + } + } else { + expr->reftime.tv_sec = xtimegm(&tm); + if (expr->reftime.tv_sec == -1) { + perror("xtimegm()"); + return -1; + } + + int offset = 60*tz_hour + tz_min; + if (tz_negative) { + offset = -offset; + } + expr->reftime.tv_sec += offset; + } + + expr->reftime.tv_nsec = 0; + return 0; + +invalid: + parse_error(state, "%s %s: Invalid date/time.\n\n", expr->argv[0], expr->argv[1]); + fprintf(stderr, "Supported date/time formats are ISO 8601-like, e.g.\n\n"); + + if (xlocaltime(&state->now.tv_sec, &tm) != 0) { + perror("xlocaltime()"); + return -1; + } + + int year = tm.tm_year + 1900; + int month = tm.tm_mon + 1; + fprintf(stderr, " - %04d-%02d-%02d\n", year, month, tm.tm_mday); + fprintf(stderr, " - %04d-%02d-%02dT%02d:%02d:%02d\n", year, month, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec); + + tz_hour = -timezone/3600; + tz_min = (labs(timezone)/60)%60; + fprintf(stderr, " - %04d-%02d-%02dT%02d:%02d:%02d%+03d:%02d\n", + year, month, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, tz_hour, tz_min); + + if (xgmtime(&state->now.tv_sec, &tm) != 0) { + perror("xgmtime()"); + return -1; + } + + year = tm.tm_year + 1900; + month = tm.tm_mon + 1; + fprintf(stderr, " - %04d-%02d-%02dT%02d:%02d:%02dZ\n", year, month, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec); + + return -1; +} + /** * Parse -newerXY. */ @@ -1653,8 +1826,9 @@ static struct expr *parse_newerxy(struct parser_state *state, int arg1, int arg2 } if (arg[7] == 't') { - parse_error(state, "%s: Explicit reference times ('t') are not supported.\n", arg); - goto fail; + if (parse_reftime(state, expr) != 0) { + goto fail; + } } else { enum bfs_stat_field field = parse_newerxy_field(arg[7]); if (!field) { diff --git a/tests.sh b/tests.sh index dac6f1b..fb73e4d 100755 --- a/tests.sh +++ b/tests.sh @@ -337,6 +337,7 @@ bsd_tests=( test_H_newer test_newerma + test_newermt test_nogroup test_nogroup_ulimit @@ -485,6 +486,7 @@ gnu_tests=( test_H_newer test_newerma + test_newermt test_nogroup test_nogroup_ulimit @@ -1349,6 +1351,10 @@ function test_newerma() { bfs_diff times -newerma times/a } +function test_newermt() { + bfs_diff times -newermt 1991-12-14T00:01 +} + function test_size() { bfs_diff basic -type f -size 0 } diff --git a/tests/test_newermt.out b/tests/test_newermt.out new file mode 100644 index 0000000..650e550 --- /dev/null +++ b/tests/test_newermt.out @@ -0,0 +1,3 @@ +times +times/c +times/l diff --git a/util.c b/util.c index 691402c..88f90aa 100644 --- a/util.c +++ b/util.c @@ -174,6 +174,60 @@ int xlocaltime(const time_t *timep, struct tm *result) { } } +int xgmtime(const time_t *timep, struct tm *result) { + // Should be called before gmtime_r() according to POSIX.1-2004 + tzset(); + + if (gmtime_r(timep, result)) { + return 0; + } else { + return -1; + } +} + +time_t xtimegm(struct tm *tm) { + // Some man pages for timegm() recommend this as a portable approach + time_t ret = -1; + int error; + + char *old_tz = getenv("TZ"); + if (old_tz) { + old_tz = strdup(old_tz); + if (!old_tz) { + error = errno; + goto fail; + } + } + + if (setenv("TZ", "UTC0", true) != 0) { + error = errno; + goto fail; + } + + ret = mktime(tm); + error = errno; + + if (old_tz) { + if (setenv("TZ", old_tz, true) != 0) { + ret = -1; + error = errno; + goto fail; + } + } else { + if (unsetenv("TZ") != 0) { + ret = -1; + error = errno; + goto fail; + } + } + + tzset(); +fail: + free(old_tz); + errno = error; + return ret; +} + void format_mode(mode_t mode, char str[11]) { strcpy(str, "----------"); diff --git a/util.h b/util.h index e7b844e..ef546d4 100644 --- a/util.h +++ b/util.h @@ -170,6 +170,27 @@ char *xregerror(int err, const regex_t *regex); */ int xlocaltime(const time_t *timep, struct tm *result); +/** + * gmtime_r() wrapper that calls tzset() first. + * + * @param timep + * The time_t to convert. + * @param result + * Buffer to hold the result. + * @return 0 on success, -1 on failure. + */ +int xgmtime(const time_t *timep, struct tm *result); + +/** + * A portable timegm(), the inverse of gmtime(). + * + * @param tm + * The struct tm to convert. + * @return + * The converted time on success, or -1 on failure. + */ +time_t xtimegm(struct tm *tm); + /** * Format a mode like ls -l (e.g. -rw-r--r--). * -- cgit v1.2.3