agendafs

A filesystem for your calendar.

git clone git://mccd.space/agendafs
commit 3e9ad1f5addff1205b8b90f1a1790dfe459abec0
parent e807aad93c181a99b256dd97212f74704357d59c
Author: Marc Coquand <marc@coquand.email>
Date:   Sat, 31 May 2025 22:16:28 +0100

fix memory leaks

Diffstat:
MMakefile | 5+++--
Mmain.c | 539+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
2 files changed, 311 insertions(+), 233 deletions(-)
diff --git a/Makefile b/Makefile
@@ -1,5 +1,6 @@
 CC = gcc
-CFLAGS = -Wall -g
+CFLAGS = -Wall -O3
+CFLAGS_DEBUG = -Wall -g
 LIBS = `pkg-config fuse3 --cflags --libs`
 
 all: journalfs
@@ -8,7 +9,7 @@ journalfs: main.c hashmap.c
 	$(CC) $(CFLAGS) main.c hashmap.c -o journalfs $(LIBS)
 
 debug: main.c hashmap.c
-	$(CC) $(CFLAGS) $(DEBUG_FLAGS) main.c hashmap.c -o journalfs-debug $(LIBS)
+	$(CC) $(CFLAGS_DEBUG) $(DEBUG_FLAGS) main.c hashmap.c -o journalfs-debug $(LIBS)
 
 clean:
 	rm -f journalfs journalfs-debug
diff --git a/main.c b/main.c
@@ -23,7 +23,9 @@
 #define EVENT_BUF_LEN (1024 * (sizeof(struct inotify_event) + 16))
 
 const char *display_date_fmt = "%b %d, %Y at %H:%M";
-const char *date_title_fmt = "%Y%m%d-%H%M%S";
+const char *date_title_fmt = "%Y%m%d-%H:%M";
+regex_t regex_summary, regex_description, regex_dtstamp, regex_lastmod,
+    regex_uid;
 
 typedef struct {
 	char *filename;
@@ -35,46 +37,26 @@ typedef struct {
 	time_t created_at;
 } journal_entry;
 
-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)
-{
-	if (entry) {
+struct hashmap *entries_fuse_key = NULL;
+struct hashmap *entries_original_key = NULL;
 
-		free(entry->uid);
-		free(entry->filename);
-		free(entry->filename_original);
-		free(entry->summary);
-		free(entry->description);
-	}
-}
+static pthread_mutex_t entries_mutex = PTHREAD_MUTEX_INITIALIZER;
 
 void free_journal_entry(void *val)
 {
 	journal_entry *entry = (journal_entry *)val;
 	if (!entry)
 		return;
-	free(entry->filename_original);
+	free(entry->uid);
 	free(entry->filename);
+	free(entry->filename_original);
 	free(entry->summary);
 	free(entry->description);
 	free(entry);
 }
 
-static void free_all_entries()
-{
-	for (size_t i = 0; i < entry_count; i++) {
-		free_entry(&entries[i]);
-	}
-	free(entries);
-	entries = NULL;
-	entry_count = 0;
-}
-
 static time_t parse_datetime(const char *datetime)
 {
 	struct tm tm = {0};
@@ -116,12 +98,12 @@ static int compile_regexes(regex_t *regex_summary, regex_t *regex_description,
 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)
+	char *m_str = calloc(len + 1, sizeof(char));
+	if (!m_str)
 		return NULL;
-	memcpy(str, content + match.rm_so, len);
-	str[len] = '\0';
-	return str;
+	memcpy(m_str, content + match.rm_so, len);
+	m_str[len] = '\0';
+	return m_str;
 }
 
 // RFC 5545 specifies how human-readable text is escaped
@@ -221,7 +203,7 @@ static journal_entry *get_entry_from_path(const char *path)
 	}
 
 	LOG("get_entry_from_path: looking up key '%s'\n", key);
