ref: 3cd1ee7c4f5d66a1f2bc1f0a1df429d9829c28df
author: phil9 <telephil9@gmail.com>
date: Mon Mar 7 15:08:39 EST 2022
initial import
--- /dev/null
+++ b/LICENSE
@@ -1,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 phil9 <telephil9@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
--- /dev/null
+++ b/README.md
@@ -1,0 +1,33 @@
+# calfs
+A 9p calendar filesystem.
+calfs reads ical files and exposes calendar events with the following structure:
+```
+<mtpt>
+|- <year>
+ |- <month>
+ |- <day>
+ |- <n>
+ |- uid
+ |- summary
+ |- description
+ |- location
+ |- start
+ |- end
+ |- created
+ |- last-modifed
+```
+
+*NB:* this is work in progress, the ical parser is just a quick hack and will surely break.
+
+## Usage
+```sh
+% mk install
+% calfs <file.ics>...
+```
+
+## License
+MIT
+
+## Bugs
+Guaranteed.
+
--- /dev/null
+++ b/a.h
@@ -1,0 +1,35 @@
+#include <u.h>
+#include <libc.h>
+#include <fcall.h>
+#include <thread.h>
+#include <9p.h>
+#include <bio.h>
+
+typedef struct Event Event;
+
+struct Event
+{
+ char *uid;
+ char *summary;
+ char *description;
+ char *location;
+ Tm start;
+ Tm end;
+ Tm lastmod;
+ Tm created;
+};
+
+/* ical */
+int readical(const char*);
+
+/* utils */
+void* emalloc(ulong);
+void* erealloc(void*, ulong);
+File* ecreatefile(File*, char*, char*, ulong, void*);
+void readtm(Req*, Tm*);
+int cmpevent(void*, void*);
+
+extern Event **events;
+extern usize nevents;
+extern usize eventsz;
+
--- /dev/null
+++ b/calfs.c
@@ -1,0 +1,153 @@
+#include "a.h"
+
+int debug = 0;
+Event **events;
+usize nevents;
+usize eventsz;
+
+void
+fsread(Req *r)
+{
+ File *f;
+ Event *e;
+
+ f = r->fid->file;
+ e = f->aux;
+ if(strcmp(f->name, "uid") == 0)
+ readstr(r, e->uid);
+ else if(strcmp(f->name, "summary") == 0)
+ readstr(r, e->summary);
+ else if(strcmp(f->name, "description") == 0)
+ readstr(r, e->description);
+ else if(strcmp(f->name, "location") == 0)
+ readstr(r, e->location);
+ else if(strcmp(f->name, "start") == 0)
+ readtm(r, &e->start);
+ else if(strcmp(f->name, "end") == 0)
+ readtm(r, &e->end);
+ else if(strcmp(f->name, "last-modified") == 0)
+ readtm(r, &e->lastmod);
+ else if(strcmp(f->name, "created") == 0)
+ readtm(r, &e->created);
+ else{
+ respond(r, "no such file or directory");
+ return;
+ }
+ respond(r, nil);
+}
+
+Srv fs = {
+ .read = fsread,
+};
+
+void
+createevtfiles(File *p, Event *e)
+{
+ const char *filenames[] = {
+ "uid",
+ "summary",
+ "description",
+ "location",
+ "start",
+ "end",
+ "last-modified",
+ "created",
+ };
+ int i;
+
+ for(i = 0; i < nelem(filenames); i++)
+ createfile(p, filenames[i], nil, 0644, e);
+}
+
+
+enum { Cnone, Cyear, Cmonth, Cday };
+
+int
+change(Event *e, int y, int m, int d)
+{
+ if(e->start.year != y)
+ return Cyear;
+ else if(e->start.mon != m)
+ return Cmonth;
+ else if(e->start.mday != d)
+ return Cday;
+ return Cnone;
+}
+
+Tree*
+buildtree(void)
+{
+ Tree *tree;
+ int i, n, y, m, d;
+ File *yd, *md, *dd, *nd;
+ Event *e;
+ char buf[16] = {0};
+
+ tree = alloctree(nil, nil, DMDIR|0555, nil);
+ y = m = d = -1;
+ yd = md = dd = nil;
+ n = 1;
+ for(i = 0; i < nevents; i++){
+ e = events[i];
+ switch(change(e, y, m, d)){
+ case Cyear:
+ y = e->start.year;
+ snprint(buf, sizeof buf, "%d", y+1900);
+ yd = ecreatefile(tree->root, buf, nil, DMDIR|0555, nil);
+ case Cmonth:
+ m = e->start.mon;
+ snprint(buf, sizeof buf, "%02d", m+1);
+ md = ecreatefile(yd, buf, nil, DMDIR|0555, nil);
+ case Cday:
+ d = e->start.mday;
+ n = 1;
+ snprint(buf, sizeof buf, "%02d", d);
+ dd = ecreatefile(md, buf, nil, DMDIR|0555, nil);
+ }
+ snprint(buf, sizeof buf, "%d", n);
+ nd = createfile(dd, buf, nil, DMDIR|0555, nil);
+ createevtfiles(nd, e);
+ n += 1;
+ }
+ return tree;
+}
+
+void
+usage(void)
+{
+ fprint(2, "usage: %s <filename>...\n", argv0);
+ exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+ char *mtpt;
+ int i;
+
+ mtpt = "/mnt/cal";
+
+ ARGBEGIN{
+ case 'D':
+ chatty9p++;
+ break;
+ case 'd':
+ debug++;
+ break;
+ case 'm':
+ mtpt = ARGF();
+ break;
+ }ARGEND;
+
+ tmfmtinstall();
+ if(argc < 1)
+ usage();
+ for(i = 0; i < argc; i++){
+ if(readical(argv[i]) < 0)
+ sysfatal("unable to read ical file '%s': %r", argv[i]);
+ }
+ qsort(events, nevents, sizeof(Event*), cmpevent);
+ fs.tree = buildtree();
+ postmountsrv(&fs, nil, mtpt, 0);
+ exits(0);
+}
--- /dev/null
+++ b/ical.c
@@ -1,0 +1,99 @@
+#include "a.h"
+
+void
+addevent(Event *e)
+{
+ if(events == nil){
+ eventsz = 128;
+ nevents = 0;
+ events = emalloc(128 * sizeof(Event*));
+ }else if(nevents == eventsz){
+ eventsz *= 2;
+ events = erealloc(events, eventsz * sizeof(Event*));
+ }
+ events[nevents++] = e;
+}
+
+int
+startswith(char *s, char *t)
+{
+ return strncmp(s, t, strlen(t)) == 0;
+}
+
+Tm
+parsedate(char *s)
+{
+ Tm tm, *res;
+
+ res = tmparse(&tm, "YYYYMMDDThhmmss", s, nil, nil);
+ if(res == nil)
+ fprint(2, "unable to parse date '%s'\n", s);
+ return tm;
+}
+
+Event*
+readevent(Biobuf *bp)
+{
+ Event *e;
+ char *s;
+ int done;
+
+ e = emalloc(sizeof *e);
+ done = 0;
+ while(!done){
+ s = Brdstr(bp, '\n', 1);
+ if(s == nil){
+ free(e);
+ werrstr("unexpected end of file while parsing event");
+ return nil;
+ }
+ if(startswith(s, "END:VEVENT"))
+ done = 1;
+ else if(startswith(s, "UID"))
+ e->uid = strdup(s+4);
+ else if(startswith(s, "SUMMARY"))
+ e->summary = strdup(s+8);
+ else if(startswith(s, "DESCRIPTION"))
+ e->description = strdup(s+13);
+ else if(startswith(s, "LOCATION"))
+ e->location = strdup(s+9);
+ else if(startswith(s, "DTSTART"))
+ e->start = parsedate(s+8);
+ else if(startswith(s, "DTEND"))
+ e->end = parsedate(s+6);
+ else if(startswith(s, "LAST-MODIFIED"))
+ e->lastmod = parsedate(s+14);
+ else if(startswith(s, "CREATED"))
+ e->created = parsedate(s+8);
+ free(s);
+ }
+ return e;
+}
+
+int
+readical(const char *f)
+{
+ Biobuf *bp;
+ char *s;
+ Event *e;
+
+ bp = Bopen(f, OREAD);
+ if(bp == nil)
+ return -1;
+ for(;;){
+ s = Brdstr(bp, '\n', 1);
+ if(s == nil)
+ break;
+ if(strncmp(s, "BEGIN:VEVENT", 12) == 0){
+ e = readevent(bp);
+ free(s);
+ if(e != nil)
+ addevent(e);
+ else
+ fprint(2, "unable to parse event: %r\n");
+ }
+ }
+ Bterm(bp);
+ return 0;
+}
+
--- /dev/null
+++ b/mkfile
@@ -1,0 +1,9 @@
+</$objtype/mkfile
+
+TARG=calfs
+BIN=/$objtype/bin
+OFILES=calfs.$O ical.$O utils.$O
+HFILES=a.h
+
+</sys/src/cmd/mkone
+
--- /dev/null
+++ b/utils.c
@@ -1,0 +1,57 @@
+#include "a.h"
+
+void*
+emalloc(ulong size)
+{
+ void *p;
+
+ p = malloc(size);
+ if(p == nil)
+ sysfatal("malloc: %r");
+ return p;
+}
+
+void*
+erealloc(void *p, ulong size)
+{
+ void *q;
+
+ q = realloc(p, size);
+ if(q == nil)
+ sysfatal("realloc: %r");
+ return q;
+}
+
+File*
+ecreatefile(File *dir, char *name, char *uid, ulong mode, void *aux)
+{
+ File *f;
+
+ f = createfile(dir, name, uid, mode, aux);
+ if(f == nil)
+ sysfatal("createfile: %r");
+ return f;
+}
+
+void
+readtm(Req *r, Tm *tm)
+{
+ char buf[64] = {0};
+
+ snprint(buf, sizeof buf, "%τ", tmfmt(tm, nil));
+ readstr(r, buf);
+}
+
+int
+cmpevent(void *a, void *b)
+{
+ Event **e0, **e1;
+ vlong t0, t1;
+
+ e0 = a;
+ e1 = b;
+ t0 = tmnorm(&(*e0)->start);
+ t1 = tmnorm(&(*e1)->start);
+ return t0 - t1;
+}
+