agendafs
A filesystem for your calendar.
git clone git://mccd.space/agendafscommit 9e874d9358fba49a4f306aab581044323cdb3bca
parent bdd8da33645557dbd7b88d1ba6cecf6e93bba807
Author: Marc Coquand <marc@coquand.email>
Date: Fri, 30 May 2025 22:27:13 +0100
Cleanup
Diffstat:
| M | main.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(®ex_summary, ®ex_description, ®ex_dtstamp, ®ex_lastmod) != 0) {
+ if (compile_regexes(®ex_summary, ®ex_description, ®ex_dtstamp, ®ex_lastmod) != 0)
+ {
fprintf(stderr, "Failed to compile regular expressions\n");
goto cleanup;
}
// Extract 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);
// Extract DESCRIPTION
- if (regexec(®ex_description, file_content, 2, matches_description, 0) != 0) {
+ 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) {
+ 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(®ex_dtstamp, file_content, 2, matches_dtstamp, 0) == 0) {
+ 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 (!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(®ex_dtstamp, file_content, 2, matches_dtstamp, 0) == 0) {
+ 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 (!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(®ex_lastmod, file_content, 2, matches_lastmod, 0) == 0) {
+ if (regexec(®ex_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);