-	journal_entry *entry = hashmap_get(entries_map, key);
+	journal_entry *entry = hashmap_get(entries_fuse_key, key);
 	if (!entry)
 		LOG("get_entry_from_path: no entry found for key '%s'\n", key);
 	return entry;
@@ -250,19 +232,9 @@ static int parse_ics_file(const char *file_path, journal_entry *entry)
 	file_content[file_size] = '\0'; // Null-terminate the string
 	fclose(file);
 
-	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(&regex_summary, &regex_description, &regex_dtstamp,
-			    &regex_lastmod, &regex_uid) != 0) {
-		fprintf(stderr, "Failed to compile regular expressions\n");
-		goto cleanup;
-	}
-
 	// SUMMARY
 	if (regexec(&regex_summary, file_content, 2, matches_summary, 0) != 0) {
 		fprintf(stderr, "SUMMARY field not found in: %s\n", file_path);
@@ -342,102 +314,168 @@ static int parse_ics_file(const char *file_path, journal_entry *entry)
 		entry->timestamp = entry->created_at;
 	}
 
-	// Success and goto cleanup
-	return_code = 0;
+	// Success
+
+	free(file_content);
+	return 0;
 
 cleanup:
+	// Cleanup in case of error
+	if (entry->summary)
+		free(entry->summary);
+	if (entry->description)
+		free(entry->description);
+	if (entry->uid)
+		free(entry->uid);
 	free(file_content);
-	regfree(&regex_summary);
-	regfree(&regex_description);
-	regfree(&regex_dtstamp);
-	regfree(&regex_lastmod);
-	return return_code;
+	return -1;
+}
+
+// Dynamically create a time string using format
+char *time_string(time_t time_val, const char *format)
+{
+	struct tm tm_info;
+	if (!localtime_r(&time_val, &tm_info)) {
+		return NULL;
+	}
+
+	// Allocate a reasonably large buffer
+	size_t buf_size = 64;
+	char *buffer = malloc(buf_size);
+	if (!buffer)
+		return NULL;
+
+	size_t len = strftime(buffer, buf_size, format, &tm_info);
+	if (len == 0) {
+		free(buffer);
+		return NULL; // Formatting failed (buffer too small)
+	}
+	return buffer; // caller must free this
+}
+
+// Uses created_at as filename.
+// However, in case of collission, set the add 1,2 et.c.
+char *get_unique_filename(time_t created_at, struct hashmap *entries_map)
+{
+	char *filename_time_str = time_string(created_at, date_title_fmt);
+	if (!filename_time_str) {
+		fprintf(stderr, "Failed to format date string\n");
+		return NULL;
+	}
+
+	// Prepare base filename leaving room for suffix and extension
+	char base_filename[128];
+	size_t base_len = strnlen(filename_time_str, strlen(filename_time_str));
+	if (base_len > sizeof(base_filename) - 12) {
+		base_len = sizeof(base_filename) - 12;
+	}
+	strncpy(base_filename, filename_time_str, base_len);
+	base_filename[base_len] = '\0';
+
+	free(filename_time_str);
+
+	// Attempt filename and increment suffix if needed
+	char unique_filename[140];
+	snprintf(unique_filename, sizeof(unique_filename), "%s.txt",
+		 base_filename);
+
+	int suffix = 1;
+	while (hashmap_get(entries_map, unique_filename) != NULL) {
+		snprintf(unique_filename, sizeof(unique_filename), "%s_%d.txt",
+			 base_filename, suffix++);
+	}
+
+	return strdup(unique_filename);
 }
 
 static void load_journal_entries()
 {
 	pthread_mutex_lock(&entries_mutex);
 
-	if (entries_map) {
-		hashmap_free(entries_map);
+	if (entries_original_key) {
+		hashmap_free(entries_original_key);
+	}
+	entries_original_key = hashmap_new(NULL);
+
+	if (entries_fuse_key) {
+		hashmap_free(entries_fuse_key);
 	}
-	entries_map = hashmap_new(free_journal_entry);
+	entries_fuse_key = hashmap_new(free_journal_entry);
 
 	LOG("Loading journal entries from: %s\n", ICS_DIR);
+
 	DIR *dir = opendir(ICS_DIR);
 	if (!dir) {
-		pthread_mutex_unlock(&entries_mutex);
 		perror("opendir");
+		pthread_mutex_unlock(&entries_mutex);
 		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);
+
+			// Allocate memory for a new journal entry
 			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);
-				}
+			if (!new_entry->filename_original) {
+				perror("strdup failed");
+				free(new_entry);
+				continue;
 			}
