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("")
-}