support time zone conversion and date-time parsing - ics2txt - convert icalenda… | |
git clone git://bitreich.org/ics2txt git://enlrupgkhuxnvlhsf6lc3fziv5h2hhfrinws… | |
Log | |
Files | |
Refs | |
Tags | |
README | |
--- | |
commit 58a1a9df90b5751ae05fba076cd9e664e3d9f3c1 | |
parent b72092250747c7443e20fee06bee232b236f441e | |
Author: Josuah Demangeon <[email protected]> | |
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(-) | |
--- | |
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); | |
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 *); | |
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; | |
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; | |
} | |
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 |