agendafs

A filesystem for your calendar.

git clone git://mccd.space/agendafs
commit 6e1f7ca146b7ae8ad2eb5ec5706038a1bade8f92
parent 44dfa5e43b07a58410e0e7de00254a67b89917cf
Author: Marc Coquand <marc@coquand.email>
Date:   Mon, 28 Jul 2025 23:42:35 +0200

Support arbitrary extensions

Implements: https://todo.sr.ht/~marcc/agendafs/14

Diffstat:
Mfuse_node.c | 3+++
Mical_extra.c | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mical_extra.h | 16++++++++++++++++
Mmain.c | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mutil.c | 7+++++++
Mutil.h | 3+++
6 files changed, 144 insertions(+), 2 deletions(-)
diff --git a/fuse_node.c b/fuse_node.c
@@ -356,6 +356,7 @@ append_category_to_memstream(FILE *memstream, const char *category,
 char *
 get_node_categories(memory_region *mreg, const struct tree_node *node)
 {
+	// TODO: Take this as parameter instead...
 	icalcomponent *component = get_icalcomponent_from_node(mreg, node);
 	if (!component) {
 		return NULL;
@@ -452,6 +453,8 @@ set_node_categories(memory_region *mreg, const struct tree_node *node,
 const char *
 get_node_class(memory_region *mreg, const struct tree_node *node)
 {
+
+	// TODO: Take this as parameter instead...
 	icalcomponent *component = get_icalcomponent_from_node(mreg, node);
 	if (!component) {
 		return 0;
diff --git a/ical_extra.c b/ical_extra.c
@@ -3,10 +3,13 @@
 #include "mregion.h"
 #include "path.h"
 #include "sys/stat.h"
+#include "util.h"
 #include "uuid/uuid.h"
 #include <stdbool.h>
 const char *IS_DIRECTORY_PROPERTY = "X-CALDAVFS-ISDIRECTORY";
 const char *FILE_EXTENSION_PROPERTY = "X-CALDAVFS-FILEEXT";
+const char *CUSTOM_PROPERTY_PREFIX = "X-CALDAVFS-CUSTOM-";
+size_t CUSTOM_PROPERTY_PREFIX_LEN = 18;
 
 const char *
 icalcomponent_get_uniq_x_value(icalcomponent *component, const char *key)
@@ -336,6 +339,36 @@ icalcomponent_remove_x_prop(icalcomponent *component, const char *key)
 }
 
 void
+icalcomponent_write_x_props(FILE *memstream, icalcomponent *component)
+{
+	icalcomponent *inner = icalcomponent_get_innermost(component);
+	icalproperty *prop_iter =
+	    icalcomponent_get_first_property(inner, ICAL_X_PROPERTY);
+	while (prop_iter != NULL) {
+		icalproperty *next_prop =
+		    icalcomponent_get_next_property(inner, ICAL_X_PROPERTY);
+		const char *prop_name = icalproperty_get_x_name(prop_iter);
+		if (starts_with_str(prop_name, CUSTOM_PROPERTY_PREFIX)) {
+			const char *key =
+			    prop_name + CUSTOM_PROPERTY_PREFIX_LEN;
+			fprintf(memstream, "user.%s", key);
+			fputc('\0', memstream);
+		}
+		prop_iter = next_prop;
+	}
+}
+
+void
+icalcomponent_remove_custom_prop(memory_region *mreg, icalcomponent *component,
+				 const char *key)
+{
+
+	char *x_key = NULL;
+	rasprintf(mreg, &x_key, "%s%s", CUSTOM_PROPERTY_PREFIX, key);
+	icalcomponent_remove_x_prop(component, x_key);
+}
+
+void
 icalcomponent_set_unique_x_value(icalcomponent *component, const char *key,
 				 const char *value)
 {
@@ -350,9 +383,29 @@ icalcomponent_set_unique_x_value(icalcomponent *component, const char *key,
 
 	icalcomponent_add_property(inner, fileext_prop);
 }
+void
+icalcomponent_set_custom_x_value(memory_region *mreg, icalcomponent *component,
+				 const char *key, const char *value)
+{
+	char *x_key = NULL;
+	rasprintf(mreg, &x_key, "%s%s", CUSTOM_PROPERTY_PREFIX, key);
+
+	icalcomponent_set_unique_x_value(component, x_key, value);
+}
+
+const char *
+icalcomponent_get_custom_x_value(memory_region *mreg, icalcomponent *component,
+				 const char *key)
+{
+	char *x_key = NULL;
+	rasprintf(mreg, &x_key, "%s%s", CUSTOM_PROPERTY_PREFIX, key);
+
+	return icalcomponent_get_uniq_x_value(component, x_key);
+}
 
-// Since empty values are considered null, we store a disallowed extension (..)
-// to indicate that the system has explicitly not set an extension.
+// Since empty values are considered null, we store a disallowed
+// extension (..) to indicate that the system has explicitly not set an
+// extension.
 void
 icalcomponent_set_file_extension(icalcomponent *component,
 				 const char *extension)
diff --git a/ical_extra.h b/ical_extra.h
@@ -61,4 +61,20 @@ icalcomponent_get_file_extension(icalcomponent *component);
 
 void
 icalcomponent_mark_as_directory(icalcomponent *ic);
+
+void
+icalcomponent_set_custom_x_value(memory_region *mreg, icalcomponent *component,
+				 const char *key, const char *value);
+
+const char *
+icalcomponent_get_custom_x_value(memory_region *mreg, icalcomponent *component,
+				 const char *key);
+
+void
+icalcomponent_remove_custom_prop(memory_region *mreg, icalcomponent *component,
+				 const char *key);
+
+void
+icalcomponent_write_x_props(FILE *memstream, icalcomponent *component);
+
 #endif // ical_extra_h_INCLUDED
diff --git a/main.c b/main.c
@@ -393,6 +393,20 @@ fuse_removexattr(const char *path, const char *header)
 		status = delete_node_class(mreg, node);
 		goto cleanup_return;
 	}
+	else if (starts_with_str(header, "user.")) {
+		struct tree_node *node = get_node_by_path(mreg, path);
+		if (!node) {
+			status = -ENONET;
+			goto cleanup_return;
+		}
+
+		icalcomponent *ic = get_icalcomponent_from_node(mreg, node);
+
+		const char *key = header + 5;
+		icalcomponent_remove_custom_prop(mreg, ic, key);
+		status = write_ical_file(mreg, node, ic);
+		goto cleanup_return;
+	}
 
 cleanup_return:
 	pthread_mutex_unlock(&entries_mutex);
@@ -438,6 +452,20 @@ fuse_setxattr(const char *path, const char *header, const char *new_attributes,
 		status = set_node_class(mreg, node, iclass);
 		goto cleanup_return;
 	}
+	else if (starts_with_str(header, "user.")) {
+		struct tree_node *node = get_node_by_path(mreg, path);
+		if (!node) {
+			status = -ENONET;
+			goto cleanup_return;
+		}
+
+		icalcomponent *ic = get_icalcomponent_from_node(mreg, node);
+
+		const char *key = header + 5;
+		icalcomponent_set_custom_x_value(mreg, ic, key, new_attributes);
+		status = write_ical_file(mreg, node, ic);
+		goto cleanup_return;
+	}
 	else {
 		status = -EINVAL;
 	}
@@ -466,6 +494,14 @@ fuse_listxattr(const char *path, char *list, size_t size)
 		goto cleanup_return;
 	}
 
+	if (is_root_node(node)) {
+		return_i = -EPERM;
+		goto cleanup_return;
+	}
+
+	icalcomponent *comp = get_icalcomponent_from_node(mreg, node);
+	assert(comp);
+
 	stream = open_memstream(&membuffer, &membuffer_len);
 	if (stream == NULL) {
 		return_i = -EIO;
@@ -484,6 +520,8 @@ fuse_listxattr(const char *path, char *list, size_t size)
 		fputc('\0', stream);
 	}
 
+	icalcomponent_write_x_props(stream, comp);
+
 	if (fclose(stream) != 0) {
 		return_i = -EIO;
 		goto cleanup_return;
@@ -562,6 +600,28 @@ fuse_getxattr(const char *path, const char *header, char *buf, size_t s)
 		status = string_len;
 		goto cleanup_return;
 	}
+	else if (starts_with_str(header, "user.")) {
+		struct tree_node *node = get_node_by_path(mreg, path);
+		if (!node) {
+			status = -ENONET;
+			goto cleanup_return;
+		}
+
+		icalcomponent *ic = get_icalcomponent_from_node(mreg, node);
+
+		const char *key = header + 5;
+		const char *value =
+		    icalcomponent_get_custom_x_value(mreg, ic, key);
+		if (!value) {
+			status = -ENODATA;
+			goto cleanup_return;
+		}
+
+		int string_len = strlen(value);
+		snprintf(buf, s, "%s", value);
+		status = string_len;
+		goto cleanup_return;
+	}
 
 cleanup_return:
 	pthread_mutex_unlock(&entries_mutex);
diff --git a/util.c b/util.c
@@ -65,3 +65,10 @@ xreallocarray(void *buf, size_t n, size_t m)
 	return buf;
 }
 
+bool
+starts_with_str(const char *str, const char *prefix)
+{
+	size_t len = strlen(prefix);
+	return strncmp(str, prefix, len) == 0;
+}
+
diff --git a/util.h b/util.h
@@ -1,6 +1,7 @@
 #ifndef UTIL_H
 #define UTIL_H
 
+#include <stdbool.h>
 #include <stdlib.h>
 
 #ifdef DEBUG
@@ -20,4 +21,6 @@ void *
 xcalloc(size_t, size_t);
 char *
 xstrdup(const char *s);
+bool
+starts_with_str(const char *str, const char *prefix);
 #endif