agendafs

A filesystem for your calendar.

git clone git://mccd.space/agendafs
commit 8cb9906c357665cc99fb4f8d3bd0a09951c76108
parent b52c9e57068b0c124bd693e716cbcf1940da53b6
Author: Marc Coquand <marc@coquand.email>
Date:   Mon,  2 Jun 2025 18:59:54 +0100

Begin migration over to libical

Obviously I should've used this from the beginning.

This moves over the parsing logic, but the idea is to us
this library wherever possible.

Diffstat:
MMakefile | 2+-
MREADME.md | 2+-
Mmain.c | 181+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Mshell.nix | 1+
4 files changed, 107 insertions(+), 79 deletions(-)
diff --git a/Makefile b/Makefile
@@ -1,7 +1,7 @@
 CC = gcc
 CFLAGS = -Wall -O3
 CFLAGS_DEBUG = -Wall -g
-LIBS = `pkg-config uuid fuse3 --cflags --libs`
+LIBS = `pkg-config uuid fuse3 libical --cflags --libs`
 
 all: caldavfs
 
diff --git a/README.md b/README.md
@@ -78,7 +78,7 @@ Early alpha. Very WIP.
 
 ## Dependencies
 
-Some C compiler along with libfuse and pkg-config.
+Some C compiler along with libfuse, libical and pkg-config.
 
 Right now you also need a tool that syncs .ics files with a caldav server,
 like [vdirsyncer](https://vdirsyncer.pimutils.org/en/stable/when.html#).
diff --git a/main.c b/main.c
@@ -5,6 +5,7 @@
 #include <dirent.h>
 #include <errno.h>
 #include <fuse3/fuse.h>
+#include <libical/ical.h>
 #include <pthread.h>
 #include <regex.h>
 #include <stdbool.h>
@@ -30,12 +31,15 @@ regex_t regex_dtstamp, regex_lastmod, regex_fuse_file;
 
 time_t INVALID_TIME = (time_t)-1;
 
+#define ENOTJOURNAL 1001
+
 typedef struct {
 	char *filename;
 	char *filename_original;
 	char *summary;
 	char *description;
 	char *uid;
+	char *categories;
 	time_t modified_at;
 	time_t created_at;
 } journal_entry;
@@ -58,7 +62,9 @@ free_journal_entry(void *val)
 	if (entry->summary != NULL && strcmp(entry->summary, "") != 0) {
 		free(entry->summary);
 	}
-
+	if (entry->categories != NULL && strcmp(entry->summary, "") != 0) {
+		free(entry->categories);
+	}
 	if (entry->description != NULL && strcmp(entry->description, "") != 0) {
 		free(entry->description);
 	}
@@ -280,96 +286,114 @@ get_entry_from_fuse_path(const char *path)
 	return entry;
 }
 
-int
-parse_ics_file(const char *file_path, journal_entry *entry)
+time_t
+icaltime_to_time_t(icaltimetype tt)
 {
-	FILE *file = fopen(file_path, "r");
-	if (!file) {
-		perror("Failed to open .ics file");
-		return -1;
-	}
-
-	// Read the whole file into a string
-	fseek(file, 0, SEEK_END);
-	long file_size = ftell(file);
-	fseek(file, 0, SEEK_SET);
+	struct tm t = {.tm_year = tt.year - 1900,
+		       .tm_mon = tt.month - 1,
+		       .tm_mday = tt.day,
+		       .tm_hour = tt.hour,
+		       .tm_min = tt.minute,
+		       .tm_sec = tt.second,
+		       .tm_isdst = -1};
+	return mktime(&t);
+}
 
-	char *file_content = calloc(file_size + 1, sizeof(char));
+char *
+read_stream(char *s, size_t size, void *d)
+{
+	return fgets(s, (int)size, (FILE *)d);
+}
 
-	fread(file_content, 1, file_size, file);
-	file_content[file_size] = '\0';
-	fclose(file);
+icalcomponent *
+parse_ics_file(const char *filename)
+{
+	icalcomponent *component = NULL;
+	FILE *stream = fopen(filename, "r");
+	assert(stream != 0);
+
+	icalparser *parser = icalparser_new();
+	icalparser_set_gen_data(parser, stream);
+
+	char *line;
+	do {
+		line = icalparser_get_line(parser, read_stream);
+		icalcomponent *temp = icalparser_add_line(parser, line);
+		if (temp != NULL && component == NULL) {
+			component = temp; // Save the first completed component
+		}
+	} while (line != NULL);
 
-	regmatch_t matches_dtstamp[2], matches_lastmod[2];
+	fclose(stream);
+	icalparser_free(parser);
+	return component;
+}
 
