agendafs
A filesystem for your calendar.
git clone git://mccd.space/agendafscommit 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:
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(®ex_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(®ex_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
];
}