agendafs

A filesystem for your calendar.

git clone git://mccd.space/agendafs
commit 06037496d64e767850cdea46d260e587af288b45
parent 9666d06c8660caedcd5f0bffa70bd951a5f6abcb
Author: Marc Coquand <marc@coquand.email>
Date:   Fri, 30 May 2025 20:01:27 +0100

Support multiline, createdat

Diffstat:
Mmain.c | 100++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Asample_ics/2025-01-04.ics | 10++++++++++
2 files changed, 99 insertions(+), 11 deletions(-)
diff --git a/main.c b/main.c
@@ -17,8 +17,8 @@ typedef struct {
     char *summary;
     char *description;
     time_t timestamp;
+    time_t created_at;
 } journal_entry;
-
 static journal_entry *entries = NULL;
 static size_t entry_count = 0;
 
@@ -37,6 +37,20 @@ static void free_all_entries() {
     free(entries);
 }
 
+static time_t parse_datetime(const char *datetime) {
+    struct tm tm = {0};
+    int ret = sscanf(datetime, "%4d%2d%2dT%2d%2d%2dZ",
+                      &tm.tm_year, &tm.tm_mon, &tm.tm_mday,
+                      &tm.tm_hour, &tm.tm_min, &tm.tm_sec);
+    if (ret == 6) {
+        tm.tm_year -= 1900;  // tm_year is years since 1900
+        tm.tm_mon -= 1;      // tm_mon is months since January (0-11)
+        tm.tm_isdst = -1;    // Let mktime determine if daylight savings is needed
+        return mktime(&tm);
+    }
+    return -1;
+}
+
 static int parse_ics_file(const char *file_path, journal_entry *entry) {
     FILE *file = fopen(file_path, "r");
     if (!file) {
@@ -60,12 +74,16 @@ static int parse_ics_file(const char *file_path, journal_entry *entry) {
     fclose(file);
 
     // Regular expressions for SUMMARY and DESCRIPTION fields
-    regex_t regex_summary, regex_description;
-    regmatch_t matches_summary[2], matches_description[2];
+    regex_t regex_summary, regex_description, regex_dtstamp, regex_lastmod;
+    regmatch_t matches_summary[2], matches_description[2], matches_dtstamp[2], matches_lastmod[2];
 
     // Compile regular expressions
     if (regcomp(&regex_summary, "SUMMARY:([^\r\n]+)", REG_EXTENDED) != 0 ||
-        regcomp(&regex_description, "DESCRIPTION:([^\n\r]*)", REG_EXTENDED) != 0) {
+        regcomp(&regex_description, "DESCRIPTION:([^\r\n]*)", REG_EXTENDED) != 0 || 
+        regcomp(&regex_dtstamp, "DTSTAMP:([0-9T]+Z)", REG_EXTENDED) != 0 ||
+        regcomp(&regex_lastmod, "LAST-MODIFIED:([0-9T]+Z)", REG_EXTENDED) != 0) {
+
+
         fprintf(stderr, "Failed to compile regular expressions\n");
         free(file_content);
         return -1;
@@ -92,10 +110,10 @@ static int parse_ics_file(const char *file_path, journal_entry *entry) {
         return -1;
     }
 
-    // Extract the DESCRIPTION
+    // Extract the DESCRIPTION (including multiline handling)
     if (regexec(&regex_description, file_content, 2, matches_description, 0) == 0) {
         size_t description_len = matches_description[1].rm_eo - matches_description[1].rm_so;
-        entry->description = malloc(description_len + 1);
+        entry->description = malloc(description_len + 1);  // +1 for null-termination
         if (!entry->description) {
             perror("Memory allocation failed for DESCRIPTION");
             free(file_content);
@@ -103,9 +121,17 @@ static int parse_ics_file(const char *file_path, journal_entry *entry) {
             regfree(&regex_description);
             return -1;
         }
-        strncpy(entry->description, file_content + matches_description[1].rm_so, description_len);
-        entry->description[description_len] = '\0';
-        printf("Matched DESCRIPTION: %s\n", entry->description); // Debugging line
+
+        // Copy the string using memcpy and ensure null termination
+        memcpy(entry->description, file_content + matches_description[1].rm_so, description_len);
+        entry->description[description_len] = '\0';  // Ensure null-termination
+
+        // Replace literal "\n" with actual newlines
+        char *newline_pos = NULL;
+        while ((newline_pos = strstr(entry->description, "\\n")) != NULL) {
+            *newline_pos = '\n';  // Replace literal '\n' with actual newline
+            memmove(newline_pos + 1, newline_pos + 2, strlen(newline_pos + 2) + 1);  // Shift the rest
+        }
     } else {
         fprintf(stderr, "DESCRIPTION field not found in: %s\n", file_path);
         free(file_content);
@@ -113,10 +139,56 @@ static int parse_ics_file(const char *file_path, journal_entry *entry) {
         regfree(&regex_description);
         return -1;
     }
+    char *escaped_comma_pos = NULL;
+    while ((escaped_comma_pos = strstr(entry->description, "\\,")) != NULL) {
+        *escaped_comma_pos = ',';  // Replace escaped comma with actual comma
+        memmove(escaped_comma_pos + 1, escaped_comma_pos + 2, strlen(escaped_comma_pos + 2) + 1);  // Shift the rest of the string
+    }
 
     // Set timestamp to the current time
-    entry->timestamp = time(NULL);
 
+    // Extract the DTSTAMP
+    if (regexec(&regex_dtstamp, file_content, 2, matches_dtstamp, 0) == 0) {
+        size_t dtstamp_len = matches_dtstamp[1].rm_eo - matches_dtstamp[1].rm_so;
+        char dtstamp_str[dtstamp_len + 1];
+        strncpy(dtstamp_str, file_content + matches_dtstamp[1].rm_so, dtstamp_len);
+        dtstamp_str[dtstamp_len] = '\0';
+
+        entry->created_at = parse_datetime(dtstamp_str);
+        if (entry->created_at == -1) {
+            fprintf(stderr, "Failed to parse DTSTAMP in: %s\n", file_path);
+            free(file_content);
+            regfree(&regex_summary);
+            regfree(&regex_description);
+            regfree(&regex_dtstamp);
+            regfree(&regex_lastmod);
+            return -1;
+        }
+    }
+
+    // Extract the LAST-MODIFIED (if present)
+    else if (regexec(&regex_lastmod, file_content, 2, matches_lastmod, 0) == 0) {
+        size_t lastmod_len = matches_lastmod[1].rm_eo - matches_lastmod[1].rm_so;
+        char lastmod_str[lastmod_len + 1];
+        strncpy(lastmod_str, file_content + matches_lastmod[1].rm_so, lastmod_len);
+        lastmod_str[lastmod_len] = '\0';
+
+        entry->timestamp = parse_datetime(lastmod_str);
+        if (entry->timestamp == -1) {
+            fprintf(stderr, "Failed to parse LAST-MODIFIED in: %s\n", file_path);
+            free(file_content);
+            regfree(&regex_summary);
+            regfree(&regex_description);
+            regfree(&regex_dtstamp);
+            regfree(&regex_lastmod);
+            return -1;
+        }
+    }
+
+    // If neither DTSTAMP nor LAST-MODIFIED found, set timestamp to current time
+    if (entry->timestamp == 0) {
+        entry->timestamp = time(NULL);
+    }
     // Clean up
     free(file_content);
     regfree(&regex_summary);
@@ -168,13 +240,19 @@ static int journal_getattr(const char *path, struct stat *stbuf, struct fuse_fil
             stbuf->st_mode = S_IFREG | 0444;
             stbuf->st_nlink = 1;
             stbuf->st_size = strlen(entries[i].summary) + strlen(entries[i].description) + 2;
+
+            // Set the times
+            stbuf->st_mtime = entries[i].timestamp;  // Use the timestamp as the modification time
+            stbuf->st_ctime = entries[i].timestamp;  // Set the change time to the same as the modification time
+            // Access time is current time
+            stbuf->st_atime = time(NULL);             
+
             return 0;
         }
     }
 
     return -ENOENT;
 }
-
 static int journal_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
                            off_t offset, struct fuse_file_info *fi,
                            enum fuse_readdir_flags flags) {
diff --git a/sample_ics/2025-01-04.ics b/sample_ics/2025-01-04.ics
@@ -0,0 +1,10 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+BEGIN:VEVENT
+UID:20251110T120000Z-002@host.com
+DTSTAMP:20251110T120000Z
+SUMMARY:Team Lunch
+DESCRIPTION:Lunch at The Bistro to celebrate recent achievements.
+LAST-MODIFIED:20251110T130000Z
+END:VEVENT
+END:VCALENDAR