agendafs

A filesystem for your calendar.

git clone git://mccd.space/agendafs
commit 5fbe24b3c74068345e4a69342db653b5bdda759a
parent 4ab4a8d3921063c124b2c40179f3139d5e76d2d4
Author: Marc Coquand <marc@coquand.email>
Date:   Sat, 14 Jun 2025 23:20:15 +0100

Add rename with file name being the summary

Diffstat:
Mhashmap.c | 127+++++++++++++++++++++++++++++++------------------------------------------------
Mmain.c | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
2 files changed, 165 insertions(+), 121 deletions(-)
diff --git a/hashmap.c b/hashmap.c
@@ -9,7 +9,8 @@
 
 #ifdef HM_DEBUG
 #include <stdarg.h>
-static void debug_print(char *fmt, ...)
+static void
+debug_print(char *fmt, ...)
 {
 	va_list ap;
 	va_start(ap, fmt);
@@ -18,10 +19,14 @@ static void debug_print(char *fmt, ...)
 }
 #else
 /* compiler should just optimize this away */
-static void debug_print() {}
+static void
+debug_print()
+{
+}
 #endif
 
-static int hm_check_mem(void *ptr)
+static int
+hm_check_mem(void *ptr)
 {
 	if (ptr != NULL)
 		return 0;
@@ -35,12 +40,14 @@ static int hm_check_mem(void *ptr)
 }
 
 /* hash a single byte */
-static uint32_t fnv1a(unsigned char byte, uint32_t hash)
+static uint32_t
+fnv1a(unsigned char byte, uint32_t hash)
 {
 	return (byte ^ hash) * HM_FNV_PRIME;
 }
 
-static uint32_t fnv1a_hash(const char *str)
+static uint32_t
+fnv1a_hash(const char *str)
 {
 	uint32_t hash = HM_FNV_SEED;
 	while (*str)
@@ -49,7 +56,8 @@ static uint32_t fnv1a_hash(const char *str)
 }
 
 /* will duplicate key */
-static struct bucket *new_bucket(const char *key, void *val)
+static struct bucket *
+new_bucket(const char *key, void *val)
 {
 	if (key == NULL)
 		return NULL;
@@ -76,7 +84,8 @@ static struct bucket *new_bucket(const char *key, void *val)
 	return b;
 }
 
-struct hashmap *hashmap_new(void (*val_free_func)(void *))
+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;
@@ -97,7 +106,8 @@ struct hashmap *hashmap_new(void (*val_free_func)(void *))
 	return hm;
 }
 
-static void free_bucket(struct bucket *b, void (*val_free_func)(void *))
+static void
+free_bucket(struct bucket *b, void (*val_free_func)(void *))
 {
 	if (b == NULL)
 		return;
@@ -114,7 +124,8 @@ static void free_bucket(struct bucket *b, void (*val_free_func)(void *))
 	}
 }
 
-void hashmap_free(struct hashmap *hm)
+void
+hashmap_free(struct hashmap *hm)
 {
 	if (hm == NULL)
 		return;
@@ -127,7 +138,8 @@ void hashmap_free(struct hashmap *hm)
 	free(hm);
 }
 
-int hashmap_resize(struct hashmap *hm)
+int
+hashmap_resize(struct hashmap *hm)
 {
 	if (hm == NULL)
 		return -1;
@@ -183,7 +195,8 @@ int hashmap_resize(struct hashmap *hm)
 	return 0;
 }
 
-int hashmap_insert(struct hashmap *hm, const char *key, void *val)
+int
+hashmap_insert(struct hashmap *hm, const char *key, void *val)
 {
 	if (hm == NULL || key == NULL)
 		return 1;
@@ -239,7 +252,8 @@ int hashmap_insert(struct hashmap *hm, const char *key, void *val)
 	return 0;
 }
 
-void *hashmap_get(struct hashmap *hm, const char *key)
+void *
+hashmap_get(struct hashmap *hm, const char *key)
 {
 	if (hm == NULL || key == NULL)
 		return NULL;
@@ -290,83 +304,37 @@ void *hashmap_get(struct hashmap *hm, const char *key)
  * 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)
+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;
+	struct bucket *prev = NULL;
 
