diff --git a/.gitignore b/.gitignore index 65f36d4b..e7ab0e90 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ stamp-* xdg-app xdg-app-helper xdg-app-session-helper +xdg-document-portal doc/*.1 *~ profile/xdg-app.sh @@ -37,4 +38,8 @@ xdg-dbus-proxy *.service xdg-app.env xdg-app.sh +document-portal/xdp-dbus.c +document-portal/xdp-dbus.h +document-portal/xdp-resources.c +.dirstamp diff --git a/Makefile.am b/Makefile.am index 0c235a7d..c23cff8c 100644 --- a/Makefile.am +++ b/Makefile.am @@ -3,11 +3,20 @@ NULL = bin_PROGRAMS = $(NULL) libexec_PROGRAMS = $(NULL) DISTCLEANFILES= $(NULL) +BUILT_SOURCES = $(NULL) if BUILD_DOCUMENTATION SUBDIRS = doc endif +# D-BUS service file +%.service: %.service.in config.log + $(AM_V_GEN) $(SED) -e "s|\@libexecdir\@|$(libexecdir)|" $< > $@ + +servicedir = $(DBUS_SERVICE_DIR) +service_in_files = $(NULL) +service_DATA = $(NULL) + AM_CPPFLAGS = \ -DXDG_APP_BINDIR=\"$(bindir)\" \ -DXDG_APP_SYSTEMDIR=\"$(localstatedir)/xdg-app\"\ @@ -40,6 +49,7 @@ include lib/Makefile.am.inc include app/Makefile.am.inc include session-helper/Makefile.am.inc include dbus-proxy/Makefile.am.inc +include document-portal/Makefile.am.inc completiondir = $(datadir)/bash-completion/completions completion_DATA = completion/xdg-app diff --git a/configure.ac b/configure.ac index 4c4e4edf..2c2be124 100644 --- a/configure.ac +++ b/configure.ac @@ -63,6 +63,9 @@ AC_SUBST(LIBSECCOMP_LIBS) PKG_CHECK_MODULES(OSTREE, [libgsystem >= 2015.1 ostree-1 >= 2015.3]) AC_SUBST(OSTREE_CFLAGS) AC_SUBST(OSTREE_LIBS) +PKG_CHECK_MODULES(FUSE, [fuse]) +AC_SUBST(FUSE_CFLAGS) +AC_SUBST(FUSE_LIBS) AC_ARG_ENABLE([userns], AC_HELP_STRING([--disable-userns], diff --git a/data/org.freedesktop.portal.documents.xml b/data/org.freedesktop.portal.documents.xml new file mode 100644 index 00000000..3acf79eb --- /dev/null +++ b/data/org.freedesktop.portal.documents.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/document-portal/Makefile.am.inc b/document-portal/Makefile.am.inc new file mode 100644 index 00000000..9b470ca6 --- /dev/null +++ b/document-portal/Makefile.am.inc @@ -0,0 +1,49 @@ +libexec_PROGRAMS += \ + xdg-document-portal \ + $(NULL) + +xdp_dbus_built_sources = document-portal/xdp-dbus.c document-portal/xdp-dbus.h +BUILT_SOURCES += $(xdp_dbus_built_sources) + +$(xdp_dbus_built_sources) : data/org.freedesktop.portal.documents.xml + $(AM_V_GEN) $(GDBUS_CODEGEN) \ + --interface-prefix org.freedesktop.portal. \ + --c-namespace XdpDbus \ + --generate-c-code $(builddir)/document-portal/xdp-dbus \ + $(srcdir)/data/org.freedesktop.portal.documents.xml \ + $(NULL) + +xdp_resource_files = $(shell $(GLIB_COMPILE_RESOURCES) --sourcedir=$(srcdir) --generate-dependencies $(srcdir)/document-portal/xdg-document-portal.gresource.xml) + +document-portal/xdp-resources.h: document-portal/xdg-document-portal.gresource.xml + $(AM_V_GEN) $(GLIB_COMPILE_RESOURCES) $< \ + --target=$@ --sourcedir=$(srcdir) --c-name _xdg_document --generate-header + +document-portal/xdp-resources.c: document-portal/xdg-document-portal.gresource.xml $(xdp_resource_files) + $(AM_V_GEN) $(GLIB_COMPILE_RESOURCES) $< \ + --target=$@ --sourcedir=$(srcdir) --c-name _xdg_document --generate-source + +service_in_files += document-portal/org.freedesktop.portal.Documents.service.in +service_DATA += document-portal/org.freedesktop.portal.Documents.service +DISTCLEANFILES += document-portal/org.freedesktop.portal.Documents.service + +xdg_document_portal_SOURCES = \ + document-portal/xdp-main.c \ + document-portal/xdp-error.c \ + document-portal/xdp-error.h \ + document-portal/xdp-enums.h \ + document-portal/xdp-doc-db.h \ + document-portal/xdp-doc-db.c \ + document-portal/gvdb/gvdb-reader.c \ + document-portal/gvdb/gvdb-builder.c \ + $(xdp_dbus_built_sources) \ + document-portal/xdp-resources.h \ + document-portal/xdp-resources.c \ + document-portal/xdp-util.h \ + document-portal/xdp-util.c \ + document-portal/xdp-fuse.h \ + document-portal/xdp-fuse.c \ + $(NULL) + +xdg_document_portal_LDADD = $(BASE_LIBS) $(FUSE_LIBS) +xdg_document_portal_CFLAGS = $(BASE_CFLAGS) $(FUSE_CFLAGS) -I$(src)/document-portal diff --git a/document-portal/gvdb/.gitignore b/document-portal/gvdb/.gitignore new file mode 100644 index 00000000..8b5dee6a --- /dev/null +++ b/document-portal/gvdb/.gitignore @@ -0,0 +1,2 @@ +libgvdb.a +libgvdb-shared.a diff --git a/document-portal/gvdb/README b/document-portal/gvdb/README new file mode 100644 index 00000000..94e6c5d1 --- /dev/null +++ b/document-portal/gvdb/README @@ -0,0 +1,7 @@ +DO NOT MODIFY ANY FILE IN THIS DIRECTORY + +(except maybe the Makefile.am) + +This directory is the result of a git subtree merge with the 'gvdb' +module on git.gnome.org. Please apply fixes to the 'gvdb' module and +perform a git merge. diff --git a/document-portal/gvdb/gvdb-builder.c b/document-portal/gvdb/gvdb-builder.c new file mode 100644 index 00000000..90ea50b7 --- /dev/null +++ b/document-portal/gvdb/gvdb-builder.c @@ -0,0 +1,521 @@ +/* + * Copyright © 2010 Codethink Limited + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the licence, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + * + * Author: Ryan Lortie + */ + +#include "gvdb-builder.h" +#include "gvdb-format.h" + +#include +#include +#if !defined(G_OS_WIN32) || !defined(_MSC_VER) +#include +#endif +#include + + +struct _GvdbItem +{ + gchar *key; + guint32 hash_value; + guint32_le assigned_index; + GvdbItem *parent; + GvdbItem *sibling; + GvdbItem *next; + + /* one of: + * this: + */ + GVariant *value; + + /* this: */ + GHashTable *table; + + /* or this: */ + GvdbItem *child; +}; + +static void +gvdb_item_free (gpointer data) +{ + GvdbItem *item = data; + + g_free (item->key); + + if (item->value) + g_variant_unref (item->value); + + if (item->table) + g_hash_table_unref (item->table); + + g_slice_free (GvdbItem, item); +} + +GHashTable * +gvdb_hash_table_new (GHashTable *parent, + const gchar *name_in_parent) +{ + GHashTable *table; + + table = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, gvdb_item_free); + + if (parent) + { + GvdbItem *item; + + item = gvdb_hash_table_insert (parent, name_in_parent); + gvdb_item_set_hash_table (item, table); + } + + return table; +} + +static guint32 +djb_hash (const gchar *key) +{ + guint32 hash_value = 5381; + + while (*key) + hash_value = hash_value * 33 + *(signed char *)key++; + + return hash_value; +} + +GvdbItem * +gvdb_hash_table_insert (GHashTable *table, + const gchar *key) +{ + GvdbItem *item; + + item = g_slice_new0 (GvdbItem); + item->key = g_strdup (key); + item->hash_value = djb_hash (key); + + g_hash_table_insert (table, g_strdup (key), item); + + return item; +} + +void +gvdb_hash_table_insert_string (GHashTable *table, + const gchar *key, + const gchar *value) +{ + GvdbItem *item; + + item = gvdb_hash_table_insert (table, key); + gvdb_item_set_value (item, g_variant_new_string (value)); +} + +void +gvdb_item_set_value (GvdbItem *item, + GVariant *value) +{ + g_return_if_fail (!item->value && !item->table && !item->child); + + item->value = g_variant_ref_sink (value); +} + +void +gvdb_item_set_hash_table (GvdbItem *item, + GHashTable *table) +{ + g_return_if_fail (!item->value && !item->table && !item->child); + + item->table = g_hash_table_ref (table); +} + +void +gvdb_item_set_parent (GvdbItem *item, + GvdbItem *parent) +{ + GvdbItem **node; + + g_return_if_fail (g_str_has_prefix (item->key, parent->key)); + g_return_if_fail (!parent->value && !parent->table); + g_return_if_fail (!item->parent && !item->sibling); + + for (node = &parent->child; *node; node = &(*node)->sibling) + if (strcmp ((*node)->key, item->key) > 0) + break; + + item->parent = parent; + item->sibling = *node; + *node = item; +} + +typedef struct +{ + GvdbItem **buckets; + gint n_buckets; +} HashTable; + +static HashTable * +hash_table_new (gint n_buckets) +{ + HashTable *table; + + table = g_slice_new (HashTable); + table->buckets = g_new0 (GvdbItem *, n_buckets); + table->n_buckets = n_buckets; + + return table; +} + +static void +hash_table_free (HashTable *table) +{ + g_free (table->buckets); + + g_slice_free (HashTable, table); +} + +static void +hash_table_insert (gpointer key, + gpointer value, + gpointer data) +{ + guint32 hash_value, bucket; + HashTable *table = data; + GvdbItem *item = value; + + hash_value = djb_hash (key); + bucket = hash_value % table->n_buckets; + item->next = table->buckets[bucket]; + table->buckets[bucket] = item; +} + +static guint32_le +item_to_index (GvdbItem *item) +{ + if (item != NULL) + return item->assigned_index; + + return guint32_to_le (-1u); +} + +typedef struct +{ + GQueue *chunks; + guint64 offset; + gboolean byteswap; +} FileBuilder; + +typedef struct +{ + gsize offset; + gsize size; + gpointer data; +} FileChunk; + +static gpointer +file_builder_allocate (FileBuilder *fb, + guint alignment, + gsize size, + struct gvdb_pointer *pointer) +{ + FileChunk *chunk; + + if (size == 0) + return NULL; + + fb->offset += (-fb->offset) & (alignment - 1); + chunk = g_slice_new (FileChunk); + chunk->offset = fb->offset; + chunk->size = size; + chunk->data = g_malloc (size); + + pointer->start = guint32_to_le (fb->offset); + fb->offset += size; + pointer->end = guint32_to_le (fb->offset); + + g_queue_push_tail (fb->chunks, chunk); + + return chunk->data; +} + +static void +file_builder_add_value (FileBuilder *fb, + GVariant *value, + struct gvdb_pointer *pointer) +{ + GVariant *variant, *normal; + gpointer data; + gsize size; + + if (fb->byteswap) + { + value = g_variant_byteswap (value); + variant = g_variant_new_variant (value); + g_variant_unref (value); + } + else + variant = g_variant_new_variant (value); + + normal = g_variant_get_normal_form (variant); + g_variant_unref (variant); + + size = g_variant_get_size (normal); + data = file_builder_allocate (fb, 8, size, pointer); + g_variant_store (normal, data); + g_variant_unref (normal); +} + +static void +file_builder_add_string (FileBuilder *fb, + const gchar *string, + guint32_le *start, + guint16_le *size) +{ + FileChunk *chunk; + gsize length; + + length = strlen (string); + + chunk = g_slice_new (FileChunk); + chunk->offset = fb->offset; + chunk->size = length; + chunk->data = g_malloc (length); + memcpy (chunk->data, string, length); + + *start = guint32_to_le (fb->offset); + *size = guint16_to_le (length); + fb->offset += length; + + g_queue_push_tail (fb->chunks, chunk); +} + +static void +file_builder_allocate_for_hash (FileBuilder *fb, + gsize n_buckets, + gsize n_items, + guint bloom_shift, + gsize n_bloom_words, + guint32_le **bloom_filter, + guint32_le **hash_buckets, + struct gvdb_hash_item **hash_items, + struct gvdb_pointer *pointer) +{ + guint32_le bloom_hdr, table_hdr; + guchar *data; + gsize size; + + g_assert (n_bloom_words < (1u << 27)); + + bloom_hdr = guint32_to_le (bloom_shift << 27 | n_bloom_words); + table_hdr = guint32_to_le (n_buckets); + + size = sizeof bloom_hdr + sizeof table_hdr + + n_bloom_words * sizeof (guint32_le) + + n_buckets * sizeof (guint32_le) + + n_items * sizeof (struct gvdb_hash_item); + + data = file_builder_allocate (fb, 4, size, pointer); + +#define chunk(s) (size -= (s), data += (s), data - (s)) + memcpy (chunk (sizeof bloom_hdr), &bloom_hdr, sizeof bloom_hdr); + memcpy (chunk (sizeof table_hdr), &table_hdr, sizeof table_hdr); + *bloom_filter = (guint32_le *) chunk (n_bloom_words * sizeof (guint32_le)); + *hash_buckets = (guint32_le *) chunk (n_buckets * sizeof (guint32_le)); + *hash_items = (struct gvdb_hash_item *) chunk (n_items * + sizeof (struct gvdb_hash_item)); + g_assert (size == 0); +#undef chunk + + memset (*bloom_filter, 0, n_bloom_words * sizeof (guint32_le)); + + /* NOTE - the code to actually fill in the bloom filter here is missing. + * Patches welcome! + * + * http://en.wikipedia.org/wiki/Bloom_filter + * http://0pointer.de/blog/projects/bloom.html + */ +} + +static void +file_builder_add_hash (FileBuilder *fb, + GHashTable *table, + struct gvdb_pointer *pointer) +{ + guint32_le *buckets, *bloom_filter; + struct gvdb_hash_item *items; + HashTable *mytable; + GvdbItem *item; + guint32 index; + gint bucket; + + mytable = hash_table_new (g_hash_table_size (table)); + g_hash_table_foreach (table, hash_table_insert, mytable); + index = 0; + + for (bucket = 0; bucket < mytable->n_buckets; bucket++) + for (item = mytable->buckets[bucket]; item; item = item->next) + item->assigned_index = guint32_to_le (index++); + + file_builder_allocate_for_hash (fb, mytable->n_buckets, index, 5, 0, + &bloom_filter, &buckets, &items, pointer); + + index = 0; + for (bucket = 0; bucket < mytable->n_buckets; bucket++) + { + buckets[bucket] = guint32_to_le (index); + + for (item = mytable->buckets[bucket]; item; item = item->next) + { + struct gvdb_hash_item *entry = items++; + const gchar *basename; + + g_assert (index == guint32_from_le (item->assigned_index)); + entry->hash_value = guint32_to_le (item->hash_value); + entry->parent = item_to_index (item->parent); + entry->unused = 0; + + if (item->parent != NULL) + basename = item->key + strlen (item->parent->key); + else + basename = item->key; + + file_builder_add_string (fb, basename, + &entry->key_start, + &entry->key_size); + + if (item->value != NULL) + { + g_assert (item->child == NULL && item->table == NULL); + + file_builder_add_value (fb, item->value, &entry->value.pointer); + entry->type = 'v'; + } + + if (item->child != NULL) + { + guint32 children = 0, i = 0; + guint32_le *offsets; + GvdbItem *child; + + g_assert (item->table == NULL); + + for (child = item->child; child; child = child->sibling) + children++; + + offsets = file_builder_allocate (fb, 4, 4 * children, + &entry->value.pointer); + entry->type = 'L'; + + for (child = item->child; child; child = child->sibling) + offsets[i++] = child->assigned_index; + + g_assert (children == i); + } + + if (item->table != NULL) + { + entry->type = 'H'; + file_builder_add_hash (fb, item->table, &entry->value.pointer); + } + + index++; + } + } + + hash_table_free (mytable); +} + +static FileBuilder * +file_builder_new (gboolean byteswap) +{ + FileBuilder *builder; + + builder = g_slice_new (FileBuilder); + builder->chunks = g_queue_new (); + builder->offset = sizeof (struct gvdb_header); + builder->byteswap = byteswap; + + return builder; +} + +static GString * +file_builder_serialise (FileBuilder *fb, + struct gvdb_pointer root) +{ + struct gvdb_header header = { { 0, }, }; + GString *result; + + if (fb->byteswap) + { + header.signature[0] = GVDB_SWAPPED_SIGNATURE0; + header.signature[1] = GVDB_SWAPPED_SIGNATURE1; + } + else + { + header.signature[0] = GVDB_SIGNATURE0; + header.signature[1] = GVDB_SIGNATURE1; + } + + result = g_string_new (NULL); + + header.root = root; + g_string_append_len (result, (gpointer) &header, sizeof header); + + while (!g_queue_is_empty (fb->chunks)) + { + FileChunk *chunk = g_queue_pop_head (fb->chunks); + + if (result->len != chunk->offset) + { + gchar zero[8] = { 0, }; + + g_assert (chunk->offset > result->len); + g_assert (chunk->offset - result->len < 8); + + g_string_append_len (result, zero, chunk->offset - result->len); + g_assert (result->len == chunk->offset); + } + + g_string_append_len (result, chunk->data, chunk->size); + g_free (chunk->data); + + g_slice_free (FileChunk, chunk); + } + + g_queue_free (fb->chunks); + g_slice_free (FileBuilder, fb); + + return result; +} + +gboolean +gvdb_table_write_contents (GHashTable *table, + const gchar *filename, + gboolean byteswap, + GError **error) +{ + struct gvdb_pointer root; + gboolean status; + FileBuilder *fb; + GString *str; + + fb = file_builder_new (byteswap); + file_builder_add_hash (fb, table, &root); + str = file_builder_serialise (fb, root); + + status = g_file_set_contents (filename, str->str, str->len, error); + g_string_free (str, TRUE); + + return status; +} diff --git a/document-portal/gvdb/gvdb-builder.h b/document-portal/gvdb/gvdb-builder.h new file mode 100644 index 00000000..8ec05c8b --- /dev/null +++ b/document-portal/gvdb/gvdb-builder.h @@ -0,0 +1,55 @@ +/* + * Copyright © 2010 Codethink Limited + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the licence, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + * + * Author: Ryan Lortie + */ + +#ifndef __gvdb_builder_h__ +#define __gvdb_builder_h__ + +#include + +typedef struct _GvdbItem GvdbItem; + +G_GNUC_INTERNAL +GHashTable * gvdb_hash_table_new (GHashTable *parent, + const gchar *key); + +G_GNUC_INTERNAL +GvdbItem * gvdb_hash_table_insert (GHashTable *table, + const gchar *key); +G_GNUC_INTERNAL +void gvdb_hash_table_insert_string (GHashTable *table, + const gchar *key, + const gchar *value); + +G_GNUC_INTERNAL +void gvdb_item_set_value (GvdbItem *item, + GVariant *value); +G_GNUC_INTERNAL +void gvdb_item_set_hash_table (GvdbItem *item, + GHashTable *table); +G_GNUC_INTERNAL +void gvdb_item_set_parent (GvdbItem *item, + GvdbItem *parent); + +G_GNUC_INTERNAL +gboolean gvdb_table_write_contents (GHashTable *table, + const gchar *filename, + gboolean byteswap, + GError **error); + +#endif /* __gvdb_builder_h__ */ diff --git a/document-portal/gvdb/gvdb-format.h b/document-portal/gvdb/gvdb-format.h new file mode 100644 index 00000000..486e8547 --- /dev/null +++ b/document-portal/gvdb/gvdb-format.h @@ -0,0 +1,85 @@ +/* + * Copyright © 2010 Codethink Limited + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the licence, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + * + * Author: Ryan Lortie + */ + +#ifndef __gvdb_format_h__ +#define __gvdb_format_h__ + +#include + +typedef struct { guint16 value; } guint16_le; +typedef struct { guint32 value; } guint32_le; + +struct gvdb_pointer { + guint32_le start; + guint32_le end; +}; + +struct gvdb_hash_header { + guint32_le n_bloom_words; + guint32_le n_buckets; +}; + +struct gvdb_hash_item { + guint32_le hash_value; + guint32_le parent; + + guint32_le key_start; + guint16_le key_size; + gchar type; + gchar unused; + + union + { + struct gvdb_pointer pointer; + gchar direct[8]; + } value; +}; + +struct gvdb_header { + guint32 signature[2]; + guint32_le version; + guint32_le options; + + struct gvdb_pointer root; +}; + +static inline guint32_le guint32_to_le (guint32 value) { + guint32_le result = { GUINT32_TO_LE (value) }; + return result; +} + +static inline guint32 guint32_from_le (guint32_le value) { + return GUINT32_FROM_LE (value.value); +} + +static inline guint16_le guint16_to_le (guint16 value) { + guint16_le result = { GUINT16_TO_LE (value) }; + return result; +} + +static inline guint16 guint16_from_le (guint16_le value) { + return GUINT16_FROM_LE (value.value); +} + +#define GVDB_SIGNATURE0 1918981703 +#define GVDB_SIGNATURE1 1953390953 +#define GVDB_SWAPPED_SIGNATURE0 GUINT32_SWAP_LE_BE (GVDB_SIGNATURE0) +#define GVDB_SWAPPED_SIGNATURE1 GUINT32_SWAP_LE_BE (GVDB_SIGNATURE1) + +#endif /* __gvdb_format_h__ */ diff --git a/document-portal/gvdb/gvdb-reader.c b/document-portal/gvdb/gvdb-reader.c new file mode 100644 index 00000000..08b5bc8c --- /dev/null +++ b/document-portal/gvdb/gvdb-reader.c @@ -0,0 +1,718 @@ +/* + * Copyright © 2010 Codethink Limited + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the licence, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + * + * Author: Ryan Lortie + */ + +#include "gvdb-reader.h" +#include "gvdb-format.h" + +#include + +struct _GvdbTable { + GBytes *bytes; + + const gchar *data; + gsize size; + + gboolean byteswapped; + gboolean trusted; + + const guint32_le *bloom_words; + guint32 n_bloom_words; + guint bloom_shift; + + const guint32_le *hash_buckets; + guint32 n_buckets; + + struct gvdb_hash_item *hash_items; + guint32 n_hash_items; +}; + +static const gchar * +gvdb_table_item_get_key (GvdbTable *file, + const struct gvdb_hash_item *item, + gsize *size) +{ + guint32 start, end; + + start = guint32_from_le (item->key_start); + *size = guint16_from_le (item->key_size); + end = start + *size; + + if G_UNLIKELY (start > end || end > file->size) + return NULL; + + return file->data + start; +} + +static gconstpointer +gvdb_table_dereference (GvdbTable *file, + const struct gvdb_pointer *pointer, + gint alignment, + gsize *size) +{ + guint32 start, end; + + start = guint32_from_le (pointer->start); + end = guint32_from_le (pointer->end); + + if G_UNLIKELY (start > end || end > file->size || start & (alignment - 1)) + return NULL; + + *size = end - start; + + return file->data + start; +} + +static void +gvdb_table_setup_root (GvdbTable *file, + const struct gvdb_pointer *pointer) +{ + const struct gvdb_hash_header *header; + guint32 n_bloom_words; + guint32 n_buckets; + gsize size; + + header = gvdb_table_dereference (file, pointer, 4, &size); + + if G_UNLIKELY (header == NULL || size < sizeof *header) + return; + + size -= sizeof *header; + + n_bloom_words = guint32_from_le (header->n_bloom_words); + n_buckets = guint32_from_le (header->n_buckets); + n_bloom_words &= (1u << 27) - 1; + + if G_UNLIKELY (n_bloom_words * sizeof (guint32_le) > size) + return; + + file->bloom_words = (gpointer) (header + 1); + size -= n_bloom_words * sizeof (guint32_le); + file->n_bloom_words = n_bloom_words; + + if G_UNLIKELY (n_buckets > G_MAXUINT / sizeof (guint32_le) || + n_buckets * sizeof (guint32_le) > size) + return; + + file->hash_buckets = file->bloom_words + file->n_bloom_words; + size -= n_buckets * sizeof (guint32_le); + file->n_buckets = n_buckets; + + if G_UNLIKELY (size % sizeof (struct gvdb_hash_item)) + return; + + file->hash_items = (gpointer) (file->hash_buckets + n_buckets); + file->n_hash_items = size / sizeof (struct gvdb_hash_item); +} + +/** + * gvdb_table_new_from_bytes: + * @bytes: the #GBytes with the data + * @trusted: if the contents of @bytes are trusted + * @error: %NULL, or a pointer to a %NULL #GError + * @returns: a new #GvdbTable + * + * Creates a new #GvdbTable from the contents of @bytes. + * + * This call can fail if the header contained in @bytes is invalid. + * + * You should call gvdb_table_free() on the return result when you no + * longer require it. + **/ +GvdbTable * +gvdb_table_new_from_bytes (GBytes *bytes, + gboolean trusted, + GError **error) +{ + const struct gvdb_header *header; + GvdbTable *file; + + file = g_slice_new0 (GvdbTable); + file->bytes = g_bytes_ref (bytes); + file->data = g_bytes_get_data (bytes, &file->size); + file->trusted = trusted; + + if (file->size < sizeof (struct gvdb_header)) + goto invalid; + + header = (gpointer) file->data; + + if (header->signature[0] == GVDB_SIGNATURE0 && + header->signature[1] == GVDB_SIGNATURE1 && + guint32_from_le (header->version) == 0) + file->byteswapped = FALSE; + + else if (header->signature[0] == GVDB_SWAPPED_SIGNATURE0 && + header->signature[1] == GVDB_SWAPPED_SIGNATURE1 && + guint32_from_le (header->version) == 0) + file->byteswapped = TRUE; + + else + goto invalid; + + gvdb_table_setup_root (file, &header->root); + + return file; + +invalid: + g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_INVAL, "invalid gvdb header"); + + g_bytes_unref (file->bytes); + + g_slice_free (GvdbTable, file); + + return NULL; +} + +/** + * gvdb_table_new: + * @filename: a filename + * @trusted: if the contents of @bytes are trusted + * @error: %NULL, or a pointer to a %NULL #GError + * @returns: a new #GvdbTable + * + * Creates a new #GvdbTable using the #GMappedFile for @filename as the + * #GBytes. + **/ +GvdbTable * +gvdb_table_new (const gchar *filename, + gboolean trusted, + GError **error) +{ + GMappedFile *mapped; + GvdbTable *table; + GBytes *bytes; + + mapped = g_mapped_file_new (filename, FALSE, error); + if (!mapped) + return NULL; + + bytes = g_mapped_file_get_bytes (mapped); + table = gvdb_table_new_from_bytes (bytes, trusted, error); + g_mapped_file_unref (mapped); + g_bytes_unref (bytes); + + g_prefix_error (error, "%s: ", filename); + + return table; +} + +static gboolean +gvdb_table_bloom_filter (GvdbTable *file, + guint32 hash_value) +{ + guint32 word, mask; + + if (file->n_bloom_words == 0) + return TRUE; + + word = (hash_value / 32) % file->n_bloom_words; + mask = 1 << (hash_value & 31); + mask |= 1 << ((hash_value >> file->bloom_shift) & 31); + + return (guint32_from_le (file->bloom_words[word]) & mask) == mask; +} + +static gboolean +gvdb_table_check_name (GvdbTable *file, + struct gvdb_hash_item *item, + const gchar *key, + guint key_length) +{ + const gchar *this_key; + gsize this_size; + guint32 parent; + + this_key = gvdb_table_item_get_key (file, item, &this_size); + + if G_UNLIKELY (this_key == NULL || this_size > key_length) + return FALSE; + + key_length -= this_size; + + if G_UNLIKELY (memcmp (this_key, key + key_length, this_size) != 0) + return FALSE; + + parent = guint32_from_le (item->parent); + if (key_length == 0 && parent == 0xffffffffu) + return TRUE; + + if G_LIKELY (parent < file->n_hash_items && this_size > 0) + return gvdb_table_check_name (file, + &file->hash_items[parent], + key, key_length); + + return FALSE; +} + +static const struct gvdb_hash_item * +gvdb_table_lookup (GvdbTable *file, + const gchar *key, + gchar type) +{ + guint32 hash_value = 5381; + guint key_length; + guint32 bucket; + guint32 lastno; + guint32 itemno; + + if G_UNLIKELY (file->n_buckets == 0 || file->n_hash_items == 0) + return NULL; + + for (key_length = 0; key[key_length]; key_length++) + hash_value = (hash_value * 33) + ((signed char *) key)[key_length]; + + if (!gvdb_table_bloom_filter (file, hash_value)) + return NULL; + + bucket = hash_value % file->n_buckets; + itemno = guint32_from_le (file->hash_buckets[bucket]); + + if (bucket == file->n_buckets - 1 || + (lastno = guint32_from_le(file->hash_buckets[bucket + 1])) > file->n_hash_items) + lastno = file->n_hash_items; + + while G_LIKELY (itemno < lastno) + { + struct gvdb_hash_item *item = &file->hash_items[itemno]; + + if (hash_value == guint32_from_le (item->hash_value)) + if G_LIKELY (gvdb_table_check_name (file, item, key, key_length)) + if G_LIKELY (item->type == type) + return item; + + itemno++; + } + + return NULL; +} + +static gboolean +gvdb_table_list_from_item (GvdbTable *table, + const struct gvdb_hash_item *item, + const guint32_le **list, + guint *length) +{ + gsize size; + + *list = gvdb_table_dereference (table, &item->value.pointer, 4, &size); + + if G_LIKELY (*list == NULL || size % 4) + return FALSE; + + *length = size / 4; + + return TRUE; +} + +/** + * gvdb_table_get_names: + * @table: a #GvdbTable + * @length: the number of items returned, or %NULL + * + * Gets a list of all names contained in @table. + * + * No call to gvdb_table_get_table(), gvdb_table_list() or + * gvdb_table_get_value() will succeed unless it is for one of the + * names returned by this function. + * + * Note that some names that are returned may still fail for all of the + * above calls in the case of the corrupted file. Note also that the + * returned strings may not be utf8. + * + * Returns: a %NULL-terminated list of strings, of length @length + **/ +gchar ** +gvdb_table_get_names (GvdbTable *table, + gint *length) +{ + gchar **names; + gint n_names; + gint filled; + gint total; + gint i; + + /* We generally proceed by iterating over the list of items in the + * hash table (in order of appearance) recording them into an array. + * + * Each item has a parent item (except root items). The parent item + * forms part of the name of the item. We could go fetching the + * parent item chain at the point that we encounter each item but then + * we would need to implement some sort of recursion along with checks + * for self-referential items. + * + * Instead, we do a number of passes. Each pass will build up one + * level of names (starting from the root). We continue to do passes + * until no more items are left. The first pass will only add root + * items and each further pass will only add items whose direct parent + * is an item added in the immediately previous pass. It's also + * possible that items get filled if they follow their parent within a + * particular pass. + * + * At most we will have a number of passes equal to the depth of the + * tree. Self-referential items will never be filled in (since their + * parent will have never been filled in). We continue until we have + * a pass that fills in no additional items. + * + * This takes an O(n) algorithm and turns it into O(n*m) where m is + * the depth of the tree, but in all sane cases the tree won't be very + * deep and the constant factor of this algorithm is lower (and the + * complexity of coding it, as well). + */ + + n_names = table->n_hash_items; + names = g_new0 (gchar *, n_names + 1); + + /* 'names' starts out all-NULL. On each pass we record the number + * of items changed from NULL to non-NULL in 'filled' so we know if we + * should repeat the loop. 'total' counts the total number of items + * filled. If 'total' ends up equal to 'n_names' then we know that + * 'names' has been completely filled. + */ + + total = 0; + do + { + /* Loop until we have filled no more entries */ + filled = 0; + + for (i = 0; i < n_names; i++) + { + const struct gvdb_hash_item *item = &table->hash_items[i]; + const gchar *name; + gsize name_length; + guint32 parent; + + /* already got it on a previous pass */ + if (names[i] != NULL) + continue; + + parent = guint32_from_le (item->parent); + + if (parent == 0xffffffffu) + { + /* it's a root item */ + name = gvdb_table_item_get_key (table, item, &name_length); + + if (name != NULL) + { + names[i] = g_strndup (name, name_length); + filled++; + } + } + + else if (parent < n_names && names[parent] != NULL) + { + /* It's a non-root item whose parent was filled in already. + * + * Calculate the name of this item by combining it with + * its parent name. + */ + name = gvdb_table_item_get_key (table, item, &name_length); + + if (name != NULL) + { + const gchar *parent_name = names[parent]; + gsize parent_length; + gchar *fullname; + + parent_length = strlen (parent_name); + fullname = g_malloc (parent_length + name_length + 1); + memcpy (fullname, parent_name, parent_length); + memcpy (fullname + parent_length, name, name_length); + fullname[parent_length + name_length] = '\0'; + names[i] = fullname; + filled++; + } + } + } + + total += filled; + } + while (filled && total < n_names); + + /* If the table was corrupted then 'names' may have holes in it. + * Collapse those. + */ + if G_UNLIKELY (total != n_names) + { + GPtrArray *fixed_names; + + fixed_names = g_ptr_array_new (); + for (i = 0; i < n_names; i++) + if (names[i] != NULL) + g_ptr_array_add (fixed_names, names[i]); + + g_free (names); + n_names = fixed_names->len; + g_ptr_array_add (fixed_names, NULL); + names = (gchar **) g_ptr_array_free (fixed_names, FALSE); + } + + if (length) + *length = n_names; + + return names; +} + +/** + * gvdb_table_list: + * @file: a #GvdbTable + * @key: a string + * @returns: a %NULL-terminated string array + * + * List all of the keys that appear below @key. The nesting of keys + * within the hash file is defined by the program that created the hash + * file. One thing is constant: each item in the returned array can be + * concatenated to @key to obtain the full name of that key. + * + * It is not possible to tell from this function if a given key is + * itself a path, a value, or another hash table; you are expected to + * know this for yourself. + * + * You should call g_strfreev() on the return result when you no longer + * require it. + **/ +gchar ** +gvdb_table_list (GvdbTable *file, + const gchar *key) +{ + const struct gvdb_hash_item *item; + const guint32_le *list; + gchar **strv; + guint length; + guint i; + + if ((item = gvdb_table_lookup (file, key, 'L')) == NULL) + return NULL; + + if (!gvdb_table_list_from_item (file, item, &list, &length)) + return NULL; + + strv = g_new (gchar *, length + 1); + for (i = 0; i < length; i++) + { + guint32 itemno = guint32_from_le (list[i]); + + if (itemno < file->n_hash_items) + { + const struct gvdb_hash_item *item; + const gchar *string; + gsize strsize; + + item = file->hash_items + itemno; + + string = gvdb_table_item_get_key (file, item, &strsize); + + if (string != NULL) + strv[i] = g_strndup (string, strsize); + else + strv[i] = g_malloc0 (1); + } + else + strv[i] = g_malloc0 (1); + } + + strv[i] = NULL; + + return strv; +} + +/** + * gvdb_table_has_value: + * @file: a #GvdbTable + * @key: a string + * @returns: %TRUE if @key is in the table + * + * Checks for a value named @key in @file. + * + * Note: this function does not consider non-value nodes (other hash + * tables, for example). + **/ +gboolean +gvdb_table_has_value (GvdbTable *file, + const gchar *key) +{ + static const struct gvdb_hash_item *item; + gsize size; + + item = gvdb_table_lookup (file, key, 'v'); + + if (item == NULL) + return FALSE; + + return gvdb_table_dereference (file, &item->value.pointer, 8, &size) != NULL; +} + +static GVariant * +gvdb_table_value_from_item (GvdbTable *table, + const struct gvdb_hash_item *item) +{ + GVariant *variant, *value; + gconstpointer data; + GBytes *bytes; + gsize size; + + data = gvdb_table_dereference (table, &item->value.pointer, 8, &size); + + if G_UNLIKELY (data == NULL) + return NULL; + + bytes = g_bytes_new_from_bytes (table->bytes, ((gchar *) data) - table->data, size); + variant = g_variant_new_from_bytes (G_VARIANT_TYPE_VARIANT, bytes, table->trusted); + value = g_variant_get_variant (variant); + g_variant_unref (variant); + g_bytes_unref (bytes); + + return value; +} + +/** + * gvdb_table_get_value: + * @file: a #GvdbTable + * @key: a string + * @returns: a #GVariant, or %NULL + * + * Looks up a value named @key in @file. + * + * If the value is not found then %NULL is returned. Otherwise, a new + * #GVariant instance is returned. The #GVariant does not depend on the + * continued existence of @file. + * + * You should call g_variant_unref() on the return result when you no + * longer require it. + **/ +GVariant * +gvdb_table_get_value (GvdbTable *file, + const gchar *key) +{ + const struct gvdb_hash_item *item; + GVariant *value; + + if ((item = gvdb_table_lookup (file, key, 'v')) == NULL) + return NULL; + + value = gvdb_table_value_from_item (file, item); + + if (value && file->byteswapped) + { + GVariant *tmp; + + tmp = g_variant_byteswap (value); + g_variant_unref (value); + value = tmp; + } + + return value; +} + +/** + * gvdb_table_get_raw_value: + * @table: a #GvdbTable + * @key: a string + * @returns: a #GVariant, or %NULL + * + * Looks up a value named @key in @file. + * + * This call is equivalent to gvdb_table_get_value() except that it + * never byteswaps the value. + **/ +GVariant * +gvdb_table_get_raw_value (GvdbTable *table, + const gchar *key) +{ + const struct gvdb_hash_item *item; + + if ((item = gvdb_table_lookup (table, key, 'v')) == NULL) + return NULL; + + return gvdb_table_value_from_item (table, item); +} + +/** + * gvdb_table_get_table: + * @file: a #GvdbTable + * @key: a string + * @returns: a new #GvdbTable, or %NULL + * + * Looks up the hash table named @key in @file. + * + * The toplevel hash table in a #GvdbTable can contain reference to + * child hash tables (and those can contain further references...). + * + * If @key is not found in @file then %NULL is returned. Otherwise, a + * new #GvdbTable is returned, referring to the child hashtable as + * contained in the file. This newly-created #GvdbTable does not depend + * on the continued existence of @file. + * + * You should call gvdb_table_free() on the return result when you no + * longer require it. + **/ +GvdbTable * +gvdb_table_get_table (GvdbTable *file, + const gchar *key) +{ + const struct gvdb_hash_item *item; + GvdbTable *new; + + item = gvdb_table_lookup (file, key, 'H'); + + if (item == NULL) + return NULL; + + new = g_slice_new0 (GvdbTable); + new->bytes = g_bytes_ref (file->bytes); + new->byteswapped = file->byteswapped; + new->trusted = file->trusted; + new->data = file->data; + new->size = file->size; + + gvdb_table_setup_root (new, &item->value.pointer); + + return new; +} + +/** + * gvdb_table_free: + * @file: a #GvdbTable + * + * Frees @file. + **/ +void +gvdb_table_free (GvdbTable *file) +{ + g_bytes_unref (file->bytes); + g_slice_free (GvdbTable, file); +} + +/** + * gvdb_table_is_valid: + * @table: a #GvdbTable + * @returns: %TRUE if @table is still valid + * + * Checks if the table is still valid. + * + * An on-disk GVDB can be marked as invalid. This happens when the file + * has been replaced. The appropriate action is typically to reopen the + * file. + **/ +gboolean +gvdb_table_is_valid (GvdbTable *table) +{ + return !!*table->data; +} diff --git a/document-portal/gvdb/gvdb-reader.h b/document-portal/gvdb/gvdb-reader.h new file mode 100644 index 00000000..241b41ae --- /dev/null +++ b/document-portal/gvdb/gvdb-reader.h @@ -0,0 +1,63 @@ +/* + * Copyright © 2010 Codethink Limited + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the licence, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + * + * Author: Ryan Lortie + */ + +#ifndef __gvdb_reader_h__ +#define __gvdb_reader_h__ + +#include + +typedef struct _GvdbTable GvdbTable; + +G_BEGIN_DECLS + +G_GNUC_INTERNAL +GvdbTable * gvdb_table_new_from_bytes (GBytes *bytes, + gboolean trusted, + GError **error); +G_GNUC_INTERNAL +GvdbTable * gvdb_table_new (const gchar *filename, + gboolean trusted, + GError **error); +G_GNUC_INTERNAL +void gvdb_table_free (GvdbTable *table); +G_GNUC_INTERNAL +gchar ** gvdb_table_get_names (GvdbTable *table, + gint *length); +G_GNUC_INTERNAL +gchar ** gvdb_table_list (GvdbTable *table, + const gchar *key); +G_GNUC_INTERNAL +GvdbTable * gvdb_table_get_table (GvdbTable *table, + const gchar *key); +G_GNUC_INTERNAL +GVariant * gvdb_table_get_raw_value (GvdbTable *table, + const gchar *key); +G_GNUC_INTERNAL +GVariant * gvdb_table_get_value (GvdbTable *table, + const gchar *key); + +G_GNUC_INTERNAL +gboolean gvdb_table_has_value (GvdbTable *table, + const gchar *key); +G_GNUC_INTERNAL +gboolean gvdb_table_is_valid (GvdbTable *table); + +G_END_DECLS + +#endif /* __gvdb_reader_h__ */ diff --git a/document-portal/gvdb/gvdb.doap b/document-portal/gvdb/gvdb.doap new file mode 100644 index 00000000..b4ae60c8 --- /dev/null +++ b/document-portal/gvdb/gvdb.doap @@ -0,0 +1,32 @@ + + + + + gvdb + GVariant Database file + + A simple database file format that stores a mapping from strings to + GVariant values in a way that is extremely efficient for lookups. + + The database is written once and can not be modified. + + Included here is reader code and a first-pass implementation of a + writer (that does not currently produce particularly optimised + output). + + It is intended that this code be used by copy-pasting into your + project or by making use of git-merge(1). + + + + + Ryan Lortie + + ryanl + + + + diff --git a/document-portal/org.freedesktop.portal.Documents.service.in b/document-portal/org.freedesktop.portal.Documents.service.in new file mode 100644 index 00000000..7517274c --- /dev/null +++ b/document-portal/org.freedesktop.portal.Documents.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=org.freedesktop.portal.Documents +Exec=@libexecdir@/xdg-document-portal diff --git a/document-portal/xdg-document-portal.gresource.xml b/document-portal/xdg-document-portal.gresource.xml new file mode 100644 index 00000000..373d1c32 --- /dev/null +++ b/document-portal/xdg-document-portal.gresource.xml @@ -0,0 +1,6 @@ + + + + data/org.freedesktop.portal.documents.xml + + diff --git a/document-portal/xdp-doc-db.c b/document-portal/xdp-doc-db.c new file mode 100644 index 00000000..9c78d3d3 --- /dev/null +++ b/document-portal/xdp-doc-db.c @@ -0,0 +1,800 @@ +#include "config.h" + +#include +#include +#include + +#include "gvdb/gvdb-reader.h" +#include "gvdb/gvdb-builder.h" + +#include "xdp-doc-db.h" + +struct _XdpDocDb { + GObject parent; + GVariant *no_doc; + + char *filename; + GvdbTable *gvdb; + + /* Map document id => GVariant (uri, title, array[(appid, perms)]) */ + GvdbTable *doc_table; + GHashTable *doc_updates; + + /* (reverse) Map app id => [ document id ]*/ + GvdbTable *app_table; + GHashTable *app_updates; + + /* (reverse) Map uri (with no title) => [ document id ]*/ + GvdbTable *uri_table; + GHashTable *uri_updates; + + gboolean dirty; +}; + +G_DEFINE_TYPE(XdpDocDb, xdp_doc_db, G_TYPE_OBJECT) + +static GVariant * +xdp_doc_new (const char *uri, + GVariant *permissions) +{ + return g_variant_new ("(&s@a(su))", uri, permissions); +} + +char * +xdp_doc_dup_basename (GVariant *doc) +{ + g_autoptr(GFile) file = g_file_new_for_uri (xdp_doc_get_uri (doc)); + + return g_file_get_basename (file); +} + +char * +xdp_doc_dup_dirname (GVariant *doc) +{ + g_autofree char *path = xdp_doc_dup_path (doc); + + return g_path_get_dirname (path); +} + +guint32 +xdb_doc_id_from_name (const char *name) +{ + return g_ascii_strtoull (name, NULL, 16); +} + +char * +xdb_doc_name_from_id (guint32 doc_id) +{ + return g_strdup_printf ("%x", doc_id); +} + +char * +xdp_doc_dup_path (GVariant *doc) +{ + g_autoptr(GFile) file = g_file_new_for_uri (xdp_doc_get_uri (doc)); + + return g_file_get_path (file); +} + +const char * +xdp_doc_get_uri (GVariant *doc) +{ + const char *res; + + g_variant_get_child (doc, 0, "&s", &res); + return res; +} + +static void +xdp_doc_db_finalize (GObject *object) +{ + XdpDocDb *db = (XdpDocDb *)object; + + g_clear_pointer (&db->filename, g_free); + g_clear_pointer (&db->no_doc, g_variant_unref); + + g_clear_pointer (&db->gvdb, gvdb_table_free); + g_clear_pointer (&db->doc_table, gvdb_table_free); + g_clear_pointer (&db->app_table, gvdb_table_free); + g_clear_pointer (&db->uri_table, gvdb_table_free); + + g_clear_pointer (&db->doc_updates, g_hash_table_unref); + g_clear_pointer (&db->app_updates, g_hash_table_unref); + g_clear_pointer (&db->uri_updates, g_hash_table_unref); + + G_OBJECT_CLASS (xdp_doc_db_parent_class)->finalize (object); +} + +static void +xdp_doc_db_class_init (XdpDocDbClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = xdp_doc_db_finalize; +} + +static void +xdp_doc_db_init (XdpDocDb *db) +{ + db->no_doc = xdp_doc_new ("NONE", + g_variant_new_array (G_VARIANT_TYPE ("(su)"), NULL, 0)); +} + +XdpDocDb * +xdp_doc_db_new (const char *filename, + GError **error) +{ + XdpDocDb *db = g_object_new (XDP_TYPE_DOC_DB, NULL); + GvdbTable *gvdb; + GError *my_error = NULL; + + gvdb = gvdb_table_new (filename, TRUE, &my_error); + if (gvdb == NULL) + { + if (g_error_matches (my_error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) + g_error_free (my_error); + else + { + g_propagate_error (error, my_error); + return NULL; + } + } + + db->filename = g_strdup (filename); + db->gvdb = gvdb; + + if (gvdb) + { + db->doc_table = gvdb_table_get_table (gvdb, "docs"); + db->app_table = gvdb_table_get_table (gvdb, "apps"); + db->uri_table = gvdb_table_get_table (gvdb, "uris"); + } + + db->doc_updates = + g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, (GDestroyNotify)g_variant_unref); + db->app_updates = + g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, (GDestroyNotify)g_variant_unref); + db->uri_updates = + g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, (GDestroyNotify)g_variant_unref); + + return db; +} + +static gboolean +uri_empty (GVariant *uri) +{ + g_autoptr(GVariant) doc_array = g_variant_get_child_value (uri, 0); + return g_variant_n_children (doc_array) == 0; +} + +static gboolean +app_empty (GVariant *app) +{ + g_autoptr(GVariant) doc_array = g_variant_get_child_value (app, 0); + return g_variant_n_children (doc_array) == 0; +} + +gboolean +xdp_doc_db_save (XdpDocDb *db, + GError **error) +{ + GHashTable *root, *docs, *apps, *uris; + GvdbTable *gvdb; + guint32 *doc_ids; + char **keys; + int i; + + root = gvdb_hash_table_new (NULL, NULL); + docs = gvdb_hash_table_new (root, "docs"); + apps = gvdb_hash_table_new (root, "apps"); + uris = gvdb_hash_table_new (root, "uris"); + g_hash_table_unref (docs); + g_hash_table_unref (apps); + g_hash_table_unref (uris); + + doc_ids = xdp_doc_db_list_docs (db); + for (i = 0; doc_ids[i] != 0; i++) + { + g_autoptr(GVariant) doc = xdp_doc_db_lookup_doc (db, doc_ids[i]); + if (doc != NULL) + { + char id_num[9]; + GvdbItem *item; + + g_sprintf (id_num, "%x", (guint32)doc_ids[i]); + item = gvdb_hash_table_insert (docs, id_num); + gvdb_item_set_value (item, doc); + } + } + g_free (doc_ids); + + keys = xdp_doc_db_list_apps (db); + for (i = 0; keys[i] != NULL; i++) + { + g_autoptr(GVariant) app = xdp_doc_db_lookup_app (db, keys[i]); + if (!app_empty (app)) + { + GvdbItem *item = gvdb_hash_table_insert (apps, keys[i]); + gvdb_item_set_value (item, app); + } + } + g_strfreev (keys); + + keys = xdp_doc_db_list_uris (db); + for (i = 0; keys[i] != NULL; i++) + { + g_autoptr(GVariant) uri = xdp_doc_db_lookup_uri (db, keys[i]); + if (!uri_empty (uri)) + { + GvdbItem *item = gvdb_hash_table_insert (uris, keys[i]); + gvdb_item_set_value (item, uri); + } + } + g_strfreev (keys); + + if (!gvdb_table_write_contents (root, db->filename, FALSE, error)) + { + g_hash_table_unref (root); + return FALSE; + } + + g_hash_table_unref (root); + + gvdb = gvdb_table_new (db->filename, TRUE, error); + if (gvdb == NULL) + return FALSE; + + g_clear_pointer (&db->gvdb, gvdb_table_free); + g_clear_pointer (&db->doc_table, gvdb_table_free); + g_clear_pointer (&db->app_table, gvdb_table_free); + g_clear_pointer (&db->uri_table, gvdb_table_free); + + g_hash_table_remove_all (db->doc_updates); + g_hash_table_remove_all (db->app_updates); + g_hash_table_remove_all (db->uri_updates); + + db->gvdb = gvdb; + db->doc_table = gvdb_table_get_table (gvdb, "docs"); + db->app_table = gvdb_table_get_table (gvdb, "apps"); + db->uri_table = gvdb_table_get_table (gvdb, "uris"); + + db->dirty = FALSE; + + return TRUE; +} + +gboolean +xdp_doc_db_is_dirty (XdpDocDb *db) +{ + return db->dirty; +} + +void +xdp_doc_db_dump (XdpDocDb *db) +{ + int i; + guint32 *docs; + char **apps, **uris; + + g_print ("docs:\n"); + docs = xdp_doc_db_list_docs (db); + for (i = 0; docs[i] != 0; i++) + { + g_autoptr(GVariant) doc = xdp_doc_db_lookup_doc (db, docs[i]); + if (doc) + g_print (" %x: %s\n", docs[i], g_variant_print (doc, FALSE)); + } + g_free (docs); + + g_print ("apps:\n"); + apps = xdp_doc_db_list_apps (db); + for (i = 0; apps[i] != NULL; i++) + { + g_autoptr(GVariant) app = xdp_doc_db_lookup_app (db, apps[i]); + g_print (" %s: %s\n", apps[i], g_variant_print (app, FALSE)); + } + g_strfreev (apps); + + g_print ("uris:\n"); + uris = xdp_doc_db_list_apps (db); + for (i = 0; apps[i] != NULL; i++) + { + g_autoptr(GVariant) uri = xdp_doc_db_lookup_uri (db, uris[i]); + g_print (" %s: %s\n", uris[i], g_variant_print (uri, FALSE)); + } + g_strfreev (uris); +} + +GVariant * +xdp_doc_db_lookup_doc_name (XdpDocDb *db, const char *doc_id) +{ + GVariant *res; + + res = g_hash_table_lookup (db->doc_updates, doc_id); + if (res) + { + if (res == db->no_doc) + return NULL; + return g_variant_ref (res); + } + + if (db->doc_table) + { + res = gvdb_table_get_value (db->doc_table, doc_id); + if (res) + return g_variant_ref (res); + } + + return NULL; +} + +GVariant * +xdp_doc_db_lookup_doc (XdpDocDb *db, + guint32 doc_id) +{ + char id_num[9]; + + g_sprintf (id_num, "%x", (guint32)doc_id); + return xdp_doc_db_lookup_doc_name (db, id_num); +} + +guint32 * +xdp_doc_db_list_docs (XdpDocDb *db) +{ + GHashTableIter iter; + gpointer key, value; + GArray *res; + + res = g_array_new (TRUE, FALSE, sizeof (guint32)); + + g_hash_table_iter_init (&iter, db->doc_updates); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + guint32 doc_id = xdb_doc_id_from_name (key); + g_array_append_val (res, doc_id); + } + + if (db->doc_table) + { + char **table_docs = gvdb_table_get_names (db->doc_table, NULL); + int i; + + for (i = 0; table_docs[i] != NULL; i++) + { + char *doc = table_docs[i]; + + if (g_hash_table_lookup (db->doc_updates, doc) == NULL) + { + guint32 doc_id = xdb_doc_id_from_name (doc); + g_array_append_val (res, doc_id); + } + } + g_strfreev (table_docs); + } + + return (guint32 *)g_array_free (res, FALSE); +} + +char ** +xdp_doc_db_list_apps (XdpDocDb *db) +{ + GHashTableIter iter; + gpointer key, value; + GPtrArray *res; + + res = g_ptr_array_new (); + + g_hash_table_iter_init (&iter, db->app_updates); + while (g_hash_table_iter_next (&iter, &key, &value)) + g_ptr_array_add (res, g_strdup (key)); + + if (db->app_table) + { + char **table_apps = gvdb_table_get_names (db->app_table, NULL); + int i; + + for (i = 0; table_apps[i] != NULL; i++) + { + char *app = table_apps[i]; + + if (g_hash_table_lookup (db->app_updates, app) != NULL) + g_free (app); + else + g_ptr_array_add (res, app); + } + g_free (table_apps); + } + + g_ptr_array_add (res, NULL); + return (char **)g_ptr_array_free (res, FALSE); +} + +char ** +xdp_doc_db_list_uris (XdpDocDb *db) +{ + GHashTableIter iter; + gpointer key, value; + GPtrArray *res; + + res = g_ptr_array_new (); + + g_hash_table_iter_init (&iter, db->uri_updates); + while (g_hash_table_iter_next (&iter, &key, &value)) + g_ptr_array_add (res, g_strdup (key)); + + if (db->uri_table) + { + char **table_uris = gvdb_table_get_names (db->uri_table, NULL); + int i; + + for (i = 0; table_uris[i] != NULL; i++) + { + char *uri = table_uris[i]; + + if (g_hash_table_lookup (db->uri_updates, uri) != NULL) + g_free (uri); + else + g_ptr_array_add (res, uri); + } + g_free (table_uris); + } + + g_ptr_array_add (res, NULL); + return (char **)g_ptr_array_free (res, FALSE); +} + +GVariant * +xdp_doc_db_lookup_app (XdpDocDb *db, + const char *app_id) +{ + GVariant *res; + + res = g_hash_table_lookup (db->app_updates, app_id); + if (res) + return g_variant_ref (res); + + if (db->app_table) + { + res = gvdb_table_get_value (db->app_table, app_id); + if (res) + return g_variant_ref (res); + } + + return NULL; +} + +GVariant * +xdp_doc_db_lookup_uri (XdpDocDb *db, const char *uri) +{ + GVariant *res; + + res = g_hash_table_lookup (db->uri_updates, uri); + if (res) + return g_variant_ref (res); + + if (db->uri_table) + { + res = gvdb_table_get_value (db->uri_table, uri); + if (res) + return g_variant_ref (res); + } + + return NULL; +} + +static void +xdp_doc_db_update_uri_docs (XdpDocDb *db, + const char *uri, + guint32 doc_id, + gboolean added) +{ + g_autoptr(GVariant) old_uri; + GVariantBuilder builder; + GVariantIter iter; + GVariant *child; + GVariant *res, *array; + g_autoptr(GVariant) doc_array = NULL; + + old_uri = xdp_doc_db_lookup_app (db, uri); + + g_variant_builder_init (&builder, G_VARIANT_TYPE_ARRAY); + + if (old_uri) + { + doc_array = g_variant_get_child_value (old_uri, 0); + g_variant_iter_init (&iter, doc_array); + while ((child = g_variant_iter_next_value (&iter))) + { + guint32 child_doc_id = g_variant_get_uint32 (child); + + if (doc_id == child_doc_id) + { + if (added) + g_warning ("added doc already exist"); + } + else + g_variant_builder_add_value (&builder, child); + + g_variant_unref (child); + } + } + + if (added) + g_variant_builder_add (&builder, "u", doc_id); + + array = g_variant_builder_end (&builder); + res = g_variant_new_tuple (&array, 1); + + g_hash_table_insert (db->uri_updates, g_strdup (uri), + g_variant_ref_sink (res)); +} + +static void +xdp_doc_db_insert_doc (XdpDocDb *db, + guint32 doc_id, + GVariant *doc) +{ + g_hash_table_insert (db->doc_updates, xdb_doc_name_from_id (doc_id), + g_variant_ref_sink (doc)); + db->dirty = TRUE; + + xdp_doc_db_update_uri_docs (db, xdp_doc_get_uri (doc), doc_id, TRUE); +} + +guint32 +xdp_doc_db_create_doc (XdpDocDb *db, + const char *uri) +{ + GVariant *doc; + guint32 doc_id; + g_autoptr (GVariant) uri_v = NULL; + + /* Reuse pre-existing entry with same uri */ + uri_v = xdp_doc_db_lookup_uri (db, uri); + if (uri_v != NULL) + { + g_autoptr(GVariant) doc_array = g_variant_get_child_value (uri_v, 0); + if (g_variant_n_children (doc_array) > 0) + { + g_variant_get_child (doc_array, 0, "u", &doc_id); + return doc_id; + } + } + + while (TRUE) + { + g_autoptr(GVariant) existing_doc = NULL; + + doc_id = (guint32)g_random_int (); + + existing_doc = xdp_doc_db_lookup_doc (db, doc_id); + if (existing_doc == NULL) + break; + } + + doc = xdp_doc_new (uri, + g_variant_new_array (G_VARIANT_TYPE ("(su)"), NULL, 0)); + xdp_doc_db_insert_doc (db, doc_id, doc); + + return doc_id; +} + +static void +xdp_doc_db_update_app_docs (XdpDocDb *db, + const char *app_id, + guint32 doc_id, + gboolean added) +{ + g_autoptr(GVariant) old_app = NULL; + GVariantBuilder builder; + GVariantIter iter; + GVariant *child; + GVariant *res, *array; + g_autoptr(GVariant) doc_array = NULL; + + old_app = xdp_doc_db_lookup_app (db, app_id); + + g_variant_builder_init (&builder, G_VARIANT_TYPE_ARRAY); + + if (old_app) + { + doc_array = g_variant_get_child_value (old_app, 0); + g_variant_iter_init (&iter, doc_array); + while ((child = g_variant_iter_next_value (&iter))) + { + guint32 child_doc_id = g_variant_get_uint32 (child); + + if (doc_id == child_doc_id) + { + if (added) + g_warning ("added doc already exist"); + } + else + g_variant_builder_add_value (&builder, child); + + g_variant_unref (child); + } + + } + + if (added) + g_variant_builder_add (&builder, "u", doc_id); + + array = g_variant_builder_end (&builder); + res = g_variant_new_tuple (&array, 1); + + g_hash_table_insert (db->app_updates, g_strdup (app_id), + g_variant_ref_sink (res)); +} + +gboolean +xdp_doc_db_delete_doc (XdpDocDb *db, + guint32 doc_id) +{ + g_autoptr(GVariant) old_doc = NULL; + g_autoptr(GVariant) old_perms = NULL; + g_autoptr (GVariant) app_array = NULL; + GVariant *child; + GVariantIter iter; + + old_doc = xdp_doc_db_lookup_doc (db, doc_id); + if (old_doc == NULL) + { + g_warning ("no doc %x found", doc_id); + return FALSE; + } + + xdp_doc_db_insert_doc (db, doc_id, db->no_doc); + + app_array = g_variant_get_child_value (old_doc, 1); + g_variant_iter_init (&iter, app_array); + while ((child = g_variant_iter_next_value (&iter))) + { + const char *child_app_id; + guint32 old_perms; + + g_variant_get (child, "(&su)", &child_app_id, &old_perms); + xdp_doc_db_update_app_docs (db, child_app_id, doc_id, FALSE); + g_variant_unref (child); + } + + xdp_doc_db_update_uri_docs (db, xdp_doc_get_uri (old_doc), + doc_id, FALSE); + return TRUE; +} + +gboolean +xdp_doc_db_set_permissions (XdpDocDb *db, + guint32 doc_id, + const char *app_id, + XdpPermissionFlags permissions, + gboolean merge) +{ + g_autoptr(GVariant) old_doc; + g_autoptr (GVariant) app_array = NULL; + GVariant *doc; + GVariantIter iter; + GVariant *child; + GVariantBuilder builder; + gboolean found = FALSE; + + old_doc = xdp_doc_db_lookup_doc (db, doc_id); + if (old_doc == NULL) + { + g_warning ("no doc %x found", doc_id); + return FALSE; + } + + g_variant_builder_init (&builder, G_VARIANT_TYPE_ARRAY); + + app_array = g_variant_get_child_value (old_doc, 1); + g_variant_iter_init (&iter, app_array); + while ((child = g_variant_iter_next_value (&iter))) + { + const char *child_app_id; + guint32 old_perms; + + g_variant_get (child, "(&su)", &child_app_id, &old_perms); + + if (strcmp (app_id, child_app_id) == 0) + { + found = TRUE; + if (merge) + permissions = permissions | old_perms; + if (permissions != 0) + g_variant_builder_add (&builder, "(&su)", + app_id, (guint32)permissions); + } + else + g_variant_builder_add_value (&builder, child); + + g_variant_unref (child); + } + + if (permissions != 0 && !found) + g_variant_builder_add (&builder, "(&su)", app_id, (guint32)permissions); + + doc = xdp_doc_new (xdp_doc_get_uri (old_doc), + g_variant_builder_end (&builder)); + g_hash_table_insert (db->doc_updates, xdb_doc_name_from_id (doc_id), + g_variant_ref_sink (doc)); + + if (found && permissions == 0) + xdp_doc_db_update_app_docs (db, app_id, doc_id, FALSE); + else if (!found && permissions != 0) + xdp_doc_db_update_app_docs (db, app_id, doc_id, TRUE); + + db->dirty = TRUE; + + return TRUE; +} + +XdpPermissionFlags +xdp_doc_get_permissions (GVariant *doc, + const char *app_id) +{ + g_autoptr(GVariant) app_array = NULL; + GVariantIter iter; + GVariant *child; + + if (strcmp (app_id, "") == 0) + return XDP_PERMISSION_FLAGS_ALL; + + app_array = g_variant_get_child_value (doc, 1); + + g_variant_iter_init (&iter, app_array); + while ((child = g_variant_iter_next_value (&iter))) + { + const char *child_app_id; + guint32 perms; + + g_variant_get_child (child, 0, "&s", &child_app_id); + + if (strcmp (app_id, child_app_id) == 0) + { + g_variant_get_child (child, 1, "u", &perms); + return perms; + } + + g_variant_unref (child); + } + + return 0; +} + +gboolean +xdp_doc_has_permissions (GVariant *doc, + const char *app_id, + XdpPermissionFlags perms) +{ + XdpPermissionFlags current_perms; + + current_perms = xdp_doc_get_permissions (doc, app_id); + return (current_perms & perms) == perms; +} + +guint32 * +xdp_app_list_docs (GVariant *app) +{ + g_autoptr(GVariant) doc_array = NULL; + GVariantIter iter; + GVariant *child; + GArray *res; + + res = g_array_new (TRUE, FALSE, sizeof (guint32)); + + doc_array = g_variant_get_child_value (app, 0); + + g_variant_iter_init (&iter, doc_array); + while ((child = g_variant_iter_next_value (&iter))) + { + guint32 child_id = g_variant_get_uint32 (child); + g_array_append_val (res, child_id); + g_variant_unref (child); + } + + return (guint32 *)g_array_free (res, FALSE); +} diff --git a/document-portal/xdp-doc-db.h b/document-portal/xdp-doc-db.h new file mode 100644 index 00000000..74aca992 --- /dev/null +++ b/document-portal/xdp-doc-db.h @@ -0,0 +1,55 @@ +#ifndef XDP_DB +#define XDP_DB + +#include + +#include "xdp-enums.h" + +G_BEGIN_DECLS + +#define XDP_TYPE_DOC_DB (xdp_doc_db_get_type()) + +G_DECLARE_FINAL_TYPE(XdpDocDb, xdp_doc_db, XDP, DOC_DB, GObject); + +XdpDocDb * xdp_doc_db_new (const char *filename, + GError **error); +gboolean xdp_doc_db_save (XdpDocDb *db, + GError **error); +gboolean xdp_doc_db_is_dirty (XdpDocDb *db); +void xdp_doc_db_dump (XdpDocDb *db); +GVariant * xdp_doc_db_lookup_doc_name (XdpDocDb *db, + const char *doc_name); +GVariant * xdp_doc_db_lookup_doc (XdpDocDb *db, + guint32 doc_id); +GVariant * xdp_doc_db_lookup_app (XdpDocDb *db, + const char *app_id); +GVariant * xdp_doc_db_lookup_uri (XdpDocDb *db, + const char *uri); +guint32* xdp_doc_db_list_docs (XdpDocDb *db); +char ** xdp_doc_db_list_apps (XdpDocDb *db); +char ** xdp_doc_db_list_uris (XdpDocDb *db); +guint32 xdp_doc_db_create_doc (XdpDocDb *db, + const char *uri); +gboolean xdp_doc_db_delete_doc (XdpDocDb *db, + guint32 doc_id); +gboolean xdp_doc_db_set_permissions (XdpDocDb *db, + guint32 doc_id, + const char *app_id, + XdpPermissionFlags permissions, + gboolean add); + +XdpPermissionFlags xdp_doc_get_permissions (GVariant *doc, + const char *app_id); +gboolean xdp_doc_has_permissions (GVariant *doc, + const char *app_id, + XdpPermissionFlags permissions); +guint32 xdb_doc_id_from_name (const char *name); +char * xdb_doc_name_from_id (guint32 doc_id); +const char * xdp_doc_get_uri (GVariant *doc); +char * xdp_doc_dup_path (GVariant *doc); +char * xdp_doc_dup_basename (GVariant *doc); +char * xdp_doc_dup_dirname (GVariant *doc); + +G_END_DECLS + +#endif /* XDP_DB */ diff --git a/document-portal/xdp-enums.h b/document-portal/xdp-enums.h new file mode 100644 index 00000000..4169fabe --- /dev/null +++ b/document-portal/xdp-enums.h @@ -0,0 +1,17 @@ +#ifndef XDP_ENUMS_H +#define XDP_ENUMS_H + +G_BEGIN_DECLS + +typedef enum { + XDP_PERMISSION_FLAGS_READ = (1<<0), + XDP_PERMISSION_FLAGS_WRITE = (1<<1), + XDP_PERMISSION_FLAGS_GRANT_PERMISSIONS = (1<<2), + XDP_PERMISSION_FLAGS_DELETE = (1<<3), + + XDP_PERMISSION_FLAGS_ALL = ((1<<4) - 1) +} XdpPermissionFlags; + +G_END_DECLS + +#endif /* XDP_ENUMS_H */ diff --git a/document-portal/xdp-error.c b/document-portal/xdp-error.c new file mode 100644 index 00000000..d144393d --- /dev/null +++ b/document-portal/xdp-error.c @@ -0,0 +1,22 @@ +#include "xdp-error.h" + +#include + +static const GDBusErrorEntry xdp_error_entries[] = { + {XDP_ERROR_FAILED, "org.freedesktop.portal.document.Failed"}, + {XDP_ERROR_NOT_FOUND, "org.freedesktop.portal.document.NotFound"}, + {XDP_ERROR_NOT_ALLOWED, "org.freedesktop.portal.document.NotAllowed"}, + {XDP_ERROR_INVALID_ARGUMENT, "org.freedesktop.portal.document.InvalidArgument"}, +}; + +GQuark +xdp_error_quark (void) +{ + static volatile gsize quark_volatile = 0; + + g_dbus_error_register_error_domain ("xdg--error-quark", + &quark_volatile, + xdp_error_entries, + G_N_ELEMENTS (xdp_error_entries)); + return (GQuark) quark_volatile; +} diff --git a/document-portal/xdp-error.h b/document-portal/xdp-error.h new file mode 100644 index 00000000..3eab9603 --- /dev/null +++ b/document-portal/xdp-error.h @@ -0,0 +1,25 @@ +#ifndef XDP_ERROR_H +#define XDP_ERROR_H + +#include + +G_BEGIN_DECLS + +/** + * XdpErrorEnum: + */ +typedef enum { + XDP_ERROR_FAILED = 0, + XDP_ERROR_NOT_FOUND, + XDP_ERROR_NOT_ALLOWED, + XDP_ERROR_INVALID_ARGUMENT, +} XdpErrorEnum; + + +#define XDP_ERROR xdp_error_quark() + +GQuark xdp_error_quark (void); + +G_END_DECLS + +#endif /* XDP_ERROR_H */ diff --git a/document-portal/xdp-fuse.c b/document-portal/xdp-fuse.c new file mode 100644 index 00000000..b6feea68 --- /dev/null +++ b/document-portal/xdp-fuse.c @@ -0,0 +1,1787 @@ +#include "config.h" + +#define FUSE_USE_VERSION 26 + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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) + + "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; +} diff --git a/document-portal/xdp-fuse.h b/document-portal/xdp-fuse.h new file mode 100644 index 00000000..f335ccc8 --- /dev/null +++ b/document-portal/xdp-fuse.h @@ -0,0 +1,15 @@ +#ifndef XDP_FUSE_H +#define XDP_FUSE_H + +#include +#include "xdp-doc-db.h" + +G_BEGIN_DECLS + +gboolean xdp_fuse_init (XdpDocDb *db, + GError **error); +void xdp_fuse_exit (void); + +G_END_DECLS + +#endif /* XDP_FUSE_H */ diff --git a/document-portal/xdp-main.c b/document-portal/xdp-main.c new file mode 100644 index 00000000..7cd1ad3e --- /dev/null +++ b/document-portal/xdp-main.c @@ -0,0 +1,564 @@ +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include "xdp-dbus.h" +#include "xdp-doc-db.h" +#include "xdp-error.h" +#include "xdp-util.h" +#include "xdp-fuse.h" + +typedef struct +{ + char *doc_id; + int fd; + char *owner; + guint flags; + + GDBusMethodInvocation *finish_invocation; +} XdpDocUpdate; + + +static GMainLoop *loop = NULL; +static XdpDocDb *db = NULL; +static GDBusNodeInfo *introspection_data = NULL; + +static guint save_timeout = 0; + +static gboolean +queue_db_save_timeout (gpointer user_data) +{ + g_autoptr(GError) error = NULL; + + save_timeout = 0; + + if (xdp_doc_db_is_dirty (db)) + { + if (!xdp_doc_db_save (db, &error)) + g_warning ("db save: %s\n", error->message); + } + + return FALSE; +} + +static void +queue_db_save (void) +{ + if (save_timeout != 0) + return; + + if (xdp_doc_db_is_dirty (db)) + save_timeout = g_timeout_add_seconds (10, queue_db_save_timeout, NULL); +} + +static XdpPermissionFlags +parse_permissions (const char **permissions, GError **error) +{ + XdpPermissionFlags perms; + int i; + + perms = 0; + for (i = 0; permissions[i]; i++) + { + if (strcmp (permissions[i], "read") == 0) + perms |= XDP_PERMISSION_FLAGS_READ; + else if (strcmp (permissions[i], "write") == 0) + perms |= XDP_PERMISSION_FLAGS_WRITE; + else if (strcmp (permissions[i], "grant-permissions") == 0) + perms |= XDP_PERMISSION_FLAGS_GRANT_PERMISSIONS; + else if (strcmp (permissions[i], "delete") == 0) + perms |= XDP_PERMISSION_FLAGS_DELETE; + else + { + g_set_error (error, XDP_ERROR, XDP_ERROR_INVALID_ARGUMENT, + "No such permission: %s", permissions[i]); + return -1; + } + } + + return perms; +} + +static void +portal_grant_permissions (GDBusMethodInvocation *invocation, + GVariant *parameters, + const char *app_id) +{ + const char *target_app_id; + guint32 doc_id; + g_autoptr(GVariant) doc = NULL; + g_autoptr(GError) error = NULL; + const char **permissions; + XdpPermissionFlags perms; + + g_variant_get (parameters, "(u&s^a&s)", &doc_id, &target_app_id, &permissions); + + doc = xdp_doc_db_lookup_doc (db, doc_id); + if (doc == NULL) + { + g_dbus_method_invocation_return_error (invocation, XDP_ERROR, XDP_ERROR_NOT_FOUND, + "No such document: %d", doc_id); + return; + } + + perms = parse_permissions (permissions, &error); + if (perms == -1) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return; + } + + /* Must have grant-permissions and all the newly granted permissions */ + if (!xdp_doc_has_permissions (doc, app_id, XDP_PERMISSION_FLAGS_GRANT_PERMISSIONS | perms)) + { + g_dbus_method_invocation_return_error (invocation, XDP_ERROR, XDP_ERROR_NOT_ALLOWED, + "Not enough permissions"); + return; + } + + xdp_doc_db_set_permissions (db, doc_id, target_app_id, perms, TRUE); + queue_db_save (); + + g_dbus_method_invocation_return_value (invocation, g_variant_new ("()")); + +} + +static void +portal_revoke_permissions (GDBusMethodInvocation *invocation, + GVariant *parameters, + const char *app_id) +{ + const char *target_app_id; + g_autoptr(GVariant) doc = NULL; + g_autoptr(GError) error = NULL; + guint32 doc_id; + const char **permissions; + XdpPermissionFlags perms; + + g_variant_get (parameters, "(u&s^a&s)", &doc_id, &target_app_id, &permissions); + + doc = xdp_doc_db_lookup_doc (db, doc_id); + if (doc == NULL) + { + g_dbus_method_invocation_return_error (invocation, XDP_ERROR, XDP_ERROR_NOT_FOUND, + "No such document: %d", doc_id); + return; + } + + perms = parse_permissions (permissions, &error); + if (perms == -1) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return; + } + + /* Must have grant-permissions, or be itself */ + if (!xdp_doc_has_permissions (doc, app_id, XDP_PERMISSION_FLAGS_GRANT_PERMISSIONS) || + strcmp (app_id, target_app_id) == 0) + { + g_dbus_method_invocation_return_error (invocation, XDP_ERROR, XDP_ERROR_NOT_ALLOWED, + "Not enough permissions"); + return; + } + + xdp_doc_db_set_permissions (db, doc_id, target_app_id, + xdp_doc_get_permissions (doc, target_app_id) & ~perms, + FALSE); + queue_db_save (); + + g_dbus_method_invocation_return_value (invocation, g_variant_new ("()")); +} + +static void +portal_delete (GDBusMethodInvocation *invocation, + GVariant *parameters, + const char *app_id) +{ + guint32 doc_id; + g_autoptr(GVariant) doc = NULL; + + g_variant_get (parameters, "(u)", &doc_id); + + doc = xdp_doc_db_lookup_doc (db, doc_id); + if (doc == NULL) + { + g_dbus_method_invocation_return_error (invocation, XDP_ERROR, XDP_ERROR_NOT_FOUND, + "No such document: %d", doc_id); + return; + } + + if (!xdp_doc_has_permissions (doc, app_id, XDP_PERMISSION_FLAGS_DELETE)) + { + g_dbus_method_invocation_return_error (invocation, XDP_ERROR, XDP_ERROR_NOT_ALLOWED, + "Not enough permissions"); + return; + } + + xdp_doc_db_delete_doc (db, doc_id); + queue_db_save (); + + g_dbus_method_invocation_return_value (invocation, g_variant_new ("()")); +} + +static void +portal_add (GDBusMethodInvocation *invocation, + GVariant *parameters, + const char *app_id) +{ + guint32 id; + const char *uri; + + if (app_id[0] != '\0') + { + /* don't allow this from within the sandbox */ + g_dbus_method_invocation_return_error (invocation, + XDP_ERROR, XDP_ERROR_NOT_ALLOWED, + "Not allowed inside sandbox"); + return; + } + + g_variant_get (parameters, "(&s)", &uri); + + id = xdp_doc_db_create_doc (db, uri); + g_dbus_method_invocation_return_value (invocation, + g_variant_new ("(u)", id)); + queue_db_save (); +} + +static void +portal_add_local (GDBusMethodInvocation *invocation, + GVariant *parameters, + const char *app_id) +{ + GDBusMessage *message; + GUnixFDList *fd_list; + guint32 id; + g_autofree char *proc_path = NULL; + g_autofree char *uri = NULL; + int fd_id, fd, fds_len, fd_flags; + const int *fds; + char path_buffer[PATH_MAX+1]; + g_autoptr(GFile) file = NULL; + ssize_t symlink_size; + struct stat st_buf, real_st_buf; + + g_variant_get (parameters, "(h)", &fd_id); + + message = g_dbus_method_invocation_get_message (invocation); + fd_list = g_dbus_message_get_unix_fd_list (message); + + fd = -1; + if (fd_list != NULL) + { + fds = g_unix_fd_list_peek_fds (fd_list, &fds_len); + if (fd_id < fds_len) + fd = fds[fd_id]; + } + + proc_path = g_strdup_printf ("/proc/self/fd/%d", fd); + + if (fd == -1 || + /* Must be able to fstat */ + fstat (fd, &st_buf) < 0 || + /* Must be a regular file */ + (st_buf.st_mode & S_IFMT) != S_IFREG || + /* Must be able to get fd flags */ + (fd_flags = fcntl (fd, F_GETFL)) == -1 || + /* Must be able to read */ + (fd_flags & O_ACCMODE) == O_WRONLY || + /* Must be able to read path from /proc/self/fd */ + (symlink_size = readlink (proc_path, path_buffer, sizeof (path_buffer) - 1)) < 0) + { + g_dbus_method_invocation_return_error (invocation, + XDP_ERROR, XDP_ERROR_INVALID_ARGUMENT, + "Invalid fd passed"); + return; + } + + path_buffer[symlink_size] = 0; + + if (lstat (path_buffer, &real_st_buf) < 0 || + st_buf.st_dev != real_st_buf.st_dev || + st_buf.st_ino != real_st_buf.st_ino) + { + /* Don't leak any info about real file path existance, etc */ + g_dbus_method_invocation_return_error (invocation, + XDP_ERROR, XDP_ERROR_INVALID_ARGUMENT, + "Invalid fd passed"); + return; + } + + file = g_file_new_for_path (path_buffer); + uri = g_file_get_uri (file); + + id = xdp_doc_db_create_doc (db, uri); + + if (app_id[0] != '\0') + { + /* also grant app-id perms based on file_mode */ + guint32 perms = XDP_PERMISSION_FLAGS_GRANT_PERMISSIONS | XDP_PERMISSION_FLAGS_READ; + if ((fd_flags & O_ACCMODE) == O_RDWR) + perms |= XDP_PERMISSION_FLAGS_WRITE; + xdp_doc_db_set_permissions (db, id, app_id, perms, TRUE); + } + + g_dbus_method_invocation_return_value (invocation, + g_variant_new ("(u)", id)); + queue_db_save (); +} + +typedef void (*PortalMethod) (GDBusMethodInvocation *invocation, + GVariant *parameters, + const char *app_id); + +static void +got_app_id_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GDBusMethodInvocation *invocation = G_DBUS_METHOD_INVOCATION (source_object); + g_autoptr(GError) error = NULL; + g_autofree char *app_id = NULL; + PortalMethod portal_method = user_data; + + app_id = xdp_invocation_lookup_app_id_finish (invocation, res, &error); + + if (app_id == NULL) + g_dbus_method_invocation_return_gerror (invocation, error); + else + portal_method (invocation, g_dbus_method_invocation_get_parameters (invocation), app_id); +} + +static gboolean +handle_method (GCallback method_callback, + GDBusMethodInvocation *invocation) +{ + xdp_invocation_lookup_app_id (invocation, NULL, got_app_id_cb, method_callback); + + return TRUE; +} + +static void +on_bus_acquired (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + XdpDbusDocuments *helper; + GError *error = NULL; + + helper = xdp_dbus_documents_skeleton_new (); + + g_signal_connect_swapped (helper, "handle-add", G_CALLBACK (handle_method), portal_add); + g_signal_connect_swapped (helper, "handle-add-local", G_CALLBACK (handle_method), portal_add_local); + g_signal_connect_swapped (helper, "handle-grant-permissions", G_CALLBACK (handle_method), portal_grant_permissions); + g_signal_connect_swapped (helper, "handle-revoke-permissions", G_CALLBACK (handle_method), portal_revoke_permissions); + g_signal_connect_swapped (helper, "handle-delete", G_CALLBACK (handle_method), portal_delete); + + xdp_connection_track_name_owners (connection); + + if (!g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (helper), + connection, + "/org/freedesktop/portal/documents", + &error)) + { + g_warning ("error: %s\n", error->message); + g_error_free (error); + } +} + +static void +on_name_acquired (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + g_autoptr(GError) error = NULL; + if (!xdp_fuse_init (db, &error)) + { + g_printerr ("fuse init failed: %s\n", error->message); + exit (1); + } + +} + +static void +on_name_lost (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + g_main_loop_quit (loop); +} + +static void +session_bus_closed (GDBusConnection *connection, + gboolean remote_peer_vanished, + GError *bus_error) +{ + g_main_loop_quit (loop); +} + +static void +exit_handler (int sig) +{ + g_main_loop_quit (loop); +} + +static int +set_one_signal_handler (int sig, + void (*handler)(int), + int remove) +{ + struct sigaction sa; + struct sigaction old_sa; + + memset (&sa, 0, sizeof (struct sigaction)); + sa.sa_handler = remove ? SIG_DFL : handler; + sigemptyset (&(sa.sa_mask)); + sa.sa_flags = 0; + + if (sigaction (sig, NULL, &old_sa) == -1) + { + g_warning ("cannot get old signal handler"); + return -1; + } + + if (old_sa.sa_handler == (remove ? handler : SIG_DFL) && + sigaction (sig, &sa, NULL) == -1) + { + g_warning ("cannot set signal handler"); + return -1; + } + + return 0; +} + +static gboolean opt_verbose; + +static GOptionEntry entries[] = { + { "verbose", 'v', 0, G_OPTION_ARG_NONE, &opt_verbose, "Print debug information during command processing", NULL }, + { NULL } +}; + +static void +message_handler (const gchar *log_domain, + GLogLevelFlags log_level, + const gchar *message, + gpointer user_data) +{ + /* Make this look like normal console output */ + if (log_level & G_LOG_LEVEL_DEBUG) + g_printerr ("XDP: %s\n", message); + else + g_printerr ("%s: %s\n", g_get_prgname (), message); +} + +int +main (int argc, + char **argv) +{ + guint owner_id; + GBytes *introspection_bytes; + g_autoptr(GList) object_types = NULL; + g_autoptr(GError) error = NULL; + g_autofree char *data_path = NULL; + g_autofree char *db_path = NULL; + g_autoptr(GFile) data_dir = NULL; + g_autoptr(GFile) db_file = NULL; + GDBusConnection *session_bus; + GOptionContext *context; + + setlocale (LC_ALL, ""); + + /* Avoid even loading gvfs to avoid accidental confusion */ + g_setenv ("GIO_USE_VFS", "local", TRUE); + + context = g_option_context_new ("- document portal"); + g_option_context_add_main_entries (context, entries, NULL); + if (!g_option_context_parse (context, &argc, &argv, &error)) + { + g_print ("option parsing failed: %s\n", error->message); + return 1; + } + + if (opt_verbose) + g_log_set_handler (NULL, G_LOG_LEVEL_DEBUG, message_handler, NULL); + + g_set_prgname (argv[0]); + + loop = g_main_loop_new (NULL, FALSE); + + data_path = g_build_filename (g_get_user_data_dir(), "xdg-document-portal", NULL); + if (g_mkdir_with_parents (data_path, 0700)) + { + g_printerr ("Unable to create dir %s\n", data_path); + return 1; + } + + data_dir = g_file_new_for_path (data_path); + db_file = g_file_get_child (data_dir, "main.gvdb"); + db_path = g_file_get_path (db_file); + + db = xdp_doc_db_new (db_path, &error); + if (db == NULL) + { + g_print ("%s\n", error->message); + return 2; + } + + session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); + if (session_bus == NULL) + { + g_print ("No session bus: %s\n", error->message); + return 3; + } + + /* We want do do our custom post-mainloop exit */ + g_dbus_connection_set_exit_on_close (session_bus, FALSE); + + g_signal_connect (session_bus, "closed", G_CALLBACK (session_bus_closed), NULL); + + if (set_one_signal_handler(SIGHUP, exit_handler, 0) == -1 || + set_one_signal_handler(SIGINT, exit_handler, 0) == -1 || + set_one_signal_handler(SIGTERM, exit_handler, 0) == -1 || + set_one_signal_handler(SIGPIPE, SIG_IGN, 0) == -1) + return -1; + + introspection_bytes = g_resources_lookup_data ("/org/freedesktop/portal/Documents/org.freedesktop.portal.documents.xml", 0, NULL); + g_assert (introspection_bytes != NULL); + + introspection_data = g_dbus_node_info_new_for_xml (g_bytes_get_data (introspection_bytes, NULL), NULL); + + owner_id = g_bus_own_name (G_BUS_TYPE_SESSION, + "org.freedesktop.portal.Documents", + G_BUS_NAME_OWNER_FLAGS_NONE, + on_bus_acquired, + on_name_acquired, + on_name_lost, + NULL, + NULL); + + g_main_loop_run (loop); + + if (xdp_doc_db_is_dirty (db)) + { + g_autoptr(GError) error = NULL; + if (!xdp_doc_db_save (db, &error)) + g_warning ("db save: %s\n", error->message); + } + + xdp_fuse_exit (); + + g_bus_unown_name (owner_id); + + g_dbus_node_info_unref (introspection_data); + + return 0; +} diff --git a/document-portal/xdp-util.c b/document-portal/xdp-util.c new file mode 100644 index 00000000..1ef6194e --- /dev/null +++ b/document-portal/xdp-util.c @@ -0,0 +1,204 @@ +#include "config.h" +#include +#include +#include +#include "xdp-error.h" + + +static GHashTable *app_ids; + +typedef struct { + char *name; + char *app_id; + gboolean exited; + GList *pending; +} AppIdInfo; + +static void +app_id_info_free (AppIdInfo *info) +{ + g_free (info->name); + g_free (info->app_id); + g_free (info); +} + +static void +ensure_app_ids (void) +{ + if (app_ids == NULL) + app_ids = g_hash_table_new_full (g_str_hash, g_str_equal, + NULL, (GDestroyNotify)app_id_info_free); +} + +static void +got_credentials_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + AppIdInfo *info = user_data; + g_autoptr (GDBusMessage) reply = NULL; + g_autoptr (GError) error = NULL; + GList *l; + + reply = g_dbus_connection_send_message_with_reply_finish (G_DBUS_CONNECTION (source_object), + res, &error); + + if (!info->exited && reply != NULL) + { + GVariant *body = g_dbus_message_get_body (reply); + guint32 pid; + g_autofree char *path = NULL; + g_autofree char *content = NULL; + + g_variant_get (body, "(u)", &pid); + + path = g_strdup_printf ("/proc/%u/cgroup", pid); + + if (g_file_get_contents (path, &content, NULL, NULL)) + { + gchar **lines = g_strsplit (content, "\n", -1); + int i; + + for (i = 0; lines[i] != NULL; i++) + { + if (g_str_has_prefix (lines[i], "1:name=systemd:")) + { + const char *unit = lines[i] + strlen ("1:name=systemd:"); + g_autofree char *scope = g_path_get_basename (unit); + + if (g_str_has_prefix (scope, "xdg-app-") && + g_str_has_suffix (scope, ".scope")) + { + const char *name = scope + strlen("xdg-app-"); + char *dash = strchr (name, '-'); + if (dash != NULL) + { + *dash = 0; + info->app_id = g_strdup (name); + } + } + else + info->app_id = g_strdup (""); + } + } + g_strfreev (lines); + } + } + + for (l = info->pending; l != NULL; l = l->next) + { + GTask *task = l->data; + + if (info->app_id == NULL) + g_task_return_new_error (task, XDP_ERROR, XDP_ERROR_FAILED, + "Can't find app id"); + else + g_task_return_pointer (task, g_strdup (info->app_id), g_free); + } + + g_list_free_full (info->pending, g_object_unref); + info->pending = NULL; + + if (info->app_id == NULL) + g_hash_table_remove (app_ids, info->name); +} + +void +xdp_invocation_lookup_app_id (GDBusMethodInvocation *invocation, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GDBusConnection *connection = g_dbus_method_invocation_get_connection (invocation); + const gchar *sender = g_dbus_method_invocation_get_sender (invocation); + g_autoptr(GTask) task = NULL; + AppIdInfo *info; + + task = g_task_new (invocation, cancellable, callback, user_data); + + ensure_app_ids (); + + info = g_hash_table_lookup (app_ids, sender); + + if (info == NULL) + { + info = g_new0 (AppIdInfo, 1); + info->name = g_strdup (sender); + g_hash_table_insert (app_ids, info->name, info); + } + + if (info->app_id) + g_task_return_pointer (task, g_strdup (info->app_id), g_free); + else + { + if (info->pending == NULL) + { + g_autoptr (GDBusMessage) msg = g_dbus_message_new_method_call ("org.freedesktop.DBus", + "/org/freedesktop/DBus", + "org.freedesktop.DBus", + "GetConnectionUnixProcessID"); + g_dbus_message_set_body (msg, g_variant_new ("(s)", sender)); + + g_dbus_connection_send_message_with_reply (connection, msg, + G_DBUS_SEND_MESSAGE_FLAGS_NONE, + 30000, + NULL, + cancellable, + got_credentials_cb, + info); + } + + info->pending = g_list_prepend (info->pending, g_object_ref (task)); + } +} + +char * +xdp_invocation_lookup_app_id_finish (GDBusMethodInvocation *invocation, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_pointer (G_TASK (result), error); +} + +static void +name_owner_changed (GDBusConnection *connection, + const gchar *sender_name, + const gchar *object_path, + const gchar *interface_name, + const gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + const char *name, *from, *to; + g_variant_get (parameters, "(sss)", &name, &from, &to); + + ensure_app_ids (); + + if (name[0] == ':' && + strcmp (name, from) == 0 && + strcmp (to, "") == 0) + { + AppIdInfo *info = g_hash_table_lookup (app_ids, name); + + if (info != NULL) + { + info->exited = TRUE; + if (info->pending == NULL) + g_hash_table_remove (app_ids, name); + } + } +} + +void +xdp_connection_track_name_owners (GDBusConnection *connection) +{ + g_dbus_connection_signal_subscribe (connection, + "org.freedesktop.DBus", + "org.freedesktop.DBus", + "NameOwnerChanged", + "/org/freedesktop/DBus", + NULL, + G_DBUS_SIGNAL_FLAGS_NONE, + name_owner_changed, + NULL, NULL); +} diff --git a/document-portal/xdp-util.h b/document-portal/xdp-util.h new file mode 100644 index 00000000..314755e9 --- /dev/null +++ b/document-portal/xdp-util.h @@ -0,0 +1,21 @@ +#ifndef XDP_UTIL_H +#define XDP_UTIL_H + +#include + +G_BEGIN_DECLS + +void xdp_invocation_lookup_app_id (GDBusMethodInvocation *invocation, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +char *xdp_invocation_lookup_app_id_finish (GDBusMethodInvocation *invocation, + GAsyncResult *result, + GError **error); + +void xdp_connection_track_name_owners (GDBusConnection *connection); + +G_END_DECLS + +#endif /* XDP_UTIL_H */ diff --git a/lib/Makefile.am.inc b/lib/Makefile.am.inc index 319f5ed0..f42a3dea 100644 --- a/lib/Makefile.am.inc +++ b/lib/Makefile.am.inc @@ -2,7 +2,7 @@ noinst_LTLIBRARIES += libxdgapp.la dbus_built_sources = lib/xdg-app-dbus.c lib/xdg-app-dbus.h systemd_dbus_built_sources = lib/xdg-app-systemd-dbus.c lib/xdg-app-systemd-dbus.h -BUILT_SOURCES = $(dbus_built_sources) $(systemd_dbus_built_sources) +BUILT_SOURCES += $(dbus_built_sources) $(systemd_dbus_built_sources) $(dbus_built_sources) : data/xdg-app-dbus-interfaces.xml $(AM_V_GEN) $(GDBUS_CODEGEN) \ diff --git a/session-helper/Makefile.am.inc b/session-helper/Makefile.am.inc index e6b438ba..c44626c8 100644 --- a/session-helper/Makefile.am.inc +++ b/session-helper/Makefile.am.inc @@ -12,14 +12,9 @@ session-helper/xdg-app-resources.c: session-helper/xdg-app.gresource.xml $(resou $(AM_V_GEN) $(GLIB_COMPILE_RESOURCES) $< \ --target=$@ --sourcedir=$(srcdir) --c-name _gtk --generate-source -# D-BUS service file -%.service: %.service.in config.log - $(AM_V_GEN) $(SED) -e "s|\@libexecdir\@|$(libexecdir)|" $< > $@ - -servicedir = $(DBUS_SERVICE_DIR) -service_in_files = session-helper/xdg-app-session.service.in -service_DATA = session-helper/xdg-app-session.service -DISTCLEANFILES += $(service_DATA) +service_in_files += session-helper/xdg-app-session.service.in +service_DATA += session-helper/xdg-app-session.service +DISTCLEANFILES += session-helper/xdg-app-session.service EXTRA_DIST += session-helper/xdg-app.gresource.xml $(service_in_files)