diff options
Diffstat (limited to 'corebinutils/date/date.c')
| -rw-r--r-- | corebinutils/date/date.c | 879 |
1 files changed, 879 insertions, 0 deletions
diff --git a/corebinutils/date/date.c b/corebinutils/date/date.c new file mode 100644 index 0000000000..86c6896199 --- /dev/null +++ b/corebinutils/date/date.c @@ -0,0 +1,879 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1985, 1987, 1988, 1993 + * The Regents of the University of California. All rights reserved. + * + * Copyright (c) 2026 + * Project Tick. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include <sys/stat.h> +#include <sys/types.h> + +#include <ctype.h> +#include <errno.h> +#include <inttypes.h> +#include <limits.h> +#include <locale.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#include "vary.h" + +#ifndef TM_YEAR_BASE +#define TM_YEAR_BASE 1900 +#endif + +#define DEFAULT_PLUS_FORMAT "%a %b %e %T %Z %Y" + +struct iso8601_fmt { + const char *refname; + const char *format_string; + bool include_zone; +}; + +struct strbuf { + char *data; + size_t len; + size_t cap; +}; + +struct options { + const char *input_format; + const char *output_zone; + const char *reference_arg; + const char *time_operand; + const char *format_operand; + struct vary *vary_chain; + const struct iso8601_fmt *iso8601_selected; + bool no_set; + bool rfc2822; + bool use_utc; +}; + +static const struct iso8601_fmt iso8601_fmts[] = { + { "date", "%Y-%m-%d", false }, + { "hours", "%Y-%m-%dT%H", true }, + { "minutes", "%Y-%m-%dT%H:%M", true }, + { "seconds", "%Y-%m-%dT%H:%M:%S", true }, + { "ns", "%Y-%m-%dT%H:%M:%S,%N", true }, +}; + +static const char *progname = "date"; +static const char *rfc2822_format = "%a, %d %b %Y %T %z"; + +static void usage(void) __attribute__((__noreturn__)); +static void die(const char *fmt, ...) __attribute__((__noreturn__, format(printf, 1, 2))); +static void die_errno(const char *fmt, ...) __attribute__((__noreturn__, format(printf, 1, 2))); + +static void strbuf_init(struct strbuf *buf); +static void strbuf_reserve(struct strbuf *buf, size_t extra); +static void strbuf_append_mem(struct strbuf *buf, const char *data, size_t len); +static void strbuf_append_char(struct strbuf *buf, char ch); +static void strbuf_append_str(struct strbuf *buf, const char *text); +static char *strbuf_finish(struct strbuf *buf); + +static void set_progname(const char *argv0); +static void parse_args(int argc, char **argv, struct options *options); +static const char *require_option_arg(int argc, char **argv, int *index, + const char *arg, const char *option_name); +static void parse_iso8601_arg(struct options *options, const char *arg); +static void parse_operands(int argc, char **argv, int start_index, + struct options *options); +static void validate_options(const struct options *options); +static void set_timezone_or_die(const char *tz_value, const char *what); +static time_t int64_to_time_t(int64_t value, const char *what); +static void read_reference_time(const char *arg, struct timespec *ts); +static void read_current_time(struct timespec *ts, struct timespec *resolution); +static void localtime_or_die(time_t value, struct tm *tm); +static void parse_legacy_time(const char *text, const struct timespec *base, + struct timespec *ts); +static void parse_formatted_time(const char *format, const char *text, + const struct timespec *base, struct timespec *ts); +static void parse_time_operand(const struct options *options, + const struct timespec *base, struct timespec *ts); +static void set_system_time(const struct timespec *ts); +static void apply_variations(const struct options *options, struct tm *tm); +static char *expand_format_string(const char *format, long nsec, long resolution); +static void append_nsec_digits(struct strbuf *buf, const char *pending, size_t len, + long nsec, long resolution); +static char *render_format(const char *format, const struct tm *tm, long nsec, + long resolution); +static char *render_iso8601(const struct iso8601_fmt *selection, + const struct tm *tm, long nsec, long resolution); +static char *render_numeric_timezone(const struct tm *tm); +static void print_line_and_exit(const char *text) __attribute__((__noreturn__)); + +int +main(int argc, char **argv) +{ + struct options options; + struct timespec ts; + struct timespec resolution = { 0, 1 }; + struct tm tm; + const char *format; + char *output; + + set_progname(argv[0]); + parse_args(argc, argv, &options); + validate_options(&options); + + setlocale(LC_TIME, ""); + + if (options.use_utc) + set_timezone_or_die("UTC0", "TZ=UTC0"); + + if (options.reference_arg != NULL) + read_reference_time(options.reference_arg, &ts); + else + read_current_time(&ts, &resolution); + + if (options.time_operand != NULL) { + parse_time_operand(&options, &ts, &ts); + if (!options.no_set) + set_system_time(&ts); + } else if (options.input_format != NULL) { + die("option -f requires an input date operand"); + } + + if (options.output_zone != NULL) + set_timezone_or_die(options.output_zone, "TZ"); + + localtime_or_die(ts.tv_sec, &tm); + apply_variations(&options, &tm); + + if (options.iso8601_selected != NULL) { + output = render_iso8601(options.iso8601_selected, &tm, ts.tv_nsec, + resolution.tv_nsec); + print_line_and_exit(output); + } + + if (options.rfc2822) { + if (setlocale(LC_TIME, "C") == NULL) + die("failed to activate C locale for -R"); + format = rfc2822_format; + } else if (options.format_operand != NULL) { + format = options.format_operand; + } else { + format = "%+"; + } + + output = render_format(format, &tm, ts.tv_nsec, resolution.tv_nsec); + print_line_and_exit(output); +} + +static void +strbuf_init(struct strbuf *buf) +{ + buf->data = NULL; + buf->len = 0; + buf->cap = 0; +} + +static void +strbuf_reserve(struct strbuf *buf, size_t extra) +{ + size_t needed; + size_t newcap; + char *newdata; + + needed = buf->len + extra + 1; + if (needed <= buf->cap) + return; + + newcap = buf->cap == 0 ? 64 : buf->cap; + while (newcap < needed) { + if (newcap > SIZE_MAX / 2) + die("buffer too large"); + newcap *= 2; + } + + newdata = realloc(buf->data, newcap); + if (newdata == NULL) + die_errno("realloc"); + + buf->data = newdata; + buf->cap = newcap; +} + +static void +strbuf_append_mem(struct strbuf *buf, const char *data, size_t len) +{ + strbuf_reserve(buf, len); + memcpy(buf->data + buf->len, data, len); + buf->len += len; + buf->data[buf->len] = '\0'; +} + +static void +strbuf_append_char(struct strbuf *buf, char ch) +{ + strbuf_reserve(buf, 1); + buf->data[buf->len++] = ch; + buf->data[buf->len] = '\0'; +} + +static void +strbuf_append_str(struct strbuf *buf, const char *text) +{ + strbuf_append_mem(buf, text, strlen(text)); +} + +static char * +strbuf_finish(struct strbuf *buf) +{ + char *result; + + if (buf->data == NULL) { + result = strdup(""); + if (result == NULL) + die_errno("strdup"); + return (result); + } + + result = buf->data; + buf->data = NULL; + buf->len = 0; + buf->cap = 0; + return (result); +} + +static void +set_progname(const char *argv0) +{ + const char *base; + + if (argv0 == NULL || argv0[0] == '\0') + return; + + base = strrchr(argv0, '/'); + progname = base != NULL ? base + 1 : argv0; +} + +static void +parse_args(int argc, char **argv, struct options *options) +{ + int i; + int j; + + memset(options, 0, sizeof(*options)); + + for (i = 1; i < argc; i++) { + const char *arg; + + arg = argv[i]; + if (arg[0] != '-' || arg[1] == '\0') + break; + if (strcmp(arg, "--") == 0) { + i++; + break; + } + + if (strncmp(arg, "-I", 2) == 0) { + parse_iso8601_arg(options, arg[2] == '\0' ? NULL : arg + 2); + continue; + } + if (strncmp(arg, "-f", 2) == 0) { + options->input_format = require_option_arg(argc, argv, &i, + arg + 2, "-f"); + continue; + } + if (strncmp(arg, "-r", 2) == 0) { + options->reference_arg = require_option_arg(argc, argv, &i, + arg + 2, "-r"); + continue; + } + if (strncmp(arg, "-v", 2) == 0) { + options->vary_chain = vary_append(options->vary_chain, + require_option_arg(argc, argv, &i, arg + 2, "-v")); + continue; + } + if (strncmp(arg, "-z", 2) == 0) { + options->output_zone = require_option_arg(argc, argv, &i, + arg + 2, "-z"); + continue; + } + + for (j = 1; arg[j] != '\0'; j++) { + switch (arg[j]) { + case 'j': + options->no_set = true; + break; + case 'n': + die("option -n is not supported on Linux"); + case 'R': + options->rfc2822 = true; + break; + case 'u': + options->use_utc = true; + break; + default: + usage(); + } + } + } + + parse_operands(argc, argv, i, options); +} + +static const char * +require_option_arg(int argc, char **argv, int *index, const char *arg, + const char *option_name) +{ + if (arg != NULL && arg[0] != '\0') + return (arg); + if (*index + 1 >= argc) + die("option %s requires an argument", option_name); + (*index)++; + return (argv[*index]); +} + +static void +parse_iso8601_arg(struct options *options, const char *arg) +{ + size_t i; + + options->iso8601_selected = &iso8601_fmts[0]; + if (arg == NULL) + return; + + for (i = 0; i < sizeof(iso8601_fmts) / sizeof(iso8601_fmts[0]); i++) { + if (strcmp(arg, iso8601_fmts[i].refname) == 0) { + options->iso8601_selected = &iso8601_fmts[i]; + return; + } + } + + die("invalid argument '%s' for -I", arg); +} + +static void +parse_operands(int argc, char **argv, int start_index, struct options *options) +{ + int i; + + for (i = start_index; i < argc; i++) { + if (argv[i][0] == '+') { + if (options->format_operand != NULL) + usage(); + options->format_operand = argv[i] + 1; + continue; + } + if (options->time_operand != NULL) + usage(); + options->time_operand = argv[i]; + } +} + +static void +validate_options(const struct options *options) +{ + if (options->iso8601_selected != NULL && options->rfc2822) + die("multiple output formats specified"); + if (options->iso8601_selected != NULL && options->format_operand != NULL) + die("multiple output formats specified"); + if (options->rfc2822 && options->format_operand != NULL) + die("multiple output formats specified"); +} + +static void +set_timezone_or_die(const char *tz_value, const char *what) +{ + if (setenv("TZ", tz_value, 1) != 0) + die_errno("setenv(%s)", what); + tzset(); +} + +static time_t +int64_to_time_t(int64_t value, const char *what) +{ + time_t converted; + + converted = (time_t)value; + if ((int64_t)converted != value) + die("%s out of range: %" PRId64, what, value); + return (converted); +} + +static void +read_reference_time(const char *arg, struct timespec *ts) +{ + struct stat sb; + char *end; + long long seconds; + + errno = 0; + seconds = strtoll(arg, &end, 10); + if (errno == 0 && arg[0] != '\0' && *end == '\0') { + ts->tv_sec = int64_to_time_t((int64_t)seconds, "seconds"); + ts->tv_nsec = 0; + return; + } + + if (stat(arg, &sb) != 0) + die_errno("%s", arg); + + ts->tv_sec = sb.st_mtim.tv_sec; + ts->tv_nsec = sb.st_mtim.tv_nsec; +} + +static void +read_current_time(struct timespec *ts, struct timespec *resolution) +{ + if (clock_gettime(CLOCK_REALTIME, ts) != 0) + die_errno("clock_gettime"); + if (clock_getres(CLOCK_REALTIME, resolution) != 0) + die_errno("clock_getres"); +} + +static void +localtime_or_die(time_t value, struct tm *tm) +{ + if (localtime_r(&value, tm) == NULL) + die("invalid time"); +} + +static int +parse_two_digits(const char *text, const char *what) +{ + if (!isdigit((unsigned char)text[0]) || !isdigit((unsigned char)text[1])) + die("illegal time format"); + return ((text[0] - '0') * 10 + (text[1] - '0')); +} + +static void +convert_tm_or_die(struct tm *tm, struct timespec *ts) +{ + time_t converted; + + tm->tm_yday = -1; + converted = mktime(tm); + if (tm->tm_yday == -1) + die("nonexistent time"); + ts->tv_sec = converted; + ts->tv_nsec = 0; +} + +static void +parse_legacy_time(const char *text, const struct timespec *base, struct timespec *ts) +{ + struct tm tm; + const char *dot; + size_t digits_len; + int second; + + localtime_or_die(base->tv_sec, &tm); + tm.tm_isdst = -1; + ts->tv_nsec = 0; + + dot = strchr(text, '.'); + if (dot != NULL) { + if (strchr(dot + 1, '.') != NULL || strlen(dot + 1) != 2) + die("illegal time format"); + second = parse_two_digits(dot + 1, "seconds"); + if (second > 61) + die("illegal time format"); + tm.tm_sec = second; + digits_len = (size_t)(dot - text); + } else { + tm.tm_sec = 0; + digits_len = strlen(text); + } + + if (digits_len == 0) + die("illegal time format"); + for (size_t i = 0; i < digits_len; i++) { + if (!isdigit((unsigned char)text[i])) + die("illegal time format"); + } + + switch (digits_len) { + case 12: + tm.tm_year = parse_two_digits(text, "century") * 100 - TM_YEAR_BASE; + tm.tm_year += parse_two_digits(text + 2, "year"); + tm.tm_mon = parse_two_digits(text + 4, "month") - 1; + tm.tm_mday = parse_two_digits(text + 6, "day"); + tm.tm_hour = parse_two_digits(text + 8, "hour"); + tm.tm_min = parse_two_digits(text + 10, "minute"); + break; + case 10: { + int year; + + year = parse_two_digits(text, "year"); + tm.tm_year = year < 69 ? year + 100 : year; + tm.tm_mon = parse_two_digits(text + 2, "month") - 1; + tm.tm_mday = parse_two_digits(text + 4, "day"); + tm.tm_hour = parse_two_digits(text + 6, "hour"); + tm.tm_min = parse_two_digits(text + 8, "minute"); + break; + } + case 8: + tm.tm_mon = parse_two_digits(text, "month") - 1; + tm.tm_mday = parse_two_digits(text + 2, "day"); + tm.tm_hour = parse_two_digits(text + 4, "hour"); + tm.tm_min = parse_two_digits(text + 6, "minute"); + break; + case 6: + tm.tm_mday = parse_two_digits(text, "day"); + tm.tm_hour = parse_two_digits(text + 2, "hour"); + tm.tm_min = parse_two_digits(text + 4, "minute"); + break; + case 4: + tm.tm_hour = parse_two_digits(text, "hour"); + tm.tm_min = parse_two_digits(text + 2, "minute"); + break; + case 2: + tm.tm_min = parse_two_digits(text, "minute"); + break; + default: + die("illegal time format"); + } + + if (tm.tm_mon < 0 || tm.tm_mon > 11 || tm.tm_mday < 1 || tm.tm_mday > 31 || + tm.tm_hour < 0 || tm.tm_hour > 23 || tm.tm_min < 0 || tm.tm_min > 59) + die("illegal time format"); + + convert_tm_or_die(&tm, ts); +} + +static void +parse_formatted_time(const char *format, const char *text, const struct timespec *base, + struct timespec *ts) +{ + struct tm tm; + char *end; + + localtime_or_die(base->tv_sec, &tm); + tm.tm_isdst = -1; + end = strptime(text, format, &tm); + if (end == NULL) + die("failed conversion of '%s' using format '%s'", text, format); + if (*end != '\0') + die("input did not fully match format '%s': %s", format, text); + + convert_tm_or_die(&tm, ts); +} + +static void +parse_time_operand(const struct options *options, const struct timespec *base, + struct timespec *ts) +{ + if (options->input_format != NULL) + parse_formatted_time(options->input_format, options->time_operand, base, ts); + else + parse_legacy_time(options->time_operand, base, ts); +} + +static void +set_system_time(const struct timespec *ts) +{ + if (clock_settime(CLOCK_REALTIME, ts) != 0) + die_errno("clock_settime"); +} + +static void +apply_variations(const struct options *options, struct tm *tm) +{ + const struct vary *failed; + + failed = vary_apply(options->vary_chain, tm); + if (failed != NULL) + die("cannot apply date adjustment: %s", failed->arg); +} + +static bool +pending_is_nsec_prefix(const char *pending, size_t len) +{ + size_t i; + + if (len == 0 || pending[0] != '%') + return (false); + for (i = 1; i < len; i++) { + if (pending[i] == '-' && i == 1) + continue; + if (!isdigit((unsigned char)pending[i])) + return (false); + } + return (true); +} + +static void +append_nsec_digits(struct strbuf *buf, const char *pending, size_t len, long nsec, + long resolution) +{ + size_t i; + int width; + int zeroes; + long value; + char digits[32]; + int printed; + + width = 0; + zeroes = 0; + + if (len == 2 && pending[1] == '-') { + long number; + + for (width = 9, number = resolution; width > 0 && number > 0; + width--, number /= 10) + ; + } else if (len > 1) { + for (i = 1; i < len; i++) { + if (width > (INT_MAX - (pending[i] - '0')) / 10) + die("nanosecond width is too large"); + width = width * 10 + (pending[i] - '0'); + } + } + + if (width == 0) { + width = 9; + } else if (width > 9) { + zeroes = width - 9; + width = 9; + } + + value = nsec; + for (i = 0; i < (size_t)(9 - width); i++) + value /= 10; + + printed = snprintf(digits, sizeof(digits), "%0*ld", width, value); + if (printed < 0 || (size_t)printed >= sizeof(digits)) + die("failed to render nanoseconds"); + + strbuf_append_mem(buf, digits, (size_t)printed); + while (zeroes-- > 0) + strbuf_append_char(buf, '0'); +} + +static char * +expand_format_string(const char *format, long nsec, long resolution) +{ + struct strbuf result; + char pending[64]; + size_t pending_len; + size_t i; + bool in_percent; + + strbuf_init(&result); + pending_len = 0; + in_percent = false; + + for (i = 0; format[i] != '\0';) { + if (!in_percent) { + if (format[i] == '%') { + pending[0] = '%'; + pending_len = 1; + in_percent = true; + } else { + strbuf_append_char(&result, format[i]); + } + i++; + continue; + } + + if (format[i] == 'N' && pending_is_nsec_prefix(pending, pending_len)) { + append_nsec_digits(&result, pending, pending_len, nsec, resolution); + in_percent = false; + i++; + continue; + } + if (format[i] == '+' && pending_len == 1) { + strbuf_append_str(&result, DEFAULT_PLUS_FORMAT); + in_percent = false; + i++; + continue; + } + if (format[i] == '%' ) { + strbuf_append_mem(&result, pending, pending_len); + strbuf_append_char(&result, '%'); + in_percent = false; + i++; + continue; + } + if (format[i] == '-' && pending_len == 1) { + pending[pending_len++] = '-'; + i++; + continue; + } + if (isdigit((unsigned char)format[i]) && + pending_is_nsec_prefix(pending, pending_len) && + !(pending_len == 2 && pending[1] == '-')) { + if (pending_len >= sizeof(pending) - 1) + die("format specifier too long"); + pending[pending_len++] = format[i]; + i++; + continue; + } + + strbuf_append_mem(&result, pending, pending_len); + in_percent = false; + } + + if (in_percent) + strbuf_append_mem(&result, pending, pending_len); + + return (strbuf_finish(&result)); +} + +static char * +render_format(const char *format, const struct tm *tm, long nsec, long resolution) +{ + char *expanded; + char *result; + size_t size; + size_t produced; + + if (format[0] == '\0') { + result = strdup(""); + if (result == NULL) + die_errno("strdup"); + return (result); + } + + expanded = expand_format_string(format, nsec, resolution); + size = 128; + for (;;) { + result = malloc(size); + if (result == NULL) + die_errno("malloc"); + + produced = strftime(result, size, expanded, tm); + if (produced != 0) { + free(expanded); + return (result); + } + + free(result); + if (size > 1 << 20) + die("formatted output is too large"); + size *= 2; + } +} + +static char * +render_numeric_timezone(const struct tm *tm) +{ + char raw[16]; + struct strbuf buf; + size_t len; + + len = strftime(raw, sizeof(raw), "%z", tm); + if (len == 0) + die("failed to render timezone offset"); + + if (len != 5 || (raw[0] != '+' && raw[0] != '-')) { + char *copy; + + copy = strdup(raw); + if (copy == NULL) + die_errno("strdup"); + return (copy); + } + + strbuf_init(&buf); + strbuf_append_mem(&buf, raw, 3); + strbuf_append_char(&buf, ':'); + strbuf_append_mem(&buf, raw + 3, 2); + return (strbuf_finish(&buf)); +} + +static char * +render_iso8601(const struct iso8601_fmt *selection, const struct tm *tm, long nsec, + long resolution) +{ + char *timestamp; + char *zone; + struct strbuf result; + + timestamp = render_format(selection->format_string, tm, nsec, resolution); + if (!selection->include_zone) + return (timestamp); + + zone = render_numeric_timezone(tm); + strbuf_init(&result); + strbuf_append_str(&result, timestamp); + strbuf_append_str(&result, zone); + free(timestamp); + free(zone); + return (strbuf_finish(&result)); +} + +static void +print_line_and_exit(const char *text) +{ + if (printf("%s\n", text) < 0 || fflush(stdout) != 0) + die_errno("stdout"); + free((void *)text); + exit(EXIT_SUCCESS); +} + +static void +usage(void) +{ + fprintf(stderr, "%s\n%s\n%s\n", + "usage: date [-jRu] [-I[date|hours|minutes|seconds|ns]] [-f input_fmt]", + " " + "[ -z output_zone ] [-r filename|seconds] [-v[+|-]val[y|m|w|d|H|M|S]]", + " " + "[[[[[[cc]yy]mm]dd]HH]MM[.SS] | new_date] [+output_fmt]"); + exit(EXIT_FAILURE); +} + +static void +die(const char *fmt, ...) +{ + va_list ap; + + fprintf(stderr, "%s: ", progname); + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fputc('\n', stderr); + exit(EXIT_FAILURE); +} + +static void +die_errno(const char *fmt, ...) +{ + int saved_errno; + va_list ap; + + saved_errno = errno; + fprintf(stderr, "%s: ", progname); + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fprintf(stderr, ": %s\n", strerror(saved_errno)); + exit(EXIT_FAILURE); +} |