-			else {
+
+			// Parse the ICS file and check for errors
+			if (parse_ics_file(filepath, new_entry) != 0) {
+				LOG("Failed to parse entry: %s", entry->d_name);
+				free(new_entry->filename_original);
+				free(new_entry);
+				continue;
+			}
+
+			LOG("Parsed entry");
+
+			// Generate a unique filename for the journal entry
+			char *unique_filename = get_unique_filename(
+			    new_entry->created_at, entries_fuse_key);
+			if (!unique_filename) {
+				LOG("Failed to generate unique filename for %s",
+				    entry->d_name);
+				free(new_entry->filename_original);
+				free(new_entry);
+				continue;
+			}
+
+			new_entry->filename = unique_filename;
+
+			// Insert into hashmaps and handle errors
+			if (hashmap_insert(entries_fuse_key, unique_filename,
+					   new_entry) != 0 ||
+			    hashmap_insert(entries_original_key,
+					   new_entry->filename_original,
+					   new_entry) != 0) {
+				LOG("Failed to insert entry into one of the "
+				    "hashmaps");
 				free(new_entry->filename);
+				free(new_entry->filename_original);
+				free(new_entry);
+				continue;
 			}
 		}
 	}
-	closedir(dir);
 
+	closedir(dir);
 	pthread_mutex_unlock(&entries_mutex);
 }
-
 int journal_getattr(const char *path, struct stat *stbuf,
 		    struct fuse_file_info *fi)
 {
@@ -454,30 +492,22 @@ int journal_getattr(const char *path, struct stat *stbuf,
 	}
 
 	journal_entry *entry = get_entry_from_path(path);
-	if (entry) {
-		stbuf->st_mode = S_IFREG | 0444;
-		stbuf->st_nlink = 1;
-
-		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);
-
-		int size =
-		    snprintf(NULL, 0, "%s\n%s\n---\n%s\n", entry->summary,
-			     timestamp_str, entry->description);
-		stbuf->st_size = (off_t)size;
-
-		stbuf->st_mtime = entry->timestamp;
-		stbuf->st_ctime = entry->timestamp;
-		stbuf->st_atime = time(NULL);
-
-		ret_code = 0;
+	if (!entry) {
+		ret_code = -ENOENT;
 		goto unlock_and_return;
 	}
+	stbuf->st_mode = S_IFREG | 0444;
+	stbuf->st_nlink = 1;
+
+	int size = snprintf(NULL, 0, "%s\n---\n%s", entry->summary,
+			    entry->description);
+	stbuf->st_size = (off_t)size;
 
-	ret_code = -ENOENT;
+	stbuf->st_mtime = entry->timestamp;
+	stbuf->st_ctime = entry->timestamp;
+	stbuf->st_atime = time(NULL);
+
+	ret_code = 0;
 
 unlock_and_return:
 	pthread_mutex_unlock(&entries_mutex);
@@ -491,7 +521,7 @@ static int journal_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
 
 	pthread_mutex_lock(&entries_mutex);
 	size_t n_keys = 0;
-	char **keys = hashmap_get_keys(entries_map, &n_keys);
+	char **keys = hashmap_get_keys(entries_fuse_key, &n_keys);
 	if (!keys) {
 		pthread_mutex_unlock(&entries_mutex);
 		return -ENOMEM;
@@ -530,17 +560,9 @@ static int journal_read(const char *path, char *buf, size_t size, off_t offset,
 		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);
+	int needed_len = (size_t)snprintf(NULL, 0, "%s\n---\n%s",
+					  entry->summary, entry->description);
 	if (needed_len < 0) {
 		ret_code = -EIO;
 		goto unlock_and_return;
@@ -553,8 +575,8 @@ static int journal_read(const char *path, char *buf, size_t size, off_t offset,
 		goto unlock_and_return;
 	}
 
-	snprintf(full_content, needed_len + 1, "%s\n%s\n---\n%s\n",
-		 entry->summary, timestamp_str, entry->description);
+	snprintf(full_content, needed_len + 1, "%s\n---\n%s", entry->summary,
+		 entry->description);
 
 	if (offset >= needed_len) {
 		// EOF
@@ -576,57 +598,6 @@ unlock_and_return:
 	return ret_code;
 }
 
-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);
-	if (wd < 0) {
-		perror("inotify_add_watch");
-		close(fd);
-		return NULL;
-	}
-
-	char buffer[EVENT_BUF_LEN];
-
-	while (1) {
-		ssize_t length = read(fd, buffer, EVENT_BUF_LEN);
-		if (length < 0) {
-			if (errno == EAGAIN || errno == EINTR) {
-				sleep(1);
-				continue;
-			}
-			perror("read");
-			break;
-		}
-
-		size_t i = 0;
-		while (i < (size_t)length) {
-			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;
-			}
-
-			i += sizeof(struct inotify_event) + event->len;
-		}
-	}
-
-	inotify_rm_watch(fd, wd);
-	close(fd);
-	return NULL;
-}
-
 char *journal_entry_to_ical(const journal_entry *entry)
 {
 	char *escaped_summary =
@@ -681,65 +652,74 @@ char *journal_entry_to_ical(const journal_entry *entry)
 	return ical;
 }
 