-	// SUMMARY
-	entry->summary = extract_ical_field(file_content, "SUMMARY");
-	if (!entry->summary) {
-		fprintf(stderr, "SUMMARY field not found in: %s\n", file_path);
-		free_journal_entry(entry);
+int
+parse_ics_to_journal_entry(const char *filename, journal_entry *entry)
+{
+	icalcomponent *component = parse_ics_file(filename);
+	if (component == NULL) {
+		LOG("No component");
 		return -1;
 	}
-	unescape_text(entry->summary);
-
-	// UID
-	entry->uid = extract_ical_field(file_content, "UID");
-	if (!entry->uid) {
-		fprintf(stderr, "UID field not found in: %s\n", file_path);
-		free_journal_entry(entry);
-		return -1;
+	icalcomponent *journal =
+	    icalcomponent_get_first_real_component(component);
+	if (journal == NULL) {
+		LOG("No journal component");
+		return -2;
 	}
-	unescape_text(entry->uid);
 
-	// DESCRIPTION
-	entry->description = extract_ical_field(file_content, "DESCRIPTION");
-	if (!entry->description) {
-		fprintf(stderr, "DESCRIPTION field not found in: %s\n",
-			file_path);
+	icalproperty *prop =
+	    icalcomponent_get_first_property(journal, ICAL_ANY_PROPERTY);
+	while (prop != NULL) {
+		icalproperty_kind kind = icalproperty_isa(prop);
+		switch (kind) {
+		case ICAL_SUMMARY_PROPERTY:
+			free(entry->summary);
+			entry->summary = strdup(icalproperty_get_summary(prop));
+			break;
 
-		free_journal_entry(entry);
-		return -1;
-	}
+		case ICAL_DESCRIPTION_PROPERTY:
+			free(entry->description);
+			entry->description =
+			    strdup(icalproperty_get_description(prop));
+			break;
 
-	unescape_text(entry->description);
+		case ICAL_UID_PROPERTY:
+			free(entry->uid);
+			entry->uid = strdup(icalproperty_get_uid(prop));
+			break;
 
-	// Extract the DTSTAMP, our created_at
-	if (regexec(&regex_dtstamp, file_content, 2, matches_dtstamp, 0) == 0) {
-		char *dtstamp_str =
-		    extract_match(file_content, matches_dtstamp[1]);
-		entry->created_at = parse_datetime(dtstamp_str);
-		free(dtstamp_str);
-		if (entry->created_at == INVALID_TIME) {
-			fprintf(stderr, "Failed to parse DTSTAMP in: %s\n",
-				file_path);
+		case ICAL_CATEGORIES_PROPERTY:
+			free(entry->categories);
+			entry->categories =
+			    strdup(icalproperty_get_categories(prop));
+			break;
 
-			free_journal_entry(entry);
-			return -1;
+		case ICAL_LASTMODIFIED_PROPERTY: {
+			struct icaltimetype tt = icalproperty_get_dtstamp(prop);
+			entry->modified_at = icaltime_as_timet(tt);
+			break;
 		}
-	}
-	else {
-		entry->created_at = time(NULL);
-	}
 
-	// LAST-MODIFIED
-	if (regexec(&regex_lastmod, file_content, 2, matches_lastmod, 0) == 0) {
-		char *lastmod_str =
-		    extract_match(file_content, matches_lastmod[1]);
-		entry->modified_at = parse_datetime(lastmod_str);
-		free(lastmod_str);
-		if (entry->modified_at == INVALID_TIME) {
-			fprintf(stderr,
-				"Failed to parse LAST-MODIFIED in: %s\n",
-				file_path);
-
-			free_journal_entry(entry);
-			return -1;
+		case ICAL_DTSTAMP_PROPERTY: {
+			struct icaltimetype tt = icalproperty_get_created(prop);
+			entry->created_at = icaltime_as_timet(tt);
+			break;
+		}
+		case ICAL_CREATED_PROPERTY: {
+			struct icaltimetype tt = icalproperty_get_created(prop);
+			entry->created_at = icaltime_as_timet(tt);
+			break;
 		}
-	}
-	else {
-		entry->modified_at = entry->created_at;
-	}
 
-	free(file_content);
+		default:
+			break;
+		}
+		prop =
+		    icalcomponent_get_next_property(journal, ICAL_ANY_PROPERTY);
+	}
 	return 0;
 }
 
@@ -468,7 +492,8 @@ load_journal_entries()
 			}
 
 			// Parse the ICS file and check for errors
-			if (parse_ics_file(filepath, new_entry) != 0) {
+			if (parse_ics_to_journal_entry(filepath, new_entry) !=
+			    0) {
 				LOG("Failed to parse entry: %s", entry->d_name);
 
 				free(filepath);
@@ -686,12 +711,14 @@ journal_entry_to_ical(const journal_entry *entry)
 		 "BEGIN:VJOURNAL\n"
 		 "UID:%s\n"
 		 "DTSTAMP:%s\n"
+		 "CREATED:%s\n"
 		 "LAST-MODIFIED:%s\n"
 		 "SUMMARY:%s\n"
+		 "STATUS:FINAL\n"
 		 "DESCRIPTION:%s\n"
 		 "END:VJOURNAL\n"
 		 "END:VCALENDAR\n",
-		 entry->uid, dtstamp, lastmod, escaped_summary,
+		 entry->uid, dtstamp, dtstamp, lastmod, escaped_summary,
 		 escaped_description);
 
 	free(escaped_summary);
@@ -1044,7 +1071,7 @@ update_fuse_entry_from_original(const char *filename)
 
 	LOG("Parsing ICS file %s", filename);
 
-	if (parse_ics_file(filename, new_entry) != 0) {
+	if (parse_ics_to_journal_entry(filename, new_entry) != 0) {
 		// Parsing failed, clean up and maybe remove old entry if any
 		free_journal_entry(new_entry);
 		pthread_mutex_unlock(&entries_mutex);
diff --git a/shell.nix b/shell.nix
@@ -14,5 +14,6 @@ pkgs.mkShell {
     pkg-config
     valgrind
     libuuid
+    libical
   ];
 }