summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexander Larsson <alexl@redhat.com>2015-07-10 16:46:27 +0200
committerAlexander Larsson <alexl@redhat.com>2015-07-10 16:56:39 +0200
commit929071ad1083494f717f6a5ab6dfcf817d143700 (patch)
tree0f01ac260bc5fe087bb478582f27cd6da832bbed
parentb0e1124f0262d9d4cbfe9c6715eae3a3dcfb4875 (diff)
downloadxdg-app-929071ad1083494f717f6a5ab6dfcf817d143700.tar.gz
Import xdg-document-portal from github repo
This pulls in the daemon code from: https://github.com/alexlarsson/xdg-document-portal/ We need this in xdg-app because we need to set up the mounts correctly.
-rw-r--r--.gitignore5
-rw-r--r--Makefile.am10
-rw-r--r--configure.ac3
-rw-r--r--data/org.freedesktop.portal.documents.xml50
-rw-r--r--document-portal/Makefile.am.inc49
-rw-r--r--document-portal/gvdb/.gitignore2
-rw-r--r--document-portal/gvdb/README7
-rw-r--r--document-portal/gvdb/gvdb-builder.c521
-rw-r--r--document-portal/gvdb/gvdb-builder.h55
-rw-r--r--document-portal/gvdb/gvdb-format.h85
-rw-r--r--document-portal/gvdb/gvdb-reader.c718
-rw-r--r--document-portal/gvdb/gvdb-reader.h63
-rw-r--r--document-portal/gvdb/gvdb.doap32
-rw-r--r--document-portal/org.freedesktop.portal.Documents.service.in3
-rw-r--r--document-portal/xdg-document-portal.gresource.xml6
-rw-r--r--document-portal/xdp-doc-db.c800
-rw-r--r--document-portal/xdp-doc-db.h55
-rw-r--r--document-portal/xdp-enums.h17
-rw-r--r--document-portal/xdp-error.c22
-rw-r--r--document-portal/xdp-error.h25
-rw-r--r--document-portal/xdp-fuse.c1787
-rw-r--r--document-portal/xdp-fuse.h15
-rw-r--r--document-portal/xdp-main.c564
-rw-r--r--document-portal/xdp-util.c204
-rw-r--r--document-portal/xdp-util.h21
-rw-r--r--lib/Makefile.am.inc2
-rw-r--r--session-helper/Makefile.am.inc11
27 files changed, 5123 insertions, 9 deletions
diff --git a/.gitignore b/.gitignore
index 65f36d4..e7ab0e9 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 0c235a7..c23cff8 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 4c4e4ed..2c2be12 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 0000000..3acf79e
--- /dev/null
+++ b/data/org.freedesktop.portal.documents.xml
@@ -0,0 +1,50 @@
+<!DOCTYPE node PUBLIC
+"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
+"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
+
+<!--
+ Copyright (C) 2015 Red Hat, Inc.
+
+ 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 License, 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, write to the
+ Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ Boston, MA 02110-1301, USA.
+
+ Author: Alexander Larsson <alexl@redhat.com>
+-->
+
+<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd">
+ <interface name='org.freedesktop.portal.Documents'>
+ <method name="Add">
+ <arg type='s' name='uri' direction='in'/>
+ <arg type='u' name='doc_id' direction='out'/>
+ </method>
+ <method name="AddLocal">
+ <arg type='h' name='fd' direction='in'/>
+ <arg type='u' name='doc_id' direction='out'/>
+ </method>
+ <method name="GrantPermissions">
+ <arg type='u' name='doc_id' direction='in'/>
+ <arg type='s' name='app_id' direction='in'/>
+ <arg type='as' name='permissions' direction='in'/>
+ </method>
+ <method name="RevokePermissions">
+ <arg type='u' name='doc_id' direction='in'/>
+ <arg type='s' name='app_id' direction='in'/>
+ <arg type='as' name='permissions' direction='in'/>
+ </method>
+ <method name="Delete">
+ <arg type='u' name='doc_id' direction='in'/>
+ </method>
+ </interface>
+</node>
diff --git a/document-portal/Makefile.am.inc b/document-portal/Makefile.am.inc
new file mode 100644
index 0000000..9b470ca
--- /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 0000000..8b5dee6
--- /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 0000000..94e6c5d
--- /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 0000000..90ea50b
--- /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 <http://www.gnu.org/licenses/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#include "gvdb-builder.h"
+#include "gvdb-format.h"
+
+#include <glib.h>
+#include <fcntl.h>
+#if !defined(G_OS_WIN32) || !defined(_MSC_VER)
+#include <unistd.h>
+#endif
+#include <string.h>
+
+
+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 0000000..8ec05c8
--- /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 <http://www.gnu.org/licenses/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#ifndef __gvdb_builder_h__
+#define __gvdb_builder_h__
+
+#include <gio/gio.h>
+
+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 0000000..486e854
--- /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 <http://www.gnu.org/licenses/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#ifndef __gvdb_format_h__
+#define __gvdb_format_h__
+
+#include <glib.h>
+
+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 0000000..08b5bc8
--- /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 <http://www.gnu.org/licenses/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#include "gvdb-reader.h"
+#include "gvdb-format.h"
+
+#include <string.h>
+
+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 0000000..241b41a
--- /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 <http://www.gnu.org/licenses/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#ifndef __gvdb_reader_h__
+#define __gvdb_reader_h__
+
+#include <glib.h>
+
+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 0000000..b4ae60c
--- /dev/null
+++ b/document-portal/gvdb/gvdb.doap
@@ -0,0 +1,32 @@
+<?xml version='1.0' encoding='utf-8'?>
+
+<Project xmlns='http://usefulinc.com/ns/doap#'
+ xmlns:foaf='http://xmlns.com/foaf/0.1/'
+ xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'
+ xmlns:gnome='http://api.gnome.org/doap-extensions#'>
+
+ <name xml:lang='en'>gvdb</name>
+ <shortdesc xml:lang='en'>GVariant Database file</shortdesc>
+ <description xml:lang='en'>
+ 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).
+ </description>
+
+ <maintainer>
+ <foaf:Person>
+ <foaf:name>Ryan Lortie</foaf:name>
+ <foaf:mbox rdf:resource='mailto:desrt@desrt.ca'/>
+ <gnome:userid>ryanl</gnome:userid>
+ </foaf:Person>
+ </maintainer>
+
+</Project>
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 0000000..7517274
--- /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 0000000..373d1c3
--- /dev/null
+++ b/document-portal/xdg-document-portal.gresource.xml
@@ -0,0 +1,6 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<gresources>
+ <gresource prefix='/org/freedesktop/portal/Documents'>
+ <file alias="org.freedesktop.portal.documents.xml">data/org.freedesktop.portal.documents.xml</file>
+ </gresource>
+</gresources>
diff --git a/document-portal/xdp-doc-db.c b/document-portal/xdp-doc-db.c
new file mode 100644
index 0000000..9c78d3d
--- /dev/null
+++ b/document-portal/xdp-doc-db.c
@@ -0,0 +1,800 @@
+#include "config.h"
+
+#include <string.h>
+#include <glib/gprintf.h>
+#include <gio/gio.h>
+
+#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 0000000..74aca99
--- /dev/null
+++ b/document-portal/xdp-doc-db.h
@@ -0,0 +1,55 @@
+#ifndef XDP_DB
+#define XDP_DB
+
+#include <glib-object.h>
+
+#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 0000000..4169fab
--- /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 0000000..d144393
--- /dev/null
+++ b/document-portal/xdp-error.c
@@ -0,0 +1,22 @@
+#include "xdp-error.h"
+
+#include <gio/gio.h>
+
+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 0000000..3eab960
--- /dev/null
+++ b/document-portal/xdp-error.h
@@ -0,0 +1,25 @@
+#ifndef XDP_ERROR_H
+#define XDP_ERROR_H
+
+#include <glib.h>
+
+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 0000000..b6feea6
--- /dev/null
+++ b/document-portal/xdp-fuse.c
@@ -0,0 +1,1787 @@
+#include "config.h"
+
+#define FUSE_USE_VERSION 26
+
+#include <glib-unix.h>
+
+#include <fuse_lowlevel.h>
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <assert.h>
+#include <glib/gprintf.h>
+#include <gio/gio.h>
+
+#include "xdp-error.h"
+#include "xdp-fuse.h"
+
+/* Layout:
+
+ "/ (STD_DIRS:1)
+ "by-app/" (STD_DIRS:2)
+ "org.gnome.gedit/" (APP_DIR:app id)
+ "$id/" (APP_DOC_DIR:app_id<<32|doc_id)
+ <same as DOC_DIR>
+ "in-homedir/" (APP_DOC_DIR:1)
+ "$id" (DOC_DIR:doc_idid)
+ $basename (DOC_FILE:doc_id)
+ $tmpfile (TMPFILE:tmp_id)
+*/
+
+#define BY_APP_INO 2
+
+#define IN_HOMEDIR_APP_ID 1
+
+#define NON_DOC_DIR_PERMS 0500
+#define DOC_DIR_PERMS 0700
+
+/* The (fake) directories don't really change */
+#define DIRS_ATTR_CACHE_TIME 60.0
+
+/* We pretend that the file is hardlinked. This causes most apps to do
+ a truncating overwrite, which suits us better, as we do the atomic
+ rename ourselves anyway. This way we don't weirdly change the inode
+ after the rename. */
+#define DOC_FILE_NLINK 2
+
+typedef enum {
+ STD_DIRS_INO_CLASS,
+ DOC_DIR_INO_CLASS,
+ DOC_FILE_INO_CLASS,
+ TMPFILE_INO_CLASS,
+ APP_DIR_INO_CLASS,
+ APP_DOC_DIR_INO_CLASS,
+} XdpInodeClass;
+
+#define BY_APP_NAME "by-app"
+#define IN_HOMEDIR_NAME "in-homedir"
+
+static XdpDocDb *db;
+
+static GHashTable *app_name_to_id;
+static GHashTable *app_id_to_name;
+static guint32 next_app_id;
+
+static guint32 next_tmp_id;
+
+typedef struct
+{
+ guint64 parent_inode;
+ char *name;
+
+ char *backing_path;
+ guint32 tmp_id;
+} XdpTmp;
+
+typedef struct
+{
+ int fd;
+ fuse_ino_t inode;
+ int trunc_fd;
+ char *trunc_path;
+ char *real_path;
+ gboolean truncated;
+ gboolean readonly;
+ guint32 tmp_id;
+} XdpFh;
+
+static GList *tmp_files = NULL;
+static GList *open_files = NULL;
+
+static XdpTmp *
+find_tmp_by_name (guint64 parent_inode,
+ const char *name)
+{
+ GList *l;
+
+ for (l = tmp_files; l != NULL; l = l->next)
+ {
+ XdpTmp *tmp = l->data;
+ if (tmp->parent_inode == parent_inode &&
+ strcmp (tmp->name, name) == 0)
+ return tmp;
+ }
+
+ return NULL;
+}
+
+static XdpTmp *
+find_tmp_by_id (guint32 tmp_id)
+{
+ GList *l;
+
+ for (l = tmp_files; l != NULL; l = l->next)
+ {
+ XdpTmp *tmp = l->data;
+ if (tmp->tmp_id == tmp_id)
+ return tmp;
+ }
+
+ return NULL;
+}
+
+static XdpInodeClass
+get_class (guint64 inode)
+{
+ return (inode >> (64-8)) & 0xff;
+}
+
+static guint64
+get_class_ino (guint64 inode)
+{
+ return inode & ((1L << (64-8)) - 1);
+}
+
+static guint32
+get_app_id_from_app_doc_ino (guint64 inode)
+{
+ return inode >> 32;
+}
+
+static guint32
+get_doc_id_from_app_doc_ino (guint64 inode)
+{
+ return inode & 0xffffffff;
+}
+
+static guint64
+make_inode (XdpInodeClass class, guint64 inode)
+{
+ return ((guint64)class) << (64-8) | (inode & 0xffffffffffffff);
+}
+
+static guint64
+make_app_doc_dir_inode (guint32 app_id, guint32 doc_id)
+{
+ return make_inode (APP_DOC_DIR_INO_CLASS,
+ ((guint64)app_id << 32) | (guint64)doc_id);
+}
+
+static gboolean
+name_looks_like_id (const char *name)
+{
+ int i;
+
+ /* No zeros in front, we need canonical form */
+ if (name[0] == '0')
+ return FALSE;
+
+ for (i = 0; i < 8; i++)
+ {
+ char c = name[i];
+ if (c == 0)
+ break;
+
+ if (!g_ascii_isdigit(c) &&
+ !(c >= 'a' && c <= 'f'))
+ return FALSE;
+ }
+
+ if (name[i] != 0)
+ return FALSE;
+
+ return TRUE;
+}
+
+static guint32
+get_app_id_from_name (const char *name)
+{
+ guint32 id;
+ char *myname;
+
+ id = GPOINTER_TO_UINT (g_hash_table_lookup (app_name_to_id, name));
+
+ if (id != 0)
+ return id;
+
+ id = next_app_id++;
+
+ /* We rely this to not overwrap into the high byte in the inode */
+ g_assert (id < 0x00ffffff);
+
+ myname = g_strdup (name);
+ g_hash_table_insert (app_name_to_id, myname, GUINT_TO_POINTER (id));
+ g_hash_table_insert (app_id_to_name, GUINT_TO_POINTER (id), myname);
+ return id;
+}
+
+static const char *
+get_app_name_from_id (guint32 id)
+{
+ return g_hash_table_lookup (app_id_to_name, GUINT_TO_POINTER (id));
+}
+
+static void
+fill_app_name_hash (void)
+{
+ char **keys;
+ int i;
+
+ keys = xdp_doc_db_list_apps (db);
+ for (i = 0; keys[i] != NULL; i++)
+ get_app_id_from_name (keys[i]);
+ g_strfreev (keys);
+}
+
+static XdpFh *
+xdp_fh_new (fuse_ino_t inode, struct fuse_file_info *fi, int fd, XdpTmp *tmp)
+{
+ XdpFh *fh = g_new0 (XdpFh, 1);
+ fh->inode = inode;
+ fh->fd = fd;
+ if (tmp)
+ fh->tmp_id = tmp->tmp_id;
+ fh->trunc_fd = -1;
+
+ open_files = g_list_prepend (open_files, fh);
+
+ fi->fh = (gsize)fh;
+ return fh;
+}
+
+static void
+xdp_fh_free (XdpFh *fh)
+{
+ open_files = g_list_remove (open_files, fh);
+
+ if (fh->truncated)
+ {
+ fsync (fh->trunc_fd);
+ if (rename (fh->trunc_path, fh->real_path) != 0)
+ g_warning ("Unable to replace truncated document");
+ }
+ else if (fh->trunc_path)
+ unlink (fh->trunc_path);
+
+ if (fh->fd >= 0)
+ close (fh->fd);
+ if (fh->trunc_fd >= 0)
+ close (fh->trunc_fd);
+
+ g_clear_pointer (&fh->trunc_path, g_free);
+ g_clear_pointer (&fh->real_path, g_free);
+
+ g_free (fh);
+}
+
+static int
+xdp_fh_get_fd (XdpFh *fh)
+{
+ if (fh->truncated)
+ return fh->trunc_fd;
+ else
+ return fh->fd;
+}
+
+static int
+get_user_perms (const struct stat *stbuf)
+{
+ /* Strip out exec and setuid bits */
+ return stbuf->st_mode & 0666;
+}
+
+static double
+get_attr_cache_time (int st_mode)
+{
+ if (S_ISDIR (st_mode))
+ return DIRS_ATTR_CACHE_TIME;
+ return 0.0;
+}
+
+static double
+get_entry_cache_time (int st_mode)
+{
+ if (S_ISDIR (st_mode))
+ return DIRS_ATTR_CACHE_TIME;
+ return 1.0;
+}
+
+static gboolean
+app_can_see_doc (GVariant *doc, guint32 app_id)
+{
+ const char *app_name = get_app_name_from_id (app_id);
+ if (app_name != NULL &&
+ xdp_doc_has_permissions (doc, app_name, XDP_PERMISSION_FLAGS_READ))
+ return TRUE;
+
+ if (app_id == IN_HOMEDIR_APP_ID)
+ {
+ g_autofree char *path = xdp_doc_dup_path (doc);
+
+ if (g_str_has_prefix (path, g_get_home_dir ()))
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static int
+xdp_stat (fuse_ino_t ino,
+ struct stat *stbuf,
+ GVariant **doc_out)
+{
+ XdpInodeClass class = get_class (ino);
+ guint64 class_ino = get_class_ino (ino);
+ g_autoptr (GVariant) doc = NULL;
+ g_autofree char *path = NULL;
+ struct stat tmp_stbuf;
+ XdpTmp *tmp;
+
+ stbuf->st_ino = ino;
+
+ switch (class)
+ {
+ case STD_DIRS_INO_CLASS:
+
+ switch (class_ino)
+ {
+ case FUSE_ROOT_ID:
+ stbuf->st_mode = S_IFDIR | NON_DOC_DIR_PERMS;
+ stbuf->st_nlink = 2;
+ break;
+
+ case BY_APP_INO:
+ stbuf->st_mode = S_IFDIR | NON_DOC_DIR_PERMS;
+ stbuf->st_nlink = 2;
+ break;
+
+ default:
+ return ENOENT;
+ }
+ break;
+
+ case APP_DIR_INO_CLASS:
+ if (class_ino != IN_HOMEDIR_APP_ID && get_app_name_from_id (class_ino) == 0)
+ return ENOENT;
+
+ stbuf->st_mode = S_IFDIR | NON_DOC_DIR_PERMS;
+ stbuf->st_nlink = 2;
+ break;
+
+ case APP_DOC_DIR_INO_CLASS:
+ {
+ guint32 app_id = get_app_id_from_app_doc_ino (class_ino);
+ guint32 doc_id = get_doc_id_from_app_doc_ino (class_ino);
+
+ doc = xdp_doc_db_lookup_doc (db, doc_id);
+ if (doc == NULL ||
+ !app_can_see_doc (doc, app_id))
+ return ENOENT;
+
+ stbuf->st_mode = S_IFDIR | DOC_DIR_PERMS;
+ stbuf->st_nlink = 2;
+ break;
+ }
+
+ case DOC_DIR_INO_CLASS:
+ doc = xdp_doc_db_lookup_doc (db, class_ino);
+
+ if (doc == NULL)
+ return ENOENT;
+
+ stbuf->st_mode = S_IFDIR | DOC_DIR_PERMS;
+ stbuf->st_nlink = 2;
+ break;
+
+ case DOC_FILE_INO_CLASS:
+ doc = xdp_doc_db_lookup_doc (db, class_ino);
+
+ if (doc == NULL)
+ return ENOENT;
+
+ stbuf->st_nlink = DOC_FILE_NLINK;
+
+ path = xdp_doc_dup_path (doc);
+ if (stat (path, &tmp_stbuf) != 0)
+ return ENOENT;
+
+ stbuf->st_mode = S_IFREG | get_user_perms (&tmp_stbuf);
+ stbuf->st_size = tmp_stbuf.st_size;
+ stbuf->st_uid = tmp_stbuf.st_uid;
+ stbuf->st_gid = tmp_stbuf.st_gid;
+ stbuf->st_blksize = tmp_stbuf.st_blksize;
+ stbuf->st_blocks = tmp_stbuf.st_blocks;
+ stbuf->st_atim = tmp_stbuf.st_atim;
+ stbuf->st_mtim = tmp_stbuf.st_mtim;
+ stbuf->st_ctim = tmp_stbuf.st_ctim;
+ break;
+
+ case TMPFILE_INO_CLASS:
+ tmp = find_tmp_by_id (class_ino);
+ if (tmp == NULL)
+ return ENOENT;
+
+ stbuf->st_mode = S_IFREG;
+ stbuf->st_nlink = DOC_FILE_NLINK;
+
+ if (stat (tmp->backing_path, &tmp_stbuf) != 0)
+ return ENOENT;
+
+ stbuf->st_mode = S_IFREG | get_user_perms (&tmp_stbuf);
+ stbuf->st_size = tmp_stbuf.st_size;
+ stbuf->st_uid = tmp_stbuf.st_uid;
+ stbuf->st_gid = tmp_stbuf.st_gid;
+ stbuf->st_blksize = tmp_stbuf.st_blksize;
+ stbuf->st_blocks = tmp_stbuf.st_blocks;
+ stbuf->st_atim = tmp_stbuf.st_atim;
+ stbuf->st_mtim = tmp_stbuf.st_mtim;
+ stbuf->st_ctim = tmp_stbuf.st_ctim;
+ break;
+
+ default:
+ return ENOENT;
+ }
+
+ if (doc && doc_out)
+ *doc_out = g_steal_pointer (&doc);
+
+ return 0;
+}
+
+static int
+xdp_fstat (XdpFh *fh,
+ struct stat *stbuf)
+{
+ struct stat tmp_stbuf;
+ int fd;
+
+ fd = xdp_fh_get_fd (fh);
+ if (fd < 0)
+ return -ENOSYS;
+
+ if (fstat (fd, &tmp_stbuf) != 0)
+ return -errno;
+
+ stbuf->st_nlink = DOC_FILE_NLINK;
+ stbuf->st_mode = S_IFREG | get_user_perms (&tmp_stbuf);
+ stbuf->st_size = tmp_stbuf.st_size;
+ stbuf->st_uid = tmp_stbuf.st_uid;
+ stbuf->st_gid = tmp_stbuf.st_gid;
+ stbuf->st_blksize = tmp_stbuf.st_blksize;
+ stbuf->st_blocks = tmp_stbuf.st_blocks;
+ stbuf->st_atim = tmp_stbuf.st_atim;
+ stbuf->st_mtim = tmp_stbuf.st_mtim;
+ stbuf->st_ctim = tmp_stbuf.st_ctim;
+
+ return 0;
+}
+
+static void
+xdp_fuse_getattr (fuse_req_t req,
+ fuse_ino_t ino,
+ struct fuse_file_info *fi)
+{
+ struct stat stbuf = { 0 };
+ GList *l;
+ int res;
+
+ g_debug ("xdp_fuse_getattr %lx (fi=%p)", ino, fi);
+
+ /* Fuse passes fi in to verify EOF during read/write/seek, but not during fstat */
+ if (fi != NULL)
+ {
+ XdpFh *fh = (gpointer)fi->fh;
+
+ res = xdp_fstat (fh, &stbuf);
+ if (res == 0)
+ {
+ fuse_reply_attr (req, &stbuf, get_attr_cache_time (stbuf.st_mode));
+ return;
+ }
+ }
+
+ for (l = open_files; l != NULL; l = l->next)
+ {
+ XdpFh *fh = l->data;
+ if (fh->inode == ino)
+ {
+ res = xdp_fstat (fh, &stbuf);
+ if (res == 0)
+ {
+ fuse_reply_attr (req, &stbuf, get_attr_cache_time (stbuf.st_mode));
+ return;
+ }
+ }
+ }
+
+ if ((res = xdp_stat (ino, &stbuf, NULL)) != 0)
+ fuse_reply_err (req, res);
+ else
+ fuse_reply_attr (req, &stbuf, get_attr_cache_time (stbuf.st_mode));
+}
+
+static int
+xdp_lookup (fuse_ino_t parent,
+ const char *name,
+ fuse_ino_t *inode,
+ struct stat *stbuf,
+ GVariant **doc_out,
+ XdpTmp **tmp_out)
+{
+ XdpInodeClass parent_class = get_class (parent);
+ guint64 parent_class_ino = get_class_ino (parent);
+ g_autoptr (GVariant) doc = NULL;
+ XdpTmp *tmp;
+
+ if (doc_out)
+ *doc_out = NULL;
+ if (tmp_out)
+ *tmp_out = NULL;
+
+ switch (parent_class)
+ {
+ case STD_DIRS_INO_CLASS:
+
+ switch (parent_class_ino)
+ {
+ case FUSE_ROOT_ID:
+ if (strcmp (name, BY_APP_NAME) == 0)
+ {
+ *inode = make_inode (STD_DIRS_INO_CLASS, BY_APP_INO);
+ if (xdp_stat (*inode, stbuf, NULL) == 0)
+ return 0;
+ }
+ else if (strcmp (name, IN_HOMEDIR_NAME) == 0)
+ {
+ *inode = make_inode (APP_DIR_INO_CLASS, IN_HOMEDIR_APP_ID);
+ if (xdp_stat (*inode, stbuf, NULL) == 0)
+ return 0;
+ }
+ else if (name_looks_like_id (name))
+ {
+ *inode = make_inode (DOC_DIR_INO_CLASS,
+ xdb_doc_id_from_name (name));
+ if (xdp_stat (*inode, stbuf, NULL) == 0)
+ return 0;
+ }
+
+ break;
+
+ case BY_APP_INO:
+ if (g_dbus_is_name (name) && !g_dbus_is_unique_name (name))
+ {
+ guint32 app_id = get_app_id_from_name (name);
+ *inode = make_inode (APP_DIR_INO_CLASS, app_id);
+ if (xdp_stat (*inode, stbuf, NULL) == 0)
+ return 0;
+ }
+
+ break;
+
+ default:
+ break;
+ }
+ break;
+
+ case APP_DIR_INO_CLASS:
+ {
+ if (name_looks_like_id (name))
+ {
+ *inode = make_app_doc_dir_inode (parent_class_ino,
+ xdb_doc_id_from_name (name));
+ if (xdp_stat (*inode, stbuf, NULL) == 0)
+ return 0;
+ }
+ }
+
+ break;
+
+ case APP_DOC_DIR_INO_CLASS:
+ case DOC_DIR_INO_CLASS:
+ if (parent_class == APP_DOC_DIR_INO_CLASS)
+ doc = xdp_doc_db_lookup_doc (db, get_doc_id_from_app_doc_ino (parent_class_ino));
+ else
+ doc = xdp_doc_db_lookup_doc (db, parent_class_ino);
+ if (doc != NULL)
+ {
+ g_autofree char *basename = xdp_doc_dup_basename (doc);
+ if (strcmp (name, basename) == 0)
+ {
+ *inode = make_inode (DOC_FILE_INO_CLASS, parent_class_ino);
+ if (xdp_stat (*inode, stbuf, NULL) == 0)
+ {
+ if (doc_out)
+ *doc_out = g_steal_pointer (&doc);
+ return 0;
+ }
+
+ break;
+ }
+ }
+
+ tmp = find_tmp_by_name (parent, name);
+ if (tmp != NULL)
+ {
+ *inode = make_inode (TMPFILE_INO_CLASS, tmp->tmp_id);
+ if (xdp_stat (*inode, stbuf, NULL) == 0)
+ {
+ if (doc_out)
+ *doc_out = g_steal_pointer (&doc);
+ if (tmp_out)
+ *tmp_out = tmp;
+ return 0;
+ }
+
+ break;
+ }
+
+ break;
+
+ case TMPFILE_INO_CLASS:
+ case DOC_FILE_INO_CLASS:
+ return ENOTDIR;
+
+ default:
+ break;
+ }
+
+ return ENOENT;
+}
+
+static void
+xdp_fuse_lookup (fuse_req_t req,
+ fuse_ino_t parent,
+ const char *name)
+{
+ struct fuse_entry_param e = {0};
+ int res;
+
+ g_debug ("xdp_fuse_lookup %lx/%s", parent, name);
+
+ memset (&e, 0, sizeof(e));
+
+ res = xdp_lookup (parent, name, &e.ino, &e.attr, NULL, NULL);
+
+ if (res == 0)
+ {
+ e.attr_timeout = get_attr_cache_time (e.attr.st_mode);
+ e.entry_timeout = get_entry_cache_time (e.attr.st_mode);
+ fuse_reply_entry (req, &e);
+ }
+ else
+ {
+ fuse_reply_err (req, res);
+ }
+}
+
+struct dirbuf {
+ char *p;
+ size_t size;
+};
+
+static void
+dirbuf_add (fuse_req_t req,
+ struct dirbuf *b,
+ const char *name,
+ fuse_ino_t ino)
+{
+ struct stat stbuf;
+
+ size_t oldsize = b->size;
+ b->size += fuse_add_direntry (req, NULL, 0, name, NULL, 0);
+ b->p = (char *) g_realloc (b->p, b->size);
+ memset (&stbuf, 0, sizeof (stbuf));
+ stbuf.st_ino = ino;
+ fuse_add_direntry (req, b->p + oldsize,
+ b->size - oldsize,
+ name, &stbuf,
+ b->size);
+}
+
+static void
+dirbuf_add_docs (fuse_req_t req,
+ struct dirbuf *b,
+ guint32 app_id)
+{
+ g_autofree guint32 *docs = NULL;
+ guint64 inode;
+ int i;
+ g_autofree char *doc_name = NULL;
+
+ docs = xdp_doc_db_list_docs (db);
+ for (i = 0; docs[i] != 0; i++)
+ {
+ if (app_id)
+ {
+ g_autoptr(GVariant) doc = xdp_doc_db_lookup_doc (db, docs[i]);
+ if (doc == NULL ||
+ !app_can_see_doc (doc, app_id))
+ continue;
+ }
+ if (app_id)
+ inode = make_app_doc_dir_inode (app_id, docs[i]);
+ else
+ inode = make_inode (DOC_DIR_INO_CLASS, docs[i]);
+ doc_name = xdb_doc_name_from_id (docs[i]);
+ dirbuf_add (req, b, doc_name, inode);
+
+ }
+}
+
+static void
+dirbuf_add_doc_file (fuse_req_t req,
+ struct dirbuf *b,
+ GVariant *doc,
+ guint32 doc_id)
+{
+ struct stat tmp_stbuf;
+ g_autofree char *path = xdp_doc_dup_path (doc);
+ g_autofree char *basename = xdp_doc_dup_basename (doc);
+ if (stat (path, &tmp_stbuf) == 0)
+ dirbuf_add (req, b, basename,
+ make_inode (DOC_FILE_INO_CLASS, doc_id));
+}
+
+static void
+dirbuf_add_tmp_files (fuse_req_t req,
+ struct dirbuf *b,
+ guint64 dir_inode)
+{
+ GList *l;
+
+ for (l = tmp_files; l != NULL; l = l->next)
+ {
+ XdpTmp *tmp = l->data;
+ if (tmp->parent_inode == dir_inode)
+ dirbuf_add (req, b, tmp->name,
+ make_inode (TMPFILE_INO_CLASS, tmp->tmp_id));
+ }
+}
+
+static int
+reply_buf_limited (fuse_req_t req,
+ const char *buf,
+ size_t bufsize,
+ off_t off,
+ size_t maxsize)
+{
+ if (off < bufsize)
+ return fuse_reply_buf (req, buf + off,
+ MIN (bufsize - off, maxsize));
+ else
+ return fuse_reply_buf (req, NULL, 0);
+}
+
+static void
+xdp_fuse_readdir (fuse_req_t req, fuse_ino_t ino, size_t size,
+ off_t off, struct fuse_file_info *fi)
+{
+ struct dirbuf *b = (struct dirbuf *)(fi->fh);
+
+ reply_buf_limited (req, b->p, b->size, off, size);
+}
+
+static void
+xdp_fuse_opendir (fuse_req_t req,
+ fuse_ino_t ino,
+ struct fuse_file_info *fi)
+{
+ struct stat stbuf = {0};
+ struct dirbuf b = {0};
+ XdpInodeClass class;
+ guint64 class_ino;
+ g_autoptr (GVariant) doc = NULL;
+ g_autofree char *basename = NULL;
+ int res;
+
+ g_debug ("xdp_fuse_opendir %lx", ino);
+
+ if ((res = xdp_stat (ino, &stbuf, &doc)) != 0)
+ {
+ fuse_reply_err (req, res);
+ return;
+ }
+
+ if ((stbuf.st_mode & S_IFMT) != S_IFDIR)
+ {
+ fuse_reply_err (req, ENOTDIR);
+ return;
+ }
+
+ class = get_class (ino);
+ class_ino = get_class_ino (ino);
+
+ switch (class)
+ {
+ case STD_DIRS_INO_CLASS:
+ switch (class_ino)
+ {
+ case FUSE_ROOT_ID:
+ dirbuf_add (req, &b, ".", FUSE_ROOT_ID);
+ dirbuf_add (req, &b, "..", FUSE_ROOT_ID);
+ dirbuf_add (req, &b, BY_APP_NAME,
+ make_inode (STD_DIRS_INO_CLASS, BY_APP_INO));
+ dirbuf_add (req, &b, IN_HOMEDIR_NAME,
+ make_inode (APP_DIR_INO_CLASS, IN_HOMEDIR_APP_ID));
+ dirbuf_add_docs (req, &b, 0);
+ break;
+
+ case BY_APP_INO:
+ dirbuf_add (req, &b, ".", ino);
+ dirbuf_add (req, &b, "..", FUSE_ROOT_ID);
+
+ /* Update for any possible new app */
+ fill_app_name_hash ();
+
+ {
+ GHashTableIter iter;
+ gpointer key, value;
+
+ g_hash_table_iter_init (&iter, app_name_to_id);
+ while (g_hash_table_iter_next (&iter, &key, &value))
+ {
+ const char *name = key;
+ guint32 id = GPOINTER_TO_UINT(value);
+
+ dirbuf_add (req, &b, name,
+ make_inode (APP_DIR_INO_CLASS, id));
+ }
+ }
+ break;
+
+ default:
+ break;
+ }
+ break;
+
+ case APP_DIR_INO_CLASS:
+ {
+ dirbuf_add (req, &b, ".", ino);
+ dirbuf_add (req, &b, "..", make_inode (STD_DIRS_INO_CLASS, BY_APP_INO));
+ dirbuf_add_docs (req, &b, class_ino);
+ break;
+ }
+
+ break;
+
+ case DOC_DIR_INO_CLASS:
+ dirbuf_add (req, &b, ".", ino);
+ dirbuf_add (req, &b, "..", FUSE_ROOT_ID);
+ dirbuf_add_doc_file (req, &b, doc, class_ino);
+ dirbuf_add_tmp_files (req, &b, ino);
+ break;
+
+ case APP_DOC_DIR_INO_CLASS:
+ dirbuf_add (req, &b, ".", ino);
+ dirbuf_add (req, &b, "..", make_inode (APP_DIR_INO_CLASS,
+ get_app_id_from_app_doc_ino (class_ino)));
+ dirbuf_add_doc_file (req, &b, doc,
+ get_doc_id_from_app_doc_ino (class_ino));
+ dirbuf_add_tmp_files (req, &b, ino);
+ break;
+
+ case DOC_FILE_INO_CLASS:
+ case TMPFILE_INO_CLASS:
+ /* These should have returned ENOTDIR above */
+ default:
+ break;
+ }
+
+ if (b.p == NULL)
+ fuse_reply_err (req, EIO);
+ else
+ {
+ fi->fh = (gsize)g_memdup (&b, sizeof (b));
+ if (fuse_reply_open (req, fi) == -ENOENT)
+ {
+ g_free (b.p);
+ g_free ((gpointer)(fi->fh));
+ }
+ }
+}
+
+static void
+xdp_fuse_releasedir (fuse_req_t req,
+ fuse_ino_t ino,
+ struct fuse_file_info *fi)
+{
+ struct dirbuf *b = (struct dirbuf *)(fi->fh);
+ g_free (b->p);
+ g_free (b);
+ fuse_reply_err (req, 0);
+}
+
+static int
+get_open_flags (struct fuse_file_info *fi)
+{
+ /* TODO: Maybe limit the flags set more */
+ return fi->flags & ~(O_EXCL|O_CREAT);
+}
+
+static char *
+create_tmp_for_doc (GVariant *doc, int flags, int *fd_out)
+{
+ g_autofree char *dirname = xdp_doc_dup_dirname (doc);
+ g_autofree char *basename = xdp_doc_dup_basename (doc);
+ g_autofree char *template = g_strconcat (dirname, "/.", basename, ".XXXXXX", NULL);
+ int fd;
+
+ fd = g_mkstemp_full (template, flags, 0600);
+ if (fd == -1)
+ return NULL;
+
+ *fd_out = fd;
+ return g_steal_pointer (&template);
+}
+
+
+static XdpTmp *
+tmpfile_new (fuse_ino_t parent,
+ const char *name,
+ GVariant *doc,
+ int flags,
+ int *fd_out)
+{
+ XdpTmp *tmp;
+ g_autofree char *path = NULL;
+ int fd;
+
+ path = create_tmp_for_doc (doc, flags, &fd);
+ if (path == NULL)
+ return NULL;
+
+ tmp = g_new0 (XdpTmp, 1);
+ tmp->parent_inode = parent;
+ tmp->name = g_strdup (name);
+ tmp->backing_path = g_steal_pointer (&path);
+ tmp->tmp_id = next_tmp_id++;
+
+ if (fd_out)
+ *fd_out = fd;
+ else
+ close (fd);
+
+ tmp_files = g_list_prepend (tmp_files, tmp);
+
+ return tmp;
+}
+
+static void
+tmpfile_free (XdpTmp *tmp)
+{
+ GList *l;
+
+ tmp_files = g_list_remove (tmp_files, tmp);
+
+ for (l = open_files; l != NULL; l = l->next)
+ {
+ XdpFh *fh = l->data;
+ if (fh->tmp_id == tmp->tmp_id)
+ fh->tmp_id = 0;
+ }
+
+ if (tmp->backing_path)
+ unlink (tmp->backing_path);
+
+ g_free (tmp->name);
+ g_free (tmp->backing_path);
+ g_free (tmp);
+}
+
+static void
+xdp_fuse_open (fuse_req_t req,
+ fuse_ino_t ino,
+ struct fuse_file_info *fi)
+{
+ XdpInodeClass class = get_class (ino);
+ guint64 class_ino = get_class_ino (ino);
+ struct stat stbuf = {0};
+ g_autoptr (GVariant) doc = NULL;
+ g_autofree char *path = NULL;
+ XdpTmp *tmp;
+ int fd, res;
+ XdpFh *fh;
+
+ g_debug ("xdp_fuse_open %lx", ino);
+
+ if ((res = xdp_stat (ino, &stbuf, &doc)) != 0)
+ {
+ fuse_reply_err (req, res);
+ return;
+ }
+
+ if ((stbuf.st_mode & S_IFMT) != S_IFREG)
+ {
+ fuse_reply_err (req, EISDIR);
+ return;
+ }
+
+ if (doc && class == DOC_FILE_INO_CLASS)
+ {
+ g_autofree char *write_path = NULL;
+ int write_fd = -1;
+
+ path = xdp_doc_dup_path (doc);
+
+ if ((fi->flags & 3) != O_RDONLY)
+ {
+ if (access (path, W_OK) != 0)
+ {
+ fuse_reply_err (req, errno);
+ return;
+ }
+ write_path = create_tmp_for_doc (doc, O_RDWR, &write_fd);
+ if (write_path == NULL)
+ {
+ fuse_reply_err (req, errno);
+ return;
+ }
+ }
+
+ fd = open (path, O_RDONLY);
+ if (fd < 0)
+ {
+ int errsv = errno;
+ if (write_fd >= 0)
+ close (write_fd);
+ fuse_reply_err (req, errsv);
+ return;
+ }
+ fh = xdp_fh_new (ino, fi, fd, NULL);
+ fh->trunc_fd = write_fd;
+ fh->trunc_path = g_steal_pointer (&write_path);
+ fh->real_path = g_steal_pointer (&path);
+ if (fuse_reply_open (req, fi))
+ xdp_fh_free (fh);
+ }
+ else if (class == TMPFILE_INO_CLASS &&
+ (tmp = find_tmp_by_id (class_ino)))
+ {
+ fd = open (tmp->backing_path, get_open_flags (fi));
+ if (fd < 0)
+ {
+ fuse_reply_err (req, errno);
+ return;
+ }
+ fh = xdp_fh_new (ino, fi, fd, tmp);
+ if (fuse_reply_open (req, fi))
+ xdp_fh_free (fh);
+ }
+ else
+ fuse_reply_err (req, EIO);
+}
+
+static void
+xdp_fuse_create (fuse_req_t req,
+ fuse_ino_t parent,
+ const char *name,
+ mode_t mode,
+ struct fuse_file_info *fi)
+{
+ struct fuse_entry_param e = {0};
+ XdpInodeClass parent_class = get_class (parent);
+ struct stat stbuf;
+ XdpFh *fh;
+ g_autoptr(GVariant) doc = NULL;
+ g_autofree char *basename = NULL;
+ g_autofree char *path = NULL;
+ XdpTmp *tmpfile;
+ int fd, res;
+
+ g_debug ("xdp_fuse_create %lx/%s, flags %o", parent, name, fi->flags);
+
+ if ((res = xdp_stat (parent, &stbuf, &doc)) != 0)
+ {
+ fuse_reply_err (req, res);
+ return;
+ }
+
+ if ((stbuf.st_mode & S_IFMT) != S_IFDIR)
+ {
+ fuse_reply_err (req, ENOTDIR);
+ return;
+ }
+
+ if (parent_class != APP_DOC_DIR_INO_CLASS &&
+ parent_class != DOC_DIR_INO_CLASS)
+ {
+ fuse_reply_err (req, EACCES);
+ return;
+ }
+
+ basename = xdp_doc_dup_basename (doc);
+ if (strcmp (name, basename) == 0)
+ {
+ g_autofree char *write_path = NULL;
+ int write_fd = -1;
+ guint32 doc_id = xdb_doc_id_from_name (name);
+
+ write_path = create_tmp_for_doc (doc, O_RDWR, &write_fd);
+ if (write_path == NULL)
+ {
+ fuse_reply_err (req, errno);
+ return;
+ }
+
+ path = xdp_doc_dup_path (doc);
+
+ fd = open (path, O_CREAT|O_EXCL|O_RDONLY);
+ if (fd < 0)
+ {
+ int errsv = errno;
+ if (write_fd >= 0)
+ close (write_fd);
+ fuse_reply_err (req, errsv);
+ return;
+ }
+
+ e.ino = make_inode (DOC_FILE_INO_CLASS, doc_id);
+
+ fh = xdp_fh_new (e.ino, fi, fd, NULL);
+ fh->truncated = TRUE;
+ fh->trunc_fd = write_fd;
+ fh->trunc_path = g_steal_pointer (&write_path);
+ fh->real_path = g_steal_pointer (&path);
+
+ if (xdp_fstat (fh, &e.attr) != 0)
+ {
+ xdp_fh_free (fh);
+ fuse_reply_err (req, EIO);
+ return;
+ }
+
+ e.attr_timeout = get_attr_cache_time (e.attr.st_mode);
+ e.entry_timeout = get_entry_cache_time (e.attr.st_mode);
+
+ if (fuse_reply_create (req, &e, fi))
+ xdp_fh_free (fh);
+ }
+ else
+ {
+ tmpfile = find_tmp_by_name (parent, name);
+ if (tmpfile != NULL && fi->flags & O_EXCL)
+ {
+ fuse_reply_err (req, EEXIST);
+ return;
+ }
+
+ if (tmpfile)
+ {
+ fd = open (tmpfile->backing_path, get_open_flags (fi));
+ if (fd == -1)
+ {
+ fuse_reply_err (req, errno);
+ return;
+ }
+ }
+ else
+ {
+ tmpfile = tmpfile_new (parent, name, doc, get_open_flags (fi), &fd);
+ if (tmpfile == NULL)
+ {
+ fuse_reply_err (req, errno);
+ return;
+ }
+ }
+
+ e.ino = make_inode (TMPFILE_INO_CLASS, tmpfile->tmp_id);
+ if (xdp_stat (e.ino, &e.attr, NULL) != 0)
+ {
+ fuse_reply_err (req, EIO);
+ return;
+ }
+ e.attr_timeout = get_attr_cache_time (e.attr.st_mode);
+ e.entry_timeout = get_entry_cache_time (e.attr.st_mode);
+
+ fh = xdp_fh_new (e.ino, fi, fd, tmpfile);
+ if (fuse_reply_create (req, &e, fi))
+ xdp_fh_free (fh);
+ }
+}
+
+static void
+xdp_fuse_read (fuse_req_t req,
+ fuse_ino_t ino,
+ size_t size,
+ off_t off,
+ struct fuse_file_info *fi)
+{
+ XdpFh *fh = (gpointer)fi->fh;
+ struct fuse_bufvec bufv = FUSE_BUFVEC_INIT (size);
+ static char c = 'x';
+ int fd;
+
+ fd = xdp_fh_get_fd (fh);
+ if (fd == -1)
+ {
+ bufv.buf[0].flags = 0;
+ bufv.buf[0].mem = &c;
+ bufv.buf[0].size = 0;
+
+ fuse_reply_data (req, &bufv, FUSE_BUF_NO_SPLICE);
+ return;
+ }
+
+ bufv.buf[0].flags = FUSE_BUF_IS_FD | FUSE_BUF_FD_SEEK;
+ bufv.buf[0].fd = fd;
+ bufv.buf[0].pos = off;
+
+ fuse_reply_data (req, &bufv, FUSE_BUF_SPLICE_MOVE);
+}
+
+static void
+xdp_fuse_write (fuse_req_t req,
+ fuse_ino_t ino,
+ const char *buf,
+ size_t size,
+ off_t off,
+ struct fuse_file_info *fi)
+{
+ XdpFh *fh = (gpointer)fi->fh;
+ gssize res;
+ int fd;
+
+ if (fh->readonly)
+ {
+ fuse_reply_err (req, EACCES);
+ return;
+ }
+
+ fd = xdp_fh_get_fd (fh);
+ if (fd == -1)
+ {
+ fuse_reply_err (req, EIO);
+ return;
+ }
+
+ res = pwrite (fd, buf, size, off);
+ if (res < 0)
+ fuse_reply_err (req, errno);
+ else
+ fuse_reply_write (req, res);
+}
+
+static void
+xdp_fuse_write_buf (fuse_req_t req,
+ fuse_ino_t ino,
+ struct fuse_bufvec *bufv,
+ off_t off,
+ struct fuse_file_info *fi)
+{
+ XdpFh *fh = (gpointer)fi->fh;
+ struct fuse_bufvec dst = FUSE_BUFVEC_INIT(fuse_buf_size(bufv));
+ gssize res;
+ int fd;
+
+ if (fh->readonly)
+ {
+ fuse_reply_err (req, EACCES);
+ return;
+ }
+
+ fd = xdp_fh_get_fd (fh);
+ if (fd == -1)
+ {
+ fuse_reply_err (req, EIO);
+ return;
+ }
+
+ dst.buf[0].flags = FUSE_BUF_IS_FD | FUSE_BUF_FD_SEEK;
+ dst.buf[0].fd = fd;
+ dst.buf[0].pos = off;
+
+ res = fuse_buf_copy (&dst, bufv, FUSE_BUF_SPLICE_NONBLOCK);
+ if (res < 0)
+ fuse_reply_err (req, -res);
+ else
+ fuse_reply_write (req, res);
+}
+
+static void
+xdp_fuse_release (fuse_req_t req,
+ fuse_ino_t ino,
+ struct fuse_file_info *fi)
+{
+ XdpFh *fh = (gpointer)fi->fh;
+ xdp_fh_free (fh);
+ fuse_reply_err (req, 0);
+}
+
+static void
+xdp_fuse_rename (fuse_req_t req,
+ fuse_ino_t parent,
+ const char *name,
+ fuse_ino_t newparent,
+ const char *newname)
+{
+ XdpInodeClass parent_class = get_class (parent);
+ g_autoptr (GVariant) doc = NULL;
+ int res;
+ fuse_ino_t inode;
+ struct stat stbuf = {0};
+ g_autofree char *basename = NULL;
+ XdpTmp *other_tmp, *tmp;
+ GList *l;
+
+ g_debug ("xdp_fuse_rename %lx/%s -> %lx/%s", parent, name, newparent, newname);
+
+ res = xdp_lookup (parent, name, &inode, &stbuf, &doc, &tmp);
+ if (res != 0)
+ {
+ fuse_reply_err (req, res);
+ return;
+ }
+
+ /* Only allow renames in (app) doc dirs, and only inside the same dir */
+ if ((parent_class != DOC_DIR_INO_CLASS &&
+ parent_class != APP_DOC_DIR_INO_CLASS) ||
+ parent != newparent ||
+ doc == NULL ||
+ /* Also, don't allow renaming non-tmpfiles */
+ tmp == NULL)
+ {
+ fuse_reply_err (req, EACCES);
+ return;
+ }
+
+ basename = xdp_doc_dup_basename (doc);
+
+ if (strcmp (newname, basename) == 0)
+ {
+ g_autofree char *real_path = xdp_doc_dup_path (doc);
+ /* Rename tmpfile to regular file */
+
+ /* Stop writes to all outstanding fds to the temp file */
+ for (l = open_files; l != NULL; l = l->next)
+ {
+ XdpFh *fh = l->data;
+ if (fh->tmp_id == tmp->tmp_id && fh->fd >= 0)
+ fh->readonly = TRUE;
+ }
+
+ if (rename (tmp->backing_path, real_path) != 0)
+ {
+ fuse_reply_err (req, errno);
+ return;
+ }
+
+ /* Clear backing path so we don't unlink it when freeing tmp */
+ g_clear_pointer (&tmp->backing_path, g_free);
+ tmpfile_free (tmp);
+
+ fuse_reply_err (req, 0);
+ }
+ else
+ {
+ /* Rename tmpfile to other tmpfile name */
+
+ other_tmp = find_tmp_by_name (newparent, newname);
+ if (other_tmp)
+ tmpfile_free (other_tmp);
+
+ g_free (tmp->name);
+ tmp->name = g_strdup (newname);
+ fuse_reply_err (req, 0);
+ }
+}
+
+static int
+fh_truncate (XdpFh *fh, off_t size, struct stat *newattr)
+{
+ int fd;
+
+ if (fh->trunc_fd >= 0 && !fh->truncated)
+ {
+ if (size != 0)
+ return -EACCES;
+
+ fh->truncated = TRUE;
+ fd = fh->trunc_fd;
+ }
+ else
+ {
+ fd = xdp_fh_get_fd (fh);
+ if (fd == -1)
+ return -EIO;
+
+ if (ftruncate (fd, size) != 0)
+ return - errno;
+ }
+
+ if (newattr)
+ {
+ int res = xdp_fstat (fh, newattr);
+ if (res < 0)
+ return res;
+ }
+
+ return 0;
+}
+
+static void
+xdp_fuse_setattr (fuse_req_t req,
+ fuse_ino_t ino,
+ struct stat *attr,
+ int to_set,
+ struct fuse_file_info *fi)
+{
+ g_debug ("xdp_fuse_setattr %lx %x %p", ino, to_set, fi);
+
+ if (to_set == FUSE_SET_ATTR_SIZE && fi != NULL)
+ {
+ XdpFh *fh = (gpointer)fi->fh;
+ int res;
+ struct stat newattr = {0};
+
+ /* ftruncate */
+
+ res = fh_truncate (fh, attr->st_size, &newattr);
+ if (res < 0)
+ {
+ fuse_reply_err (req, res);
+ return;
+ }
+
+ fuse_reply_attr (req, &newattr, get_attr_cache_time (newattr.st_mode));
+ }
+ else if (to_set == FUSE_SET_ATTR_SIZE && fi == NULL)
+ {
+ gboolean found = FALSE;
+ int res = 0;
+ GList *l;
+ struct stat newattr = {0};
+ struct stat *newattrp = &newattr;
+
+ /* truncate, truncate any open files (but EACCES if not open) */
+
+ for (l = open_files; l != NULL; l = l->next)
+ {
+ XdpFh *fh = l->data;
+ if (fh->inode == ino)
+ {
+ found = TRUE;
+ res = fh_truncate (fh, attr->st_size, newattrp);
+ newattrp = NULL;
+ }
+ }
+
+ if (!found)
+ {
+ fuse_reply_err (req, EACCES);
+ return;
+ }
+
+ if (res < 0)
+ {
+ fuse_reply_err (req, -res);
+ return;
+ }
+
+ fuse_reply_attr (req, &newattr, get_attr_cache_time (newattr.st_mode));
+ }
+ else if (to_set == FUSE_SET_ATTR_MODE)
+ {
+ gboolean found = FALSE;
+ int res, err = -1;
+ GList *l;
+ struct stat newattr = {0};
+
+ for (l = open_files; l != NULL; l = l->next)
+ {
+ XdpFh *fh = l->data;
+
+ if (fh->inode == ino)
+ {
+ int fd = xdp_fh_get_fd (fh);
+ if (fd != -1)
+ {
+ res = fchmod (fd, get_user_perms (attr));
+ if (!found)
+ {
+ if (res != 0)
+ err = -errno;
+ else
+ err = xdp_fstat (fh, &newattr);
+ found = TRUE;
+ }
+ }
+ }
+ }
+
+ if (!found)
+ {
+ fuse_reply_err (req, EACCES);
+ return;
+ }
+
+ if (err < 0)
+ {
+ fuse_reply_err (req, -err);
+ return;
+ }
+
+ fuse_reply_attr (req, &newattr, get_attr_cache_time (newattr.st_mode));
+ }
+ else
+ fuse_reply_err (req, ENOSYS);
+}
+
+static void
+xdp_fuse_fsyncdir (fuse_req_t req,
+ fuse_ino_t ino,
+ int datasync,
+ struct fuse_file_info *fi)
+{
+ XdpInodeClass class = get_class (ino);
+ guint64 class_ino = get_class_ino (ino);
+ guint32 doc_id;
+
+ if (class == DOC_DIR_INO_CLASS ||
+ class == APP_DOC_DIR_INO_CLASS)
+ {
+ g_autoptr (GVariant) doc = NULL;
+ if (class == APP_DOC_DIR_INO_CLASS)
+ doc_id = get_doc_id_from_app_doc_ino (class_ino);
+ else
+ doc_id = class_ino;
+
+ doc = xdp_doc_db_lookup_doc (db, doc_id);
+ if (doc != NULL)
+ {
+ g_autofree char *dirname = xdp_doc_dup_dirname (doc);
+ int fd = open (dirname, O_DIRECTORY|O_RDONLY);
+ if (fd >= 0)
+ {
+ if (datasync)
+ fdatasync (fd);
+ else
+ fsync (fd);
+ close (fd);
+ }
+ }
+ }
+
+ fuse_reply_err (req, 0);
+}
+
+static void
+xdp_fuse_fsync (fuse_req_t req,
+ fuse_ino_t ino,
+ int datasync,
+ struct fuse_file_info *fi)
+{
+ XdpInodeClass class = get_class (ino);
+
+ if (class == DOC_FILE_INO_CLASS ||
+ class == TMPFILE_INO_CLASS)
+ {
+ XdpFh *fh = (gpointer)fi->fh;
+ if (fh->fd >= 0)
+ fsync (fh->fd);
+ if (fh->truncated && fh->trunc_fd >= 0)
+ fsync (fh->trunc_fd);
+ }
+
+ fuse_reply_err (req, 0);
+}
+
+static void
+xdp_fuse_unlink (fuse_req_t req,
+ fuse_ino_t parent,
+ const char *name)
+{
+ XdpInodeClass parent_class = get_class (parent);
+ g_autoptr (GVariant) doc = NULL;
+ int res;
+ fuse_ino_t inode;
+ struct stat stbuf = {0};
+ g_autofree char *basename = NULL;
+ XdpTmp *tmp;
+
+ g_debug ("xdp_fuse_unlink %lx/%s", parent, name);
+
+ res = xdp_lookup (parent, name, &inode, &stbuf, &doc, &tmp);
+ if (res != 0)
+ {
+ fuse_reply_err (req, res);
+ return;
+ }
+
+ /* Only allow unlink in (app) doc dirs */
+ if ((parent_class != DOC_DIR_INO_CLASS &&
+ parent_class != APP_DOC_DIR_INO_CLASS) ||
+ doc == NULL)
+ {
+ fuse_reply_err (req, EACCES);
+ return;
+ }
+
+ basename = xdp_doc_dup_basename (doc);
+ if (strcmp (name, basename) == 0)
+ {
+ g_autofree char *real_path = xdp_doc_dup_path (doc);
+
+ if (unlink (real_path) != 0)
+ {
+ fuse_reply_err (req, errno);
+ return;
+ }
+
+ fuse_reply_err (req, 0);
+ }
+ else
+ {
+ tmpfile_free (tmp);
+
+ fuse_reply_err (req, 0);
+ }
+}
+
+static struct fuse_lowlevel_ops xdp_fuse_oper = {
+ .lookup = xdp_fuse_lookup,
+ .getattr = xdp_fuse_getattr,
+ .opendir = xdp_fuse_opendir,
+ .readdir = xdp_fuse_readdir,
+ .releasedir = xdp_fuse_releasedir,
+ .fsyncdir = xdp_fuse_fsyncdir,
+ .open = xdp_fuse_open,
+ .create = xdp_fuse_create,
+ .read = xdp_fuse_read,
+ .write = xdp_fuse_write,
+ .write_buf = xdp_fuse_write_buf,
+ .release = xdp_fuse_release,
+ .rename = xdp_fuse_rename,
+ .setattr = xdp_fuse_setattr,
+ .fsync = xdp_fuse_fsync,
+ .unlink = xdp_fuse_unlink,
+};
+
+typedef struct
+{
+ GSource source;
+
+ struct fuse_chan *ch;
+ gpointer fd_tag;
+} FuseSource;
+
+static gboolean
+fuse_source_dispatch (GSource *source,
+ GSourceFunc func,
+ gpointer user_data)
+{
+ FuseSource *fs = (FuseSource *)source;
+ struct fuse_chan *ch = fs->ch;
+ struct fuse_session *se = fuse_chan_session (ch);
+ gsize bufsize = fuse_chan_bufsize (ch);
+
+ if (g_source_query_unix_fd (source, fs->fd_tag) != 0)
+ {
+ int res = 0;
+ char *buf = (char *) g_malloc (bufsize);
+
+ while (TRUE)
+ {
+ struct fuse_chan *tmpch = ch;
+ struct fuse_buf fbuf = {
+ .mem = buf,
+ .size = bufsize,
+ };
+ res = fuse_session_receive_buf (se, &fbuf, &tmpch);
+ if (res == -EINTR)
+ continue;
+ if (res <= 0)
+ break;
+
+ fuse_session_process_buf (se, &fbuf, tmpch);
+ }
+ g_free (buf);
+ }
+
+ return TRUE;
+}
+
+static GSource *
+fuse_source_new (struct fuse_chan *ch)
+{
+ static GSourceFuncs source_funcs = {
+ NULL, NULL,
+ fuse_source_dispatch
+ /* should have a finalize, but it will never happen */
+ };
+ FuseSource *fs;
+ GSource *source;
+ GError *error = NULL;
+ int fd;
+
+ source = g_source_new (&source_funcs, sizeof (FuseSource));
+ fs = (FuseSource *) source;
+ fs->ch = ch;
+
+ g_source_set_name (source, "fuse source");
+
+ fd = fuse_chan_fd(ch);
+ g_unix_set_fd_nonblocking (fd, TRUE, &error);
+ g_assert_no_error (error);
+
+ fs->fd_tag = g_source_add_unix_fd (source, fd, G_IO_IN);
+
+ return source;
+}
+
+static struct fuse_session *session = NULL;
+static struct fuse_chan *main_ch = NULL;
+static char *mount_path = NULL;
+
+void
+xdp_fuse_exit (void)
+{
+ if (session)
+ fuse_session_reset (session);
+ if (main_ch)
+ fuse_session_remove_chan (main_ch);
+ if (session)
+ fuse_session_destroy (session);
+ if (main_ch)
+ fuse_unmount (mount_path, main_ch);
+}
+
+gboolean
+xdp_fuse_init (XdpDocDb *_db,
+ GError **error)
+{
+ char *argv[] = { "xdp-fuse", "-osplice_write,splice_move,splice_read" };
+ struct fuse_args args = FUSE_ARGS_INIT(G_N_ELEMENTS(argv), argv);
+ struct fuse_chan *ch;
+ GSource *source;
+
+ db = _db;
+ app_name_to_id =
+ g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+ app_id_to_name =
+ g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, NULL);
+ next_app_id = IN_HOMEDIR_APP_ID + 1;
+ next_tmp_id = 1;
+
+ mount_path = g_build_filename (g_get_user_runtime_dir(), "doc", NULL);
+ if (g_mkdir_with_parents (mount_path, 0700))
+ {
+ g_set_error (error, XDP_ERROR, XDP_ERROR_FAILED,
+ "Unable to create dir %s\n", mount_path);
+ return FALSE;
+ }
+
+ main_ch = ch = fuse_mount (mount_path, &args);
+ if (ch == NULL)
+ {
+ g_set_error (error, XDP_ERROR, XDP_ERROR_FAILED, "Can't mount fuse fs");
+ return FALSE;
+ }
+
+ session = fuse_lowlevel_new (&args, &xdp_fuse_oper,
+ sizeof (xdp_fuse_oper), NULL);
+ if (session == NULL)
+ {
+ g_set_error (error, XDP_ERROR, XDP_ERROR_FAILED,
+ "Can't create fuse session");
+ return FALSE;
+ }
+
+ fuse_session_add_chan (session, ch);
+
+ source = fuse_source_new (ch);
+ g_source_attach (source, NULL);
+
+ return TRUE;
+}
diff --git a/document-portal/xdp-fuse.h b/document-portal/xdp-fuse.h
new file mode 100644
index 0000000..f335ccc
--- /dev/null
+++ b/document-portal/xdp-fuse.h
@@ -0,0 +1,15 @@
+#ifndef XDP_FUSE_H
+#define XDP_FUSE_H
+
+#include <glib.h>
+#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 0000000..7cd1ad3
--- /dev/null
+++ b/document-portal/xdp-main.c
@@ -0,0 +1,564 @@
+#include "config.h"
+
+#include <locale.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+
+#include <gio/gio.h>
+#include <gio/gunixfdlist.h>
+#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 0000000..1ef6194
--- /dev/null
+++ b/document-portal/xdp-util.c
@@ -0,0 +1,204 @@
+#include "config.h"
+#include <string.h>
+#include <errno.h>
+#include <gio/gio.h>
+#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 0000000..314755e
--- /dev/null
+++ b/document-portal/xdp-util.h
@@ -0,0 +1,21 @@
+#ifndef XDP_UTIL_H
+#define XDP_UTIL_H
+
+#include <gio/gio.h>
+
+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 319f5ed..f42a3de 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 e6b438b..c44626c 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)