-	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;
+	while (target != NULL) {
+		if (target->hash == hash && strcmp(target->key, key) == 0) {
+			if (prev == NULL) {
+				// Removing first node in bucket chain
+				hm->buckets[idx] = target->next;
+			}
+			else {
+				prev->next = target->next;
+			}
+			target->next = NULL;
+			free_bucket(target, hm->val_free_func);
 			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;
+		prev = 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)
+int
+hashmap_remove(struct hashmap *hm, const char *key)
 {
 	if (hm == NULL || key == NULL)
 		return 1;
@@ -401,7 +369,8 @@ int hashmap_remove(struct hashmap *hm, const char *key)
 }
 
 /* only works for hashmap with strings as values */
-void hashmap_print(struct hashmap *hm)
+void
+hashmap_print(struct hashmap *hm)
 {
 	if (hm == NULL) {
 		printf("NULL\n");
@@ -430,7 +399,8 @@ void hashmap_print(struct hashmap *hm)
 	}
 }
 
-char **hashmap_get_keys(struct hashmap *hm, size_t *n_keys)
+char **
+hashmap_get_keys(struct hashmap *hm, size_t *n_keys)
 {
 	if (!hm || !n_keys)
 		return NULL;
@@ -452,7 +422,8 @@ char **hashmap_get_keys(struct hashmap *hm, size_t *n_keys)
 	return keys;
 }
 
-void hashmap_free_keys(char **keys, size_t n_keys)
+void
+hashmap_free_keys(char **keys, size_t n_keys)
 {
 	if (!keys)
 		return;
diff --git a/main.c b/main.c
@@ -33,10 +33,19 @@ time_t INVALID_TIME = (time_t)-1;
 
 icaltimezone *local_ical_time;
 
-#define ENOTJOURNAL 1001
-
+// TODO: Move this to a file structure to a separate module, refactor all
+// operations to use the module exposed files
+// Such as
+// - rename
+// - remove
+// - get filename
+// et.c.
 typedef struct {
+	// filename is full path relative to fuse directory
+	// I.E. hello world.txt
 	char *filename;
+	// filename_original is full path relative to fuse directory
+	// I.E. 910319208nrao19p.ics
 	char *filename_original;
 	icalcomponent *component;
 } journal_entry;
@@ -86,6 +95,14 @@ free_journal_entry(void *val)
 	free(entry);
 }
 
+char *
+get_filename(const char *full_path)
+{
+	char *base_name = strrchr(full_path, '/');
+	if (!base_name)
+		return (char *)full_path;
+	return base_name + 1;
+}
 int
 compile_regexes(regex_t *regex_fuse_file)
 {
@@ -121,7 +138,7 @@ get_entry_from_fuse_path(const char *path)
 		return NULL;
 	}
 
-	LOG("Looking up key: '%s'\n", key);
+	LOG("Looking up key: '%s'", key);
 	journal_entry *entry = hashmap_get(entries_fuse, key);
 
 	if (!entry) {
@@ -129,8 +146,8 @@ get_entry_from_fuse_path(const char *path)
 		return NULL;
 	}
 
-	LOG("Entry found: filename = '%s', original = '%s'\n", entry->filename,
-	    entry->filename_original);
+	LOG("Entry found: filename = '%s', original = '%s', path = '%s'",
+	    entry->filename, entry->filename_original, path);
 	return entry;
 }
 
@@ -166,7 +183,7 @@ parse_ics_file(const char *filename)
 
 // We assume that there is only one entry in the file
 int
