From 40e77ab83085c3b92cafcc45a6be6f75b1b21e73 Mon Sep 17 00:00:00 2001 From: Eike Flath Date: Sun, 21 Apr 2024 15:18:08 +0200 Subject: [PATCH 1/7] Implemented date --- src/date/date.c | 117 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 113 insertions(+), 4 deletions(-) diff --git a/src/date/date.c b/src/date/date.c index 6e6d777..c501a39 100644 --- a/src/date/date.c +++ b/src/date/date.c @@ -1,6 +1,115 @@ +#define _POSIX_C_SOURCE 200112L // for setenv +#define _DEFAULT_SOURCE // for settimeofday + +#include +#include #include +#include +#include +#include +#include + +#define NAME "date (canoutils)" +#define VERSION "1.0.0" +#define AUTHOR "Eike Flath" + +#include "version_info.h" + +#define INITIAL_BUF_SIZE 512 + +static struct tm *get_time(void) { + time_t t; + if (time(&t) == -1) { + fprintf(stderr, "date: failed to retrieve time: %s\n", strerror(errno)); + exit(EXIT_FAILURE); + } + return localtime(&t); +} + +static int set_time(char *str) { +#define s(i) (str[i] - '0') + struct tm tm; + memcpy(&tm, get_time(), sizeof tm); + size_t n = strlen(str); + bool century_specified = false; + switch (n) { + case 12: + tm.tm_year = 100 * (10 * s(n - 4) + s(n - 3)); + century_specified = true; + // fallthrough + case 10: + if (century_specified) { + tm.tm_year += 10 * s(n - 2) + s(n - 1); + } else { + int yy = 10 * s(n - 2) + s(n - 1); + int cc = yy >= 69 ? 19 : 20; + tm.tm_year = 100 * cc + yy; + } + tm.tm_year -= 1900; + // fallthrough + case 8: + tm.tm_mon = 10 * s(0) + s(1) - 1; // tm.tm_mon is in [0,11] + tm.tm_mday = 10 * s(2) + s(3); + tm.tm_hour = 10 * s(4) + s(5); + tm.tm_min = 10 * s(6) + s(7); + tm.tm_isdst = -1; + break; + default: + fprintf(stderr, "%s", + "date: specify time in the format %m%d%H%M[[%C]%y]\n"); + return EXIT_FAILURE; + } +#undef s + time_t t = mktime(&tm); + if (t == -1) { + fprintf(stderr, "date: '%s' does not specify a valid time\n", str); + return EXIT_FAILURE; + } + // printf("%s\n", ctime(&t)); + struct timeval tv = {.tv_sec = t}; + if (settimeofday(&tv, NULL)) { + fprintf(stderr, "date: cannot settimeofday: %s\n", strerror(errno)); + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + +int main(int argc, char **argv) { + char *operand = "+%a %b %e %H:%M:%S %Z %Y"; + bool parse_options = true; + for (int i = 1; i < argc; i++) { + if (parse_options) { + if (!strcmp(argv[i], "--")) { + parse_options = false; + continue; + } + if (!strcmp(argv[i], "--version")) { + print_version(); + return EXIT_SUCCESS; + } + if (!strcmp(argv[i], "-u")) { + if (setenv("TZ", "UTC", 1) == -1) { + fprintf(stderr, "date: cannot setenv TZ: %s\n", strerror(errno)); + return EXIT_FAILURE; + } + continue; + } + } + operand = argv[i]; + } + + if (operand[0] != '+') + return set_time(operand); + + struct tm *tm = get_time(); -int main(void) { - printf("Hello, World!\n"); - return 0; -} \ No newline at end of file + size_t cap = INITIAL_BUF_SIZE; + char *buf = malloc(cap); + while (!strftime(buf, cap, &operand[1], tm)) { + cap *= 2; + buf = realloc(buf, cap); + } + printf("%s\n", buf); + free(buf); + return EXIT_SUCCESS; +} From 2ceb3592f272052dba7593a41e78d7aa29077cdd Mon Sep 17 00:00:00 2001 From: Eike Flath Date: Sun, 21 Apr 2024 17:59:16 +0200 Subject: [PATCH 2/7] prevented date from parsing invalid dates --- src/date/date.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/date/date.c b/src/date/date.c index c501a39..6ae887e 100644 --- a/src/date/date.c +++ b/src/date/date.c @@ -1,6 +1,7 @@ #define _POSIX_C_SOURCE 200112L // for setenv #define _DEFAULT_SOURCE // for settimeofday +#include #include #include #include @@ -27,10 +28,17 @@ static struct tm *get_time(void) { } static int set_time(char *str) { + size_t n; + for (n = 0; str[n]; n++) { + if (!isdigit(str[n])) { + fprintf(stderr, "date: invalid date '%s'\n", str); + return EXIT_FAILURE; + } + } + #define s(i) (str[i] - '0') struct tm tm; memcpy(&tm, get_time(), sizeof tm); - size_t n = strlen(str); bool century_specified = false; switch (n) { case 12: @@ -55,8 +63,7 @@ static int set_time(char *str) { tm.tm_isdst = -1; break; default: - fprintf(stderr, "%s", - "date: specify time in the format %m%d%H%M[[%C]%y]\n"); + fprintf(stderr, "date: invalid date '%s'\n", str); return EXIT_FAILURE; } #undef s From 8b14fa53f86477060145f791bc813841543415a4 Mon Sep 17 00:00:00 2001 From: Eike Flath Date: Sun, 21 Apr 2024 18:41:48 +0200 Subject: [PATCH 3/7] use clock_settime (which is specified by POSIX) instead of settimeofday --- src/date/date.c | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/date/date.c b/src/date/date.c index 6ae887e..f6e8bb1 100644 --- a/src/date/date.c +++ b/src/date/date.c @@ -1,5 +1,4 @@ #define _POSIX_C_SOURCE 200112L // for setenv -#define _DEFAULT_SOURCE // for settimeofday #include #include @@ -7,7 +6,6 @@ #include #include #include -#include #include #define NAME "date (canoutils)" @@ -72,10 +70,9 @@ static int set_time(char *str) { fprintf(stderr, "date: '%s' does not specify a valid time\n", str); return EXIT_FAILURE; } - // printf("%s\n", ctime(&t)); - struct timeval tv = {.tv_sec = t}; - if (settimeofday(&tv, NULL)) { - fprintf(stderr, "date: cannot settimeofday: %s\n", strerror(errno)); + struct timespec ts = {.tv_sec = t}; + if (clock_settime(CLOCK_REALTIME, &ts)) { + fprintf(stderr, "date: cannot set date: %s\n", strerror(errno)); return EXIT_FAILURE; } return EXIT_SUCCESS; From 42370e1c3f5753c35165c0480bab499d196d978b Mon Sep 17 00:00:00 2001 From: Eike Flath Date: Sun, 21 Apr 2024 20:36:33 +0200 Subject: [PATCH 4/7] added malloc/realloc call checks --- src/date/date.c | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/date/date.c b/src/date/date.c index f6e8bb1..21ad89b 100644 --- a/src/date/date.c +++ b/src/date/date.c @@ -109,9 +109,19 @@ int main(int argc, char **argv) { size_t cap = INITIAL_BUF_SIZE; char *buf = malloc(cap); + if (!buf) { + fprintf(stderr, "date: malloc failed: %s\n", strerror(errno)); + return EXIT_FAILURE; + } while (!strftime(buf, cap, &operand[1], tm)) { cap *= 2; - buf = realloc(buf, cap); + char *new_buf = realloc(buf, cap); + if (!new_buf) { + free(buf); + fprintf(stderr, "date: realloc failed: %s\n", strerror(errno)); + return EXIT_FAILURE; + } + buf = new_buf; } printf("%s\n", buf); free(buf); From 0ca95d740e5fb6ae8dc64fd52064f9e3838d4acc Mon Sep 17 00:00:00 2001 From: Eike Flath Date: Wed, 24 Apr 2024 12:33:10 +0200 Subject: [PATCH 5/7] implemented --help and started on --iso-8601 --- src/date/date.c | 116 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 100 insertions(+), 16 deletions(-) diff --git a/src/date/date.c b/src/date/date.c index 21ad89b..e0c60d5 100644 --- a/src/date/date.c +++ b/src/date/date.c @@ -16,6 +16,8 @@ #define INITIAL_BUF_SIZE 512 +static char *operand; + static struct tm *get_time(void) { time_t t; if (time(&t) == -1) { @@ -78,29 +80,111 @@ static int set_time(char *str) { return EXIT_SUCCESS; } +static void set_operand(char *o) { + if (operand) { + fprintf(stderr, "date: multiple output formats or dates specified\n"); + exit(EXIT_FAILURE); + } + operand = o; +} + +static void use_utc(void) { + if (setenv("TZ", "UTC", 1) == -1) { + fprintf(stderr, "date: cannot setenv TZ: %s\n", strerror(errno)); + exit(EXIT_FAILURE); + } +} + +static void use_iso_fmt(char *iso) { + if (!strcmp(iso, "hours")) { + set_operand("+%FT%H%:z"); + } else if (!strcmp(iso, "minutes")) { + set_operand("+%FT%H:%M%:z"); + } else if (!strcmp(iso, "date")) { + set_operand("+%F"); + } else if (!strcmp(iso, "seconds")) { + set_operand("+%FT%H:%M:%S%:z"); + } else if (!strcmp(iso, "ns")) { + set_operand("+%FT%H:%M:%S,%N%:z"); + } else { + fprintf(stderr, "date: invalid argument '%s' for '--iso-8601'\n", iso); + fprintf(stderr, + "must be one of 'hours', 'minutes', 'date', 'seconds', 'ns'\n"); + exit(EXIT_FAILURE); + } +} + +static void short_opt(char *opt) { + switch (*opt) { + case 'u': + use_utc(); + break; + case 'I': + if (opt[1]) + use_iso_fmt(opt + 1); + else + use_iso_fmt("date"); + break; + default: + fprintf(stderr, "date: unknown option '-%c'\n", *opt); + exit(EXIT_FAILURE); + } +} + +static bool strpre(const char *pre, const char *str) { + return !strncmp(pre, str, strlen(pre)); +} + +static void long_opt(char *opt) { + if (!strcmp(opt, "version")) { + print_version(); + exit(EXIT_SUCCESS); + } + if (!strcmp(opt, "help")) + exit(system("man date")); + if (!strcmp(opt, "utc") || !strcmp(opt, "universal")) { + use_utc(); + return; + } + if (strpre("iso-8601", opt)) { + char *arg = opt + strlen("iso-8601"); + if (*arg) { + if (*arg != '=') + goto unknown_opt; + use_iso_fmt(arg + 1); + } else { + use_iso_fmt("date"); + } + return; + } +unknown_opt: + fprintf(stderr, "date: unknown option '--%s'\n", opt); + exit(EXIT_FAILURE); +} + int main(int argc, char **argv) { - char *operand = "+%a %b %e %H:%M:%S %Z %Y"; + operand = NULL; bool parse_options = true; for (int i = 1; i < argc; i++) { - if (parse_options) { - if (!strcmp(argv[i], "--")) { - parse_options = false; + if (parse_options && argv[i][0] == '-') { + if (argv[i][1] == '-') { + char *opt = argv[i] + 2; + if (!*opt) + parse_options = false; + else + long_opt(opt); continue; - } - if (!strcmp(argv[i], "--version")) { - print_version(); - return EXIT_SUCCESS; - } - if (!strcmp(argv[i], "-u")) { - if (setenv("TZ", "UTC", 1) == -1) { - fprintf(stderr, "date: cannot setenv TZ: %s\n", strerror(errno)); - return EXIT_FAILURE; + } else { + if (argv[i][1]) { + short_opt(argv[i] + 1); + continue; } - continue; } } - operand = argv[i]; + set_operand(argv[i]); } + if (!operand) + operand = "+%a %b %e %H:%M:%S %Z %Y"; if (operand[0] != '+') return set_time(operand); @@ -113,7 +197,7 @@ int main(int argc, char **argv) { fprintf(stderr, "date: malloc failed: %s\n", strerror(errno)); return EXIT_FAILURE; } - while (!strftime(buf, cap, &operand[1], tm)) { + while (!strftime(buf, cap, operand + 1, tm)) { cap *= 2; char *new_buf = realloc(buf, cap); if (!new_buf) { From bdaa0a311fe919a37482afe29bf7824624c8ec57 Mon Sep 17 00:00:00 2001 From: Eike Flath Date: Sun, 28 Apr 2024 18:41:55 +0200 Subject: [PATCH 6/7] Added %N, %:z, --iso-8601 is fully implemented --- src/date/date.c | 111 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 103 insertions(+), 8 deletions(-) diff --git a/src/date/date.c b/src/date/date.c index e0c60d5..681986c 100644 --- a/src/date/date.c +++ b/src/date/date.c @@ -18,13 +18,15 @@ static char *operand; -static struct tm *get_time(void) { - time_t t; - if (time(&t) == -1) { +static struct tm *get_time(long *nanos) { + struct timespec ts; + if (clock_gettime(CLOCK_REALTIME, &ts)) { fprintf(stderr, "date: failed to retrieve time: %s\n", strerror(errno)); exit(EXIT_FAILURE); } - return localtime(&t); + if (nanos) + *nanos = ts.tv_nsec; + return localtime(&ts.tv_sec); } static int set_time(char *str) { @@ -38,7 +40,8 @@ static int set_time(char *str) { #define s(i) (str[i] - '0') struct tm tm; - memcpy(&tm, get_time(), sizeof tm); + long ns; + memcpy(&tm, get_time(&ns), sizeof tm); bool century_specified = false; switch (n) { case 12: @@ -72,7 +75,7 @@ static int set_time(char *str) { fprintf(stderr, "date: '%s' does not specify a valid time\n", str); return EXIT_FAILURE; } - struct timespec ts = {.tv_sec = t}; + struct timespec ts = {.tv_sec = t, .tv_nsec = ns}; if (clock_settime(CLOCK_REALTIME, &ts)) { fprintf(stderr, "date: cannot set date: %s\n", strerror(errno)); return EXIT_FAILURE; @@ -162,6 +165,97 @@ static void long_opt(char *opt) { exit(EXIT_FAILURE); } +/** + * Works just like strftime, except it recognizes formats like %N or %:z which + * are specified by the GNU date utility but not by the glibc strftime + * implementation. + * + * Currently missing: %::z, %:::z + */ +static size_t extended_strftime(char *restrict buf, size_t max, + const char *restrict fmt, + const struct tm *restrict tm, long nanos) { + size_t fmt_size = strlen(fmt) + 1; + char *f = malloc(fmt_size); + if (!f) { + fprintf(stderr, "date: malloc failed: %s\n", strerror(errno)); + exit(EXIT_FAILURE); + } + // copy the fmt string into buffer so that we can mutate it + memcpy(f, fmt, fmt_size); + size_t n = 0; + char c; + int i, j = 0; + for (i = 0; (c = f[i]); i++) { + if (c != '%') + continue; + switch (f[i + 1]) { + case '%': + i++; // skip additional % + continue; + case ':': + if (f[i + 2] != 'z') + continue; + // fallthrough + case 'N': + if (i > j) { + // evaluate the "normal" format string until i using strftime + f[i] = '\0'; // tells strftime to stop at i + size_t m = strftime(buf, max - n, f + j, tm); + if (m == 0) { + free(f); + return 0; + } + n += m; + buf += m; + } + break; + default: + continue; + } + switch (f[i + 1]) { + case ':': // means %:z + if (max - n < 7) { // need at least 7 bytes: +hh:mm\0 + free(f); + return 0; + } + strftime(buf, 6, "%z", tm); // write +hhmm\0 + memmove(buf + 4, buf + 3, 2); + buf[3] = ':'; + n += 6; + buf += 6; + j = i + 3; + break; + case 'N': + if (max - n < 10) { + free(f); + return 0; + } + snprintf(buf, 10, "%09ld", nanos); + n += 9; + buf += 9; + j = i + 2; + break; + default: + __builtin_unreachable(); + } + i = j - 1; + } + if (i > j) { + // evaluate the "normal" format string until i using strftime + size_t m = strftime(buf, max - n, f + j, tm); + if (m == 0) { + free(f); + return 0; + } + n += m; + buf += m; + } + *buf = '\0'; + free(f); + return n; +} + int main(int argc, char **argv) { operand = NULL; bool parse_options = true; @@ -189,7 +283,8 @@ int main(int argc, char **argv) { if (operand[0] != '+') return set_time(operand); - struct tm *tm = get_time(); + long ns; + struct tm *tm = get_time(&ns); size_t cap = INITIAL_BUF_SIZE; char *buf = malloc(cap); @@ -197,7 +292,7 @@ int main(int argc, char **argv) { fprintf(stderr, "date: malloc failed: %s\n", strerror(errno)); return EXIT_FAILURE; } - while (!strftime(buf, cap, operand + 1, tm)) { + while (!extended_strftime(buf, cap, operand + 1, tm, ns)) { cap *= 2; char *new_buf = realloc(buf, cap); if (!new_buf) { From fea3e1b077632eb3e264e64ea51021f1367b91f0 Mon Sep 17 00:00:00 2001 From: Eike Flath Date: Sun, 28 Apr 2024 18:59:58 +0200 Subject: [PATCH 7/7] Added RFC options --- src/date/date.c | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/date/date.c b/src/date/date.c index 681986c..e7a3a87 100644 --- a/src/date/date.c +++ b/src/date/date.c @@ -106,9 +106,9 @@ static void use_iso_fmt(char *iso) { } else if (!strcmp(iso, "date")) { set_operand("+%F"); } else if (!strcmp(iso, "seconds")) { - set_operand("+%FT%H:%M:%S%:z"); + set_operand("+%FT%T%:z"); } else if (!strcmp(iso, "ns")) { - set_operand("+%FT%H:%M:%S,%N%:z"); + set_operand("+%FT%T,%N%:z"); } else { fprintf(stderr, "date: invalid argument '%s' for '--iso-8601'\n", iso); fprintf(stderr, @@ -117,6 +117,22 @@ static void use_iso_fmt(char *iso) { } } +static void use_rfc3339_fmt(char *rfc) { + if (!strcmp(rfc, "date")) { + set_operand("+%F"); + } else if (!strcmp(rfc, "seconds")) { + set_operand("+%F %T%:z"); + } else if (!strcmp(rfc, "ns")) { + set_operand("+%F %T.%N%:z"); + } else { + fprintf(stderr, "date: invalid argument '%s' for '--rfc-3339'\n", rfc); + fprintf(stderr, "must be one of 'date', 'seconds', 'ns'\n"); + exit(EXIT_FAILURE); + } +} + +static void use_rfc_mail_fmt(void) { set_operand("+%a, %d %b %Y %T %z"); } + static void short_opt(char *opt) { switch (*opt) { case 'u': @@ -128,6 +144,9 @@ static void short_opt(char *opt) { else use_iso_fmt("date"); break; + case 'R': + use_rfc_mail_fmt(); + break; default: fprintf(stderr, "date: unknown option '-%c'\n", *opt); exit(EXIT_FAILURE); @@ -149,6 +168,10 @@ static void long_opt(char *opt) { use_utc(); return; } + if (!strcmp(opt, "rfc-email")) { + use_rfc_mail_fmt(); + return; + } if (strpre("iso-8601", opt)) { char *arg = opt + strlen("iso-8601"); if (*arg) { @@ -160,6 +183,13 @@ static void long_opt(char *opt) { } return; } + if (strpre("rfc-3339", opt)) { + char *arg = opt + strlen("rfc-3339"); + if (*arg != '=') + goto unknown_opt; + use_rfc3339_fmt(arg + 1); + return; + } unknown_opt: fprintf(stderr, "date: unknown option '--%s'\n", opt); exit(EXIT_FAILURE);