agendafs

A filesystem for your calendar.

git clone git://mccd.space/agendafs
commit 9e874d9358fba49a4f306aab581044323cdb3bca
parent bdd8da33645557dbd7b88d1ba6cecf6e93bba807
Author: Marc Coquand <marc@coquand.email>
Date:   Fri, 30 May 2025 22:27:13 +0100

Cleanup

Diffstat:
Mmain.c | 218++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
1 file changed, 149 insertions(+), 69 deletions(-)
diff --git a/main.c b/main.c
@@ -14,37 +14,46 @@
 
 const char *display_date_fmt = "%b %d, %Y at %H:%M";
 
-typedef struct {
+typedef struct 
+{
 	char *filename;
 	char *summary;
 	char *description;
 	time_t timestamp;
 	time_t created_at;
 } journal_entry;
+
 static journal_entry *entries = NULL;
 static size_t entry_count = 0;
 
-static void free_entry(journal_entry *entry) {
-	if (entry) {
+static void free_entry(journal_entry *entry) 
+{
+	if (entry) 
+    	{
 		free(entry->filename);
 		free(entry->summary);
 		free(entry->description);
 	}
 }
 
-static void free_all_entries() {
-	for (size_t i = 0; i < entry_count; i++) {
+static void free_all_entries() 
+{
+	for (size_t i = 0; i < entry_count; i++) 
+    	{
 		free_entry(&entries[i]);
 	}
 	free(entries);
 }
 
-static time_t parse_datetime(const char *datetime) {
-	struct tm tm = {0};
+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, &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
@@ -53,16 +62,20 @@ static time_t parse_datetime(const char *datetime) {
 	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) 
+{
+	// Parse text after SUMMARY: up until \r\n
 	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;
 	return 0;
 }
 
-static char *extract_match(const char *content, regmatch_t match) {
+// Extracts the matched text from regexp
+static char *extract_match(const char *content, regmatch_t match) 
+{
 	size_t len = match.rm_eo - match.rm_so;
 	char *str = malloc(len + 1);
 	if (!str) return NULL;
@@ -71,22 +84,51 @@ static char *extract_match(const char *content, regmatch_t match) {
 	return str;
 }
 
-static void unescape_description(char *desc) {
-    char *pos;
-    while ((pos = strstr(desc, "\\n")) != NULL) {
-        *pos = '\n';
-        memmove(pos + 1, pos + 2, strlen(pos + 2) + 1);
-    }
-    // Commas are saved as \,
-    while ((pos = strstr(desc, "\\,")) != NULL) {
-        *pos = ',';
-        memmove(pos + 1, pos + 2, strlen(pos + 2) + 1);
-    }
+// 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) 
+{	
+	char *src = desc;
+	char *dst = desc;
+
+	while (*src) {
+		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;
+			}
+			src++;
+		} else {
+			*dst++ = *src++;
+		}
+	}
+	done:
+		*dst = '\0';
 }
 
-static int parse_ics_file(const char *file_path, journal_entry *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;
 	}
@@ -97,7 +139,8 @@ static int parse_ics_file(const char *file_path, journal_entry *entry) {
 	fseek(file, 0, SEEK_SET);
 
 	char *file_content = malloc(file_size + 1);
-	if (!file_content) {
+	if (!file_content) 
+    	{
 		perror("Memory allocation failed");
 		fclose(file);
 		return -1;
@@ -111,84 +154,101 @@ static int parse_ics_file(const char *file_path, journal_entry *entry) {
 
 	size_t return_code = -1;
 
-	if (compile_regexes(&regex_summary, &regex_description, &regex_dtstamp, &regex_lastmod) != 0) {
+	if (compile_regexes(&regex_summary, &regex_description, &regex_dtstamp, &regex_lastmod) != 0) 
+    	{
 		fprintf(stderr, "Failed to compile regular expressions\n");
 		goto cleanup;
 	}
 
 	// Extract SUMMARY
-	if (regexec(&regex_summary, file_content, 2, matches_summary, 0) != 0) {
+	if (regexec(&regex_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);
 
 	// Extract DESCRIPTION
-	if (regexec(&regex_description, file_content, 2, matches_description, 0) != 0) {
+	if (regexec(&regex_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) {
+	if (!entry->description) 
+    	{
 		perror("Memory allocation failed for DESCRIPTION");
 		goto cleanup;
 	}
-	unescape_description(entry->description);
+	unescape_text(entry->description);
 
 
 	// Extract the DTSTAMP, our created_at
-	if (regexec(&regex_dtstamp, file_content, 2, matches_dtstamp, 0) == 0) {
+	if (regexec(&regex_dtstamp, file_content, 2, matches_dtstamp, 0) == 0) 
+    	{
 		char *dtstamp_str = extract_match(file_content, matches_dtstamp[1]);
-		if (!dtstamp_str) {
+		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) {
+		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);
 	}
 
-	if (regexec(&regex_dtstamp, file_content, 2, matches_dtstamp, 0) == 0) {
+	if (regexec(&regex_dtstamp, file_content, 2, matches_dtstamp, 0) == 0) 
+    	{
 		char *dtstamp_str = extract_match(file_content, matches_dtstamp[1]);
-		if (!dtstamp_str) {
+		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) {
+		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 (timestamp)
-	if (regexec(&regex_lastmod, file_content, 2, matches_lastmod, 0) == 0) {
+	if (regexec(&regex_lastmod, file_content, 2, matches_lastmod, 0) == 0) 
+    	{
 		char *lastmod_str = extract_match(file_content, matches_lastmod[1]);
-		if (!lastmod_str) {
+		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) {
+		if (entry->timestamp == -1) 
+    		{
 			fprintf(stderr, "Failed to parse LAST-MODIFIED in: %s\n", file_path);
 			goto cleanup;
 		}
-	} else {
+	} else 
+    	{
 		entry->timestamp = entry->created_at;  // fallback
 	}
 
@@ -204,26 +264,32 @@ cleanup:
 	return return_code;
 }
 
-static void load_journal_entries() {
+static void load_journal_entries() 
+{
 	printf("Loading journal entries from: %s\n", ICS_DIR);
 	DIR *dir = opendir(ICS_DIR);
-	if (!dir) {
+	if (!dir) 
+    	{
 		perror("opendir");
 		return;
 	}
 
 	struct dirent *entry;
 	char filepath[512];
-	while ((entry = readdir(dir))) {
-		if (entry->d_type == DT_REG && strstr(entry->d_name, ".ics")) {
+	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 = strdup(entry->d_name);
-			if (parse_ics_file(filepath, &new_entry) == 0) {
+			if (parse_ics_file(filepath, &new_entry) == 0) 
+    			{
 				entries = realloc(entries, sizeof(journal_entry) * (entry_count + 1));
 				entries[entry_count] = new_entry;
 				entry_count++;
-			} else {
+			} else 
+    			{
 				free(new_entry.filename);
 			}
 		}
@@ -232,19 +298,23 @@ static void load_journal_entries() {
 	closedir(dir);
 }
 
-static int journal_getattr(const char *path, struct stat *stbuf, struct fuse_file_info *fi) {
+static int journal_getattr(const char *path, struct stat *stbuf, struct fuse_file_info *fi) 
+{
 	memset(stbuf, 0, sizeof(struct stat));
 
-	if (strcmp(path, "/") == 0) {
+	if (strcmp(path, "/") == 0) 
+    	{
 		stbuf->st_mode = S_IFDIR | 0755;
 		stbuf->st_nlink = 2;
 		return 0;
 	}
 
-	for (size_t i = 0; i < entry_count; i++) {
+	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) {
+		if (strcmp(path, entry_path) == 0) 
+    		{
 			stbuf->st_mode = S_IFREG | 0444;
 			stbuf->st_nlink = 1;
 			struct tm *tm_info = localtime(&entries[i].timestamp);
@@ -271,14 +341,16 @@ static int journal_getattr(const char *path, struct stat *stbuf, struct fuse_fil
 }
 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) {
+						   enum fuse_readdir_flags flags) 
+{
 	if (strcmp(path, "/") != 0)
 		return -ENOENT;
 
 	filler(buf, ".", NULL, 0, 0);
 	filler(buf, "..", NULL, 0, 0);
 
-	for (size_t i = 0; i < entry_count; i++) {
+	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);
@@ -287,24 +359,28 @@ static int journal_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
 	return 0;
 }
 
-static int journal_open(const char *path, struct fuse_file_info *fi) {
-	for (size_t i = 0; i < entry_count; i++) {
+static int journal_open(const char *path, struct fuse_file_info *fi) 
+{
+	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) {
+		if (strcmp(path, entry_path) == 0) 
 			return 0;
-		}
 	}
 	return -ENOENT;
 }
 
 static int journal_read(const char *path, char *buf, size_t size, off_t offset,
-						struct fuse_file_info *fi) {
-	for (size_t i = 0; i < entry_count; i++) {
+						struct fuse_file_info *fi) 
+{
+	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) {
+		if (strcmp(path, entry_path) == 0) 
+    		{
 			// Format timestamp
 			struct tm *tm_info = localtime(&entries[i].created_at);
 			char timestamp_str[64];
@@ -317,7 +393,8 @@ static int journal_read(const char *path, char *buf, size_t size, off_t offset,
 				return -EIO; // snprintf error
 
 			char *full_content = malloc(needed_len + 1);
-			if (!full_content) {
+			if (!full_content) 
+    			{
 				perror("Memory allocation failed");
 				return -ENOMEM;
 			}
@@ -326,7 +403,8 @@ static int journal_read(const char *path, char *buf, size_t size, off_t offset,
 					 entries[i].summary, timestamp_str, entries[i].description);
 
 
-			if (offset >= needed_len) {
+			if (offset >= needed_len) 
+    			{
 				free(full_content);
 				return 0;  // EOF
 			}
@@ -343,14 +421,16 @@ static int journal_read(const char *path, char *buf, size_t size, off_t offset,
 	return -ENOENT;
 }
 
-static const struct fuse_operations journal_oper = {
+static const struct fuse_operations journal_oper = 
+{
 	.getattr = journal_getattr,
 	.readdir = journal_readdir,
 	.open	= journal_open,
 	.read	= journal_read,
 };
 
-int main(int argc, char *argv[]) {
+int main(int argc, char *argv[]) 
+{
 	load_journal_entries();
 	int ret = fuse_main(argc, argv, &journal_oper, NULL);