-parse_ics_to_journal_entry(const char *filename, journal_entry *entry)
+parse_ics_to_journal_entry_component(const char *filename, journal_entry *entry)
 {
 	icalcomponent *component = parse_ics_file(filename);
 	if (component == NULL) {
@@ -199,7 +216,7 @@ time_string(icaltimetype t, const char *format)
 
 // Returns -1 on failure, 0 on success
 int
-load_journal_entry(char *filename, journal_entry *entry)
+load_journal_entry_from_ics_file(char *filename, journal_entry *entry)
 {
 	char *filepath = NULL;
 	if (asprintf(&filepath, "%s/%s", ICS_DIR, filename) == -1) {
@@ -213,21 +230,21 @@ load_journal_entry(char *filename, journal_entry *entry)
 		return -1;
 	}
 
-	if (parse_ics_to_journal_entry(filepath, entry) != 0) {
+	if (parse_ics_to_journal_entry_component(filepath, entry) != 0) {
 		LOG("Failed to parse entry: %s", filename);
 		free(filepath);
 		return -1;
 	}
 
-	LOG("Parsed %s", entry->filename_original);
-
 	// Make sure filename, which happens when
 	// Two entries are created on the same minute
+	// TODO: Allow custom fileformats
 	if (asprintf(&entry->filename, "%s.txt",
-		     icalcomponent_get_uid(entry->component)) == -1) {
+		     icalcomponent_get_summary(entry->component)) == -1) {
 		LOG("Failed to write filename");
 		return -1;
 	};
+	LOG("Parsed %s to file %s", entry->filename_original, entry->filename);
 
 	free(filepath);
 	return 0;
@@ -241,7 +258,15 @@ void
 load_journal_entries()
 {
 	pthread_mutex_lock(&entries_mutex);
-
+	LOG("Allocating journal entries");
+	if (entries_original_ics) {
+		hashmap_free(entries_original_ics);
+		entries_original_ics = NULL;
+	}
+	if (entries_fuse) {
+		hashmap_free(entries_fuse);
+		entries_fuse = NULL;
+	}
 	entries_original_ics = hashmap_new(NULL);
 	entries_fuse = hashmap_new(free_journal_entry);
 
@@ -260,7 +285,8 @@ load_journal_entries()
 			journal_entry *new_entry =
 			    xcalloc(1, sizeof(journal_entry));
 
-			if (load_journal_entry(entry->d_name, new_entry) != 0) {
+			if (load_journal_entry_from_ics_file(entry->d_name,
+							     new_entry) != 0) {
 				LOG("FAILED TO LOAD JOURNAL ENTRY");
 				free_journal_entry(new_entry);
 				continue;
@@ -285,8 +311,7 @@ size_t
 write_entry_content(char *buf, journal_entry *entry)
 {
 	// Allocate memory for the content
-	size_t size = asprintf(&buf, "%s\n---\n%s",
-			       icalcomponent_get_summary(entry->component),
+	size_t size = asprintf(&buf, "%s",
 			       icalcomponent_get_description(entry->component));
 	if (size == -1) {
 		LOG("Out of memory");
@@ -298,8 +323,7 @@ write_entry_content(char *buf, journal_entry *entry)
 size_t
 get_entry_content_size(journal_entry *entry)
 {
-	size_t size = snprintf(NULL, 0, "%s\n---\n%s",
-			       icalcomponent_get_summary(entry->component),
+	size_t size = snprintf(NULL, 0, "%s",
 			       icalcomponent_get_description(entry->component));
 	if (size == -1) {
 		LOG("Out of mem");
@@ -360,7 +384,9 @@ build_today_hashmap()
 				LOG("SUMMARY IS NULL");
 				continue;
 			}
-			char *filename = strdup(summary);
+			char *filename;
+
+			asprintf(&filename, "%s", summary);
 
 			if (filename == NULL) {
 				LOG("Could not get filename");
@@ -421,7 +447,8 @@ build_notes_hashmap()
 				LOG("SUMMARY IS NULL");
 				continue;
 			}
-			char *filename = strdup(summary);
+			char *filename;
+			asprintf(&filename, "%s", summary);
 
 			if (filename == NULL) {
 				LOG("Could not get filename");
@@ -458,20 +485,17 @@ journal_getattr(const char *path, struct stat *stbuf, struct fuse_file_info *fi)
 		stbuf->st_nlink = 2;
 		goto unlock_and_return;
 	}
-
-	if (strcmp(path, "/entries") == 0) {
+	else if (strcmp(path, "/entries") == 0) {
 		stbuf->st_mode = S_IFDIR | 0444;
 		stbuf->st_nlink = 2;
 		goto unlock_and_return;
 	}
-
-	if (strcmp(path, "/today") == 0) {
+	else if (strcmp(path, "/today") == 0) {
 		stbuf->st_mode = S_IFDIR | 0444;
 		stbuf->st_nlink = 2;
 		goto unlock_and_return;
 	}
-
-	if (strcmp(path, "/notes") == 0) {
+	else if (strcmp(path, "/notes") == 0) {
 		stbuf->st_mode = S_IFDIR | 0444;
 		stbuf->st_nlink = 2;
 		goto unlock_and_return;
@@ -693,8 +717,7 @@ journal_read(const char *path, char *buf, size_t size, off_t offset,
 
 	// Calculate required length for full content
 	char *full_content = NULL;
-	size_t needed_len =
-	    asprintf(&full_content, "%s\n---\n%s", summary, description);
+	size_t needed_len = asprintf(&full_content, "%s", description);
 	if (needed_len == -1) {
 		LOG("Out of memory");
 		exit(1);
@@ -859,9 +882,63 @@ timestamp_from_filename(const char *filename)
 }
 
 int
-do_journal_entry_rename(time_t new_created_at, const char *old, const char *new)
+do_journal_entry_rename(const char *old, const char *new)
 {
-	return -EIO;
+	char *new_copy = strdup(new);
+	if (!new_copy)
+		return -ENOMEM;
+	char *new_filename = get_filename(new_copy);
+
+	char *old_filename = get_filename(old);
+	if (!old_filename)
+		return -EINVAL;
+
+	char *new_summary = strdup(new_filename);
+
+	// Remove everything after extension
+	// TODO: Store the extension as a separate field in the ics file
+	char *dot = strrchr(new_summary, '.');
+	if (dot) {
+		*dot = '\0';
+	}
+	LOG("Summary is now %s", new_summary);
+
+	journal_entry *old_entry = get_entry_from_fuse_path(old);
+	if (!old_entry) {
+		LOG("Entry not found '%s'", old);
+		free(new_copy);
+		free(new_summary);
+		return -EIO;
+	}
+
+	journal_entry *new_entry = copy_journal_entry(old_entry);
+	if (!new_entry) {
+		free(new_copy);
+		free(new_summary);
+		return -EIO;
+	}
+
+	icalcomponent_set_summary(new_entry->component, new_summary);
+	// TODO: Filename must be unique
+	asprintf(&new_entry->filename, "%s", new_filename);
+	LOG("New filename is now %s", new_filename);
+
+	LOG("Removed original entry from fuse");
+
+	LOG("Inserting %s", new_entry->filename_original);
+	hashmap_insert(entries_original_ics, new_entry->filename_original,
+		       new_entry);
+
+	hashmap_insert(entries_fuse, new_entry->filename, new_entry);
+
+	// Insert original ics first to avoid use after free
+	// Insert frees older entry
+	hashmap_remove(entries_fuse, old_entry->filename);
+
+	write_entry_to_ical_file(new_entry);
+	free(new_copy);
+
+	return 0;
 }
 
 // Aka remove or delete
@@ -916,19 +993,14 @@ journal_rename(const char *old, const char *new, unsigned int flags)
 {
 	pthread_mutex_lock(&entries_mutex);
 	LOG("RENAME %s -> %s", old, new);
+	if (strncmp(old, "/entries/", strlen("/entries/")) != 0 ||
+	    strncmp(new, "/entries/", strlen("/entries/")) != 0) {
 
-	// Strip path, validate just the new basename
-	const char *new_basename = strrchr(new, '/');
-	new_basename++;
-	time_t new_created_at = timestamp_from_filename(new_basename);
-
-	if (new_created_at == INVALID_TIME) {
-		LOG("Invalid filename format: %s", new_basename);
 		pthread_mutex_unlock(&entries_mutex);
-		return -EINVAL;
+		LOG("Invalid file names");
+		return -ENOTSUP;
 	}
-
-	int res = do_journal_entry_rename(new_created_at, old, new);
+	int res = do_journal_entry_rename(old, new);
 
 	pthread_mutex_unlock(&entries_mutex);
 	return res;
@@ -950,7 +1022,7 @@ create_new_unique_ics_uid()
 }
 
 icalcomponent *
-create_vjournal_entry(char *uid)
+create_vjournal_entry(char *summary)
 {
 
 	icalcomponent *calendar = icalcomponent_new_vcalendar();
@@ -961,13 +1033,14 @@ create_vjournal_entry(char *uid)
 
 	// Step 2: Create a VJOURNAL entry
 	icalcomponent *journal = icalcomponent_new_vjournal();
+	char *id = create_new_unique_ics_uid();
 
-	icalcomponent_add_property(journal, icalproperty_new_uid(uid));
+	icalcomponent_add_property(journal, icalproperty_new_uid(id));
 
 	icalcomponent_add_property(
 	    journal, icalproperty_new_dtstamp(icaltime_current_time_with_zone(
 			 icaltimezone_get_utc_timezone())));
-	icalcomponent_add_property(journal, icalproperty_new_summary(""));
+	icalcomponent_add_property(journal, icalproperty_new_summary(summary));
 	icalcomponent_add_property(journal, icalproperty_new_description(""));
 
 	icalcomponent_add_component(calendar, journal);
@@ -1044,7 +1117,7 @@ update_or_create_fuse_entry_from_original(const char *filename)
 	char *base_name = strrchr(filename, '/');
 	journal_entry *new_entry = xcalloc(1, sizeof(journal_entry));
 
-	if (load_journal_entry(base_name, new_entry) != 0) {
+	if (load_journal_entry_from_ics_file(base_name, new_entry) != 0) {
 		// Parsing failed, clean up and maybe remove old entry
 		// if any
 		LOG("PARSING FAILED");