-static int journal_write(const char *path, const char *buf, size_t size,
-			 off_t offset, struct fuse_file_info *fi)
+int update_journal_from_input_buffer(journal_entry *entry, const char *buf,
+				     size_t size)
 {
-	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);
+	char *copy = strndup(buf, size);
+	if (!copy)
 		return -ENOMEM;
-	}
 
-	// Parse new content: "<summary>\n<timestamp>\n---\n<description>"
 	char *summary = NULL, *description = NULL;
+	char *sep = strstr(copy, "\n---\n");
 
-	char *sep = strstr(content, "---\n");
 	if (!sep) {
-		free(content);
-		pthread_mutex_unlock(&entries_mutex);
-		return -EINVAL; // Bad format
+		free(copy);
+		return -EINVAL; // Malformed input
 	}
 
 	*sep = '\0';
-	description = sep + 4;
+	description = sep + 5;
 
-	// Split summary and timestamp
-	char *newline = strchr(content, '\n');
-	if (!newline) {
-		free(content);
-		pthread_mutex_unlock(&entries_mutex);
-		return -EINVAL; // Missing timestamp
+	// Summary and timestamp part
+	char *summary_line = strtok(copy, "\n");
+	if (!summary_line) {
+		free(copy);
+		return -EINVAL;
+	}
+
+	summary = strdup(summary_line);
+	if (!summary) {
+		free(copy);
+		return -ENOMEM;
 	}
 
-	*newline = '\0';
-	summary = content;
+	// Optional: parse timestamp here if needed
 
-	// Replace in-memory values
+	// Clean up old values
 	free(entry->summary);
 	free(entry->description);
-	entry->summary = strdup(summary);
-	entry->description = strdup(description);
-	entry->timestamp = time(NULL);
+	entry->summary = summary;
+	entry->description = strdup(description ? description : "");
 
-	// Write back to original .ics file
+	entry->timestamp = time(NULL); // Update modified time
+
+	free(copy);
+	return 0;
+}
+
+static int journal_write(const char *path, const char *buf, size_t size,
+			 off_t offset, struct fuse_file_info *fi)
+{
+	pthread_mutex_lock(&entries_mutex);
+	journal_entry *entry = get_entry_from_path(path);
+	if (!entry) {
+		pthread_mutex_unlock(&entries_mutex);
+		return -ENOENT;
+	}
+
+	// Update hashmap with input buffer
+	int ret = update_journal_from_input_buffer(entry, buf, size);
+	if (ret != 0) {
+		pthread_mutex_unlock(&entries_mutex);
+		return ret;
+	}
+
+	// Get filepath of original file
 	char filepath[512];
 	snprintf(filepath, sizeof(filepath), "%s/%s", ICS_DIR,
 		 entry->filename_original);
 
+	// Write back to original .ics file
 	char *ical_str = journal_entry_to_ical(entry);
 	if (!ical_str) {
 		pthread_mutex_unlock(&entries_mutex);
@@ -769,20 +749,117 @@ static const struct fuse_operations journal_oper = {
     .write = journal_write,
 };
 
+void update_or_add_entry(const char *filename)
+{
+	pthread_mutex_lock(&entries_mutex);
+	const char *base_name = strrchr(filename, '/');
+	base_name = base_name ? base_name + 1 : filename;
+	journal_entry *entry = hashmap_get(entries_original_key, base_name);
+	journal_entry *new_entry = calloc(1, sizeof(journal_entry));
+	if (!new_entry) {
+		perror("calloc");
+		pthread_mutex_unlock(&entries_mutex);
+		return;
+	}
+
+	if (parse_ics_file(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);
+		return;
+	}
+
+	new_entry->filename = strndup(entry->filename, strlen(entry->filename));
+	new_entry->filename_original =
+	    strndup(entry->filename_original, strlen(entry->filename));
+
+	hashmap_insert(entries_fuse_key, strdup(filename), new_entry);
+
+	pthread_mutex_unlock(&entries_mutex);
+}
+
+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);
+	if (wd < 0) {
+		perror("inotify_add_watch");
+		close(fd);
+		return NULL;
+	}
+
+	char buffer[EVENT_BUF_LEN];
+
+	while (1) {
+		ssize_t length = read(fd, buffer, EVENT_BUF_LEN);
+		if (length < 0) {
+			if (errno == EAGAIN || errno == EINTR) {
+				continue;
+			}
+			perror("read");
+			break;
+		}
+
+		size_t i = 0;
+		while (i < (size_t)length) {
+			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;
+			}
+
+			i += sizeof(struct inotify_event) + event->len;
+		}
+	}
+
+	inotify_rm_watch(fd, wd);
+	close(fd);
+	return NULL;
+}
+
 int main(int argc, char *argv[])
 {
-	// pthread_t watcher_thread;
-	load_journal_entries();
+	pthread_t watcher_thread;
+
+	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;
-	//}
+	if (compile_regexes(&regex_summary, &regex_description, &regex_dtstamp,
+			    &regex_lastmod, &regex_uid) != 0) {
+		fprintf(stderr, "Failed to compile regular expressions\n");
+		return -1;
+	}
 
+	load_journal_entries();
 	int ret = fuse_main(argc, argv, &journal_oper, NULL);
+	LOG("Cleaning up");
+
+	pthread_cancel(watcher_thread);	    // Stop the watcher when FUSE ends
+	pthread_join(watcher_thread, NULL); // Clean up
+	LOG("pthread cleaned up");
 
-	// pthread_cancel(watcher_thread);  // Stop the watcher when FUSE ends
-	// pthread_join(watcher_thread, NULL);  // Clean up
+	hashmap_free(entries_fuse_key);
+	hashmap_free(entries_original_key);
+	LOG("Hashmap freed");
 
-	free_all_entries();
+	regfree(&regex_summary);
+	regfree(&regex_description);
+	regfree(&regex_dtstamp);
+	regfree(&regex_lastmod);
+	regfree(&regex_uid);
+	LOG("Regexp freed");
 	return ret;
 }