agendafs
A filesystem for your calendar.
git clone git://mccd.space/agendafscommit e807aad93c181a99b256dd97212f74704357d59c
parent 5038735bcd45a07b560b204e2e36b34ac93b5cfc
Author: Marc Coquand <marc@coquand.email>
Date: Sat, 31 May 2025 20:03:07 +0100
Switch to hashmap implementation
Diffstat:
| M | Makefile | | | 10 | +++++----- |
| A | hashmap.c | | | 462 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | hashmap.h | | | 66 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | main.c | | | 1100 | +++++++++++++++++++++++++++++++++++-------------------------------------------- |
4 files changed, 1024 insertions(+), 614 deletions(-)
diff --git a/Makefile b/Makefile
@@ -4,11 +4,11 @@ LIBS = `pkg-config fuse3 --cflags --libs`
all: journalfs
-journalfs: main.c
- $(CC) $(CFLAGS) main.c -o journalfs $(LIBS)
+journalfs: main.c hashmap.c
+ $(CC) $(CFLAGS) main.c hashmap.c -o journalfs $(LIBS)
-debug: main.c
- $(CC) $(CFLAGS) $(DEBUG_FLAGS) main.c -o journalfs-debug $(LIBS)
+debug: main.c hashmap.c
+ $(CC) $(CFLAGS) $(DEBUG_FLAGS) main.c hashmap.c -o journalfs-debug $(LIBS)
clean:
- rm -f journalfs
+ rm -f journalfs journalfs-debug
diff --git a/hashmap.c b/hashmap.c
@@ -0,0 +1,462 @@
+// Taken from https://github.com/prismz/hashmap
+#include "hashmap.h"
+
+#include <inttypes.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#ifdef HM_DEBUG
+#include <stdarg.h>
+static void debug_print(char *fmt, ...)
+{
+ va_list ap;
+ va_start(ap, fmt);
+ vprintf(fmt, ap);
+ va_end(ap);
+}
+#else
+/* compiler should just optimize this away */
+static void debug_print() {}
+#endif
+
+static int hm_check_mem(void *ptr)
+{
+ if (ptr != NULL)
+ return 0;
+
+ if (HM_EXIT_ON_ALLOC_FAIL) {
+ fprintf(stderr, "failed to allocate memory\n");
+ exit(1);
+ }
+
+ return 1;
+}
+
+/* hash a single byte */
+static uint32_t fnv1a(unsigned char byte, uint32_t hash)
+{
+ return (byte ^ hash) * HM_FNV_PRIME;
+}
+
+static uint32_t fnv1a_hash(const char *str)
+{
+ uint32_t hash = HM_FNV_SEED;
+ while (*str)
+ hash = fnv1a((unsigned char)*str++, hash);
+ return hash;
+}
+
+/* will duplicate key */
+static struct bucket *new_bucket(const char *key, void *val)
+{
+ if (key == NULL)
+ return NULL;
+
+ struct bucket *b = HM_CALLOC_FUNC(1, sizeof(struct bucket));
+ if (hm_check_mem(b))
+ return NULL;
+
+ b->key = HM_STRDUP_FUNC(key);
+ if (hm_check_mem(b->key)) {
+ free(b);
+ return NULL;
+ }
+
+ b->val = val;
+ b->next = NULL;
+
+ b->hash = fnv1a_hash(key);
+
+ debug_print("creating bucket with key %s, hash of this key is %" PRIu32
+ "\n",
+ key, b->hash);
+
+ return b;
+}
+
+struct hashmap *hashmap_new(void (*val_free_func)(void *))
+{
+ /* Any smaller and we'd pretty much instantly have to resize */
+ size_t capacity = HM_DEFAULT_HASHMAP_SIZE;
+
+ struct hashmap *hm = HM_CALLOC_FUNC(1, sizeof(struct hashmap));
+ if (hm_check_mem(hm))
+ return NULL;
+
+ hm->capacity = capacity;
+ hm->n_buckets = 0;
+ hm->val_free_func = val_free_func;
+ hm->buckets = HM_CALLOC_FUNC(capacity, sizeof(struct bucket *));
+ if (hm_check_mem(hm->buckets)) {
+ free(hm);
+ return NULL;
+ }
+
+ return hm;
+}
+
+static void free_bucket(struct bucket *b, void (*val_free_func)(void *))
+{
+ if (b == NULL)
+ return;
+
+ struct bucket *curr;
+ while (b != NULL) {
+ curr = b;
+ b = b->next;
+
+ free(curr->key);
+ if (val_free_func != NULL && curr->val != NULL)
+ val_free_func(curr->val);
+ free(curr);
+ }
+}
+
+void hashmap_free(struct hashmap *hm)
+{
+ if (hm == NULL)
+ return;
+ for (size_t i = 0; i < hm->capacity; i++) {
+ if (hm->buckets[i] == NULL)
+ continue;
+ free_bucket(hm->buckets[i], hm->val_free_func);
+ }
+ free(hm->buckets);
+ free(hm);
+}
+
+int hashmap_resize(struct hashmap *hm)
+{
+ if (hm == NULL)
+ return -1;
+
+ size_t new_size = hm->capacity * HM_RESIZE_SCALE_FACTOR;
+
+ struct bucket **buckets =
+ HM_CALLOC_FUNC(new_size, sizeof(struct bucket *));
+ if (hm_check_mem(buckets))
+ return 1;
+
+ for (size_t i = 0; i < hm->capacity; i++) {
+ if (hm->buckets[i] == NULL)
+ continue;
+
+ struct bucket *target = hm->buckets[i];
+ struct bucket *curr;
+ while (target != NULL) {
+ curr = target;
+ target = target->next;
+
+ size_t curr_new_idx = curr->hash % (uint32_t)(new_size);
+
+ if (buckets[curr_new_idx] == NULL) {
+ buckets[curr_new_idx] = curr;
+ buckets[curr_new_idx]->next = NULL;
+ continue;
+ }
+
+ /* collision */
+
+ struct bucket *ptr = buckets[curr_new_idx];
+ while (ptr->next != NULL)
+ ptr = ptr->next;
+
+ ptr->next = curr;
+ ptr->next->next = NULL;
+ }
+
+ /* size_t new_idx = hm->buckets[i]->hash % (uint32_t)new_size;
+ debug_print("buckets at idx %ld move to idx %ld\n", i, new_idx);
+ if (buckets[new_idx] != NULL) {
+ free(buckets);
+ return 1;
+ }
+ buckets[new_idx] = hm->buckets[i]; */
+ }
+
+ free(hm->buckets);
+ hm->buckets = buckets;
+ hm->capacity = new_size;
+
+ return 0;
+}
+
+int hashmap_insert(struct hashmap *hm, const char *key, void *val)
+{
+ if (hm == NULL || key == NULL)
+ return 1;
+
+ if ((float)(hm->n_buckets + 1) >
+ (HM_HASHMAP_MAX_LOAD * (float)hm->capacity)) {
+ if (hashmap_resize(hm))
+ return -1;
+ }
+
+ hm->n_buckets++;
+ struct bucket *b = new_bucket(key, val);
+ if (hm_check_mem(b))
+ return 1;
+ size_t idx = (b->hash) % (uint32_t)(hm->capacity);
+
+ struct bucket *target_bucket = hm->buckets[idx];
+ if (target_bucket == NULL) {
+ debug_print("insert: bucket with key %s goes to idx %ld\n",
+ b->key, idx);
+ hm->buckets[idx] = b;
+ return 0;
+ }
+
+ /* changing value of something already in the hashmap */
+ struct bucket *curr;
+ while (target_bucket != NULL) {
+ curr = target_bucket;
+ target_bucket = target_bucket->next;
+ if (curr->hash == b->hash && strcmp(curr->key, key) == 0) {
+ debug_print("key already exists, reassigning value\n");
+
+ /* we free the old value, should probably add an
+ * option for this */
+ if (hm->val_free_func != NULL)
+ hm->val_free_func(curr->val);
+ curr->val = val;
+ free_bucket(b, NULL);
+ return 0;
+ }
+ }
+
+ debug_print("collision. appending to linked list\n");
+
+ target_bucket = hm->buckets[idx];
+
+ while (target_bucket->next != NULL)
+ target_bucket = target_bucket->next;
+
+ target_bucket->next = b;
+ b->next = NULL;
+
+ return 0;
+}
+
+void *hashmap_get(struct hashmap *hm, const char *key)
+{
+ if (hm == NULL || key == NULL)
+ return NULL;
+
+ uint32_t hash = fnv1a_hash(key);
+ size_t idx = hash % (uint32_t)(hm->capacity);
+ debug_print("retreiving object with key %s - hashes to %" PRIu32
+ " giving idx %ld\n",
+ key, hash, idx);
+
+ struct bucket *target_bucket = hm->buckets[idx];
+ if (target_bucket == NULL)
+ return NULL;
+
+ if (target_bucket->hash == hash && strcmp(target_bucket->key, key) == 0)
+ return target_bucket->val;
+
+ /* collision */
+ struct bucket *curr;
+ while (target_bucket != NULL) {
+ curr = target_bucket;
+ target_bucket = target_bucket->next;
+ if (curr->hash == hash && strcmp(curr->key, key) == 0)
+ return curr->val;
+ }
+
+#ifdef HM_DONTTRUSTHASH
+ /* search all items as a last resort */
+ for (size_t i = 0; i < hm->capacity; i++) {
+ struct bucket *b = hm->buckets[i];
+ struct bucket *curr;
+ while (b != NULL) {
+ curr = b;
+ b = b->next;
+
+ if (curr->hash == hash && strcmp(curr->key, key) == 0)
+ return curr->val;
+ }
+ }
+#endif
+
+ return NULL;
+}
+
+/*
+ * wrapper function, so that if we don't find it with normal
+ * methods, we can easily re-call the function with a custom index, allowing
+ * us to iterate through the hashmap to find the item if for some
+ * reason the hash gives an incorrect index
+ */
+static int _hashmap_remove(struct hashmap *hm, const char *key, uint32_t hash,
+ size_t idx)
+{
+ if (hm == NULL || key == NULL)
+ return 1;
+
+ // uint32_t hash = fnv1a_hash(key);
+ // size_t idx = hash % (uint32_t)(hm->capacity);
+
+ debug_print("removing object with key %s which hashes to %" PRIu32
+ ". Supplied index to search is %ld\n",
+ key, hash, idx);
+
+ struct bucket *target = hm->buckets[idx];
+ if (target == NULL)
+ return 1;
+
+ if (target->hash == hash && strcmp(target->key, key) == 0) {
+ /* bucket with single item */
+ if (target->next == NULL) {
+ debug_print(
+ "simple removal of bucket with single node\n");
+ free_bucket(hm->buckets[idx], hm->val_free_func);
+ hm->buckets[idx] = NULL;
+ hm->n_buckets--;
+ return 0;
+ }
+
+ /* bucket where first item is to be removed,
+ * but there are other items in the linked list */
+ debug_print("removing first node and preserving the rest of "
+ "the linked list\n");
+
+ struct bucket *next = hm->buckets[idx]->next;
+ hm->buckets[idx] = next;
+ target->next = NULL;
+ free_bucket(target, hm->val_free_func);
+ return 0;
+ }
+
+ /* the bucket to be removed is not the first
+ * bucket in the linked list */
+ debug_print(
+ "bucket to be removed is within the linked list - searching...\n");
+ if (target->next == NULL)
+ return 1;
+
+ target = target->next;
+
+ struct bucket *curr = target;
+ struct bucket *prev = NULL;
+
+ while (target != NULL) {
+ curr = target;
+ target = target->next;
+
+ if (curr->hash == hash && strcmp(curr->key, key) != 0) {
+ prev = curr;
+ continue;
+ }
+
+ if (prev == NULL) {
+ debug_print("this should never happen...\n");
+ return 1;
+ }
+
+ prev->next = curr->next;
+ curr->next = NULL;
+ free_bucket(curr, hm->val_free_func);
+
+ return 0;
+ }
+
+ return 1;
+}
+
+int hashmap_remove(struct hashmap *hm, const char *key)
+{
+ if (hm == NULL || key == NULL)
+ return 1;
+
+ uint32_t hash = fnv1a_hash(key);
+ size_t idx = hash % (uint32_t)(hm->capacity);
+
+ if (_hashmap_remove(hm, key, hash, idx) == 0)
+ return 0;
+
+#ifdef HM_DONTTRUSTHASH
+ debug_print("couldn't find item to remove by hashing, must search "
+ "entire hashmap\n");
+ for (size_t i = 0; i < hm->capacity; i++) {
+ /* skip if we already processed the index or if the item is NULL
+ */
+ if (hm->buckets[i] == NULL || i == idx)
+ continue;
+
+ if (_hashmap_remove(hm, key, hash, i) == 0) {
+ debug_print(
+ "successfully found and removed item at idx %ld\n",
+ i);
+ return 0;
+ }
+ }
+
+ debug_print("couldn't find item to remove anywhere, returning 1\n");
+#endif
+
+ return 1;
+}
+
+/* only works for hashmap with strings as values */
+void hashmap_print(struct hashmap *hm)
+{
+ if (hm == NULL) {
+ printf("NULL\n");
+ return;
+ }
+
+ printf("Printing hashmap containing %ld buckets with capacity %ld:\n",
+ hm->n_buckets, hm->capacity);
+
+ for (size_t i = 0; i < hm->capacity; i++) {
+ struct bucket *target = hm->buckets[i];
+ printf("bucket %02ld:\n", i);
+ if (target == NULL)
+ continue;
+
+ struct bucket *curr;
+ int n = 0;
+ while (target != NULL) {
+ curr = target;
+ target = target->next;
+ printf(" %02d: %s=%s\n", n, curr->key,
+ (char *)curr->val);
+
+ n++;
+ }
+ }
+}
+
+char **hashmap_get_keys(struct hashmap *hm, size_t *n_keys)
+{
+ if (!hm || !n_keys)
+ return NULL;
+
+ char **keys = malloc(hm->n_buckets * sizeof(char *));
+ if (!keys)
+ return NULL;
+
+ size_t count = 0;
+ for (size_t i = 0; i < hm->capacity; i++) {
+ struct bucket *b = hm->buckets[i];
+ while (b) {
+ keys[count++] = strdup(b->key);
+ b = b->next;
+ }
+ }
+
+ *n_keys = count;
+ return keys;
+}
+
+void hashmap_free_keys(char **keys, size_t n_keys)
+{
+ if (!keys)
+ return;
+ for (size_t i = 0; i < n_keys; i++)
+ free(keys[i]);
+ free(keys);
+}
diff --git a/hashmap.h b/hashmap.h
@@ -0,0 +1,66 @@
+// Taken from https://github.com/prismz/hashmap
+//
+#ifndef HASHMAP_H
+#define HASHMAP_H
+
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define HM_CALLOC_FUNC calloc
+#define HM_STRDUP_FUNC strdup
+#define HM_EXIT_ON_ALLOC_FAIL 1
+#define HM_HASHMAP_MAX_LOAD 0.70f
+#define HM_RESIZE_SCALE_FACTOR 2
+#define HM_DEFAULT_HASHMAP_SIZE 16
+
+/* enabling this will print something for
+ * each internal action, not recommended */
+// #define HM_DEBUG
+
+/* if enabled this will not trust the hash function.
+ * It will search every item in the hashmap (after trying
+ * the hashing method). */
+// #define HM_DONTTRUSTHASH
+
+/*
+ * if using structs as values, this conveniently
+ * typecasts your struct's free function
+ * so that it can be passed to new_hashmap_item()
+ */
+#define hashmap_item_free_func(a) (void (*)(void *)) a
+
+/* https://create.stephan-brumme.com/fnv-hash */
+#define HM_FNV_PRIME 0x01000193 // 16777619
+#define HM_FNV_SEED 0x811C9DC5 // 2166136261
+typedef void (*hashmap_iter_callback)(const char *key, void *value,
+ void *user_data);
+
+struct bucket;
+struct bucket {
+ uint32_t hash;
+ char *key;
+ void *val;
+
+ /* collisions are stored as linked lists */
+ struct bucket *next;
+};
+
+struct hashmap {
+ struct bucket **buckets;
+ size_t n_buckets;
+ size_t capacity;
+ void (*val_free_func)(void *);
+};
+
+struct hashmap *hashmap_new(void (*val_free_func)(void *));
+void hashmap_free(struct hashmap *hm);
+int hashmap_resize(struct hashmap *hm);
+int hashmap_insert(struct hashmap *hm, const char *key, void *val);
+void *hashmap_get(struct hashmap *hm, const char *key);
+int hashmap_remove(struct hashmap *hm, const char *key);
+void hashmap_print(struct hashmap *hm);
+char **hashmap_get_keys(struct hashmap *hm, size_t *n_keys);
+void hashmap_free_keys(char **keys, size_t n_keys);
+#endif /* HASHMAP_H */
diff --git a/main.c b/main.c
@@ -1,19 +1,23 @@
#define FUSE_USE_VERSION 31
-#include <fuse3/fuse.h>
-#include <stdio.h>
-#include <string.h>
-#include <stdlib.h>
+#include "hashmap.h"
#include <dirent.h>
#include <errno.h>
-#include <time.h>
-#include <sys/stat.h>
-#include <regex.h>
+#include <fuse3/fuse.h>
#include <pthread.h>
+#include <regex.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
#include <sys/inotify.h>
+#include <sys/stat.h>
+#include <time.h>
#include <unistd.h>
#define ICS_DIR "/home/mccd/dev/fuse-journal/sample_ics"
-#define LOG(fmt, ...) fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)
+#define LOG(fmt, ...) \
+ fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)
+
+#define MAX_BASE_FILENAME_LEN (sizeof(unique_filename) - 1 - 11)
// Listening to file changes of the original content
#define EVENT_BUF_LEN (1024 * (sizeof(struct inotify_event) + 16))
@@ -21,12 +25,12 @@
const char *display_date_fmt = "%b %d, %Y at %H:%M";
const char *date_title_fmt = "%Y%m%d-%H%M%S";
-typedef struct
-{
+typedef struct {
char *filename;
char *filename_original;
char *summary;
char *description;
+ char *uid;
time_t timestamp;
time_t created_at;
} journal_entry;
@@ -34,12 +38,14 @@ typedef struct
static journal_entry *entries = NULL;
static size_t entry_count = 0;
+struct hashmap *entries_map = NULL;
static pthread_mutex_t entries_mutex = PTHREAD_MUTEX_INITIALIZER;
-static void free_entry(journal_entry *entry)
+static void free_entry(journal_entry *entry)
{
- if (entry)
- {
+ if (entry) {
+
+ free(entry->uid);
free(entry->filename);
free(entry->filename_original);
free(entry->summary);
@@ -47,10 +53,21 @@ static void free_entry(journal_entry *entry)
}
}
-static void free_all_entries()
+void free_journal_entry(void *val)
+{
+ journal_entry *entry = (journal_entry *)val;
+ if (!entry)
+ return;
+ free(entry->filename_original);
+ free(entry->filename);
+ free(entry->summary);
+ free(entry->description);
+ free(entry);
+}
+
+static void free_all_entries()
{
- for (size_t i = 0; i < entry_count; i++)
- {
+ for (size_t i = 0; i < entry_count; i++) {
free_entry(&entries[i]);
}
free(entries);
@@ -58,52 +75,59 @@ static void free_all_entries()
entry_count = 0;
}
-static time_t parse_datetime(const char *datetime)
+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
+ 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 compile_regexes(regex_t *regex_summary, regex_t *regex_description, regex_t *regex_dtstamp, regex_t *regex_lastmod)
+static int compile_regexes(regex_t *regex_summary, regex_t *regex_description,
+ regex_t *regex_dtstamp, regex_t *regex_lastmod,
+ regex_t *regex_uid)
{
// Parse text after SUMMARY: up until \r\n
- if (regcomp(regex_summary, "SUMMARY:([^\r\n]+)", REG_EXTENDED) != 0) return -1;
+ if (regcomp(regex_summary, "SUMMARY:([^\r\n]+)", REG_EXTENDED) != 0)
+ return -1;
// Parse text after DESCRIPTION:, multiple lines up until \r\n
- if (regcomp(regex_description, "DESCRIPTION:([^\r\n]*)", REG_EXTENDED) != 0) return -1;
- if (regcomp(regex_dtstamp, "DTSTAMP:([0-9T]+Z)", REG_EXTENDED) != 0) return -1;
- if (regcomp(regex_lastmod, "LAST-MODIFIED:([0-9T]+Z)", REG_EXTENDED) != 0) return -1;
+ if (regcomp(regex_description, "DESCRIPTION:([^\r\n]*)",
+ REG_EXTENDED) != 0)
+ return -1;
+ if (regcomp(regex_dtstamp, "DTSTAMP:([0-9T]+Z)", REG_EXTENDED) != 0)
+ return -1;
+ if (regcomp(regex_lastmod, "LAST-MODIFIED:([0-9T]+Z)", REG_EXTENDED) !=
+ 0)
+ return -1;
+ if (regcomp(regex_uid, "UID:([^\r\n]+)", REG_EXTENDED) != 0)
+ return -1;
return 0;
}
// Extracts the matched text from regexp
-static char *extract_match(const char *content, regmatch_t match)
+static char *extract_match(const char *content, regmatch_t match)
{
size_t len = match.rm_eo - match.rm_so;
char *str = calloc(len + 1, sizeof(char));
- if (!str) return NULL;
+ if (!str)
+ return NULL;
memcpy(str, content + match.rm_so, len);
str[len] = '\0';
return str;
}
// RFC 5545 specifies how human-readable text is escaped
-// https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.11
-static void unescape_text(char *desc)
-{
+// https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.11
+static void unescape_text(char *desc)
+{
char *src = desc;
char *dst = desc;
@@ -111,85 +135,102 @@ static void unescape_text(char *desc)
if (*src == '\\') {
src++;
switch (*src) {
- case 'n':
- case 'N':
- *dst++ = '\n';
- break;
- case '\\':
- *dst++ = '\\';
- break;
- case ',':
- *dst++ = ',';
- break;
- case ';':
- *dst++ = ';';
- break;
- case '\0':
- goto done;
- default:
- *dst++ = '\\';
- *dst++ = *src;
- break;
+ case 'n':
+ case 'N':
+ *dst++ = '\n';
+ break;
+ case '\\':
+ *dst++ = '\\';
+ break;
+ case ',':
+ *dst++ = ',';
+ break;
+ case ';':
+ *dst++ = ';';
+ break;
+ case '\0':
+ goto done;
+ default:
+ *dst++ = '\\';
+ *dst++ = *src;
+ break;
}
src++;
- } else {
+ }
+ else {
*dst++ = *src++;
}
}
- done:
- *dst = '\0';
+done:
+ *dst = '\0';
}
-
-
-
-
// RFC 5545 specifies how human-readable text is escaped
-// https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.11
-static char *escape_text(const char *src)
+// https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.11
+static char *escape_text(const char *src)
{
- if (!src) return NULL;
+ if (!src)
+ return NULL;
size_t len = strlen(src);
// Worst-case size (every char becomes \X): len * 2 + 1
char *escaped = calloc(len * 2 + 1, 1);
- if (!escaped) return NULL;
+ if (!escaped)
+ return NULL;
char *dst = escaped;
- for (const char *p = src; *p; ++p)
- {
- switch (*p)
- {
- case '\n':
- *dst++ = '\\';
- *dst++ = 'n';
- break;
- case '\\':
- *dst++ = '\\';
- *dst++ = '\\';
- break;
- case ',':
- *dst++ = '\\';
- *dst++ = ',';
- break;
- case ';':
- *dst++ = '\\';
- *dst++ = ';';
- break;
- default:
- *dst++ = *p;
- break;
+ for (const char *p = src; *p; ++p) {
+ switch (*p) {
+ case '\n':
+ *dst++ = '\\';
+ *dst++ = 'n';
+ break;
+ case '\\':
+ *dst++ = '\\';
+ *dst++ = '\\';
+ break;
+ case ',':
+ *dst++ = '\\';
+ *dst++ = ',';
+ break;
+ case ';':
+ *dst++ = '\\';
+ *dst++ = ';';
+ break;
+ default:
+ *dst++ = *p;
+ break;
}
}
*dst = '\0';
return escaped;
}
-static int parse_ics_file(const char *file_path, journal_entry *entry)
+static journal_entry *get_entry_from_path(const char *path)
+{
+ if (!path || path[0] != '/') {
+ LOG("get_entry_from_path: invalid path '%s'\n",
+ path ? path : "NULL");
+ return NULL;
+ }
+
+ const char *key = path + 1; // Strip leading '/'
+ if (strlen(key) == 0) {
+ LOG("get_entry_from_path: empty key for path '%s'\n", path);
+ return NULL;
+ }
+
+ LOG("get_entry_from_path: looking up key '%s'\n", key);
+ journal_entry *entry = hashmap_get(entries_map, key);
+ if (!entry)
+ LOG("get_entry_from_path: no entry found for key '%s'\n", key);
+ return entry;
+}
+
+static int parse_ics_file(const char *file_path, journal_entry *entry)
{
FILE *file = fopen(file_path, "r");
- if (!file)
- {
+ if (!file) {
perror("Failed to open .ics file");
return -1;
}
@@ -200,98 +241,105 @@ static int parse_ics_file(const char *file_path, journal_entry *entry)
fseek(file, 0, SEEK_SET);
char *file_content = calloc(file_size + 1, sizeof(char));
- if (!file_content)
- {
+ if (!file_content) {
perror("Memory allocation failed");
fclose(file);
return -1;
}
fread(file_content, 1, file_size, file);
- file_content[file_size] = '\0'; // Null-terminate the string
+ file_content[file_size] = '\0'; // Null-terminate the string
fclose(file);
- regex_t regex_summary, regex_description, regex_dtstamp, regex_lastmod;
- regmatch_t matches_summary[2], matches_description[2], matches_dtstamp[2], matches_lastmod[2];
+ regex_t regex_summary, regex_description, regex_dtstamp, regex_lastmod,
+ regex_uid;
+ regmatch_t matches_summary[2], matches_description[2],
+ matches_dtstamp[2], matches_lastmod[2], matches_uid[2];
size_t return_code = -1;
- if (compile_regexes(®ex_summary, ®ex_description, ®ex_dtstamp, ®ex_lastmod) != 0)
- {
+ if (compile_regexes(®ex_summary, ®ex_description, ®ex_dtstamp,
+ ®ex_lastmod, ®ex_uid) != 0) {
fprintf(stderr, "Failed to compile regular expressions\n");
goto cleanup;
}
// SUMMARY
- if (regexec(®ex_summary, file_content, 2, matches_summary, 0) != 0)
- {
+ if (regexec(®ex_summary, file_content, 2, matches_summary, 0) != 0) {
fprintf(stderr, "SUMMARY field not found in: %s\n", file_path);
goto cleanup;
- }
+ }
entry->summary = extract_match(file_content, matches_summary[1]);
- if (!entry->summary)
- {
+ if (!entry->summary) {
perror("Memory allocation failed for SUMMARY");
goto cleanup;
}
unescape_text(entry->summary);
+ // UID
+ if (regexec(®ex_uid, file_content, 2, matches_uid, 0) == 0) {
+ entry->uid = extract_match(file_content, matches_uid[1]);
+ }
+ else {
+ // If missing, generate one (fallback)
+ LOG("FAILED TO GET UID");
+ asprintf(&entry->uid, "%ld@fuse-journal", time(NULL));
+ }
+
// DESCRIPTION
- if (regexec(®ex_description, file_content, 2, matches_description, 0) != 0)
- {
- fprintf(stderr, "DESCRIPTION field not found in: %s\n", file_path);
+ if (regexec(®ex_description, file_content, 2, matches_description,
+ 0) != 0) {
+ fprintf(stderr, "DESCRIPTION field not found in: %s\n",
+ file_path);
goto cleanup;
}
- entry->description = extract_match(file_content, matches_description[1]);
- if (!entry->description)
- {
+ entry->description =
+ extract_match(file_content, matches_description[1]);
+ if (!entry->description) {
perror("Memory allocation failed for DESCRIPTION");
goto cleanup;
}
unescape_text(entry->description);
-
// 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]);
- if (!dtstamp_str)
- {
+ if (regexec(®ex_dtstamp, file_content, 2, matches_dtstamp, 0) == 0) {
+ char *dtstamp_str =
+ extract_match(file_content, matches_dtstamp[1]);
+ if (!dtstamp_str) {
perror("Memory allocation failed for DTSTAMP");
goto cleanup;
}
entry->created_at = parse_datetime(dtstamp_str);
free(dtstamp_str);
- if (entry->created_at == -1)
- {
- fprintf(stderr, "Failed to parse DTSTAMP in: %s\n", file_path);
+ if (entry->created_at == -1) {
+ fprintf(stderr, "Failed to parse DTSTAMP in: %s\n",
+ file_path);
goto cleanup;
}
- } else
- {
+ }
+ 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]);
- if (!lastmod_str)
- {
+ // LAST-MODIFIED
+ if (regexec(®ex_lastmod, file_content, 2, matches_lastmod, 0) == 0) {
+ char *lastmod_str =
+ extract_match(file_content, matches_lastmod[1]);
+ if (!lastmod_str) {
perror("Memory allocation failed for LAST-MODIFIED");
goto cleanup;
}
entry->timestamp = parse_datetime(lastmod_str);
free(lastmod_str);
- if (entry->timestamp == -1)
- {
- fprintf(stderr, "Failed to parse LAST-MODIFIED in: %s\n", file_path);
+ if (entry->timestamp == -1) {
+ fprintf(stderr,
+ "Failed to parse LAST-MODIFIED in: %s\n",
+ file_path);
goto cleanup;
}
- } else
- {
- entry->timestamp = entry->created_at;
+ }
+ else {
+ entry->timestamp = entry->created_at;
}
// Success and goto cleanup
@@ -306,52 +354,92 @@ cleanup:
return return_code;
}
-static void load_journal_entries()
+static void load_journal_entries()
{
pthread_mutex_lock(&entries_mutex);
- free_all_entries();
+ if (entries_map) {
+ hashmap_free(entries_map);
+ }
+ entries_map = hashmap_new(free_journal_entry);
+
LOG("Loading journal entries from: %s\n", ICS_DIR);
DIR *dir = opendir(ICS_DIR);
- if (!dir)
- {
+ if (!dir) {
+ pthread_mutex_unlock(&entries_mutex);
perror("opendir");
return;
}
struct dirent *entry;
char filepath[512];
- while ((entry = readdir(dir)))
- {
- if (entry->d_type == DT_REG && strstr(entry->d_name, ".ics"))
- {
- snprintf(filepath, sizeof(filepath), "%s/%s", ICS_DIR, entry->d_name);
- journal_entry new_entry;
- new_entry.filename_original = strdup(entry->d_name);
- if (parse_ics_file(filepath, &new_entry) == 0)
- {
- char filename_time_str[64];
- struct tm tm_info;
- localtime_r(&new_entry.created_at, &tm_info);
- strftime(filename_time_str, sizeof(filename_time_str), date_title_fmt, &tm_info);
- new_entry.filename = strdup(filename_time_str);
-
- entries = realloc(entries, sizeof(journal_entry) * (entry_count + 1));
- entries[entry_count] = new_entry;
- entry_count++;
- } else
- {
- free(new_entry.filename);
+ while ((entry = readdir(dir))) {
+ if (entry->d_type == DT_REG && strstr(entry->d_name, ".ics")) {
+ snprintf(filepath, sizeof(filepath), "%s/%s", ICS_DIR,
+ entry->d_name);
+ journal_entry *new_entry =
+ calloc(1, sizeof(journal_entry));
+ if (!new_entry) {
+ perror("calloc failed");
+ continue;
+ }
+ new_entry->filename_original = strdup(entry->d_name);
+ if (parse_ics_file(filepath, new_entry) == 0) {
+ LOG("Parsed entry");
+
+ char filename_time_str[64];
+ struct tm tm_info;
+ localtime_r(&new_entry->created_at, &tm_info);
+ strftime(filename_time_str,
+ sizeof(filename_time_str),
+ date_title_fmt, &tm_info);
+
+ // Check for duplicate and append _N if exists
+ char base_filename[128];
+ strncpy(base_filename, filename_time_str,
+ sizeof(base_filename));
+ base_filename[sizeof(base_filename) - 12] =
+ '\0'; // make space for suffix + .txt
+
+ char unique_filename[140]; // 128 base + 12 for
+ // _N.txt suffix max
+ snprintf(unique_filename,
+ sizeof(unique_filename), "%s.txt",
+ base_filename);
+
+ int suffix = 1;
+ // Check if filename already exists in hashmap
+ while (hashmap_get(entries_map,
+ unique_filename) != NULL) {
+ snprintf(unique_filename,
+ sizeof(unique_filename),
+ "%s_%d.txt", base_filename,
+ suffix++);
+ }
+
+ LOG("Inserted entry %s", unique_filename);
+
+ new_entry->filename = strdup(unique_filename);
+ if (hashmap_insert(entries_map, unique_filename,
+ new_entry) != 0) {
+ LOG("Failed to insert entry %s into "
+ "hashmap\n",
+ unique_filename);
+ free_journal_entry(new_entry);
+ }
+ }
+ else {
+ free(new_entry->filename);
}
}
}
- LOG("Loaded %zu journal entries.\n", entry_count);
closedir(dir);
pthread_mutex_unlock(&entries_mutex);
}
-static int journal_getattr(const char *path, struct stat *stbuf, struct fuse_file_info *fi)
+int journal_getattr(const char *path, struct stat *stbuf,
+ struct fuse_file_info *fi)
{
pthread_mutex_lock(&entries_mutex);
@@ -359,49 +447,36 @@ static int journal_getattr(const char *path, struct stat *stbuf, struct fuse_fil
int ret_code = 0;
- if (strcmp(path, "/") == 0)
- {
+ if (strcmp(path, "/") == 0) {
stbuf->st_mode = S_IFDIR | 0755;
stbuf->st_nlink = 2;
- ret_code = 0;
goto unlock_and_return;
}
- for (size_t i = 0; i < entry_count; i++)
- {
- char entry_path[256];
- snprintf(entry_path, sizeof(entry_path), "/%s.txt", entries[i].filename);
- if (strcmp(path, entry_path) == 0)
- {
- // S_IFREG = Regular file
- stbuf->st_mode = S_IFREG | 0444;
- // Regular file has one link (name in directory)
- stbuf->st_nlink = 1;
- struct tm tm_info;
- localtime_r(&entries[i].timestamp, &tm_info);
- char timestamp_str[64];
- strftime(timestamp_str, sizeof(timestamp_str), display_date_fmt, &tm_info);
-
- // Calculate the file size as in journal_read
- int size = snprintf(NULL, 0, "%s\n%s\n---\n%s\n",
- entries[i].summary, timestamp_str, entries[i].description);
+ journal_entry *entry = get_entry_from_path(path);
+ if (entry) {
+ stbuf->st_mode = S_IFREG | 0444;
+ stbuf->st_nlink = 1;
- stbuf->st_size = (off_t)size;
+ struct tm tm_info;
+ localtime_r(&entry->timestamp, &tm_info);
+ char timestamp_str[64];
+ strftime(timestamp_str, sizeof(timestamp_str), display_date_fmt,
+ &tm_info);
- // Use the timestamp as the modification time
- stbuf->st_mtime = entries[i].timestamp;
+ int size =
+ snprintf(NULL, 0, "%s\n%s\n---\n%s\n", entry->summary,
+ timestamp_str, entry->description);
+ stbuf->st_size = (off_t)size;
- // Set the change time to the same as the modification time
- stbuf->st_ctime = entries[i].timestamp;
+ stbuf->st_mtime = entry->timestamp;
+ stbuf->st_ctime = entry->timestamp;
+ stbuf->st_atime = time(NULL);
- // Access time is current time
- stbuf->st_atime = time(NULL);
-
-
- ret_code = 0;
- goto unlock_and_return;
- }
+ ret_code = 0;
+ goto unlock_and_return;
}
+
ret_code = -ENOENT;
unlock_and_return:
@@ -410,432 +485,107 @@ unlock_and_return:
}
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)
+ off_t offset, struct fuse_file_info *fi,
+ enum fuse_readdir_flags flags)
{
pthread_mutex_lock(&entries_mutex);
- if (strcmp(path, "/") != 0)
- {
- pthread_mutex_unlock(&entries_mutex);
- return -ENOENT;
+ size_t n_keys = 0;
+ char **keys = hashmap_get_keys(entries_map, &n_keys);
+ if (!keys) {
+ pthread_mutex_unlock(&entries_mutex);
+ return -ENOMEM;
}
- filler(buf, ".", NULL, 0, 0);
- filler(buf, "..", NULL, 0, 0);
-
- for (size_t i = 0; i < entry_count; i++)
- {
- char entry_name[256];
- snprintf(entry_name, sizeof(entry_name), "%s.txt", entries[i].filename);
- filler(buf, entry_name, NULL, 0, 0);
+ for (size_t i = 0; i < n_keys; i++) {
+ filler(buf, keys[i], NULL, 0, 0);
}
+ hashmap_free_keys(keys, n_keys);
+
pthread_mutex_unlock(&entries_mutex);
return 0;
}
-static int journal_open(const char *path, struct fuse_file_info *fi)
+static int journal_open(const char *path, struct fuse_file_info *fi)
{
pthread_mutex_lock(&entries_mutex);
-
- for (size_t i = 0; i < entry_count; i++)
- {
- char entry_path[256];
- snprintf(entry_path, sizeof(entry_path), "/%s.txt", entries[i].filename);
- if (strcmp(path, entry_path) == 0)
- {
- pthread_mutex_unlock(&entries_mutex);
- return 0;
- }
- }
- pthread_mutex_unlock(&entries_mutex);
- return -ENOENT;
+ LOG("Opening journal for %s", path);
+ journal_entry *entry = get_entry_from_path(path);
+ int ret = entry ? 0 : -ENOENT;
+ pthread_mutex_unlock(&entries_mutex);
+ return ret;
}
static int journal_read(const char *path, char *buf, size_t size, off_t offset,
- struct fuse_file_info *fi)
+ struct fuse_file_info *fi)
{
-
int ret_code = 0;
pthread_mutex_lock(&entries_mutex);
+ LOG("READ on path %s", path);
- for (size_t i = 0; i < entry_count; i++)
- {
- char entry_path[256];
- snprintf(entry_path, sizeof(entry_path), "/%s.txt", entries[i].filename);
-
- if (strcmp(path, entry_path) == 0)
- {
- // Format timestamp
- struct tm tm_info;
- localtime_r(&entries[i].created_at, &tm_info);
- char timestamp_str[64];
- strftime(timestamp_str, sizeof(timestamp_str), display_date_fmt, &tm_info);
-
- // Build full content string using snprintf(NULL, 0) to get length
- int needed_len = (size_t)snprintf(NULL, 0, "%s\n%s\n---\n%s\n",
- entries[i].summary, timestamp_str, entries[i].description);
- if (needed_len < 0)
- {
- ret_code = -EIO;
- goto unlock_and_return;
- }
-
- char *full_content = calloc(needed_len + 1, sizeof(char));
- if (!full_content)
- {
- perror("Memory allocation failed");
- ret_code = -ENOMEM;
- goto unlock_and_return;
- }
-
- snprintf(full_content, needed_len + 1, "%s\n%s\n---\n%s\n",
- entries[i].summary, timestamp_str, entries[i].description);
-
-
- if (offset >= needed_len)
- {
- free(full_content);
- //EOF
- ret_code = 0;
- goto unlock_and_return;
- }
-
- if (offset + size > needed_len)
- size = needed_len - offset;
-
- memcpy(buf, full_content + offset, size);
-
- free(full_content);
-
- ret_code = size;
- goto unlock_and_return;
- }
+ journal_entry *entry = get_entry_from_path(path);
+ if (!entry) {
+ ret_code = -ENOENT;
+ goto unlock_and_return;
}
-unlock_and_return:
- pthread_mutex_unlock(&entries_mutex);
- return ret_code;
-}
-
-static int journal_truncate(const char *path, off_t size, struct fuse_file_info *fi)
-{
- (void)fi; // Unused
-
- // We only support truncate to zero
- if (size != 0)
- return -EOPNOTSUPP;
-
- int ret_code = 0;
- pthread_mutex_lock(&entries_mutex);
-
- for (size_t i = 0; i < entry_count; i++)
- {
- char entry_path[256];
- snprintf(entry_path, sizeof(entry_path), "/%s.txt", entries[i].filename);
-
- if (strcmp(path, entry_path) == 0)
- {
- // Clear summary and description
- free(entries[i].summary);
- entries[i].summary = strdup("Untitled");
-
- free(entries[i].description);
- entries[i].description = strdup("");
-
- time_t now = time(NULL);
- entries[i].timestamp = now;
-
- // Format time for LAST-MODIFIED
- struct tm *tm_info = gmtime(&now);
- char timestamp_str[32];
- strftime(timestamp_str, sizeof(timestamp_str), "%Y%m%dT%H%M%SZ", tm_info);
-
- // Load original .ics file
- char full_path[512];
- snprintf(full_path, sizeof(full_path), "%s/%s", ICS_DIR, entries[i].filename_original);
- printf("Attempting to open: %s\n", full_path);
- FILE *fp = fopen(full_path, "r+");
-
- if (!fp) {
- ret_code = -EIO;
- goto unlock_and_return;
- }
-
- fseek(fp, 0, SEEK_END);
- long length = ftell(fp);
- fseek(fp, 0, SEEK_SET);
-
- char *content = calloc(length + 1, 1);
- if (!content) {
- fclose(fp);
- ret_code = -ENOMEM;
- goto unlock_and_return;
- }
-
- fread(content, 1, length, fp);
- fclose(fp);
-
- char *updated = content;
- regex_t re;
-
- // Replace SUMMARY
- if (regcomp(&re, "SUMMARY:[^\r\n]*", REG_EXTENDED) == 0) {
- regmatch_t match;
- if (regexec(&re, content, 1, &match, 0) == 0) {
- size_t pre = match.rm_so;
- size_t post = strlen(content) - match.rm_eo;
- size_t new_len = pre + 8 + strlen("Untitled") + post + 1;
- updated = malloc(new_len);
- snprintf(updated, new_len, "%.*sSUMMARY:Untitled%s",
- (int)pre, content, content + match.rm_eo);
- free(content);
- content = updated;
- }
- regfree(&re);
- }
-
- // Replace DESCRIPTION
- if (regcomp(&re, "DESCRIPTION:[^\r\n]*", REG_EXTENDED) == 0) {
- regmatch_t match;
- if (regexec(&re, content, 1, &match, 0) == 0) {
- size_t pre = match.rm_so;
- size_t post = strlen(content) - match.rm_eo;
- size_t new_len = pre + strlen("DESCRIPTION:") + post + 1;
- updated = malloc(new_len);
- snprintf(updated, new_len, "%.*sDESCRIPTION:%s",
- (int)pre, content, content + match.rm_eo);
- free(content);
- content = updated;
- }
- regfree(&re);
- }
-
- // Replace LAST-MODIFIED
- if (regcomp(&re, "LAST-MODIFIED:[^\r\n]*", REG_EXTENDED) == 0) {
- regmatch_t match;
- if (regexec(&re, content, 1, &match, 0) == 0) {
- size_t pre = match.rm_so;
- size_t post = strlen(content) - match.rm_eo;
- size_t new_len = pre + strlen(timestamp_str) + post + 16;
- updated = malloc(new_len);
- snprintf(updated, new_len, "%.*sLAST-MODIFIED:%s%s",
- (int)pre, content, timestamp_str, content + match.rm_eo);
- free(content);
- content = updated;
- }
- regfree(&re);
- }
-
- // Write back to file
- fp = fopen(full_path, "w");
- if (!fp) {
- free(content);
- ret_code = -EIO;
- goto unlock_and_return;
- }
-
- fwrite(content, 1, strlen(content), fp);
- fclose(fp);
- free(content);
-
- ret_code = 0;
- goto unlock_and_return;
- }
+ // Format timestamp
+ struct tm tm_info;
+ localtime_r(&entry->created_at, &tm_info);
+ char timestamp_str[64];
+ strftime(timestamp_str, sizeof(timestamp_str), display_date_fmt,
+ &tm_info);
+
+ // Calculate required length for full content
+ int needed_len =
+ (size_t)snprintf(NULL, 0, "%s\n%s\n---\n%s\n", entry->summary,
+ timestamp_str, entry->description);
+ if (needed_len < 0) {
+ ret_code = -EIO;
+ goto unlock_and_return;
}
- ret_code = -ENOENT;
-
-unlock_and_return:
- pthread_mutex_unlock(&entries_mutex);
- return ret_code;
-
-}
-
-static int journal_write(const char *path, const char *buf, size_t size,
- off_t offset, struct fuse_file_info *fi)
-{
- (void)fi; // unused
-
- int ret_code = 0;
- pthread_mutex_lock(&entries_mutex);
-
- for (size_t i = 0; i < entry_count; i++)
- {
- char entry_path[256];
- snprintf(entry_path, sizeof(entry_path), "/%s.txt", entries[i].filename);
-
- if (strcmp(path, entry_path) == 0)
- {
- // Copy buffer into a temporary string for parsing
- char *temp_buf = strndup(buf, size);
- if (!temp_buf)
- {
- ret_code = -ENOMEM;
- goto unlock_and_return;
- }
-
- // Split into lines
- char *summary = strtok(temp_buf, "\n");
- char *line;
- char *description = NULL;
-
- while ((line = strtok(NULL, "\n")) != NULL) {
- if (strcmp(line, "---") == 0) {
- description = strtok(NULL, "");
- break;
- }
- }
-
- if (!summary || !description) {
- free(temp_buf);
- ret_code = -EINVAL;
- goto unlock_and_return;
- }
-
- // Escape summary and description
- char *escaped_summary = escape_text(summary);
- char *escaped_description = escape_text(description);
- if (!escaped_summary || !escaped_description) {
- free(temp_buf);
- ret_code = -ENOMEM;
- goto unlock_and_return;
- }
-
- // Escape per RFC 5545
- for (char *p = escaped_summary; *p; p++) {
- if (*p == '\n') *p = ' ';
- }
-
- // Get timestamp
- time_t now = time(NULL);
- struct tm *tm_info = gmtime(&now);
- char timestamp_str[32];
- strftime(timestamp_str, sizeof(timestamp_str), "%Y%m%dT%H%M%SZ", tm_info);
-
- // Load original file content
- char full_path[512];
- snprintf(full_path, sizeof(full_path), "%s/%s", ICS_DIR, entries[i].filename_original);
- printf("Attempting to open: %s\n", full_path);
-
- FILE *fp = fopen(full_path, "r+");
- if (!fp) {
- perror("Failed to open file for writing");
- free(temp_buf);
- ret_code = -EIO;
- goto unlock_and_return;
- }
-
- // Read original content
- fseek(fp, 0, SEEK_END);
- long length = ftell(fp);
- fseek(fp, 0, SEEK_SET);
- char *original = calloc(length + 1, 1);
- if (!original) {
- fclose(fp);
- free(temp_buf);
- ret_code = -ENOMEM;
- goto unlock_and_return;
- }
- fread(original, 1, length, fp);
- fclose(fp);
-
- // Replace the fields using regex
- regex_t re;
- char *updated = NULL;
-
- // Replace SUMMARY
- if (regcomp(&re, "SUMMARY:[^\r\n]*", REG_EXTENDED) == 0) {
- regmatch_t match;
- if (regexec(&re, original, 1, &match, 0) == 0) {
- size_t pre_len = match.rm_so;
- size_t post_len = strlen(original) - match.rm_eo;
- size_t new_len = pre_len + strlen(escaped_summary) + post_len + 8;
- updated = malloc(new_len);
- snprintf(updated, new_len, "%.*sSUMMARY:%s%s",
- (int)pre_len, original, escaped_summary, original + match.rm_eo);
- free(original);
- original = updated;
- }
- regfree(&re);
- }
-
- // Replace DESCRIPTION
- if (regcomp(&re, "DESCRIPTION:[^\r\n]*", REG_EXTENDED) == 0) {
- regmatch_t match;
- if (regexec(&re, original, 1, &match, 0) == 0) {
- size_t pre_len = match.rm_so;
- size_t post_len = strlen(original) - match.rm_eo;
- size_t new_len = pre_len + strlen(escaped_description) + post_len + 13;
- updated = malloc(new_len);
- snprintf(updated, new_len, "%.*sDESCRIPTION:%s%s",
- (int)pre_len, original, escaped_description, original + match.rm_eo);
- free(original);
- original = updated;
- }
- regfree(&re);
- }
-
- // Replace LAST-MODIFIED
- if (regcomp(&re, "LAST-MODIFIED:[^\r\n]*", REG_EXTENDED) == 0) {
- regmatch_t match;
- if (regexec(&re, original, 1, &match, 0) == 0) {
- size_t pre_len = match.rm_so;
- size_t post_len = strlen(original) - match.rm_eo;
- size_t new_len = pre_len + strlen(timestamp_str) + post_len + 20;
- updated = malloc(new_len);
- snprintf(updated, new_len, "%.*sLAST-MODIFIED:%s%s",
- (int)pre_len, original, timestamp_str, original + match.rm_eo);
- free(original);
- original = updated;
- }
- regfree(&re);
- }
-
- // Write updated content back
- fp = fopen(full_path, "w");
- if (!fp) {
- free(original);
- ret_code = -EIO;
- goto unlock_and_return;
- }
- fwrite(original, 1, strlen(original), fp);
- fclose(fp);
+ char *full_content = calloc(needed_len + 1, sizeof(char));
+ if (!full_content) {
+ perror("Memory allocation failed");
+ ret_code = -ENOMEM;
+ goto unlock_and_return;
+ }
- // Update in-memory values
- free(entries[i].summary);
- entries[i].summary = strdup(escaped_summary);
+ snprintf(full_content, needed_len + 1, "%s\n%s\n---\n%s\n",
+ entry->summary, timestamp_str, entry->description);
- free(entries[i].description);
- entries[i].description = strdup(escaped_description);
+ if (offset >= needed_len) {
+ // EOF
+ free(full_content);
+ ret_code = 0;
+ goto unlock_and_return;
+ }
- entries[i].timestamp = now;
+ if (offset + size > (size_t)needed_len)
+ size = needed_len - offset;
- free(temp_buf);
- free(escaped_summary);
- free(escaped_description);
- free(original);
- ret_code = size;
- goto unlock_and_return;
- }
- }
+ memcpy(buf, full_content + offset, size);
- ret_code = -ENOENT;
+ free(full_content);
+ ret_code = size;
unlock_and_return:
pthread_mutex_unlock(&entries_mutex);
return ret_code;
}
-void *watch_ics_dir(void *arg) {
+void *watch_ics_dir(void *arg)
+{
int fd = inotify_init1(IN_NONBLOCK);
if (fd < 0) {
perror("inotify_init");
return NULL;
}
- int wd = inotify_add_watch(fd, ICS_DIR, IN_CREATE | IN_DELETE | IN_MODIFY);
+ int wd =
+ inotify_add_watch(fd, ICS_DIR, IN_CREATE | IN_DELETE | IN_MODIFY);
if (wd < 0) {
perror("inotify_add_watch");
close(fd);
@@ -857,18 +607,19 @@ void *watch_ics_dir(void *arg) {
size_t i = 0;
while (i < (size_t)length) {
- struct inotify_event *event = (struct inotify_event *)&buffer[i];
+ struct inotify_event *event =
+ (struct inotify_event *)&buffer[i];
if (event->len && strstr(event->name, ".ics")) {
- printf("Detected change in ICS file: %s\n", event->name);
- load_journal_entries(); // Reload entries on any change
- break; // Avoid excessive reloads in batch events
+ printf("Detected change in ICS file: %s\n",
+ event->name);
+ load_journal_entries(); // Reload entries on any
+ // change
+ break;
}
i += sizeof(struct inotify_event) + event->len;
}
-
- sleep(1); // Debounce for a second
}
inotify_rm_watch(fd, wd);
@@ -876,31 +627,162 @@ void *watch_ics_dir(void *arg) {
return NULL;
}
-static const struct fuse_operations journal_oper =
+char *journal_entry_to_ical(const journal_entry *entry)
+{
+ char *escaped_summary =
+ escape_text(entry->summary ? entry->summary : "");
+ char *escaped_description =
+ escape_text(entry->description ? entry->description : "");
+
+ if (!escaped_summary || !escaped_description) {
+ free(escaped_summary);
+ free(escaped_description);
+ LOG("FAILED TO ESCAPE");
+ return NULL;
+ }
+
+ // Format timestamps
+ struct tm created_tm, modified_tm;
+ gmtime_r(&entry->created_at, &created_tm);
+ gmtime_r(&entry->timestamp, &modified_tm);
+
+ char dtstamp[32], lastmod[32];
+ strftime(dtstamp, sizeof(dtstamp), "%Y%m%dT%H%M%SZ", &created_tm);
+ strftime(lastmod, sizeof(lastmod), "%Y%m%dT%H%M%SZ", &modified_tm);
+
+ // Estimate total size (safe upper bound)
+ size_t total_size = strlen(escaped_summary) +
+ strlen(escaped_description) + strlen(entry->uid) +
+ strlen(dtstamp) + strlen(lastmod) +
+ 512; // Extra for formatting
+
+ char *ical = malloc(total_size);
+ if (!ical) {
+ free(escaped_summary);
+ free(escaped_description);
+ return NULL;
+ }
+
+ snprintf(ical, total_size,
+ "BEGIN:VCALENDAR\n"
+ "BEGIN:VJOURNAL\n"
+ "UID:%s\n"
+ "DTSTAMP:%s\n"
+ "LAST-MODIFIED:%s\n"
+ "SUMMARY:%s\n"
+ "DESCRIPTION:%s\n"
+ "END:VJOURNAL\n"
+ "END:VCALENDAR\n",
+ entry->uid, dtstamp, lastmod, escaped_summary,
+ escaped_description);
+
+ free(escaped_summary);
+ free(escaped_description);
+ return ical;
+}
+
+static int journal_write(const char *path, const char *buf, size_t size,
+ off_t offset, struct fuse_file_info *fi)
{
- .getattr = journal_getattr,
- .readdir = journal_readdir,
- .open = journal_open,
- .read = journal_read,
- .write = journal_write,
- .truncate = journal_truncate
+ pthread_mutex_lock(&entries_mutex);
+ journal_entry *entry = get_entry_from_path(path);
+ if (!entry) {
+ pthread_mutex_unlock(&entries_mutex);
+ return -ENOENT;
+ }
+
+ // Only allow overwrite of the full file for simplicity
+ if (offset != 0) {
+ pthread_mutex_unlock(&entries_mutex);
+ return -EINVAL;
+ }
+
+ // Make a copy of the input buffer and null-terminate
+ char *content = strndup(buf, size);
+ if (!content) {
+ pthread_mutex_unlock(&entries_mutex);
+ return -ENOMEM;
+ }
+
+ // Parse new content: "<summary>\n<timestamp>\n---\n<description>"
+ char *summary = NULL, *description = NULL;
+
+ char *sep = strstr(content, "---\n");
+ if (!sep) {
+ free(content);
+ pthread_mutex_unlock(&entries_mutex);
+ return -EINVAL; // Bad format
+ }
+
+ *sep = '\0';
+ description = sep + 4;
+
+ // Split summary and timestamp
+ char *newline = strchr(content, '\n');
+ if (!newline) {
+ free(content);
+ pthread_mutex_unlock(&entries_mutex);
+ return -EINVAL; // Missing timestamp
+ }
+
+ *newline = '\0';
+ summary = content;
+
+ // Replace in-memory values
+ free(entry->summary);
+ free(entry->description);
+ entry->summary = strdup(summary);
+ entry->description = strdup(description);
+ entry->timestamp = time(NULL);
+
+ // Write back to original .ics file
+ char filepath[512];
+ snprintf(filepath, sizeof(filepath), "%s/%s", ICS_DIR,
+ entry->filename_original);
+
+ char *ical_str = journal_entry_to_ical(entry);
+ if (!ical_str) {
+ pthread_mutex_unlock(&entries_mutex);
+ return -EIO;
+ }
+
+ FILE *f = fopen(filepath, "w");
+ if (!f) {
+ perror("Failed to open ICS file");
+ free(ical_str);
+ pthread_mutex_unlock(&entries_mutex);
+ return -EIO;
+ }
+
+ fputs(ical_str, f);
+ fclose(f);
+ free(ical_str);
+ pthread_mutex_unlock(&entries_mutex);
+ return size;
+}
+
+static const struct fuse_operations journal_oper = {
+ .getattr = journal_getattr,
+ .readdir = journal_readdir,
+ .open = journal_open,
+ .read = journal_read,
+ .write = journal_write,
};
-int main(int argc, char *argv[])
-{
- pthread_t watcher_thread;
- load_journal_entries();
+int main(int argc, char *argv[])
+{
+ // pthread_t watcher_thread;
+ load_journal_entries();
- if (pthread_create(&watcher_thread, NULL, watch_ics_dir, NULL) != 0) {
- perror("Failed to create inotify watcher thread");
- return 1;
- }
+ // if (pthread_create(&watcher_thread, NULL, watch_ics_dir, NULL) != 0)
+ // { perror("Failed to create inotify watcher thread"); return 1;
+ //}
- int ret = fuse_main(argc, argv, &journal_oper, NULL);
+ int ret = fuse_main(argc, argv, &journal_oper, NULL);
- pthread_cancel(watcher_thread); // Stop the watcher when FUSE ends
- pthread_join(watcher_thread, NULL); // Clean up
+ // pthread_cancel(watcher_thread); // Stop the watcher when FUSE ends
+ // pthread_join(watcher_thread, NULL); // Clean up
- free_all_entries();
- return ret;
+ free_all_entries();
+ return ret;
}