ical: improve and simplify line 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 92a5d0067b717710eb607c0465a8a60d4b4c8655 DIR parent d10df705caaa2ca4e3229af6d5ec76e0f0d301da HTML Author: Josuah Demangeon <me@josuah.net> Date: Wed, 16 Jun 2021 23:13:22 +0200 ical: improve and simplify line parsing Diffstat: M Makefile | 2 +- M bin/ics2tsv | 2 +- M ical.c | 152 +++++++++++++++++++------------ M ical.h | 25 ++++++++++++++++++------- M ics2tree.c | 8 ++++---- A ics2tsv.c | 99 +++++++++++++++++++++++++++++++ M tcal.5 | 11 +++++------ 7 files changed, 220 insertions(+), 79 deletions(-) --- DIR diff --git a/Makefile b/Makefile @@ -10,7 +10,7 @@ MANPREFIX = ${PREFIX}/man SRC = ical.c base64.c util.c HDR = ical.h base64.h util.h OBJ = ${SRC:.c=.o} -BIN = ics2tree +BIN = ics2tree ics2tsv MAN1 = ics2txt.1 MAN5 = tcal.5 DIR diff --git a/bin/ics2tsv b/bin/ics2tsv @@ -126,7 +126,7 @@ sub("^ ", "") { next if (content["name"] == "TZID") { - ical_set_tzid(content["value"]) + ical_set_tz(content["value"]) } else if (DT[content["name"]]) { vevent[content["name"]] = ical_to_epoch(content, params) } else { DIR diff --git a/ical.c b/ical.c @@ -11,14 +11,20 @@ #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 */ +char *ical_block_name[ICAL_BLOCK_OTHER + 1] = { + [ICAL_BLOCK_VEVENT] = "VEVENT", + [ICAL_BLOCK_VTODO] = "VTODO", + [ICAL_BLOCK_VJOURNAL] = "VJOURNAL", + [ICAL_BLOCK_VFREEBUSY] = "VFREEBUSY", + [ICAL_BLOCK_VALARM] = "VALARM", + [ICAL_BLOCK_OTHER] = NULL, +}; + +/* valuel 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) +ical_err(IcalParser *p, char *msg) { p->errmsg = msg; return -1; @@ -36,7 +42,7 @@ ical_get_value(IcalParser *p, char *s, size_t *len) *len = strlen(s); if (p->base64) if (base64_decode(s, len, s, len) < 0) - return ical_error(p, "invalid base64 data"); + return ical_err(p, "invalid base64 data"); return 0; } @@ -55,7 +61,7 @@ ical_get_time(IcalParser *p, char *s, time_t *t) /* date */ for (int i = 0; i < 8; i++) if (!isdigit(s[i])) - return ical_error(p, "invalid date format"); + return ical_err(p, "invalid date format"); tm.tm_year = N(0,1000) + N(1,100) + N(2,10) + N(3,1) - 1900; tm.tm_mon = N(4,10) + N(5,1) - 1; tm.tm_mday = N(6,10) + N(7,1); @@ -66,7 +72,7 @@ ical_get_time(IcalParser *p, char *s, time_t *t) s++; for (int i = 0; i < 6; i++) if (!isdigit(s[i])) - return ical_error(p, "invalid time format"); + return ical_err(p, "invalid time format"); tm.tm_hour = N(0,10) + N(1,1); tm.tm_min = N(2,10) + N(3,1); tm.tm_sec = N(4,10) + N(5,1); @@ -74,8 +80,10 @@ ical_get_time(IcalParser *p, char *s, time_t *t) tzid = "UTC"; } +#undef N + if ((*t = tztime(&tm, tzid)) == (time_t)-1) - return ical_error(p, "could not convert time"); + return ical_err(p, "could not convert time"); return 0; } @@ -84,21 +92,21 @@ ical_get_time(IcalParser *p, char *s, time_t *t) * processing time zones definition or prepare base64 decoding, and * permit to only have parsing code left to parsing functions */ -int +static int hook_entry_name(IcalParser *p, char *name) { (void)p; (void)name; return 0; } -int +static int hook_param_name(IcalParser *p, char *name) { (void)p; (void)name; return 0; } -int +static int hook_param_value(IcalParser *p, char *name, char *value) { if (strcasecmp(name, "ENCODING") == 0) @@ -110,38 +118,53 @@ hook_param_value(IcalParser *p, char *name, char *value) return 0; } -int +static 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"); + if (strlcpy(p->current->tzid, value, sizeof p->current->tzid) >= + sizeof p->current->tzid) + return ical_err(p, "TZID: name too large"); p->tzid = NULL; return 0; } -int +static 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 ical_err(p, "max recurion reached"); + if (strlcpy(p->current->name, name, sizeof p->current->name) >= + sizeof p->current->name) + return ical_err(p, "value too large"); + + for (int i = 0; ical_block_name[i] != NULL; i++) { + if (strcasecmp(ical_block_name[i], name) == 0) { + if (p->block != ICAL_BLOCK_OTHER) + return ical_err(p, "BEGIN:V* in BEGIN:V*"); + p->block = i; + } + } + return 0; } -int +static int hook_block_end(IcalParser *p, char *name) { if (strcasecmp(p->current->name, name) != 0) - return ical_error(p, "mismatching BEGIN: and END:"); + return ical_err(p, "mismatching BEGIN: and END:"); p->current--; if (p->current < p->stack) - return ical_error(p, "more END: than BEGIN:"); + return ical_err(p, "more END: than BEGIN:"); + + if (ical_block_name[p->block] != NULL && + strcasecmp(ical_block_name[p->block], name) == 0) + p->block = ICAL_BLOCK_OTHER; return 0; } @@ -162,7 +185,7 @@ ical_parse_value(IcalParser *p, char **sp, char *name) while (!iscntrl(*s) && *s != '"') s++; if (*s != '"') - return ical_error(p, "missing '\"'"); + return ical_err(p, "missing '\"'"); *s++ = '\0'; } else { val = s; @@ -188,7 +211,7 @@ ical_parse_param(IcalParser *p, char **sp) do { for (name = s; isalnum(*s) || *s == '-'; s++); if (s == name || (*s != '=')) - return ical_error(p, "invalid parameter name"); + return ical_err(p, "invalid parameter name"); *s++ = '\0'; if ((err = hook_param_name(p, name)) != 0 || (err = CALL(p, fn_param_name, name)) != 0) @@ -208,9 +231,12 @@ ical_parse_contentline(IcalParser *p, char *s) int err; char c, *name, *sep; + if (*s == '\0') + return 0; + for (name = s; isalnum(*s) || *s == '-'; s++); if (s == name || (*s != ';' && *s != ':')) - return ical_error(p, "invalid entry name"); + return ical_err(p, "invalid property name"); c = *s, *s = '\0'; if (strcasecmp(name, "BEGIN") != 0 && strcasecmp(name, "END") != 0) if ((err = hook_entry_name(p, name)) != 0 || @@ -227,7 +253,7 @@ ical_parse_contentline(IcalParser *p, char *s) } if (*s != ':') - return ical_error(p, "expected ':' delimiter"); + return ical_err(p, "expected ':' delimiter"); s++; *sep = '\0'; @@ -247,47 +273,53 @@ ical_parse_contentline(IcalParser *p, char *s) return 0; } +static ssize_t +ical_getline(char **contentline, char **line, size_t *sz, FILE *fp) +{ + size_t num = 0; + int c; + + if ((*contentline = realloc(*contentline, 1)) == NULL) + return -1; + **contentline = '\0'; + + do { + if (getline(line, sz, fp) <= 0) + goto end; + num++; + strchomp(*line); + + if (strappend(contentline, *line) < 0) + return -1; + if ((c = fgetc(fp)) == EOF) + goto end; + } while (c == ' '); + ungetc(c, fp); + assert(!ferror(fp)); +end: + return ferror(fp) ? -1 : num; +} + int ical_parse(IcalParser *p, FILE *fp) { - char *ln = NULL, *contentline = NULL; + char *line = NULL, *contentline = NULL; size_t sz = 0; - int err, c; + ssize_t l; + int err; p->current = p->stack; + p->linenum = 0; + p->block = ICAL_BLOCK_OTHER; - while (!feof(fp)) { - if ((contentline = realloc(contentline, 1)) == NULL) - return ical_error(p, strerror(errno)); - *contentline = '\0'; - - do { - do { - p->linenum++; - if (getline(&ln, &sz, fp) <= 0) { - if (ferror(fp)) - return ical_error(p, strerror(errno)); - goto end; - } - strchomp(ln); - } while (*ln == '\0'); - - if (strappend(&contentline, ln) < 0) - return ical_error(p, strerror(errno)); - if ((c = fgetc(fp)) == EOF) { - if (ferror(fp)) - return ical_error(p, strerror(errno)); - goto done; - } - } while (c == ' '); - ungetc(c, fp); -done: - assert(!ferror(fp)); - if ((err = ical_parse_contentline(p, contentline)) != 0) + do { + if ((l = ical_getline(&contentline, &line, &sz, fp)) < 0) { + err = ical_err(p, "readling line"); break; - } -end: + } + p->linenum += l; + } while (l > 0 && (err = ical_parse_contentline(p, contentline)) == 0); free(contentline); - free(ln); + free(line); return err; } DIR diff --git a/ical.h b/ical.h @@ -9,6 +9,15 @@ typedef struct IcalParser IcalParser; typedef struct IcalStack IcalStack; +typedef enum { + ICAL_BLOCK_VEVENT, + ICAL_BLOCK_VTODO, + ICAL_BLOCK_VJOURNAL, + ICAL_BLOCK_VFREEBUSY, + ICAL_BLOCK_VALARM, + ICAL_BLOCK_OTHER, +} IcalBlock; + struct IcalStack { char name[32]; char tzid[32]; @@ -25,17 +34,19 @@ struct IcalParser { /* if returning non-zero then halt the parser */ int base64; - char const *errmsg; + char *errmsg; size_t linenum; char *tzid; - + IcalBlock block; 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 *); +extern char *ical_block_name[ICAL_BLOCK_OTHER + 1]; + +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_err(IcalParser *, char *); #endif DIR diff --git a/ics2tree.c b/ics2tree.c @@ -18,6 +18,7 @@ fn_entry_name(IcalParser *p, char *name) { print_ruler(ical_get_level(p)); printf("name %s\n", name); + fflush(stdout); return 0; } @@ -26,6 +27,7 @@ fn_block_begin(IcalParser *p, char *name) { print_ruler(ical_get_level(p) - 1); printf("begin %s\n", name); + fflush(stdout); return 0; } @@ -34,6 +36,7 @@ fn_param_value(IcalParser *p, char *name, char *value) { print_ruler(ical_get_level(p) + 1); printf("param %s=%s\n", name, value); + fflush(stdout); return 0; } @@ -45,21 +48,18 @@ fn_entry_value(IcalParser *p, char *name, char *value) if (ical_get_value(p, value, &len) < 0) return -1; - 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 %lld\n", t); } else { printf("value %s\n", value); } - + fflush(stdout); return 0; } DIR diff --git a/ics2tsv.c b/ics2tsv.c @@ -0,0 +1,99 @@ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> + +#include "ical.h" +#include "util.h" + +#define FIELDS_MAX 64 + +typedef struct Event Event; + +struct Event { + time_t beg, end; + char *fields[FIELDS_MAX]; +}; + +static char *fields_time[] = { + "DTSTART", "DTEND", "DTSTAMP", "DUE", "EXDATE", "RDATE" +}; + +static char *fields_default[] = { + "ATTENDEE", "CATEGORY", "DESCRIPTION", "LOCATION", "SUMMARY", "URL" +}; + +static char **fields = fields_default; + +static int +fn_entry_name(IcalParser *p, char *name) +{ + printf("name %s\n", name); + return 0; +} + +static int +fn_block_begin(IcalParser *p, char *name) +{ + printf("begin %s\n", name); + return 0; +} + +static int +fn_param_value(IcalParser *p, char *name, char *value) +{ + printf("param %s=%s\n", name, value); + return 0; +} + +static int +fn_entry_value(IcalParser *p, char *name, char *value) +{ + size_t len; + (void)name; + + if (ical_get_value(p, value, &len) < 0) + return -1; + + if (strcasecmp(name, "DTSTART") == 0 || + strcasecmp(name, "DTSTAMP") == 0 || + strcasecmp(name, "DTEND") == 0) { + time_t t = 0; + if (ical_get_time(p, value, &t) != 0) + warn("%s: %s", p->errmsg, value); + printf("epoch %lld\n", t); + } else { + printf("value %s\n", value); + } + + return 0; +} + +int +main(int argc, char **argv) +{ + IcalParser p = {0}; + arg0 = *argv++; + + p.fn_entry_name = fn_entry_name; + p.fn_block_begin = fn_block_begin; + p.fn_param_value = fn_param_value; + p.fn_entry_value = fn_entry_value; + + if (*argv == NULL) { + if (ical_parse(&p, stdin) < 0) + err("parsing stdin:%d: %s", p.linenum, p.errmsg); + } + + for (; *argv != NULL; argv++, argc--) { + FILE *fp; + + debug("converting \"%s\"", *argv); + if ((fp = fopen(*argv, "r")) == NULL) + err("opening %s", *argv); + if (ical_parse(&p, fp) < 0) + err("parsing %s:%d: %s", *argv, p.linenum, p.errmsg); + fclose(fp); + } + return 0; +} DIR diff --git a/tcal.5 b/tcal.5 @@ -36,17 +36,16 @@ end of line. .Bd -literal TZ+0200 -2020-06-28 00:00 -2020-06-05 00:00 +2021-06-28 00:00 +2021-06-05 00:00 loc: 950-0994, Chuo Ward, Niigata, Japan sum: summer holidays -2020-06-29 13:30 -2020-06-29 15:00 - loc: online, irc.freenode.net, #bitreich-en +2021-06-29 13:30 +2021-06-29 15:00 + loc: online, irc.bitreich.org, #bitreich-en sum: bitreich irc invitation des: at this moment like all other moment, everyone invited on IRC - .Ed . .