support time zone conversion and date-time parsing - ics2txt - convert icalendar .ics file to plain text HTML git clone git://bitreich.org/ics2txt git://enlrupgkhuxnvlhsf6lc3fziv5h2hhfrinws65d7roiv6bfj7d652fid.onion/ics2txt DIR Log DIR Files DIR Refs DIR Tags DIR README --- DIR commit 58a1a9df90b5751ae05fba076cd9e664e3d9f3c1 DIR parent b72092250747c7443e20fee06bee232b236f441e HTML Author: Josuah Demangeon <me@josuah.net> Date: Mon, 14 Jun 2021 00:08:10 +0200 support time zone conversion and date-time parsing Convert dates from DT* fields to epoch on sample program. Diffstat: M ical.c | 168 ++++++++++++++++++++++++++------ M ical.h | 19 ++++++++++++++----- M ics2tree.c | 28 +++++++++++++++++++++------- M util.c | 81 ++++++++++++++++++++++--------- M util.h | 21 +++++++++++++-------- 5 files changed, 245 insertions(+), 72 deletions(-) --- DIR diff --git a/ical.c b/ical.c @@ -11,6 +11,12 @@ #include "util.h" #include "base64.h" +#define Xstrlcpy(d, s) (strlcpy((d), (s), sizeof(d)) < sizeof(d)) +#define Xstrlcat(d, s) (strlcat((d), (s), sizeof(d)) < sizeof(d)) + +/* helpers: common utilities to call within the p->fn() callbacks as + * well as in the code below */ + int ical_error(IcalParser *p, char const *msg) { @@ -19,6 +25,12 @@ ical_error(IcalParser *p, char const *msg) } int +ical_get_level(IcalParser *p) +{ + return p->current - p->stack; +} + +int ical_get_value(IcalParser *p, char *s, size_t *len) { *len = strlen(s); @@ -31,9 +43,111 @@ ical_get_value(IcalParser *p, char *s, size_t *len) int ical_get_time(IcalParser *p, char *s, time_t *t) { - return -1; + struct tm tm = {0}; + char const *tzid; + + tzid = (p->tzid) ? p->tzid : + (p->current && p->current->tzid[0] != '\0') ? p->current->tzid : + ""; + + /* date */ + for (int i = 0; i < 8; i++) + if (!isdigit(s[i])) + return ical_error(p, "invalid date format"); + tm.tm_year = s[0] * 1000 + s[1] * 100 + s[2] * 10 + s[3]; + tm.tm_mon = s[4] * 10 + s[5] - 1; + tm.tm_mday = s[6] * 10 + s[7]; + s += 8; + + if (*s == 'T') { + /* time */ + s++; + for (int i = 0; i < 6; i++) + if (!isdigit(s[i])) + return ical_error(p, "invalid time format"); + tm.tm_hour = s[0] * 10 + s[1]; + tm.tm_min = s[2] * 10 + s[3]; + tm.tm_sec = s[4] * 10 + s[5]; + if (s[6] == 'Z') + tzid = "UTC"; + } + + if ((*t = tztime(&tm, tzid)) == (time_t)-1) + return ical_error(p, "could not convert time"); + + return 0; +} + +/* hooks: called just before user functions to do extra work such as + * processing time zones definition or prepare base64 decoding, and + * permit to only have parsing code left to parsing functions */ + +int +hook_entry_name(IcalParser *p, char *name) +{ + (void)p; (void)name; + return 0; +} + +int +hook_param_name(IcalParser *p, char *name) +{ + (void)p; (void)name; + return 0; +} + +int +hook_param_value(IcalParser *p, char *name, char *value) +{ + if (strcasecmp(name, "ENCODING") == 0) + p->base64 = (strcasecmp(value, "BASE64") == 0); + + if (strcasecmp(name, "TZID") == 0) + p->tzid = value; + + return 0; +} + +int +hook_entry_value(IcalParser *p, char *name, char *value) +{ + if (strcasecmp(name, "TZID") == 0) + if (!Xstrlcpy(p->current->tzid, value)) + return ical_error(p, "TZID: name too large"); + + p->tzid = NULL; + + return 0; +} + +int +hook_block_begin(IcalParser *p, char *name) +{ + p->current++; + memset(p->current, 0, sizeof(*p->current)); + if (ical_get_level(p) >= ICAL_STACK_SIZE) + return ical_error(p, "max recurion reached"); + if (!Xstrlcpy(p->current->name, name)) + return ical_error(p, "value too large"); + + return 0; +} + +int +hook_block_end(IcalParser *p, char *name) +{ + if (strcasecmp(p->current->name, name) != 0) + return ical_error(p, "mismatching BEGIN: and END:"); + p->current--; + if (p->current < p->stack) + return ical_error(p, "more END: than BEGIN:"); + + return 0; } +/* parsers: in charge of reading from `fp`, splitting text into + * fields, and call hooks and user functions. */ + #define CALL(p, fn, ...) ((p)->fn ? (p)->fn((p), __VA_ARGS__) : 0) static int @@ -43,24 +157,23 @@ ical_parse_value(IcalParser *p, char **sp, char *name) char *s, c, *val; s = *sp; - if (*s == '"') { - ++s; - for (val = s; !iscntrl(*s) && !strchr(",;:\"", *s); s++); + val = ++s; + while (!iscntrl(*s) && *s != '"') + s++; if (*s != '"') return ical_error(p, "missing '\"'"); *s++ = '\0'; } else { - for (val = s; !iscntrl(*s) && !strchr(",;:'\"", *s); s++); + val = s; + while (!iscntrl(*s) && !strchr(",;:'\"", *s)) + s++; } - c = *s, *s = '\0'; - if ((err = CALL(p, fn_param_value, name, val)) != 0) + if ((err = hook_param_value(p, name, val)) != 0 || + (err = CALL(p, fn_param_value, name, val)) != 0) return err; - if (strcasecmp(name, "ENCODING") == 0) - p->base64 = (strcasecmp(val, "BASE64") == 0); *s = c; - *sp = s; return 0; } @@ -72,42 +185,39 @@ ical_parse_param(IcalParser *p, char **sp) char *s, *name; s = *sp; - do { for (name = s; isalnum(*s) || *s == '-'; s++); if (s == name || (*s != '=')) return ical_error(p, "invalid parameter name"); *s++ = '\0'; - if ((err = CALL(p, fn_param_name, name)) != 0) + if ((err = hook_param_name(p, name)) != 0 || + (err = CALL(p, fn_param_name, name)) != 0) return err; - do { if ((err = ical_parse_value(p, &s, name)) != 0) return err; } while (*s == ',' && s++); } while (*s == ';' && s++); - *sp = s; return 0; } static int -ical_parse_contentline(IcalParser *p, char *line) +ical_parse_contentline(IcalParser *p, char *s) { int err; - char *s, c, *name, *end; - - s = line; + char c, *name, *sep; for (name = s; isalnum(*s) || *s == '-'; s++); if (s == name || (*s != ';' && *s != ':')) return ical_error(p, "invalid entry name"); c = *s, *s = '\0'; if (strcasecmp(name, "BEGIN") != 0 && strcasecmp(name, "END") != 0) - if ((err = CALL(p, fn_entry_name, name)) != 0) + if ((err = hook_entry_name(p, name)) != 0 || + (err = CALL(p, fn_entry_name, name)) != 0) return err; *s = c; - end = s; + sep = s; p->base64 = 0; while (*s == ';') { @@ -120,20 +230,20 @@ ical_parse_contentline(IcalParser *p, char *line) return ical_error(p, "expected ':' delimiter"); s++; - *end = '\0'; + *sep = '\0'; if (strcasecmp(name, "BEGIN") == 0) { - if ((err = CALL(p, fn_block_begin, s)) != 0) + if ((err = hook_block_begin(p, s)) != 0 || + (err = CALL(p, fn_block_begin, s)) != 0) return err; - p->level++; } else if (strcasecmp(name, "END") == 0) { - if ((err = CALL(p, fn_block_end, s)) != 0) + if ((err = hook_block_end(p, s)) != 0 || + (err = CALL(p, fn_block_end, s)) != 0) return err; - p->level--; } else { - if ((err = CALL(p, fn_entry_value, name, s)) != 0) + if ((err = hook_entry_value(p, name, s)) != 0 || + (err = CALL(p, fn_entry_value, name, s)) != 0) return err; } - return 0; } @@ -144,6 +254,8 @@ ical_parse(IcalParser *p, FILE *fp) size_t sz = 0; int err, c; + p->current = p->stack; + while (!feof(fp)) { if ((contentline = realloc(contentline, 1)) == NULL) return -1; @@ -151,7 +263,7 @@ ical_parse(IcalParser *p, FILE *fp) do { do { - p->line++; + p->linenum++; if (getline(&ln, &sz, fp) <= 0) return -1; strchomp(ln); DIR diff --git a/ical.h b/ical.h @@ -4,9 +4,18 @@ #include <stdio.h> #include <time.h> +#define ICAL_STACK_SIZE 10 + typedef struct IcalParser IcalParser; +typedef struct IcalStack IcalStack; + +struct IcalStack { + char name[32]; + char tzid[32]; +}; + struct IcalParser { - /* function called on content */ + /* function called while parsing in this order */ int (*fn_entry_name)(IcalParser *, char *); int (*fn_param_name)(IcalParser *, char *); int (*fn_param_value)(IcalParser *, char *, char *); @@ -17,14 +26,14 @@ struct IcalParser { int base64; char const *errmsg; - size_t line; + size_t linenum; + char *tzid; - /* stack of blocks names: "name1\0name2\0...nameN\0\0" */ - int level; - char stack[1024]; + IcalStack stack[ICAL_STACK_SIZE], *current; }; int ical_parse(IcalParser *, FILE *); +int ical_get_level(IcalParser *); int ical_get_time(IcalParser *, char *, time_t *); int ical_get_value(IcalParser *, char *, size_t *); int ical_error(IcalParser *, char const *); DIR diff --git a/ics2tree.c b/ics2tree.c @@ -1,6 +1,7 @@ #include <stdio.h> #include <stdlib.h> #include <string.h> +#include <strings.h> #include "ical.h" #include "util.h" @@ -15,7 +16,7 @@ print_ruler(int level) static int fn_entry_name(IcalParser *p, char *name) { - print_ruler(p->level); + print_ruler(ical_get_level(p)); printf("name %s\n", name); return 0; } @@ -23,7 +24,7 @@ fn_entry_name(IcalParser *p, char *name) static int fn_block_begin(IcalParser *p, char *name) { - print_ruler(p->level); + print_ruler(ical_get_level(p) - 1); printf("begin %s\n", name); return 0; } @@ -31,7 +32,7 @@ fn_block_begin(IcalParser *p, char *name) static int fn_param_value(IcalParser *p, char *name, char *value) { - print_ruler(p->level + 1); + print_ruler(ical_get_level(p) + 1); printf("param %s=%s\n", name, value); return 0; } @@ -44,8 +45,21 @@ fn_entry_value(IcalParser *p, char *name, char *value) if (ical_get_value(p, value, &len) < 0) return -1; - print_ruler(p->level + 1); - printf("value %s\n", value); + + print_ruler(ical_get_level(p) + 1); + + if (strcasecmp(name, "DTSTART") == 0 || + strcasecmp(name, "DTSTAMP") == 0 || + strcasecmp(name, "DTEND") == 0) { + time_t t; + + if (ical_get_time(p, value, &t) != 0) + warn("%s: %s", p->errmsg, value); + printf("epoch %ld\n", t); + } else { + printf("value %s\n", value); + } + return 0; } @@ -62,7 +76,7 @@ main(int argc, char **argv) if (*argv == NULL) { if (ical_parse(&p, stdin) < 0) - err("parsing stdin:%d: %s", p.line, p.errmsg); + err("parsing stdin:%d: %s", p.linenum, p.errmsg); } for (; *argv != NULL; argv++, argc--) { @@ -72,7 +86,7 @@ main(int argc, char **argv) if ((fp = fopen(*argv, "r")) == NULL) err("opening %s", *argv); if (ical_parse(&p, fp) < 0) - err("parsing %s:%d: %s", *argv, p.line, p.errmsg); + err("parsing %s:%d: %s", *argv, p.linenum, p.errmsg); fclose(fp); } return 0; DIR diff --git a/util.c b/util.c @@ -5,20 +5,18 @@ #include <stdlib.h> #include <string.h> #include <stdio.h> +#include <time.h> char *arg0; -/* logging */ +/** logging **/ static void -_log(char const *tag, char const *fmt, va_list va) +_log(char const *fmt, va_list va) { if (arg0 != NULL) fprintf(stderr, "%s: ", arg0); - fprintf(stderr, "%s: ", tag); vfprintf(stderr, fmt, va); - if (errno != 0) - fprintf(stderr, ": %s", strerror(errno)); fprintf(stderr, "\n"); fflush(stderr); } @@ -29,7 +27,7 @@ err(char const *fmt, ...) va_list va; va_start(va, fmt); - _log("error", fmt, va); + _log( fmt, va); exit(1); } @@ -39,7 +37,7 @@ warn(char const *fmt, ...) va_list va; va_start(va, fmt); - _log("warning", fmt, va); + _log(fmt, va); } void @@ -53,23 +51,34 @@ debug(char const *fmt, ...) if (!verbose) return; va_start(va, fmt); - _log("debug", fmt, va); + _log(fmt, va); } -/* strings */ +/** strings **/ size_t -strlcpy(char *buf, char const *str, size_t sz) +strlcpy(char *d, char const *s, size_t sz) { size_t len, cpy; - len = strlen(str); + len = strlen(s); cpy = (len > sz) ? (sz) : (len); - memcpy(buf, str, cpy + 1); - buf[sz - 1] = '\0'; + memcpy(d, s, cpy + 1); + d[sz - 1] = '\0'; return len; } +size_t +strlcat(char *d, char const *s, size_t dsz) +{ + size_t dlen; + + dlen = strlen(d); + if (dlen >= dsz) + return dlen + strlen(s); + return dlen + strlcpy(d + dlen, s, dsz - dlen); +} + char * strsep(char **sp, char const *sep) { @@ -102,28 +111,52 @@ strchomp(char *line) } int -strappend(char **dstp, char const *src) +strappend(char **dp, char const *s) { - size_t dstlen, srclen; + size_t dlen, slen; void *mem; - dstlen = (*dstp == NULL) ? 0 : strlen(*dstp); - srclen = strlen(src); + dlen = (*dp == NULL) ? 0 : strlen(*dp); + slen = strlen(s); - if ((mem = realloc(*dstp, dstlen + srclen + 1)) == NULL) + if ((mem = realloc(*dp, dlen + slen + 1)) == NULL) return -1; - *dstp = mem; + *dp = mem; - memcpy(*dstp + dstlen, src, srclen + 1); + memcpy(*dp + dlen, s, slen + 1); return 0; } -/* memory */ +/** memory **/ void * -reallocarray(void *buf, size_t len, size_t sz) +reallocarray(void *mem, size_t n, size_t sz) { - if (SIZE_MAX / len < sz) + if (SIZE_MAX / n < sz) return errno=ERANGE, NULL; - return realloc(buf, len * sz); + return realloc(mem, n * sz); +} + +/** time **/ + +time_t +tztime(struct tm *tm, char const *tz) +{ + char *env, old[32]; + time_t t; + + env = getenv("TZ"); + if (strlcpy(old, env ? env : "", sizeof old) >= sizeof old) + return -1; + if (setenv("TZ", tz, 1) < 0) + return -1; + + tzset(); + t = mktime(tm); + + if (env == NULL) + unsetenv("TZ"); + else if (setenv("TZ", old, 1) < 0) + return -1; + return t; } DIR diff --git a/util.h b/util.h @@ -3,20 +3,25 @@ #include <stddef.h> #include <stdarg.h> +#include <time.h> -/* logging */ +/** logging **/ extern char *arg0; void err(char const *fmt, ...); void warn(char const *fmt, ...); void debug(char const *fmt, ...); -/* strings */ -size_t strlcpy(char *buf, char const *str, size_t sz); -char *strsep(char **str_p, char const *sep); -void strchomp(char *line); -int strappend(char **base_p, char const *s); +/** strings **/ +size_t strlcpy(char *, char const *, size_t); +char *strsep(char **, char const *); +void strchomp(char *); +int strappend(char **, char const *); +size_t strlcat(char *, char const *, size_t); -/* memory */ -void *reallocarray(void *buf, size_t len, size_t sz); +/** memory **/ +void *reallocarray(void *, size_t, size_t); + +/** time **/ +time_t tztime(struct tm *, char const *); #endif