diff options
author | Alexander Larsson <alexl@redhat.com> | 2015-07-10 16:46:27 +0200 |
---|---|---|
committer | Alexander Larsson <alexl@redhat.com> | 2015-07-10 16:56:39 +0200 |
commit | 929071ad1083494f717f6a5ab6dfcf817d143700 (patch) | |
tree | 0f01ac260bc5fe087bb478582f27cd6da832bbed /document-portal/xdp-fuse.c | |
parent | b0e1124f0262d9d4cbfe9c6715eae3a3dcfb4875 (diff) | |
download | xdg-app-929071ad1083494f717f6a5ab6dfcf817d143700.tar.gz |
Import xdg-document-portal from github repo
This pulls in the daemon code from:
https://github.com/alexlarsson/xdg-document-portal/
We need this in xdg-app because we need to set up the mounts correctly.
Diffstat (limited to 'document-portal/xdp-fuse.c')
-rw-r--r-- | document-portal/xdp-fuse.c | 1787 |
1 files changed, 1787 insertions, 0 deletions
diff --git a/document-portal/xdp-fuse.c b/document-portal/xdp-fuse.c new file mode 100644 index 0000000..b6feea6 --- /dev/null +++ b/document-portal/xdp-fuse.c @@ -0,0 +1,1787 @@ +#include "config.h" + +#define FUSE_USE_VERSION 26 + +#include <glib-unix.h> + +#include <fuse_lowlevel.h> +#include <stdio.h> +#include <string.h> +#include <errno.h> +#include <fcntl.h> +#include <stdlib.h> +#include <assert.h> +#include <glib/gprintf.h> +#include <gio/gio.h> + +#include "xdp-error.h" +#include "xdp-fuse.h" + +/* Layout: + + "/ (STD_DIRS:1) + "by-app/" (STD_DIRS:2) + "org.gnome.gedit/" (APP_DIR:app id) + "$id/" (APP_DOC_DIR:app_id<<32|doc_id) + <same as DOC_DIR> + "in-homedir/" (APP_DOC_DIR:1) + "$id" (DOC_DIR:doc_idid) + $basename (DOC_FILE:doc_id) + $tmpfile (TMPFILE:tmp_id) +*/ + +#define BY_APP_INO 2 + +#define IN_HOMEDIR_APP_ID 1 + +#define NON_DOC_DIR_PERMS 0500 +#define DOC_DIR_PERMS 0700 + +/* The (fake) directories don't really change */ +#define DIRS_ATTR_CACHE_TIME 60.0 + +/* We pretend that the file is hardlinked. This causes most apps to do + a truncating overwrite, which suits us better, as we do the atomic + rename ourselves anyway. This way we don't weirdly change the inode + after the rename. */ +#define DOC_FILE_NLINK 2 + +typedef enum { + STD_DIRS_INO_CLASS, + DOC_DIR_INO_CLASS, + DOC_FILE_INO_CLASS, + TMPFILE_INO_CLASS, + APP_DIR_INO_CLASS, + APP_DOC_DIR_INO_CLASS, +} XdpInodeClass; + +#define BY_APP_NAME "by-app" +#define IN_HOMEDIR_NAME "in-homedir" + +static XdpDocDb *db; + +static GHashTable *app_name_to_id; +static GHashTable *app_id_to_name; +static guint32 next_app_id; + +static guint32 next_tmp_id; + +typedef struct +{ + guint64 parent_inode; + char *name; + + char *backing_path; + guint32 tmp_id; +} XdpTmp; + +typedef struct +{ + int fd; + fuse_ino_t inode; + int trunc_fd; + char *trunc_path; + char *real_path; + gboolean truncated; + gboolean readonly; + guint32 tmp_id; +} XdpFh; + +static GList *tmp_files = NULL; +static GList *open_files = NULL; + +static XdpTmp * +find_tmp_by_name (guint64 parent_inode, + const char *name) +{ + GList *l; + + for (l = tmp_files; l != NULL; l = l->next) + { + XdpTmp *tmp = l->data; + if (tmp->parent_inode == parent_inode && + strcmp (tmp->name, name) == 0) + return tmp; + } + + return NULL; +} + +static XdpTmp * +find_tmp_by_id (guint32 tmp_id) +{ + GList *l; + + for (l = tmp_files; l != NULL; l = l->next) + { + XdpTmp *tmp = l->data; + if (tmp->tmp_id == tmp_id) + return tmp; + } + + return NULL; +} + +static XdpInodeClass +get_class (guint64 inode) +{ + return (inode >> (64-8)) & 0xff; +} + +static guint64 +get_class_ino (guint64 inode) +{ + return inode & ((1L << (64-8)) - 1); +} + +static guint32 +get_app_id_from_app_doc_ino (guint64 inode) +{ + return inode >> 32; +} + +static guint32 +get_doc_id_from_app_doc_ino (guint64 inode) +{ + return inode & 0xffffffff; +} + +static guint64 +make_inode (XdpInodeClass class, guint64 inode) +{ + return ((guint64)class) << (64-8) | (inode & 0xffffffffffffff); +} + +static guint64 +make_app_doc_dir_inode (guint32 app_id, guint32 doc_id) +{ + return make_inode (APP_DOC_DIR_INO_CLASS, + ((guint64)app_id << 32) | (guint64)doc_id); +} + +static gboolean +name_looks_like_id (const char *name) +{ + int i; + + /* No zeros in front, we need canonical form */ + if (name[0] == '0') + return FALSE; + + for (i = 0; i < 8; i++) + { + char c = name[i]; + if (c == 0) + break; + + if (!g_ascii_isdigit(c) && + !(c >= 'a' && c <= 'f')) + return FALSE; + } + + if (name[i] != 0) + return FALSE; + + return TRUE; +} + +static guint32 +get_app_id_from_name (const char *name) +{ + guint32 id; + char *myname; + + id = GPOINTER_TO_UINT (g_hash_table_lookup (app_name_to_id, name)); + + if (id != 0) + return id; + + id = next_app_id++; + + /* We rely this to not overwrap into the high byte in the inode */ + g_assert (id < 0x00ffffff); + + myname = g_strdup (name); + g_hash_table_insert (app_name_to_id, myname, GUINT_TO_POINTER (id)); + g_hash_table_insert (app_id_to_name, GUINT_TO_POINTER (id), myname); + return id; +} + +static const char * +get_app_name_from_id (guint32 id) +{ + return g_hash_table_lookup (app_id_to_name, GUINT_TO_POINTER (id)); +} + +static void +fill_app_name_hash (void) +{ + char **keys; + int i; + + keys = xdp_doc_db_list_apps (db); + for (i = 0; keys[i] != NULL; i++) + get_app_id_from_name (keys[i]); + g_strfreev (keys); +} + +static XdpFh * +xdp_fh_new (fuse_ino_t inode, struct fuse_file_info *fi, int fd, XdpTmp *tmp) +{ + XdpFh *fh = g_new0 (XdpFh, 1); + fh->inode = inode; + fh->fd = fd; + if (tmp) + fh->tmp_id = tmp->tmp_id; + fh->trunc_fd = -1; + + open_files = g_list_prepend (open_files, fh); + + fi->fh = (gsize)fh; + return fh; +} + +static void +xdp_fh_free (XdpFh *fh) +{ + open_files = g_list_remove (open_files, fh); + + if (fh->truncated) + { + fsync (fh->trunc_fd); + if (rename (fh->trunc_path, fh->real_path) != 0) + g_warning ("Unable to replace truncated document"); + } + else if (fh->trunc_path) + unlink (fh->trunc_path); + + if (fh->fd >= 0) + close (fh->fd); + if (fh->trunc_fd >= 0) + close (fh->trunc_fd); + + g_clear_pointer (&fh->trunc_path, g_free); + g_clear_pointer (&fh->real_path, g_free); + + g_free (fh); +} + +static int +xdp_fh_get_fd (XdpFh *fh) +{ + if (fh->truncated) + return fh->trunc_fd; + else + return fh->fd; +} + +static int +get_user_perms (const struct stat *stbuf) +{ + /* Strip out exec and setuid bits */ + return stbuf->st_mode & 0666; +} + +static double +get_attr_cache_time (int st_mode) +{ + if (S_ISDIR (st_mode)) + return DIRS_ATTR_CACHE_TIME; + return 0.0; +} + +static double +get_entry_cache_time (int st_mode) +{ + if (S_ISDIR (st_mode)) + return DIRS_ATTR_CACHE_TIME; + return 1.0; +} + +static gboolean +app_can_see_doc (GVariant *doc, guint32 app_id) +{ + const char *app_name = get_app_name_from_id (app_id); + if (app_name != NULL && + xdp_doc_has_permissions (doc, app_name, XDP_PERMISSION_FLAGS_READ)) + return TRUE; + + if (app_id == IN_HOMEDIR_APP_ID) + { + g_autofree char *path = xdp_doc_dup_path (doc); + + if (g_str_has_prefix (path, g_get_home_dir ())) + return TRUE; + } + + return FALSE; +} + +static int +xdp_stat (fuse_ino_t ino, + struct stat *stbuf, + GVariant **doc_out) +{ + XdpInodeClass class = get_class (ino); + guint64 class_ino = get_class_ino (ino); + g_autoptr (GVariant) doc = NULL; + g_autofree char *path = NULL; + struct stat tmp_stbuf; + XdpTmp *tmp; + + stbuf->st_ino = ino; + + switch (class) + { + case STD_DIRS_INO_CLASS: + + switch (class_ino) + { + case FUSE_ROOT_ID: + stbuf->st_mode = S_IFDIR | NON_DOC_DIR_PERMS; + stbuf->st_nlink = 2; + break; + + case BY_APP_INO: + stbuf->st_mode = S_IFDIR | NON_DOC_DIR_PERMS; + stbuf->st_nlink = 2; + break; + + default: + return ENOENT; + } + break; + + case APP_DIR_INO_CLASS: + if (class_ino != IN_HOMEDIR_APP_ID && get_app_name_from_id (class_ino) == 0) + return ENOENT; + + stbuf->st_mode = S_IFDIR | NON_DOC_DIR_PERMS; + stbuf->st_nlink = 2; + break; + + case APP_DOC_DIR_INO_CLASS: + { + guint32 app_id = get_app_id_from_app_doc_ino (class_ino); + guint32 doc_id = get_doc_id_from_app_doc_ino (class_ino); + + doc = xdp_doc_db_lookup_doc (db, doc_id); + if (doc == NULL || + !app_can_see_doc (doc, app_id)) + return ENOENT; + + stbuf->st_mode = S_IFDIR | DOC_DIR_PERMS; + stbuf->st_nlink = 2; + break; + } + + case DOC_DIR_INO_CLASS: + doc = xdp_doc_db_lookup_doc (db, class_ino); + + if (doc == NULL) + return ENOENT; + + stbuf->st_mode = S_IFDIR | DOC_DIR_PERMS; + stbuf->st_nlink = 2; + break; + + case DOC_FILE_INO_CLASS: + doc = xdp_doc_db_lookup_doc (db, class_ino); + + if (doc == NULL) + return ENOENT; + + stbuf->st_nlink = DOC_FILE_NLINK; + + path = xdp_doc_dup_path (doc); + if (stat (path, &tmp_stbuf) != 0) + return ENOENT; + + stbuf->st_mode = S_IFREG | get_user_perms (&tmp_stbuf); + stbuf->st_size = tmp_stbuf.st_size; + stbuf->st_uid = tmp_stbuf.st_uid; + stbuf->st_gid = tmp_stbuf.st_gid; + stbuf->st_blksize = tmp_stbuf.st_blksize; + stbuf->st_blocks = tmp_stbuf.st_blocks; + stbuf->st_atim = tmp_stbuf.st_atim; + stbuf->st_mtim = tmp_stbuf.st_mtim; + stbuf->st_ctim = tmp_stbuf.st_ctim; + break; + + case TMPFILE_INO_CLASS: + tmp = find_tmp_by_id (class_ino); + if (tmp == NULL) + return ENOENT; + + stbuf->st_mode = S_IFREG; + stbuf->st_nlink = DOC_FILE_NLINK; + + if (stat (tmp->backing_path, &tmp_stbuf) != 0) + return ENOENT; + + stbuf->st_mode = S_IFREG | get_user_perms (&tmp_stbuf); + stbuf->st_size = tmp_stbuf.st_size; + stbuf->st_uid = tmp_stbuf.st_uid; + stbuf->st_gid = tmp_stbuf.st_gid; + stbuf->st_blksize = tmp_stbuf.st_blksize; + stbuf->st_blocks = tmp_stbuf.st_blocks; + stbuf->st_atim = tmp_stbuf.st_atim; + stbuf->st_mtim = tmp_stbuf.st_mtim; + stbuf->st_ctim = tmp_stbuf.st_ctim; + break; + + default: + return ENOENT; + } + + if (doc && doc_out) + *doc_out = g_steal_pointer (&doc); + + return 0; +} + +static int +xdp_fstat (XdpFh *fh, + struct stat *stbuf) +{ + struct stat tmp_stbuf; + int fd; + + fd = xdp_fh_get_fd (fh); + if (fd < 0) + return -ENOSYS; + + if (fstat (fd, &tmp_stbuf) != 0) + return -errno; + + stbuf->st_nlink = DOC_FILE_NLINK; + stbuf->st_mode = S_IFREG | get_user_perms (&tmp_stbuf); + stbuf->st_size = tmp_stbuf.st_size; + stbuf->st_uid = tmp_stbuf.st_uid; + stbuf->st_gid = tmp_stbuf.st_gid; + stbuf->st_blksize = tmp_stbuf.st_blksize; + stbuf->st_blocks = tmp_stbuf.st_blocks; + stbuf->st_atim = tmp_stbuf.st_atim; + stbuf->st_mtim = tmp_stbuf.st_mtim; + stbuf->st_ctim = tmp_stbuf.st_ctim; + + return 0; +} + +static void +xdp_fuse_getattr (fuse_req_t req, + fuse_ino_t ino, + struct fuse_file_info *fi) +{ + struct stat stbuf = { 0 }; + GList *l; + int res; + + g_debug ("xdp_fuse_getattr %lx (fi=%p)", ino, fi); + + /* Fuse passes fi in to verify EOF during read/write/seek, but not during fstat */ + if (fi != NULL) + { + XdpFh *fh = (gpointer)fi->fh; + + res = xdp_fstat (fh, &stbuf); + if (res == 0) + { + fuse_reply_attr (req, &stbuf, get_attr_cache_time (stbuf.st_mode)); + return; + } + } + + for (l = open_files; l != NULL; l = l->next) + { + XdpFh *fh = l->data; + if (fh->inode == ino) + { + res = xdp_fstat (fh, &stbuf); + if (res == 0) + { + fuse_reply_attr (req, &stbuf, get_attr_cache_time (stbuf.st_mode)); + return; + } + } + } + + if ((res = xdp_stat (ino, &stbuf, NULL)) != 0) + fuse_reply_err (req, res); + else + fuse_reply_attr (req, &stbuf, get_attr_cache_time (stbuf.st_mode)); +} + +static int +xdp_lookup (fuse_ino_t parent, + const char *name, + fuse_ino_t *inode, + struct stat *stbuf, + GVariant **doc_out, + XdpTmp **tmp_out) +{ + XdpInodeClass parent_class = get_class (parent); + guint64 parent_class_ino = get_class_ino (parent); + g_autoptr (GVariant) doc = NULL; + XdpTmp *tmp; + + if (doc_out) + *doc_out = NULL; + if (tmp_out) + *tmp_out = NULL; + + switch (parent_class) + { + case STD_DIRS_INO_CLASS: + + switch (parent_class_ino) + { + case FUSE_ROOT_ID: + if (strcmp (name, BY_APP_NAME) == 0) + { + *inode = make_inode (STD_DIRS_INO_CLASS, BY_APP_INO); + if (xdp_stat (*inode, stbuf, NULL) == 0) + return 0; + } + else if (strcmp (name, IN_HOMEDIR_NAME) == 0) + { + *inode = make_inode (APP_DIR_INO_CLASS, IN_HOMEDIR_APP_ID); + if (xdp_stat (*inode, stbuf, NULL) == 0) + return 0; + } + else if (name_looks_like_id (name)) + { + *inode = make_inode (DOC_DIR_INO_CLASS, + xdb_doc_id_from_name (name)); + if (xdp_stat (*inode, stbuf, NULL) == 0) + return 0; + } + + break; + + case BY_APP_INO: + if (g_dbus_is_name (name) && !g_dbus_is_unique_name (name)) + { + guint32 app_id = get_app_id_from_name (name); + *inode = make_inode (APP_DIR_INO_CLASS, app_id); + if (xdp_stat (*inode, stbuf, NULL) == 0) + return 0; + } + + break; + + default: + break; + } + break; + + case APP_DIR_INO_CLASS: + { + if (name_looks_like_id (name)) + { + *inode = make_app_doc_dir_inode (parent_class_ino, + xdb_doc_id_from_name (name)); + if (xdp_stat (*inode, stbuf, NULL) == 0) + return 0; + } + } + + break; + + case APP_DOC_DIR_INO_CLASS: + case DOC_DIR_INO_CLASS: + if (parent_class == APP_DOC_DIR_INO_CLASS) + doc = xdp_doc_db_lookup_doc (db, get_doc_id_from_app_doc_ino (parent_class_ino)); + else + doc = xdp_doc_db_lookup_doc (db, parent_class_ino); + if (doc != NULL) + { + g_autofree char *basename = xdp_doc_dup_basename (doc); + if (strcmp (name, basename) == 0) + { + *inode = make_inode (DOC_FILE_INO_CLASS, parent_class_ino); + if (xdp_stat (*inode, stbuf, NULL) == 0) + { + if (doc_out) + *doc_out = g_steal_pointer (&doc); + return 0; + } + + break; + } + } + + tmp = find_tmp_by_name (parent, name); + if (tmp != NULL) + { + *inode = make_inode (TMPFILE_INO_CLASS, tmp->tmp_id); + if (xdp_stat (*inode, stbuf, NULL) == 0) + { + if (doc_out) + *doc_out = g_steal_pointer (&doc); + if (tmp_out) + *tmp_out = tmp; + return 0; + } + + break; + } + + break; + + case TMPFILE_INO_CLASS: + case DOC_FILE_INO_CLASS: + return ENOTDIR; + + default: + break; + } + + return ENOENT; +} + +static void +xdp_fuse_lookup (fuse_req_t req, + fuse_ino_t parent, + const char *name) +{ + struct fuse_entry_param e = {0}; + int res; + + g_debug ("xdp_fuse_lookup %lx/%s", parent, name); + + memset (&e, 0, sizeof(e)); + + res = xdp_lookup (parent, name, &e.ino, &e.attr, NULL, NULL); + + if (res == 0) + { + e.attr_timeout = get_attr_cache_time (e.attr.st_mode); + e.entry_timeout = get_entry_cache_time (e.attr.st_mode); + fuse_reply_entry (req, &e); + } + else + { + fuse_reply_err (req, res); + } +} + +struct dirbuf { + char *p; + size_t size; +}; + +static void +dirbuf_add (fuse_req_t req, + struct dirbuf *b, + const char *name, + fuse_ino_t ino) +{ + struct stat stbuf; + + size_t oldsize = b->size; + b->size += fuse_add_direntry (req, NULL, 0, name, NULL, 0); + b->p = (char *) g_realloc (b->p, b->size); + memset (&stbuf, 0, sizeof (stbuf)); + stbuf.st_ino = ino; + fuse_add_direntry (req, b->p + oldsize, + b->size - oldsize, + name, &stbuf, + b->size); +} + +static void +dirbuf_add_docs (fuse_req_t req, + struct dirbuf *b, + guint32 app_id) +{ + g_autofree guint32 *docs = NULL; + guint64 inode; + int i; + g_autofree char *doc_name = NULL; + + docs = xdp_doc_db_list_docs (db); + for (i = 0; docs[i] != 0; i++) + { + if (app_id) + { + g_autoptr(GVariant) doc = xdp_doc_db_lookup_doc (db, docs[i]); + if (doc == NULL || + !app_can_see_doc (doc, app_id)) + continue; + } + if (app_id) + inode = make_app_doc_dir_inode (app_id, docs[i]); + else + inode = make_inode (DOC_DIR_INO_CLASS, docs[i]); + doc_name = xdb_doc_name_from_id (docs[i]); + dirbuf_add (req, b, doc_name, inode); + + } +} + +static void +dirbuf_add_doc_file (fuse_req_t req, + struct dirbuf *b, + GVariant *doc, + guint32 doc_id) +{ + struct stat tmp_stbuf; + g_autofree char *path = xdp_doc_dup_path (doc); + g_autofree char *basename = xdp_doc_dup_basename (doc); + if (stat (path, &tmp_stbuf) == 0) + dirbuf_add (req, b, basename, + make_inode (DOC_FILE_INO_CLASS, doc_id)); +} + +static void +dirbuf_add_tmp_files (fuse_req_t req, + struct dirbuf *b, + guint64 dir_inode) +{ + GList *l; + + for (l = tmp_files; l != NULL; l = l->next) + { + XdpTmp *tmp = l->data; + if (tmp->parent_inode == dir_inode) + dirbuf_add (req, b, tmp->name, + make_inode (TMPFILE_INO_CLASS, tmp->tmp_id)); + } +} + +static int +reply_buf_limited (fuse_req_t req, + const char *buf, + size_t bufsize, + off_t off, + size_t maxsize) +{ + if (off < bufsize) + return fuse_reply_buf (req, buf + off, + MIN (bufsize - off, maxsize)); + else + return fuse_reply_buf (req, NULL, 0); +} + +static void +xdp_fuse_readdir (fuse_req_t req, fuse_ino_t ino, size_t size, + off_t off, struct fuse_file_info *fi) +{ + struct dirbuf *b = (struct dirbuf *)(fi->fh); + + reply_buf_limited (req, b->p, b->size, off, size); +} + +static void +xdp_fuse_opendir (fuse_req_t req, + fuse_ino_t ino, + struct fuse_file_info *fi) +{ + struct stat stbuf = {0}; + struct dirbuf b = {0}; + XdpInodeClass class; + guint64 class_ino; + g_autoptr (GVariant) doc = NULL; + g_autofree char *basename = NULL; + int res; + + g_debug ("xdp_fuse_opendir %lx", ino); + + if ((res = xdp_stat (ino, &stbuf, &doc)) != 0) + { + fuse_reply_err (req, res); + return; + } + + if ((stbuf.st_mode & S_IFMT) != S_IFDIR) + { + fuse_reply_err (req, ENOTDIR); + return; + } + + class = get_class (ino); + class_ino = get_class_ino (ino); + + switch (class) + { + case STD_DIRS_INO_CLASS: + switch (class_ino) + { + case FUSE_ROOT_ID: + dirbuf_add (req, &b, ".", FUSE_ROOT_ID); + dirbuf_add (req, &b, "..", FUSE_ROOT_ID); + dirbuf_add (req, &b, BY_APP_NAME, + make_inode (STD_DIRS_INO_CLASS, BY_APP_INO)); + dirbuf_add (req, &b, IN_HOMEDIR_NAME, + make_inode (APP_DIR_INO_CLASS, IN_HOMEDIR_APP_ID)); + dirbuf_add_docs (req, &b, 0); + break; + + case BY_APP_INO: + dirbuf_add (req, &b, ".", ino); + dirbuf_add (req, &b, "..", FUSE_ROOT_ID); + + /* Update for any possible new app */ + fill_app_name_hash (); + + { + GHashTableIter iter; + gpointer key, value; + + g_hash_table_iter_init (&iter, app_name_to_id); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + const char *name = key; + guint32 id = GPOINTER_TO_UINT(value); + + dirbuf_add (req, &b, name, + make_inode (APP_DIR_INO_CLASS, id)); + } + } + break; + + default: + break; + } + break; + + case APP_DIR_INO_CLASS: + { + dirbuf_add (req, &b, ".", ino); + dirbuf_add (req, &b, "..", make_inode (STD_DIRS_INO_CLASS, BY_APP_INO)); + dirbuf_add_docs (req, &b, class_ino); + break; + } + + break; + + case DOC_DIR_INO_CLASS: + dirbuf_add (req, &b, ".", ino); + dirbuf_add (req, &b, "..", FUSE_ROOT_ID); + dirbuf_add_doc_file (req, &b, doc, class_ino); + dirbuf_add_tmp_files (req, &b, ino); + break; + + case APP_DOC_DIR_INO_CLASS: + dirbuf_add (req, &b, ".", ino); + dirbuf_add (req, &b, "..", make_inode (APP_DIR_INO_CLASS, + get_app_id_from_app_doc_ino (class_ino))); + dirbuf_add_doc_file (req, &b, doc, + get_doc_id_from_app_doc_ino (class_ino)); + dirbuf_add_tmp_files (req, &b, ino); + break; + + case DOC_FILE_INO_CLASS: + case TMPFILE_INO_CLASS: + /* These should have returned ENOTDIR above */ + default: + break; + } + + if (b.p == NULL) + fuse_reply_err (req, EIO); + else + { + fi->fh = (gsize)g_memdup (&b, sizeof (b)); + if (fuse_reply_open (req, fi) == -ENOENT) + { + g_free (b.p); + g_free ((gpointer)(fi->fh)); + } + } +} + +static void +xdp_fuse_releasedir (fuse_req_t req, + fuse_ino_t ino, + struct fuse_file_info *fi) +{ + struct dirbuf *b = (struct dirbuf *)(fi->fh); + g_free (b->p); + g_free (b); + fuse_reply_err (req, 0); +} + +static int +get_open_flags (struct fuse_file_info *fi) +{ + /* TODO: Maybe limit the flags set more */ + return fi->flags & ~(O_EXCL|O_CREAT); +} + +static char * +create_tmp_for_doc (GVariant *doc, int flags, int *fd_out) +{ + g_autofree char *dirname = xdp_doc_dup_dirname (doc); + g_autofree char *basename = xdp_doc_dup_basename (doc); + g_autofree char *template = g_strconcat (dirname, "/.", basename, ".XXXXXX", NULL); + int fd; + + fd = g_mkstemp_full (template, flags, 0600); + if (fd == -1) + return NULL; + + *fd_out = fd; + return g_steal_pointer (&template); +} + + +static XdpTmp * +tmpfile_new (fuse_ino_t parent, + const char *name, + GVariant *doc, + int flags, + int *fd_out) +{ + XdpTmp *tmp; + g_autofree char *path = NULL; + int fd; + + path = create_tmp_for_doc (doc, flags, &fd); + if (path == NULL) + return NULL; + + tmp = g_new0 (XdpTmp, 1); + tmp->parent_inode = parent; + tmp->name = g_strdup (name); + tmp->backing_path = g_steal_pointer (&path); + tmp->tmp_id = next_tmp_id++; + + if (fd_out) + *fd_out = fd; + else + close (fd); + + tmp_files = g_list_prepend (tmp_files, tmp); + + return tmp; +} + +static void +tmpfile_free (XdpTmp *tmp) +{ + GList *l; + + tmp_files = g_list_remove (tmp_files, tmp); + + for (l = open_files; l != NULL; l = l->next) + { + XdpFh *fh = l->data; + if (fh->tmp_id == tmp->tmp_id) + fh->tmp_id = 0; + } + + if (tmp->backing_path) + unlink (tmp->backing_path); + + g_free (tmp->name); + g_free (tmp->backing_path); + g_free (tmp); +} + +static void +xdp_fuse_open (fuse_req_t req, + fuse_ino_t ino, + struct fuse_file_info *fi) +{ + XdpInodeClass class = get_class (ino); + guint64 class_ino = get_class_ino (ino); + struct stat stbuf = {0}; + g_autoptr (GVariant) doc = NULL; + g_autofree char *path = NULL; + XdpTmp *tmp; + int fd, res; + XdpFh *fh; + + g_debug ("xdp_fuse_open %lx", ino); + + if ((res = xdp_stat (ino, &stbuf, &doc)) != 0) + { + fuse_reply_err (req, res); + return; + } + + if ((stbuf.st_mode & S_IFMT) != S_IFREG) + { + fuse_reply_err (req, EISDIR); + return; + } + + if (doc && class == DOC_FILE_INO_CLASS) + { + g_autofree char *write_path = NULL; + int write_fd = -1; + + path = xdp_doc_dup_path (doc); + + if ((fi->flags & 3) != O_RDONLY) + { + if (access (path, W_OK) != 0) + { + fuse_reply_err (req, errno); + return; + } + write_path = create_tmp_for_doc (doc, O_RDWR, &write_fd); + if (write_path == NULL) + { + fuse_reply_err (req, errno); + return; + } + } + + fd = open (path, O_RDONLY); + if (fd < 0) + { + int errsv = errno; + if (write_fd >= 0) + close (write_fd); + fuse_reply_err (req, errsv); + return; + } + fh = xdp_fh_new (ino, fi, fd, NULL); + fh->trunc_fd = write_fd; + fh->trunc_path = g_steal_pointer (&write_path); + fh->real_path = g_steal_pointer (&path); + if (fuse_reply_open (req, fi)) + xdp_fh_free (fh); + } + else if (class == TMPFILE_INO_CLASS && + (tmp = find_tmp_by_id (class_ino))) + { + fd = open (tmp->backing_path, get_open_flags (fi)); + if (fd < 0) + { + fuse_reply_err (req, errno); + return; + } + fh = xdp_fh_new (ino, fi, fd, tmp); + if (fuse_reply_open (req, fi)) + xdp_fh_free (fh); + } + else + fuse_reply_err (req, EIO); +} + +static void +xdp_fuse_create (fuse_req_t req, + fuse_ino_t parent, + const char *name, + mode_t mode, + struct fuse_file_info *fi) +{ + struct fuse_entry_param e = {0}; + XdpInodeClass parent_class = get_class (parent); + struct stat stbuf; + XdpFh *fh; + g_autoptr(GVariant) doc = NULL; + g_autofree char *basename = NULL; + g_autofree char *path = NULL; + XdpTmp *tmpfile; + int fd, res; + + g_debug ("xdp_fuse_create %lx/%s, flags %o", parent, name, fi->flags); + + if ((res = xdp_stat (parent, &stbuf, &doc)) != 0) + { + fuse_reply_err (req, res); + return; + } + + if ((stbuf.st_mode & S_IFMT) != S_IFDIR) + { + fuse_reply_err (req, ENOTDIR); + return; + } + + if (parent_class != APP_DOC_DIR_INO_CLASS && + parent_class != DOC_DIR_INO_CLASS) + { + fuse_reply_err (req, EACCES); + return; + } + + basename = xdp_doc_dup_basename (doc); + if (strcmp (name, basename) == 0) + { + g_autofree char *write_path = NULL; + int write_fd = -1; + guint32 doc_id = xdb_doc_id_from_name (name); + + write_path = create_tmp_for_doc (doc, O_RDWR, &write_fd); + if (write_path == NULL) + { + fuse_reply_err (req, errno); + return; + } + + path = xdp_doc_dup_path (doc); + + fd = open (path, O_CREAT|O_EXCL|O_RDONLY); + if (fd < 0) + { + int errsv = errno; + if (write_fd >= 0) + close (write_fd); + fuse_reply_err (req, errsv); + return; + } + + e.ino = make_inode (DOC_FILE_INO_CLASS, doc_id); + + fh = xdp_fh_new (e.ino, fi, fd, NULL); + fh->truncated = TRUE; + fh->trunc_fd = write_fd; + fh->trunc_path = g_steal_pointer (&write_path); + fh->real_path = g_steal_pointer (&path); + + if (xdp_fstat (fh, &e.attr) != 0) + { + xdp_fh_free (fh); + fuse_reply_err (req, EIO); + return; + } + + e.attr_timeout = get_attr_cache_time (e.attr.st_mode); + e.entry_timeout = get_entry_cache_time (e.attr.st_mode); + + if (fuse_reply_create (req, &e, fi)) + xdp_fh_free (fh); + } + else + { + tmpfile = find_tmp_by_name (parent, name); + if (tmpfile != NULL && fi->flags & O_EXCL) + { + fuse_reply_err (req, EEXIST); + return; + } + + if (tmpfile) + { + fd = open (tmpfile->backing_path, get_open_flags (fi)); + if (fd == -1) + { + fuse_reply_err (req, errno); + return; + } + } + else + { + tmpfile = tmpfile_new (parent, name, doc, get_open_flags (fi), &fd); + if (tmpfile == NULL) + { + fuse_reply_err (req, errno); + return; + } + } + + e.ino = make_inode (TMPFILE_INO_CLASS, tmpfile->tmp_id); + if (xdp_stat (e.ino, &e.attr, NULL) != 0) + { + fuse_reply_err (req, EIO); + return; + } + e.attr_timeout = get_attr_cache_time (e.attr.st_mode); + e.entry_timeout = get_entry_cache_time (e.attr.st_mode); + + fh = xdp_fh_new (e.ino, fi, fd, tmpfile); + if (fuse_reply_create (req, &e, fi)) + xdp_fh_free (fh); + } +} + +static void +xdp_fuse_read (fuse_req_t req, + fuse_ino_t ino, + size_t size, + off_t off, + struct fuse_file_info *fi) +{ + XdpFh *fh = (gpointer)fi->fh; + struct fuse_bufvec bufv = FUSE_BUFVEC_INIT (size); + static char c = 'x'; + int fd; + + fd = xdp_fh_get_fd (fh); + if (fd == -1) + { + bufv.buf[0].flags = 0; + bufv.buf[0].mem = &c; + bufv.buf[0].size = 0; + + fuse_reply_data (req, &bufv, FUSE_BUF_NO_SPLICE); + return; + } + + bufv.buf[0].flags = FUSE_BUF_IS_FD | FUSE_BUF_FD_SEEK; + bufv.buf[0].fd = fd; + bufv.buf[0].pos = off; + + fuse_reply_data (req, &bufv, FUSE_BUF_SPLICE_MOVE); +} + +static void +xdp_fuse_write (fuse_req_t req, + fuse_ino_t ino, + const char *buf, + size_t size, + off_t off, + struct fuse_file_info *fi) +{ + XdpFh *fh = (gpointer)fi->fh; + gssize res; + int fd; + + if (fh->readonly) + { + fuse_reply_err (req, EACCES); + return; + } + + fd = xdp_fh_get_fd (fh); + if (fd == -1) + { + fuse_reply_err (req, EIO); + return; + } + + res = pwrite (fd, buf, size, off); + if (res < 0) + fuse_reply_err (req, errno); + else + fuse_reply_write (req, res); +} + +static void +xdp_fuse_write_buf (fuse_req_t req, + fuse_ino_t ino, + struct fuse_bufvec *bufv, + off_t off, + struct fuse_file_info *fi) +{ + XdpFh *fh = (gpointer)fi->fh; + struct fuse_bufvec dst = FUSE_BUFVEC_INIT(fuse_buf_size(bufv)); + gssize res; + int fd; + + if (fh->readonly) + { + fuse_reply_err (req, EACCES); + return; + } + + fd = xdp_fh_get_fd (fh); + if (fd == -1) + { + fuse_reply_err (req, EIO); + return; + } + + dst.buf[0].flags = FUSE_BUF_IS_FD | FUSE_BUF_FD_SEEK; + dst.buf[0].fd = fd; + dst.buf[0].pos = off; + + res = fuse_buf_copy (&dst, bufv, FUSE_BUF_SPLICE_NONBLOCK); + if (res < 0) + fuse_reply_err (req, -res); + else + fuse_reply_write (req, res); +} + +static void +xdp_fuse_release (fuse_req_t req, + fuse_ino_t ino, + struct fuse_file_info *fi) +{ + XdpFh *fh = (gpointer)fi->fh; + xdp_fh_free (fh); + fuse_reply_err (req, 0); +} + +static void +xdp_fuse_rename (fuse_req_t req, + fuse_ino_t parent, + const char *name, + fuse_ino_t newparent, + const char *newname) +{ + XdpInodeClass parent_class = get_class (parent); + g_autoptr (GVariant) doc = NULL; + int res; + fuse_ino_t inode; + struct stat stbuf = {0}; + g_autofree char *basename = NULL; + XdpTmp *other_tmp, *tmp; + GList *l; + + g_debug ("xdp_fuse_rename %lx/%s -> %lx/%s", parent, name, newparent, newname); + + res = xdp_lookup (parent, name, &inode, &stbuf, &doc, &tmp); + if (res != 0) + { + fuse_reply_err (req, res); + return; + } + + /* Only allow renames in (app) doc dirs, and only inside the same dir */ + if ((parent_class != DOC_DIR_INO_CLASS && + parent_class != APP_DOC_DIR_INO_CLASS) || + parent != newparent || + doc == NULL || + /* Also, don't allow renaming non-tmpfiles */ + tmp == NULL) + { + fuse_reply_err (req, EACCES); + return; + } + + basename = xdp_doc_dup_basename (doc); + + if (strcmp (newname, basename) == 0) + { + g_autofree char *real_path = xdp_doc_dup_path (doc); + /* Rename tmpfile to regular file */ + + /* Stop writes to all outstanding fds to the temp file */ + for (l = open_files; l != NULL; l = l->next) + { + XdpFh *fh = l->data; + if (fh->tmp_id == tmp->tmp_id && fh->fd >= 0) + fh->readonly = TRUE; + } + + if (rename (tmp->backing_path, real_path) != 0) + { + fuse_reply_err (req, errno); + return; + } + + /* Clear backing path so we don't unlink it when freeing tmp */ + g_clear_pointer (&tmp->backing_path, g_free); + tmpfile_free (tmp); + + fuse_reply_err (req, 0); + } + else + { + /* Rename tmpfile to other tmpfile name */ + + other_tmp = find_tmp_by_name (newparent, newname); + if (other_tmp) + tmpfile_free (other_tmp); + + g_free (tmp->name); + tmp->name = g_strdup (newname); + fuse_reply_err (req, 0); + } +} + +static int +fh_truncate (XdpFh *fh, off_t size, struct stat *newattr) +{ + int fd; + + if (fh->trunc_fd >= 0 && !fh->truncated) + { + if (size != 0) + return -EACCES; + + fh->truncated = TRUE; + fd = fh->trunc_fd; + } + else + { + fd = xdp_fh_get_fd (fh); + if (fd == -1) + return -EIO; + + if (ftruncate (fd, size) != 0) + return - errno; + } + + if (newattr) + { + int res = xdp_fstat (fh, newattr); + if (res < 0) + return res; + } + + return 0; +} + +static void +xdp_fuse_setattr (fuse_req_t req, + fuse_ino_t ino, + struct stat *attr, + int to_set, + struct fuse_file_info *fi) +{ + g_debug ("xdp_fuse_setattr %lx %x %p", ino, to_set, fi); + + if (to_set == FUSE_SET_ATTR_SIZE && fi != NULL) + { + XdpFh *fh = (gpointer)fi->fh; + int res; + struct stat newattr = {0}; + + /* ftruncate */ + + res = fh_truncate (fh, attr->st_size, &newattr); + if (res < 0) + { + fuse_reply_err (req, res); + return; + } + + fuse_reply_attr (req, &newattr, get_attr_cache_time (newattr.st_mode)); + } + else if (to_set == FUSE_SET_ATTR_SIZE && fi == NULL) + { + gboolean found = FALSE; + int res = 0; + GList *l; + struct stat newattr = {0}; + struct stat *newattrp = &newattr; + + /* truncate, truncate any open files (but EACCES if not open) */ + + for (l = open_files; l != NULL; l = l->next) + { + XdpFh *fh = l->data; + if (fh->inode == ino) + { + found = TRUE; + res = fh_truncate (fh, attr->st_size, newattrp); + newattrp = NULL; + } + } + + if (!found) + { + fuse_reply_err (req, EACCES); + return; + } + + if (res < 0) + { + fuse_reply_err (req, -res); + return; + } + + fuse_reply_attr (req, &newattr, get_attr_cache_time (newattr.st_mode)); + } + else if (to_set == FUSE_SET_ATTR_MODE) + { + gboolean found = FALSE; + int res, err = -1; + GList *l; + struct stat newattr = {0}; + + for (l = open_files; l != NULL; l = l->next) + { + XdpFh *fh = l->data; + + if (fh->inode == ino) + { + int fd = xdp_fh_get_fd (fh); + if (fd != -1) + { + res = fchmod (fd, get_user_perms (attr)); + if (!found) + { + if (res != 0) + err = -errno; + else + err = xdp_fstat (fh, &newattr); + found = TRUE; + } + } + } + } + + if (!found) + { + fuse_reply_err (req, EACCES); + return; + } + + if (err < 0) + { + fuse_reply_err (req, -err); + return; + } + + fuse_reply_attr (req, &newattr, get_attr_cache_time (newattr.st_mode)); + } + else + fuse_reply_err (req, ENOSYS); +} + +static void +xdp_fuse_fsyncdir (fuse_req_t req, + fuse_ino_t ino, + int datasync, + struct fuse_file_info *fi) +{ + XdpInodeClass class = get_class (ino); + guint64 class_ino = get_class_ino (ino); + guint32 doc_id; + + if (class == DOC_DIR_INO_CLASS || + class == APP_DOC_DIR_INO_CLASS) + { + g_autoptr (GVariant) doc = NULL; + if (class == APP_DOC_DIR_INO_CLASS) + doc_id = get_doc_id_from_app_doc_ino (class_ino); + else + doc_id = class_ino; + + doc = xdp_doc_db_lookup_doc (db, doc_id); + if (doc != NULL) + { + g_autofree char *dirname = xdp_doc_dup_dirname (doc); + int fd = open (dirname, O_DIRECTORY|O_RDONLY); + if (fd >= 0) + { + if (datasync) + fdatasync (fd); + else + fsync (fd); + close (fd); + } + } + } + + fuse_reply_err (req, 0); +} + +static void +xdp_fuse_fsync (fuse_req_t req, + fuse_ino_t ino, + int datasync, + struct fuse_file_info *fi) +{ + XdpInodeClass class = get_class (ino); + + if (class == DOC_FILE_INO_CLASS || + class == TMPFILE_INO_CLASS) + { + XdpFh *fh = (gpointer)fi->fh; + if (fh->fd >= 0) + fsync (fh->fd); + if (fh->truncated && fh->trunc_fd >= 0) + fsync (fh->trunc_fd); + } + + fuse_reply_err (req, 0); +} + +static void +xdp_fuse_unlink (fuse_req_t req, + fuse_ino_t parent, + const char *name) +{ + XdpInodeClass parent_class = get_class (parent); + g_autoptr (GVariant) doc = NULL; + int res; + fuse_ino_t inode; + struct stat stbuf = {0}; + g_autofree char *basename = NULL; + XdpTmp *tmp; + + g_debug ("xdp_fuse_unlink %lx/%s", parent, name); + + res = xdp_lookup (parent, name, &inode, &stbuf, &doc, &tmp); + if (res != 0) + { + fuse_reply_err (req, res); + return; + } + + /* Only allow unlink in (app) doc dirs */ + if ((parent_class != DOC_DIR_INO_CLASS && + parent_class != APP_DOC_DIR_INO_CLASS) || + doc == NULL) + { + fuse_reply_err (req, EACCES); + return; + } + + basename = xdp_doc_dup_basename (doc); + if (strcmp (name, basename) == 0) + { + g_autofree char *real_path = xdp_doc_dup_path (doc); + + if (unlink (real_path) != 0) + { + fuse_reply_err (req, errno); + return; + } + + fuse_reply_err (req, 0); + } + else + { + tmpfile_free (tmp); + + fuse_reply_err (req, 0); + } +} + +static struct fuse_lowlevel_ops xdp_fuse_oper = { + .lookup = xdp_fuse_lookup, + .getattr = xdp_fuse_getattr, + .opendir = xdp_fuse_opendir, + .readdir = xdp_fuse_readdir, + .releasedir = xdp_fuse_releasedir, + .fsyncdir = xdp_fuse_fsyncdir, + .open = xdp_fuse_open, + .create = xdp_fuse_create, + .read = xdp_fuse_read, + .write = xdp_fuse_write, + .write_buf = xdp_fuse_write_buf, + .release = xdp_fuse_release, + .rename = xdp_fuse_rename, + .setattr = xdp_fuse_setattr, + .fsync = xdp_fuse_fsync, + .unlink = xdp_fuse_unlink, +}; + +typedef struct +{ + GSource source; + + struct fuse_chan *ch; + gpointer fd_tag; +} FuseSource; + +static gboolean +fuse_source_dispatch (GSource *source, + GSourceFunc func, + gpointer user_data) +{ + FuseSource *fs = (FuseSource *)source; + struct fuse_chan *ch = fs->ch; + struct fuse_session *se = fuse_chan_session (ch); + gsize bufsize = fuse_chan_bufsize (ch); + + if (g_source_query_unix_fd (source, fs->fd_tag) != 0) + { + int res = 0; + char *buf = (char *) g_malloc (bufsize); + + while (TRUE) + { + struct fuse_chan *tmpch = ch; + struct fuse_buf fbuf = { + .mem = buf, + .size = bufsize, + }; + res = fuse_session_receive_buf (se, &fbuf, &tmpch); + if (res == -EINTR) + continue; + if (res <= 0) + break; + + fuse_session_process_buf (se, &fbuf, tmpch); + } + g_free (buf); + } + + return TRUE; +} + +static GSource * +fuse_source_new (struct fuse_chan *ch) +{ + static GSourceFuncs source_funcs = { + NULL, NULL, + fuse_source_dispatch + /* should have a finalize, but it will never happen */ + }; + FuseSource *fs; + GSource *source; + GError *error = NULL; + int fd; + + source = g_source_new (&source_funcs, sizeof (FuseSource)); + fs = (FuseSource *) source; + fs->ch = ch; + + g_source_set_name (source, "fuse source"); + + fd = fuse_chan_fd(ch); + g_unix_set_fd_nonblocking (fd, TRUE, &error); + g_assert_no_error (error); + + fs->fd_tag = g_source_add_unix_fd (source, fd, G_IO_IN); + + return source; +} + +static struct fuse_session *session = NULL; +static struct fuse_chan *main_ch = NULL; +static char *mount_path = NULL; + +void +xdp_fuse_exit (void) +{ + if (session) + fuse_session_reset (session); + if (main_ch) + fuse_session_remove_chan (main_ch); + if (session) + fuse_session_destroy (session); + if (main_ch) + fuse_unmount (mount_path, main_ch); +} + +gboolean +xdp_fuse_init (XdpDocDb *_db, + GError **error) +{ + char *argv[] = { "xdp-fuse", "-osplice_write,splice_move,splice_read" }; + struct fuse_args args = FUSE_ARGS_INIT(G_N_ELEMENTS(argv), argv); + struct fuse_chan *ch; + GSource *source; + + db = _db; + app_name_to_id = + g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + app_id_to_name = + g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, NULL); + next_app_id = IN_HOMEDIR_APP_ID + 1; + next_tmp_id = 1; + + mount_path = g_build_filename (g_get_user_runtime_dir(), "doc", NULL); + if (g_mkdir_with_parents (mount_path, 0700)) + { + g_set_error (error, XDP_ERROR, XDP_ERROR_FAILED, + "Unable to create dir %s\n", mount_path); + return FALSE; + } + + main_ch = ch = fuse_mount (mount_path, &args); + if (ch == NULL) + { + g_set_error (error, XDP_ERROR, XDP_ERROR_FAILED, "Can't mount fuse fs"); + return FALSE; + } + + session = fuse_lowlevel_new (&args, &xdp_fuse_oper, + sizeof (xdp_fuse_oper), NULL); + if (session == NULL) + { + g_set_error (error, XDP_ERROR, XDP_ERROR_FAILED, + "Can't create fuse session"); + return FALSE; + } + + fuse_session_add_chan (session, ch); + + source = fuse_source_new (ch); + g_source_attach (source, NULL); + + return TRUE; +} |