initial C implementation - ics2txt - convert icalendar .ics file to plain text
 (HTM) git clone git://bitreich.org/ics2txt git://enlrupgkhuxnvlhsf6lc3fziv5h2hhfrinws65d7roiv6bfj7d652fid.onion/ics2txt
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) Tags
 (DIR) README
       ---
 (DIR) commit 94bccd0b9ea7049ebeec4fcf2416f6f0b7d221b5
 (DIR) parent 78e0184b4deb29669bfde9a66fc945968845ced8
 (HTM) Author: Josuah Demangeon <me@josuah.net>
       Date:   Sat, 27 Jun 2020 20:31:09 +0200
       
       initial C implementation
       
       Diffstat:
         A .gitignore                          |       2 ++
         M Makefile                            |      24 +++++++++++++++++++-----
         A bin/ics2tsv                         |     141 +++++++++++++++++++++++++++++++
         R ics2txt -> bin/ics2txt              |       0 
         R tcal2tsv -> bin/tcal2tsv            |       0 
         R tsv2ics -> bin/tsv2ics              |       0 
         A bin/tsv2tcal                        |      91 +++++++++++++++++++++++++++++++
         A doc/index.md                        |      11 +++++++++++
         D ics2tsv                             |     138 ------------------------------
         A ics2tsv.c                           |      62 +++++++++++++++++++++++++++++++
         A src/ical.c                          |     108 +++++++++++++++++++++++++++++++
         A src/ical.h                          |      25 +++++++++++++++++++++++++
         A src/log.c                           |      89 +++++++++++++++++++++++++++++++
         A src/log.h                           |      15 +++++++++++++++
         A src/map.c                           |     102 +++++++++++++++++++++++++++++++
         A src/map.h                           |      23 +++++++++++++++++++++++
         A src/util.c                          |      74 +++++++++++++++++++++++++++++++
         A src/util.h                          |      13 +++++++++++++
         D tsv2tcal                            |      91 -------------------------------
       
       19 files changed, 775 insertions(+), 234 deletions(-)
       ---
 (DIR) diff --git a/.gitignore b/.gitignore
       @@ -0,0 +1,2 @@
       +*.o
       +ics2tsv
 (DIR) diff --git a/Makefile b/Makefile
       @@ -1,23 +1,37 @@
        NAME = ics2txt
        VERSION = 0.1
        
       -BIN = ics2tsv tsv2tcal tcal2tsv tsv2ics ics2txt
       -
       +W = -Wall -Wextra -std=c99 --pedantic
       +I = -Isrc
       +D = -D_POSIX_C_SOURCE=200811L -DVERSION='"${VERSION}"'
       +CFLAGS = $I $D $W -g
        PREFIX = /usr/local
        MANPREFIX = ${PREFIX}/man
        
       +SRC = src/ical.c src/map.c src/util.c src/log.c
       +HDR = src/ical.h src/map.h src/util.h src/log.h
       +OBJ = ${SRC:.c=.o}
       +BIN = ics2tsv
       +
        all: ${BIN}
        
       +.c.o:
       +        ${CC} -c ${CFLAGS} -o $@ $<
       +
       +${OBJ}: ${HDR}
       +${BIN}: ${OBJ} ${BIN:=.o}
       +        ${CC} ${LDFLAGS} -o $@ $@.o ${OBJ}
       +
        clean:
       -        rm -rf ${NAME}-${VERSION} *.gz
       +        rm -rf *.o */*.o ${BIN} ${NAME}-${VERSION} *.gz
        
        install:
                mkdir -p ${DESTDIR}$(PREFIX)/bin
       -        cp $(BIN) ${DESTDIR}$(PREFIX)/bin
       +        cp bin/* $(BIN) ${DESTDIR}$(PREFIX)/bin
                mkdir -p ${DESTDIR}$(MANPREFIX)/man1
                cp doc/*.1 ${DESTDIR}$(MANPREFIX)/man1
        
        dist: clean
                mkdir -p ${NAME}-${VERSION}
       -        cp -r README Makefile doc ${BIN} ${NAME}-${VERSION}
       +        cp -r README Makefile doc bin ${SRC} ${NAME}-${VERSION}
                tar -cf - ${NAME}-${VERSION} | gzip -c >${NAME}-${VERSION}.tar.gz
 (DIR) diff --git a/bin/ics2tsv b/bin/ics2tsv
       @@ -0,0 +1,141 @@
       +#!/usr/bin/awk -f
       +
       +function isleap(year)
       +{
       +        return (year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0)
       +}
       +
       +function mdays(mon, year)
       +{
       +        return (mon == 2) ? (28 + isleap(year)) : (30 + (mon + (mon > 7)) % 2)
       +}
       +
       +function timegm(tm,
       +        sec, mon, day)
       +{
       +        sec = tm["sec"] + tm["min"] * 60 + tm["hour"] * 3600
       +
       +        day = tm["mday"] - 1
       +
       +        for (mon = tm["mon"] - 1; mon > 0; mon--)
       +                day = day + mdays(mon, tm["year"])
       +
       +        # constants: x * 365 + x / 400 - x / 100 + x / 4
       +        day = day + int(tm["year"] / 400) * 146097
       +        day = day + int(tm["year"] % 400 / 100) * 36524
       +        day = day + int(tm["year"] % 100 / 4) * 1461
       +        day = day + int(tm["year"] % 4 / 1) * 365
       +
       +        return sec + (day - 719527) * 86400
       +}
       +
       +function print_vevent(ev, fields,
       +        i)
       +{
       +        for (i = 1; i in fields; i++)
       +                printf("%s%s", (i > 1 ? "\t" : ""), ev[fields[i]])
       +        printf("\n")
       +}
       +
       +function ical_parse_line(str, content, params,
       +        i, eq)
       +{
       +        if ((i = index(str, ":")) == 0)
       +                return -1
       +        content["value"] = substr(str, i + 1)
       +        str = substr(str, 1, i - 1)
       +
       +        if ((i = index(str, ";")) == 0) {
       +                content["name"] = str
       +                return 0
       +        }
       +        content["name"] = substr(str, 1, i - 1)
       +        str = substr(str, i + 1)
       +
       +        while ((i = index(str, ";")) > 0) {
       +                if ((eq = index(str, "=")) == 0)
       +                        return -1
       +                param[substr(str, 1, eq - 1)] = substr(str, eq + 1, i - 1)
       +                str = substr(str, eq + 1)
       +        }
       +        if ((eq = index(str, "=")) == 0)
       +                return -1
       +        params[substr(str, 1, eq - 1)] = substr(str, eq + 1)
       +        return 0
       +}
       +
       +function ical_set_tz(tzid)
       +{
       +        gsub("'", "", tzid)
       +        cmd = "TZ='" tzid "' exec date +%z" 
       +        cmd | getline tzid
       +        close(cmd)
       +        TZ = substr(tzid, 1, 1) substr(tzid, 2, 2)*3600 + substr(tzid, 4, 2)*60
       +}
       +
       +function ical_to_epoch(content, param,
       +        tz, cmd)
       +{
       +        if (param["TZID"])
       +                ical_set_tz(param["TZID"])
       +
       +        tm["year"] = substr(content["value"], 1, 4)
       +        tm["mon"] = substr(content["value"], 5, 2)
       +        tm["mday"] = substr(content["value"], 7, 2)
       +        tm["hour"] = substr(content["value"], 10, 2)
       +        tm["min"] = substr(content["value"], 12, 2)
       +        tm["sec"] = substr(content["value"], 14, 2)
       +
       +        return timegm(tm) + TZ
       +}
       +
       +BEGIN {
       +        split("DTSTART DTEND CATEGORIES LOCATION SUMMARY DESCRIPTION URL",
       +          FIELDS, " ")
       +        DT["DTSTART"] = DT["DTEND"] = DT["DUE"] = 1
       +
       +        # by default: "CATEGORIES" -> "cat", "LOCATION" -> "loc"...
       +        translate["DTSTART"] = "beg"
       +        translate["DTEND"] = "end"
       +
       +        for (i = 1; i in FIELDS; i++) {
       +                if (!(s = translate[FIELDS[i]]))
       +                        s = tolower(substr(FIELDS[i], 1, 3))
       +                printf("%s%s", (i > 1 ? "\t" : ""), s)
       +        }
       +        printf("\n")
       +
       +        FS = "[:;]"
       +}
       +
       +{
       +        gsub("\r", "")
       +        gsub("\t", "\\\\t")
       +}
       +
       +sub("^ ", "") {
       +        content["value"] = content["value"] $0
       +        next
       +}
       +
       +{
       +        delete content
       +        delete param
       +
       +        if (ical_parse_line($0, content, params) < 0)
       +                next
       +
       +        if (content["name"] == "TZID") {
       +                ical_set_tzid(content["value"])
       +        } else if (DT[content["name"]]) {
       +                vevent[content["name"]] = ical_to_epoch(content, params)
       +        } else {
       +                vevent[content["name"]] = content["value"]
       +        }
       +}
       +
       +/^END:VEVENT/ {
       +        print_vevent(vevent, FIELDS)
       +        delete vevent
       +        next
       +}
 (DIR) diff --git a/ics2txt b/bin/ics2txt
 (DIR) diff --git a/tcal2tsv b/bin/tcal2tsv
 (DIR) diff --git a/tsv2ics b/bin/tsv2ics
 (DIR) diff --git a/bin/tsv2tcal b/bin/tsv2tcal
       @@ -0,0 +1,91 @@
       +#!/usr/bin/awk -f
       +
       +function isleap(year)
       +{
       +        return (year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0)
       +}
       +
       +function mdays(mon, year)
       +{
       +        return (mon == 2) ? (28 + isleap(year)) : (30 + (mon + (mon > 7)) % 2)
       +}
       +
       +function gmtime(sec, tm)
       +{
       +        tm["year"] = 1970
       +        while (sec >= (s = 86400 * (365 + isleap(tm["year"])))) {
       +                tm["year"]++
       +                sec -= s
       +        }
       +        tm["mon"] = 1
       +        while (sec >= (s = 86400 * mdays(tm["mon"], tm["year"]))) {
       +                tm["mon"]++
       +                sec -= s
       +        }
       +        tm["mday"] = 1
       +        while (sec >= (s = 86400)) {
       +                tm["mday"]++
       +                sec -= s
       +        }
       +        tm["hour"] = 0
       +        while (sec >= 3600) {
       +                tm["hour"]++
       +                sec -= 3600
       +        }
       +        tm["min"] = 0
       +        while (sec >= 60) {
       +                tm["min"]++
       +                sec -= 60
       +        }
       +        tm["sec"] = sec
       +}
       +
       +function localtime(sec, tm,
       +        tz, h, m)
       +{
       +        return gmtime(sec + TZ, tm)
       +}
       +
       +BEGIN {
       +        "exec date +%z" | getline tz
       +        close("exec date +%z")
       +        TZ = substr(tz, 1, 1) substr(tz, 2, 2)*3600 + substr(tz, 4, 2)*60
       +
       +        print("TZ" tz)
       +
       +        FS = "\t"
       +}
       +
       +NR == 1 {
       +        for (i = 1; i <= NF; i++)
       +                name[i] = $i
       +        next
       +}
       +
       +{
       +        for (i = 1; i <= NF; i++)
       +                ev[name[i]] = $i
       +
       +        print("")
       +
       +        localtime(ev["beg"] + offset, tm)
       +        printf("%04d-%02d-%02d %02d:%02d\n",
       +          tm["year"], tm["mon"], tm["mday"], tm["hour"], tm["min"])
       +        delete ev["beg"]
       +
       +        localtime(ev["end"] + offset, tm)
       +        printf("%04d-%02d-%02d %02d:%02d\n",
       +          tm["year"], tm["mon"], tm["mday"], tm["hour"], tm["min"])
       +        delete ev["end"]
       +
       +        for (i = 1; i <= NF; i++) {
       +                if (name[i] in ev && ev[name[i]])
       +                        printf(" %s: %s\n", name[i], ev[name[i]])
       +        }
       +
       +        delete ev
       +}
       +
       +END {
       +        print("")
       +}
 (DIR) diff --git a/doc/index.md b/doc/index.md
       @@ -0,0 +1,11 @@
       +ics2txt
       +=======
       +Set of tools to work with the popular iCalendar format and converting to even
       +simpler TSV and text forms.
       +
       +Parsing have been tested with the following input formats (sample account
       +created for testing):
       +
       +* Zoom meetings generated events
       +* FOSDEM events, like <https://fosdem.org/2020/schedule/ical>
       +* Google Calendar
 (DIR) diff --git a/ics2tsv b/ics2tsv
       @@ -1,138 +0,0 @@
       -#!/usr/bin/awk -f
       -
       -function isleap(year)
       -{
       -        return (year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0)
       -}
       -
       -function mdays(mon, year)
       -{
       -        return (mon == 2) ? (28 + isleap(year)) : (30 + (mon + (mon > 7)) % 2)
       -}
       -
       -function timegm(tm,
       -        sec, mon, day)
       -{
       -        sec = tm["sec"] + tm["min"] * 60 + tm["hour"] * 3600
       -
       -        day = tm["mday"] - 1
       -
       -        for (mon = tm["mon"] - 1; mon > 0; mon--)
       -                day = day + mdays(mon, tm["year"])
       -
       -        # constants: x * 365 + x / 400 - x / 100 + x / 4
       -        day = day + int(tm["year"] / 400) * 146097
       -        day = day + int(tm["year"] % 400 / 100) * 36524
       -        day = day + int(tm["year"] % 100 / 4) * 1461
       -        day = day + int(tm["year"] % 4 / 1) * 365
       -
       -        return sec + (day - 719527) * 86400
       -}
       -
       -function print_vevent(ev, fields,
       -        i)
       -{
       -        for (i = 1; i in fields; i++)
       -                printf("%s%s", (i > 1 ? "\t" : ""), ev[fields[i]])
       -        printf("\n")
       -}
       -
       -function ical_parse_line(str, content, params,
       -        i, eq)
       -{
       -        if ((i = index(str, ":")) == 0)
       -                return -1
       -        content["value"] = substr(str, i + 1)
       -        str = substr(str, 1, i - 1)
       -
       -        if ((i = index(str, ";")) == 0) {
       -                content["name"] = str
       -                return 0
       -        }
       -        content["name"] = substr(str, 1, i - 1)
       -        str = substr(str, i + 1)
       -
       -        while ((i = index(str, ";")) > 0) {
       -                if ((eq = index(str, "=")) == 0)
       -                        return -1
       -                param[substr(str, 1, eq - 1)] = substr(str, eq + 1, i - 1)
       -                str = substr(str, eq + 1)
       -        }
       -        if ((eq = index(str, "=")) == 0)
       -                return -1
       -        params[substr(str, 1, eq - 1)] = substr(str, eq + 1)
       -        return 0
       -}
       -
       -function ical_to_epoch(content, param,
       -        tz, cmd)
       -{
       -        tz = (param["TZID"] ? param["TZID"] : vcalendar["TZID"])
       -        gsub("'", "", tz)
       -
       -        cmd = "TZ='"tz"' date +%z" 
       -        cmd | getline tz
       -        close(cmd)
       -
       -        tz = substr(tz, 1, 1) substr(tz, 2, 2)*3600 + substr(tz, 4, 2)*60
       -
       -        tm["year"] = substr(content["value"], 1, 4)
       -        tm["mon"] = substr(content["value"], 5, 2)
       -        tm["mday"] = substr(content["value"], 8, 2)
       -        tm["hour"] = substr(content["value"], 12, 2)
       -        tm["min"] = substr(content["value"], 15, 2)
       -        tm["sec"] = substr(content["value"], 18, 2)
       -
       -        return timegm(tm) + tz
       -}
       -
       -BEGIN {
       -        split("DTSTART DTEND CATEGORIES LOCATION SUMMARY DESCRIPTION",
       -          FIELDS, " ")
       -        DT["DTSTART"] = DT["DTEND"] = DT["DUE"] = 1
       -
       -        # by default: "CATEGORIES" -> "cat", "LOCATION" -> "loc"...
       -        translate["DTSTART"] = "beg"
       -        translate["DTEND"] = "end"
       -
       -        for (i = 1; i in FIELDS; i++) {
       -                if (!(s = translate[FIELDS[i]]))
       -                        s = tolower(substr(FIELDS[i], 1, 3))
       -                printf("%s%s", (i > 1 ? "\t" : ""), s)
       -        }
       -        printf("\n")
       -
       -        FS = "[:;]"
       -}
       -
       -{
       -        gsub("\r", "")
       -        gsub("\t", "\\\\t")
       -}
       -
       -sub("^ ", "") {
       -        content["value"] = content["value"] $0
       -        next
       -}
       -
       -{
       -        delete content
       -        delete param
       -
       -        if (ical_parse_line($0, content, params) < 0)
       -                next
       -
       -        if (content["name"] == "TZID") {
       -                vcalendar[content["name"]] = content["value"]
       -        } else if (DT[content["name"]]) {
       -                vevent[content["name"]] = ical_to_epoch(content, params)
       -        } else {
       -                vevent[content["name"]] = content["value"]
       -        }
       -}
       -
       -/^END:VEVENT/ {
       -        print_vevent(vevent, FIELDS)
       -        delete vevent
       -        next
       -}
 (DIR) diff --git a/ics2tsv.c b/ics2tsv.c
       @@ -0,0 +1,62 @@
       +#include <stdio.h>
       +
       +#include "ical.h"
       +#include "log.h"
       +#include "util.h"
       +
       +int
       +print_ical_to_tsv(FILE *fp)
       +{
       +        struct ical_contentline contentline;
       +        char *line = NULL;
       +        size_t sz = 0;
       +        ssize_t r;
       +
       +        ical_init_contentline(&contentline);
       +
       +        while ((r = ical_read_line(&line, &sz, fp)) > 0) {
       +                debug("readling line \"%s\"", line);
       +                if (ical_parse_contentline(&contentline, line) < 0)
       +                        die("parsing line \"%s\"", line);
       +        }
       +        return r;
       +}
       +
       +void
       +print_header(void)
       +{
       +        char *fields[] = { "", NULL };
       +
       +        printf("%s\t%s", "beg", "end");
       +
       +        for (char **f = fields; *f != NULL; f++) {
       +                fprintf(stdout, "\t%s", *f);
       +        }
       +        fprintf(stdout, "\n");
       +}
       +
       +int
       +main(int argc, char **argv)
       +{
       +        print_header();
       +
       +        log_arg0 = *argv++;
       +
       +        if (*argv == NULL) {
       +                if (print_ical_to_tsv(stdin) < 0)
       +                        die("converting stdin");
       +        }
       +
       +        for (; *argv != NULL; argv++, argc--) {
       +                FILE *fp;
       +
       +                info("converting \"%s\"", *argv);
       +                if ((fp = fopen(*argv, "r")) == NULL)
       +                        die("opening %s", *argv);
       +                if (print_ical_to_tsv(fp) < 0)
       +                        die("converting %s", *argv);
       +                fclose(fp);
       +        }
       +
       +        return 0;
       +}
 (DIR) diff --git a/src/ical.c b/src/ical.c
       @@ -0,0 +1,108 @@
       +#include "ical.h"
       +
       +#include <errno.h>
       +#include <stdio.h>
       +#include <stdlib.h>
       +#include <string.h>
       +
       +#include "util.h"
       +
       +int
       +ical_read_line(char **line, size_t *sz, FILE *fp)
       +{
       +        ssize_t r;
       +        char *tail = NULL;
       +        size_t tail_sz = 0;
       +        int c, ret = -1;
       +
       +        if ((r = getline(line, sz, fp)) <= 0)
       +                return r;
       +        strchomp(*line);
       +
       +        for (;;) {
       +                if ((c = fgetc(fp)) == EOF) {
       +                        ret = ferror(fp) ? -1 : 0;
       +                        goto end;
       +                }
       +                if (c != ' ')
       +                        break;
       +                if ((r = getline(&tail, &tail_sz, fp)) <= 0) {
       +                        ret = r;
       +                        goto end;
       +                }
       +                strchomp(tail);
       +                if (strappend(line, tail) < 0)
       +                        goto end;
       +        }
       +
       +        ret = 1;
       +end:
       +        free(tail);
       +        ungetc(c, fp);
       +        return ret;
       +}
       +
       +int
       +ical_parse_contentline(struct ical_contentline *contentline, char *line)
       +{
       +        char *column, *equal, *param, *cp;
       +        size_t sz;
       +
       +        debug("0");
       +
       +        if ((column = strchr(line, ':')) == NULL)
       +                return -1;
       +        *column = '\0';
       +
       +        {
       +                size_t len;
       +
       +                debug("1.1");
       +                len = strlen(column + 1);
       +                debug("1.2");
       +        }
       +        
       +
       +        if ((contentline->value = strdup(column + 1)) == NULL)
       +                return -1;
       +
       +        debug("2");
       +
       +        cp = strchr(line, ';');
       +        cp = (cp == NULL) ? (NULL) : (cp + 1);
       +
       +        debug("3");
       +
       +        while ((param = strsep(&cp, ";")) != NULL) {
       +                if ((equal = strchr(param, '=')) == NULL)
       +                        return -1;
       +                *equal = '\0';
       +
       +                if (map_set(&contentline->param, param, equal + 1) < 0)
       +                        return -1;
       +        }
       +
       +        debug("4");
       +
       +        sz = sizeof(contentline->name);
       +        if (strlcpy(contentline->name, line, sz) >= sz)
       +                return errno=EMSGSIZE, -1;
       +
       +        debug("5");
       +
       +        return 0;
       +}
       +
       +void
       +ical_init_contentline(struct ical_contentline *contentline)
       +{
       +        memset(contentline, 0, sizeof(*contentline));
       +}
       +
       +
       +void
       +ical_free_contentline(struct ical_contentline *contentline)
       +{
       +        map_free(&contentline->param);
       +        free(contentline->value);
       +}
 (DIR) diff --git a/src/ical.h b/src/ical.h
       @@ -0,0 +1,25 @@
       +#ifndef ICAL_H
       +#define ICAL_H
       +
       +#include <stdio.h>
       +#include <time.h>
       +
       +#include "map.h"
       +
       +struct ical_vevent {
       +        time_t beg, end;
       +        struct map map;
       +};
       +
       +struct ical_contentline {
       +        char name[32], *value;
       +        struct map param;
       +};
       +
       +/** src/ical.c **/
       +int ical_read_line(char **line, size_t *sz, FILE *fp);
       +int ical_parse_contentline(struct ical_contentline *contentline, char *line);
       +void ical_init_contentline(struct ical_contentline *contentline);
       +void ical_free_contentline(struct ical_contentline *contentline);
       +
       +#endif
 (DIR) diff --git a/src/log.c b/src/log.c
       @@ -0,0 +1,89 @@
       +#include "log.h"
       +
       +#include <assert.h>
       +#include <string.h>
       +
       +/*
       + * log.c - log to standard error according to the log level
       + *
       + * Instead of logging to syslog, delegate logging to a separate
       + * tool, such as FreeBSD's daemon(8), POSIX's logger(1).
       + */
       +
       +#include <errno.h>
       +#include <stdio.h>
       +#include <stdlib.h>
       +
       +#define LOG_DEFAULT 3 /* info */
       +
       +int log_level = -1;
       +char *log_arg0 = NULL;
       +
       +void
       +vlogf(int level, char const *flag, char const *fmt, va_list va)
       +{
       +        char *env;
       +        int e = errno;
       +
       +        if (log_level < 0) {
       +                env = getenv("LOG");
       +                log_level = (env == NULL ? 0 : atoi(env));
       +                log_level = (log_level > 0 ? log_level : LOG_DEFAULT);
       +        }
       +
       +        if (log_level < level)
       +                return;
       +
       +        if (log_arg0 != NULL)
       +                fprintf(stderr, "%s: ", log_arg0);
       +
       +        fprintf(stderr, "%s: ", flag);
       +        vfprintf(stderr, fmt, va);
       +
       +        if (e != 0)
       +                fprintf(stderr, ": %s", strerror(e));
       +
       +        fprintf(stderr, "\n");
       +        fflush(stderr);
       +}
       +
       +void
       +die(char const *fmt, ...)
       +{
       +        va_list va;
       +
       +        va_start(va, fmt);
       +        vlogf(1, "error", fmt, va);
       +        va_end(va);
       +        exit(1);
       +}
       +
       +void
       +warn(char const *fmt, ...)
       +{
       +        va_list va;
       +
       +        va_start(va, fmt);
       +        vlogf(2, "warn", fmt, va);
       +        va_end(va);
       +}
       +
       +void
       +info(char const *fmt, ...)
       +{
       +        va_list va;
       +
       +        va_start(va, fmt);
       +        vlogf(3, "info", fmt, va);
       +        va_end(va);
       +}
       +
       +void
       +debug(char const *fmt, ...)
       +{
       +        va_list va;
       +
       +        va_start(va, fmt);
       +        vlogf(4, "debug", fmt, va);
       +        va_end(va);
       +}
 (DIR) diff --git a/src/log.h b/src/log.h
       @@ -0,0 +1,15 @@
       +#ifndef LOG_H
       +#define LOG_H
       +
       +#include <stdarg.h>
       +
       +/** src/log.c **/
       +int log_level;
       +char *log_arg0;
       +void vlogf(int level, char const *flag, char const *fmt, va_list va);
       +void die(char const *fmt, ...);
       +void warn(char const *fmt, ...);
       +void info(char const *fmt, ...);
       +void debug(char const *fmt, ...);
       +
       +#endif
 (DIR) diff --git a/src/map.c b/src/map.c
       @@ -0,0 +1,102 @@
       +#include "map.h"
       +
       +#include <stdlib.h>
       +#include <string.h>
       +
       +#include "util.h"
       +
       +static int
       +map_cmp(void const *v1, void const *v2)
       +{
       +        struct map_entry const *e1 = v1, *e2 = v2;
       +
       +        return strcmp(e1->key, e2->key);
       +}
       +
       +void *
       +map_get(struct map *map, char *key)
       +{
       +        struct map_entry *entry, k = { .key = key };
       +        size_t sz;
       +
       +        sz = sizeof(*map->entry);
       +        if ((entry = bsearch(&k, map->entry, map->len, sz, map_cmp)) == NULL)
       +                return NULL;
       +        return entry->value;
       +}
       +
       +int
       +map_set(struct map *map, char *key, void *value)
       +{
       +        struct map_entry *insert, *e;
       +        size_t i, sz;
       +        void *v;
       +
       +        debug("%s: key=%s len=%zd", __func__, key, map->len);
       +
       +        for (i = 0; i < map->len; i++) {
       +                int cmp = strcmp(key, map->entry[i].key);
       +                debug("cmp(%s,%s)=%d", key, map->entry[i].key, cmp);
       +
       +                if (cmp == 0) {
       +                        map->entry[i].value = value;
       +                        return 0;
       +                }
       +                if (cmp < 0)
       +                        break;
       +        }
       +
       +        sz = sizeof(*map->entry);
       +        if ((v = reallocarray(map->entry, map->len + 1, sz)) == NULL)
       +                return -1;
       +        map->entry = v;
       +        map->len++;
       +
       +        insert = map->entry + i;
       +        e = map->entry + map->len - 1 - 1;
       +        for (; e >= insert; e--)
       +                e[1].key = e[0].key;
       +
       +        if ((insert->key = strdup(key)) == NULL)
       +                return -1;
       +        insert->value = value;
       +
       +        return 0;
       +}
       +
       +int
       +map_del(struct map *map, char *key)
       +{
       +        size_t i;
       +
       +        for (i = 0; i < map->len; i++) {
       +                int cmp = strcmp(key, map->entry[i].key);
       +
       +                if (cmp == 0)
       +                        break;
       +                if (cmp < 0)
       +                        return -1;
       +        }
       +        if (i == map->len)
       +                return -1;
       +
       +        map->len--;
       +        for (; i < map->len; i++)
       +                map->entry[i] = map->entry[i + 1];
       +        return 0;
       +}
       +
       +void
       +map_free_values(struct map *map)
       +{
       +        for (size_t i = 0; i < map->len; i++)
       +                free(map->entry[map->len - 1].value);
       +}
       +
       +void
       +map_free(struct map *map)
       +{
       +        for (size_t i = 0; i < map->len; i++)
       +                free(map->entry[map->len - 1].key);
       +        free(map->entry);
       +}
 (DIR) diff --git a/src/map.h b/src/map.h
       @@ -0,0 +1,23 @@
       +#ifndef MAP_H
       +#define MAP_H
       +
       +#include <stddef.h>
       +
       +struct map_entry {
       +        char *key;
       +        void *value;
       +};
       +
       +struct map {
       +        struct map_entry *entry;
       +        size_t len;
       +};
       +
       +/** src/map.c **/
       +void * map_get(struct map *map, char *key);
       +int map_set(struct map *map, char *key, void *value);
       +int map_del(struct map *map, char *key);
       +void map_free_values(struct map *map);
       +void map_free(struct map *map);
       +
       +#endif
 (DIR) diff --git a/src/util.c b/src/util.c
       @@ -0,0 +1,74 @@
       +#include "util.h"
       +
       +#include <errno.h>
       +#include <stdint.h>
       +#include <stdlib.h>
       +#include <string.h>
       +
       +size_t
       +strlcpy(char *buf, char const *str, size_t sz)
       +{
       +        size_t len, cpy;
       +
       +        cpy = ((len = strlen(str)) > sz) ? (sz) : (len);
       +        memcpy(buf, str, cpy + 1);
       +        buf[sz - 1] = '\0';
       +        return len;
       +}
       +
       +char *
       +strsep(char **str_p, char const *sep)
       +{
       +        char *s, *prev;
       +
       +        if (*str_p == NULL)
       +                return NULL;
       +
       +        for (s = prev = *str_p; strchr(sep, *s) == NULL; s++)
       +                continue;
       +
       +        if (*s == '\0') {
       +                *str_p = NULL;
       +        } else {
       +                *s = '\0';
       +                *str_p = s + 1;
       +        }
       +        return prev;
       +}
       +
       +void
       +strchomp(char *line)
       +{
       +        size_t len;
       +
       +        len = strlen(line);
       +        if (len > 0 && line[len - 1] == '\n')
       +                line[len-- - 1] = '\0';
       +        if (len > 0 && line[len - 1] == '\r')
       +                line[len-- - 1] = '\0';
       +}
       +
       +int
       +strappend(char **base_p, char const *s)
       +{
       +        size_t base_len, s_len;
       +        void *v;
       +
       +        base_len = strlen(*base_p);
       +        s_len = strlen(s);
       +
       +        if ((v = realloc(*base_p, base_len + s_len + 1)) == NULL)
       +                return -1;
       +
       +        *base_p = v;
       +        memcpy(*base_p + base_len, s, s_len + 1);
       +        return 0;
       +}
       +
       +void *
       +reallocarray(void *buf, size_t len, size_t sz)
       +{
       +        if (SIZE_MAX / len < sz)
       +                return errno=ERANGE, NULL;
       +        return realloc(buf, len * sz);
       +}
 (DIR) diff --git a/src/util.h b/src/util.h
       @@ -0,0 +1,13 @@
       +#ifndef UTIL_H
       +#define UTIL_H
       +
       +#include <stddef.h>
       +
       +/** src/util.c **/
       +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);
       +void * reallocarray(void *buf, size_t len, size_t sz);
       +
       +#endif
 (DIR) diff --git a/tsv2tcal b/tsv2tcal
       @@ -1,91 +0,0 @@
       -#!/usr/bin/awk -f
       -
       -function isleap(year)
       -{
       -        return (year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0)
       -}
       -
       -function mdays(mon, year)
       -{
       -        return (mon == 2) ? (28 + isleap(year)) : (30 + (mon + (mon > 7)) % 2)
       -}
       -
       -function gmtime(sec, tm)
       -{
       -        tm["year"] = 1970
       -        while (sec >= (s = 86400 * (365 + isleap(tm["year"])))) {
       -                tm["year"]++
       -                sec -= s
       -        }
       -        tm["mon"] = 1
       -        while (sec >= (s = 86400 * mdays(tm["mon"], tm["year"]))) {
       -                tm["mon"]++
       -                sec -= s
       -        }
       -        tm["mday"] = 1
       -        while (sec >= (s = 86400)) {
       -                tm["mday"]++
       -                sec -= s
       -        }
       -        tm["hour"] = 0
       -        while (sec >= 3600) {
       -                tm["hour"]++
       -                sec -= 3600
       -        }
       -        tm["min"] = 0
       -        while (sec >= 60) {
       -                tm["min"]++
       -                sec -= 60
       -        }
       -        tm["sec"] = sec
       -}
       -
       -function localtime(sec, tm,
       -        tz, h, m)
       -{
       -        return gmtime(sec + TZ, tm)
       -}
       -
       -BEGIN {
       -        "date +%z" | getline tz
       -        close("date +%z")
       -        TZ = substr(tz, 1, 1) substr(tz, 2, 2)*3600 + substr(tz, 4, 2)*60
       -
       -        print("TZ" tz)
       -
       -        FS = "\t"
       -}
       -
       -NR == 1 {
       -        for (i = 1; i <= NF; i++)
       -                name[i] = $i
       -        next
       -}
       -
       -{
       -        for (i = 1; i <= NF; i++)
       -                ev[name[i]] = $i
       -
       -        print("")
       -
       -        localtime(ev["beg"] + offset, tm)
       -        printf("%04d-%02d-%02d %02d:%02d\n",
       -          tm["year"], tm["mon"], tm["mday"], tm["hour"], tm["min"])
       -        delete ev["beg"]
       -
       -        localtime(ev["end"] + offset, tm)
       -        printf("%04d-%02d-%02d %02d:%02d\n",
       -          tm["year"], tm["mon"], tm["mday"], tm["hour"], tm["min"])
       -        delete ev["end"]
       -
       -        for (i = 1; i <= NF; i++) {
       -                if (name[i] in ev && ev[name[i]])
       -                        printf(" %s: %s\n", name[i], ev[name[i]])
       -        }
       -
       -        delete ev
       -}
       -
       -END {
       -        print("")
       -}