/*
 * Copyright © 2018 Red Hat, Inc
 * Copyright © 2023 GNOME Foundation Inc.
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 *
 * This program 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.1 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, see <http://www.gnu.org/licenses/>.
 *
 * Authors:
 *       Alexander Larsson <alexl@redhat.com>
 *       Hubert Figuière <hub@figuiere.net>
 */

#include "config.h"

#define FUSE_USE_VERSION 35

#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 <pthread.h>
#if HAVE_SYS_STATFS_H
#include <sys/statfs.h>
#endif
#include <sys/types.h>
#if HAVE_SYS_MOUNT_H
#include <sys/mount.h>
#endif
#if HAVE_SYS_XATTR_H
#include <sys/xattr.h>
#endif
#if HAVE_SYS_EXTATTR_H
#include <sys/extattr.h>
#endif
#include <sys/time.h>
#include <sys/resource.h>

#include "document-portal-fuse.h"
#include "document-store.h"
#include "src/xdp-utils.h"

#ifndef O_FSYNC
#define O_FSYNC O_SYNC
#endif

#ifndef ENODATA
#define ENODATA ENOATTR
#endif

#define XDP_XATTR_HOST_PATH "user.document-portal.host-path"
/* man listxattr:
 *   The list is the set of (null-terminated) names, one after the other.
 *
 * Example:
 *   user.name1\0system.name1\0user.name2\0
 *
 * We can statically define the string by doing this:
 *   XDP_XATTR_MY_ATTR "\0" XDP_XATTR_ANOTHER_ATTR
 */
#define XDP_XATTR_ATTRIBUTE_NAME_LIST XDP_XATTR_HOST_PATH

/* Inode ownership model
 *
 * The document portal exposes something as a filesystem that it
 * doesn't have full control over. For instance at any point some
 * other process can rename an exposed file on the real filesystem and
 * we won't be told about this. This means that in general we always
 * return 0 for the cacheable lifetimes of entries and file attributes.
 * (Except for the virtual directories we have full control of, the
 * section below only discusses real files).
 *
 * However, even though we don't have full control of the underlying
 * filesystem the *kernel* has. This means we can use that to get
 * the correct semantics.
 *
 * For example, assume that a directory is held open by a process
 * (for example, it could be the CWD of the process). When we open
 * the directory via a LOOKUP operation we return an inode to it,
 * and, for as long as the kernel has this inode around (i.e.  until it
 * sent a FORGET message), it can send operations on this inode without
 * looking it up again. For example, if the above process used a
 * relative path.
 *
 * Now, consider the case where the app chdir():ed into the fuse
 * directory, but after the backing directory was renamed outside
 * the fuse filesystem. The fuse inode representation for the inode
 * cannot be the directory name, because the expected semantics is
 * that further relative pathnames from the app will still resolve
 * to the same directory independent of its location in the tree.
 *
 * The way we do this is to keep a O_PATH file descriptor around for
 * each underlying inode. This is represented by the XdpPhysicalInode
 * type and we have a hashtable from backing dev+inode to a these
 * so that we can use one fd per backing inode even when the file
 * is visible in many places.
 *
 * Since we don't do any caching, each LOOKUP operation will do a
 * statat() on the underlying filesystem. However, we then use the
 * result of that to lookup (via backing dev+ino) the previous inode
 * (as long as it still lives) if the backing file was unchanged.
 *
 * One problem with this approach is that the kernel tends to keep
 * inodes alive for a very long time even if they are *only*
 * referenced by the dcache (Directory Entry Cache) (even though we
 * will not really use the dcache info due to the 0 valid time). This
 * is unfortunate, because it means we will keep a lot of file
 * descriptors open. But, we can not know if the kernel needs the inode
 * for some non-dcache use so we can't close the file descriptors.
 *
 * To work around this we regularly emit entry invalidation calls
 * to the kernel, which will make it forget the inodes that are
 * only pinned by the dcache.
 */


#define NON_DOC_DIR_PERMS 0500
#define DOC_DIR_PERMS_FILE 0700
#define DOC_DIR_PERMS_DIR 0500

static GThread *fuse_thread = NULL;
static struct fuse_session *session = NULL;
G_LOCK_DEFINE (session);
static char *mount_path = NULL;
static uid_t my_uid;
static gid_t my_gid;

static GList *open_files = NULL;
G_LOCK_DEFINE (open_files);

/* from libfuse */
#define FUSE_UNKNOWN_INO 0xffffffff

#define BY_APP_NAME "by-app"

typedef struct {
  ino_t ino;
  dev_t dev;
} DevIno;

typedef enum {
 XDP_DOMAIN_ROOT,
 XDP_DOMAIN_BY_APP,
 XDP_DOMAIN_APP,
 XDP_DOMAIN_DOCUMENT,
} XdpDomainType;

typedef struct _XdpDomain XdpDomain;
typedef struct _XdpInode XdpInode;

struct _XdpDomain {
  gint ref_count; /* atomic */
  XdpDomainType type;

  XdpDomain *parent;
  XdpInode *parent_inode; /* Inode of the parent domain (NULL for root) */

  char *doc_id; /* NULL for root, by-app, app */
  char *app_id; /* NULL for root, by-app, non-app id */

  /* root: by docid
   * app: by docid
   * by_app: by app
   * document: by physical
   */
  GHashTable *inodes; /* Protected by domain_inodes */

  /* Below only used for XDP_DOMAIN_DOCUMENT */

  char *doc_path; /* path to the directory the files are in */
  char *doc_file; /* != NULL for non-directory documents */
  guint64 doc_dir_device;
  guint64 doc_dir_inode;
  guint32 doc_flags;

  /* Below is mutable, protected by mutex */
  GMutex  tempfile_mutex;
  GHashTable *tempfiles; /* Name -> physical */
};

static void xdp_domain_unref (XdpDomain *domain);
G_DEFINE_AUTOPTR_CLEANUP_FUNC (XdpDomain, xdp_domain_unref)

G_LOCK_DEFINE (domain_inodes);

typedef struct {
  gint ref_count; /* atomic */
  DevIno backing_devino;
  int fd; /* O_PATH fd */
} XdpPhysicalInode;

static XdpPhysicalInode *xdp_physical_inode_ref   (XdpPhysicalInode *inode);
static void              xdp_physical_inode_unref (XdpPhysicalInode *inode);
G_DEFINE_AUTOPTR_CLEANUP_FUNC (XdpPhysicalInode, xdp_physical_inode_unref)

typedef struct {
  gint ref_count; /* atomic */

  char *name;      /* This changes over time (i.e. in renames)
                      protected by domain->tempfile_mutex,
                      used as key in domain->tempfiles */
  char *tempname;  /* Real filename on disk.
                      This can be NULLed to avoid unlink at finalize */
  XdpInode *inode;
} XdpTempfile;

static XdpTempfile *xdp_tempfile_ref   (XdpTempfile *tempfile);
static void         xdp_tempfile_unref (XdpTempfile *tempfile);
G_DEFINE_AUTOPTR_CLEANUP_FUNC (XdpTempfile, xdp_tempfile_unref)

struct _XdpInode {
  guint64 ino;
  gint ref_count; /* atomic, includes one ref if kernel_ref_count != 0 */
  gint kernel_ref_count; /* atomic */

  XdpDomain *domain;

  /* The below are only used for XDP_DOMAIN_DOCUMENT inodes */
  XdpPhysicalInode *physical;
  /* The root of the domain, or NULL for the domain.  We use this to
   * keep the root document inode alive so that when the kernel
   * forgets it and then looks it up we will not get a new inode and
   * thus a new domain. */
  XdpInode *domain_root_inode;
};

typedef struct {
  int fd;
  GList *link;
} XdpFile;


typedef struct {
  DIR *dir;
  struct dirent *entry;
  off_t offset;

  char *dirbuf;
  gsize dirbuf_size;
} XdpDir;

XdpInode *root_inode;
XdpInode *by_app_inode;

typedef struct {
  ino_t parent_ino;
  char name[0];
} XdpInvalidateData;

typedef struct {
  gboolean use_splice;
} XdpFuseOptions;

static GList *invalidate_list;
G_LOCK_DEFINE (invalidate_list);

static XdpInode *xdp_inode_ref (XdpInode *inode);
static void xdp_inode_unref (XdpInode *inode);

/* Lookup by inode for verification */
static GHashTable *all_inodes; /* guint64 -> XdpInode */
static guint64 next_virtual_inode = FUSE_ROOT_ID; /* root is the first inode created, so it gets this */
G_LOCK_DEFINE (all_inodes);

G_DEFINE_AUTOPTR_CLEANUP_FUNC (XdpInode, xdp_inode_unref)

static int ensure_docdir_inode (XdpInode *parent,
                                int o_path_fd_in, /* Takes ownership */
                                struct fuse_entry_param *e,
                                XdpInode **inode_out);

static void queue_invalidate_dentry (XdpInode *parent, const char *name);

static gboolean
app_can_write_doc (PermissionDbEntry *entry, const char *app_id)
{
  if (app_id == NULL)
    return TRUE;

  if (document_entry_has_permissions_by_app_id (entry, app_id, DOCUMENT_PERMISSION_FLAGS_WRITE))
    return TRUE;

  return FALSE;
}

static gboolean
app_can_see_doc (PermissionDbEntry *entry, const char *app_id)
{
  if (app_id == NULL)
    return TRUE;

  if (document_entry_has_permissions_by_app_id (entry, app_id, DOCUMENT_PERMISSION_FLAGS_READ))
    return TRUE;

  return FALSE;
}

static char *
fd_to_path (int fd)
{
  return g_strdup_printf ("/proc/self/fd/%d", fd);
}

static char *
open_flags_to_string (int flags)
{
  GString *s;
  const char *mode;

  switch (flags & O_ACCMODE)
    {
    case O_RDONLY:
      mode = "RDONLY";
      break;
    case O_WRONLY:
      mode = "WRONLY";
      break;
    case O_RDWR:
    default:
      mode = "RDWR";
      break;
    }

  s = g_string_new (mode);

  if (flags & O_NONBLOCK)
    g_string_append (s, ",NONBLOCK");
  if (flags & O_APPEND)
    g_string_append (s, ",APPEND");
  if (flags & O_SYNC)
    g_string_append (s, ",SYNC");
  if (flags & O_ASYNC)
    g_string_append (s, ",ASYNC");
  if (flags & O_FSYNC)
    g_string_append (s, ",FSYNC");
#ifdef O_DSYNC
  if (flags & O_DSYNC)
    g_string_append (s, ",DSYNC");
#endif
  if (flags & O_CREAT)
    g_string_append (s, ",CREAT");
  if (flags & O_TRUNC)
    g_string_append (s, ",TRUNC");
  if (flags & O_EXCL)
    g_string_append (s, ",EXCL");
  if (flags & O_CLOEXEC)
    g_string_append (s, ",CLOEXEC");
  if (flags & O_DIRECT)
    g_string_append (s, ",DIRECT");
#ifdef O_LARGEFILE
  if (flags & O_LARGEFILE)
    g_string_append (s, ",LARGEFILE");
#endif
#ifdef O_NOATIME
  if (flags & O_NOATIME)
    g_string_append (s, ",NOATIME");
#endif
  if (flags & O_NOCTTY)
    g_string_append (s, ",NOCTTY");
  if (flags & O_PATH)
    g_string_append (s, ",PATH");
#ifdef O_TMPFILE
  if (flags & O_TMPFILE)
    g_string_append (s, ",TMPFILE");
#endif

  return g_string_free (s, FALSE);
}


static char *
setattr_flags_to_string (int flags)
{
  GString *s = g_string_new ("");

  if (flags & FUSE_SET_ATTR_MODE)
    g_string_append (s, "MODE,");

  if (flags & FUSE_SET_ATTR_UID)
    g_string_append (s, "UID,");

  if (flags & FUSE_SET_ATTR_GID)
    g_string_append (s, "GID,");

  if (flags & FUSE_SET_ATTR_SIZE)
    g_string_append (s, "SIZE,");

  if (flags & FUSE_SET_ATTR_ATIME)
    g_string_append (s, "ATIME,");

  if (flags & FUSE_SET_ATTR_MTIME)
    g_string_append (s, "MTIME,");

  if (flags & FUSE_SET_ATTR_ATIME_NOW)
    g_string_append (s, "ATIME_NOW,");

  if (flags & FUSE_SET_ATTR_MTIME_NOW)
    g_string_append (s, "MTIME_NOW,");

  /* Remove last comma */
  if (s->len > 0)
    g_string_truncate (s, s->len - 1);

  return g_string_free (s, FALSE);
}

static char *
renameat2_flags_to_string (int flags)
{
#if HAVE_RENAMEAT2
  GString *s = g_string_new ("");

#ifdef RENAME_EXCHANGE
  if (flags & RENAME_EXCHANGE)
    g_string_append (s, "EXCHANGE,");
#endif

  if (flags & RENAME_NOREPLACE)
    g_string_append (s, "NOREPLACE,");

#ifdef RENAME_WHITEOUT
  if (flags & RENAME_WHITEOUT)
    g_string_append (s, "WHITEOUT,");
#endif

  /* Remove last comma */
  if (s->len > 0)
    g_string_truncate (s, s->len - 1);

  return g_string_free (s, FALSE);
#else
  return g_strdup_printf ("%#x", flags);
#endif
}

static guint
devino_hash  (gconstpointer  key)
{
  DevIno *devino = (DevIno *)key;

  return (devino->ino >> 2) ^ devino->dev;
}

static gboolean
devino_equal (gconstpointer  _a,
              gconstpointer  _b)
{
  DevIno *a = (DevIno *)_a;
  DevIno *b = (DevIno *)_b;
  return a->ino == b->ino && a->dev == b->dev;
}

/* Lookup by physical backing devino */
static GHashTable *physical_inodes;
G_LOCK_DEFINE (physical_inodes);


/* Takes ownership of the o_path fd if passed in */
static XdpPhysicalInode *
ensure_physical_inode (dev_t dev, ino_t ino, int o_path_fd)
{
  DevIno devino = {ino, dev};
  XdpPhysicalInode *inode = NULL;

  G_LOCK (physical_inodes);

  inode = g_hash_table_lookup (physical_inodes, &devino);
  if (inode != NULL)
    {
      inode = xdp_physical_inode_ref (inode);
      close (o_path_fd);
    }
  else
    {
      /* Takes ownership of fd */
      inode = g_new0 (XdpPhysicalInode, 1);
      inode->ref_count = 1;
      inode->fd = o_path_fd;
      inode->backing_devino = devino;
      g_hash_table_insert (physical_inodes, &inode->backing_devino, inode);
    }

  G_UNLOCK (physical_inodes);

  return inode;
}

static XdpPhysicalInode *
xdp_physical_inode_ref (XdpPhysicalInode *inode)
{
  g_atomic_int_inc (&inode->ref_count);
  return inode;
}

static void
xdp_physical_inode_unref (XdpPhysicalInode *inode)
{
  gint old_ref;

  /* here we want to atomically do: if (ref_count>1) { ref_count--; return; } */
retry_atomic_decrement1:
  old_ref = g_atomic_int_get (&inode->ref_count);
  if (old_ref > 1)
    {
      if (!g_atomic_int_compare_and_exchange ((int *) &inode->ref_count, old_ref, old_ref - 1))
        goto retry_atomic_decrement1;
    }
  else
    {
      if (old_ref <= 0)
        {
          g_warning ("Can't unref dead inode");
          return;
        }

      /* Might be revived from physical_inodes hash by this time, so protect by lock */
      G_LOCK (physical_inodes);

      if (!g_atomic_int_compare_and_exchange ((int *) &inode->ref_count, old_ref, old_ref - 1))
        {
          G_UNLOCK (physical_inodes);
          goto retry_atomic_decrement1;
        }
      g_hash_table_remove (physical_inodes, &inode->backing_devino);

      G_UNLOCK (physical_inodes);

      close (inode->fd);
      g_free (inode);
    }
}

static XdpDomain *
xdp_domain_ref (XdpDomain *domain)
{
  g_atomic_int_inc (&domain->ref_count);
  return domain;
}

static void
xdp_domain_unref (XdpDomain *domain)
{
  if (g_atomic_int_dec_and_test (&domain->ref_count))
    {
      g_free (domain->doc_id);
      g_free (domain->app_id);
      g_free (domain->doc_path);
      g_free (domain->doc_file);
      if (domain->inodes)
        g_assert (g_hash_table_size (domain->inodes) == 0);
      g_clear_pointer (&domain->inodes, g_hash_table_unref);
      g_clear_pointer (&domain->parent, xdp_domain_unref);
      g_clear_pointer (&domain->parent_inode, xdp_inode_unref);
      g_clear_pointer (&domain->tempfiles, g_hash_table_unref);
      g_mutex_clear (&domain->tempfile_mutex);
      g_free (domain);
    }
}

static gboolean
xdp_domain_is_virtual_type (XdpDomain *domain)
{
  return
    domain->type == XDP_DOMAIN_ROOT ||
    domain->type == XDP_DOMAIN_BY_APP ||
    domain->type == XDP_DOMAIN_APP;
}

static XdpDomain *
_xdp_domain_new (XdpDomainType type)
{
  XdpDomain *domain = g_new0 (XdpDomain, 1);
  domain->ref_count = 1;
  domain->type = type;
  g_mutex_init (&domain->tempfile_mutex);
  return domain;
}

static XdpDomain *
xdp_domain_new_root (void)
{
  XdpDomain *domain = _xdp_domain_new (XDP_DOMAIN_ROOT);
  domain->inodes = g_hash_table_new (g_str_hash, g_str_equal);
  return domain;
}

static XdpDomain *
xdp_domain_new_by_app (XdpInode *root_inode)
{
  XdpDomain *root_domain = root_inode->domain;
  XdpDomain *domain = _xdp_domain_new (XDP_DOMAIN_BY_APP);
  domain->parent = xdp_domain_ref (root_domain);
  domain->parent_inode = xdp_inode_ref (root_inode);
  domain->inodes = g_hash_table_new (g_str_hash, g_str_equal);
  return domain;
}

static XdpDomain *
xdp_domain_new_app (XdpInode   *parent_inode,
                    const char *app_id)
{
  XdpDomain *parent = parent_inode->domain;
  XdpDomain *domain = _xdp_domain_new (XDP_DOMAIN_APP);
  domain->parent = xdp_domain_ref (parent);
  domain->parent_inode = xdp_inode_ref (parent_inode);
  domain->app_id = g_strdup (app_id);
  domain->inodes = g_hash_table_new (g_str_hash, g_str_equal);
  return domain;
}

static gboolean
xdp_document_domain_is_dir (XdpDomain *domain)
{
  return (domain->doc_flags & DOCUMENT_ENTRY_FLAG_DIRECTORY) != 0;
}

static XdpDomain *
xdp_domain_new_document (XdpDomain         *parent,
                         const char        *doc_id,
                         PermissionDbEntry *doc_entry)
{
  XdpDomain *domain = _xdp_domain_new (XDP_DOMAIN_DOCUMENT);
  const char *db_path;

  domain->parent = xdp_domain_ref (parent);
  domain->doc_id = g_strdup (doc_id);
  domain->app_id = g_strdup (parent->app_id);
  domain->inodes = g_hash_table_new (g_direct_hash, g_direct_equal);

  domain->doc_flags = document_entry_get_flags (doc_entry);
  domain->doc_dir_device = document_entry_get_device (doc_entry);
  domain->doc_dir_inode =  document_entry_get_inode (doc_entry);

  db_path = document_entry_get_path (doc_entry);
  if (xdp_document_domain_is_dir (domain))
    {
      domain->doc_path = g_strdup (db_path);
      domain->doc_file = g_path_get_basename (db_path);
    }
  else
    {
      domain->doc_path = g_path_get_dirname (db_path);
      domain->doc_file = g_path_get_basename (db_path);
    }

  domain->tempfiles = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, (GDestroyNotify)xdp_tempfile_unref);

  return domain;
}

static gboolean
xdp_document_domain_can_see (XdpDomain *domain)
{
  if (domain->app_id != NULL)
    {
      g_autoptr(PermissionDbEntry) entry = xdp_lookup_doc (domain->doc_id);

      if (entry == NULL ||
          !app_can_see_doc (entry, domain->app_id))
        return FALSE;
    }

  return TRUE;
}

static gboolean
xdp_document_domain_can_write (XdpDomain *domain)
{
  if (domain->app_id != NULL)
    {
      g_autoptr(PermissionDbEntry) entry = xdp_lookup_doc (domain->doc_id);

      if (entry == NULL ||
          !app_can_write_doc (entry, domain->app_id))
        return FALSE;
    }

  return TRUE;
}

static char **
xdp_domain_get_inode_keys_as_string (XdpDomain *domain)
{
  char **res;
  guint length, i;

  g_assert (domain->type == XDP_DOMAIN_BY_APP);

  G_LOCK (domain_inodes);

  res = (char **)g_hash_table_get_keys_as_array (domain->inodes, &length);
  for (i = 0; i < length; i++)
    res[i] = g_strdup (res[i]);

  G_UNLOCK (domain_inodes);

  return res;
}

static XdpTempfile *
xdp_tempfile_ref (XdpTempfile *tempfile)
{
  g_atomic_int_inc (&tempfile->ref_count);
  return tempfile;
}

static XdpTempfile *
xdp_tempfile_new (XdpInode   *inode,
                  const char *name,
                  const char *tempname)
{
  XdpTempfile *tempfile = g_new0 (XdpTempfile, 1);

  tempfile->ref_count = 1;
  tempfile->inode = xdp_inode_ref (inode);
  tempfile->name = g_strdup (name);
  tempfile->tempname = g_strdup (tempname);

  return tempfile;
}

static void
xdp_tempfile_unref (XdpTempfile *tempfile)
{
  if (g_atomic_int_dec_and_test (&tempfile->ref_count))
    {
      if (tempfile->tempname)
        {
          g_autofree char *temppath = g_build_filename (tempfile->inode->domain->doc_path, tempfile->tempname, NULL);
          (void)unlink (temppath);
        }
      g_free (tempfile->name);
      g_free (tempfile->tempname);
      g_clear_pointer (&tempfile->inode, xdp_inode_unref);
      g_free (tempfile);
    }
}

static XdpInode *
_xdp_inode_new (void)
{
  XdpInode *inode = g_new0 (XdpInode, 1);
  inode->ref_count = 1;
  inode->kernel_ref_count = 0;

  return inode;
}

/* We try to create persistent inode number based on the backing
 * device and inode numbers, as well as the doc/app id (since the same
 * backing dev/ino should be different inodes in the fuse
 * filesystem). We do this by hashing the data to generate a value.
 * For non-physical files or accidental collisions we just pick a free
 * number by incrementing.
 */
static guint64
generate_persistent_ino (DevIno *backing_devino,
                         const char *doc_id,
                         const char *app_id)
{
  g_autoptr(GChecksum) checksum = g_checksum_new (G_CHECKSUM_MD5);
  guchar digest[64];
  gsize digest_len = 64;
  guint64 res;

  g_checksum_update (checksum, (guchar *)backing_devino, sizeof (DevIno));
  if (doc_id)
    g_checksum_update (checksum, (guchar *)doc_id, strlen (doc_id));
  if (app_id)
    g_checksum_update (checksum, (guchar *)app_id, strlen (app_id));

  g_checksum_get_digest (checksum, digest, &digest_len);

  res = *(guint64 *)digest;
  if (res == FUSE_ROOT_ID || res == 0)
    res = FUSE_ROOT_ID + 1;
  return res;
}

/* takes ownership of fd */
static XdpInode *
xdp_inode_new (XdpDomain        *domain,
               XdpPhysicalInode *physical)
{
  XdpInode *inode = _xdp_inode_new ();
  inode->domain = xdp_domain_ref (domain);
  guint64 persistent_ino, try_ino;

  if (physical)
    {
      inode->physical = xdp_physical_inode_ref (physical);
      persistent_ino = generate_persistent_ino (&physical->backing_devino,
                                                domain->doc_id,
                                                domain->app_id);

    }

  G_LOCK (all_inodes);
  if (physical)
    try_ino = persistent_ino;
  else
    try_ino = next_virtual_inode++;

  if (g_hash_table_contains (all_inodes, &try_ino))
    try_ino++;

  inode->ino = try_ino;
  g_hash_table_insert (all_inodes, &inode->ino, inode);
  G_UNLOCK (all_inodes);

  return inode;
}

static ino_t
xdp_inode_to_ino (XdpInode *inode)
{
  return inode->ino;
}

/* This is called on kernel upcalls, so it *should* be guaranteed that the inode exists due to the kernel refs */
static XdpInode *
xdp_inode_from_ino (ino_t ino)
{
  XdpInode *inode;

  G_LOCK (all_inodes);
  inode = g_hash_table_lookup (all_inodes, &ino);
  G_UNLOCK (all_inodes);

  g_assert (inode != NULL);

  /* Its safe to ref it here because we know it exists outside the lock due to the kernel refs */
  return xdp_inode_ref (inode);
}

static XdpInode *
xdp_inode_ref (XdpInode *inode)
{
  g_atomic_int_inc (&inode->ref_count);
  return inode;
}

static void
xdp_inode_unref (XdpInode *inode)
{
  gint old_ref;
  XdpDomain *domain;

  /* here we want to atomically do: if (ref_count>1) { ref_count--; return; } */
retry_atomic_decrement1:
  old_ref = g_atomic_int_get (&inode->ref_count);
  if (old_ref > 1)
    {
      if (!g_atomic_int_compare_and_exchange ((int *) &inode->ref_count, old_ref, old_ref - 1))
        goto retry_atomic_decrement1;
    }
  else
    {
      if (old_ref <= 0)
        {
          g_warning ("Can't unref dead inode");
          return;
        }

      /* Might be revived from domain->inodes hash by this time, so protect by lock */
      G_LOCK (domain_inodes);

      if (!g_atomic_int_compare_and_exchange ((int *) &inode->ref_count, old_ref, old_ref - 1))
        {
          G_UNLOCK (domain_inodes);
          goto retry_atomic_decrement1;
        }

      domain = inode->domain;

      if (domain->type == XDP_DOMAIN_APP)
        g_hash_table_remove (domain->parent->inodes, domain->app_id);
      else if (domain->type == XDP_DOMAIN_DOCUMENT)
        {
          if (inode->physical)
            {
              g_hash_table_remove (domain->inodes, inode->physical);
            }
          else
            g_hash_table_remove (domain->parent->inodes, domain->doc_id);
        }

      /* Run this under domain_inodes lock to avoid race condition in ensure_docdir_inode + xdp_inode_new
       * where we don't want a domain->inode lookup to fail, but then an all_inode lookup to succeed
       * when looking for an ino collision
       *
       * Note: After the domain->inodes removal and here we don't allow resurrection, but we may
       * still race with an all_inodes lookup (e.g. in xdp_fuse_lookup_id_for_inode), which *is*
       * allowed and it can read the inode fields (while the lock is held) as they are still valid.
       **/
      G_LOCK (all_inodes);
      g_hash_table_remove (all_inodes, &inode->ino);
      G_UNLOCK (all_inodes);

      G_UNLOCK (domain_inodes);

      /* By now we have no refs outstanding and no way to get at the inode, so free it */

      g_clear_pointer (&inode->domain_root_inode, xdp_inode_unref);
      g_clear_pointer (&inode->physical, xdp_physical_inode_unref);
      xdp_domain_unref (inode->domain);
      g_free (inode);
    }

}

static XdpInode *
xdp_inode_kernel_ref (XdpInode *inode)
{
  int old;

  old = g_atomic_int_add (&inode->kernel_ref_count, 1);

  if (old == 0)
    xdp_inode_ref (inode);
  return inode;
}

static void
xdp_inode_kernel_unref (XdpInode *inode, unsigned long count)
{
  gint old_ref, new_ref;

 retry_atomic_decrement1:
  old_ref = g_atomic_int_get (&inode->kernel_ref_count);
  if (old_ref < count)
    {
      g_warning ("Can't kernel_unref inode with no kernel refs");
      return;
    }
  new_ref = old_ref - count;
  if (!g_atomic_int_compare_and_exchange (&inode->kernel_ref_count, old_ref, new_ref))
    goto retry_atomic_decrement1;

  if (new_ref == 0)
    xdp_inode_unref (inode);
}

static int
verify_doc_dir_devino (int dirfd, XdpDomain *doc_domain)
{
  struct stat buf;

  if (fstat (dirfd, &buf) != 0)
    return -errno;

  if (buf.st_ino != doc_domain->doc_dir_inode ||
      buf.st_dev != doc_domain->doc_dir_device)
    return -ENOENT;

  return 0;
}

/* Only for toplevel dirs, not this is a bit weird for toplevel dir
   inodes as it returns the dir itself which isn't really the dirfd
   for that (nonphysical) inode */
static int
xdp_nonphysical_document_inode_opendir (XdpInode *inode)
{
  XdpDomain *domain = inode->domain;
  g_autofd int dirfd = -1;
  int res;

  g_assert (domain->type == XDP_DOMAIN_DOCUMENT);
  g_assert (inode->physical == NULL);

  dirfd = open (domain->doc_path, O_PATH | O_DIRECTORY);
  if (dirfd < 0)
    return -errno;

  res = verify_doc_dir_devino (dirfd, domain);
  if (res != 0)
    return res;

  return g_steal_fd (&dirfd);
}

static int
xdp_document_inode_ensure_dirfd (XdpInode *inode,
                                 int      *close_fd_out)
{
  int close_fd;

  g_assert (inode->domain->type == XDP_DOMAIN_DOCUMENT);

  *close_fd_out = -1;

  if (inode->physical)
    return inode->physical->fd;
  else
    {
      if (xdp_document_domain_is_dir (inode->domain))
        {
          /* There is no dirfd for the toplevel dirs, happens for example
             if renaming into toplevel, so just return EPERM */
          return -EPERM;
        }

      close_fd = xdp_nonphysical_document_inode_opendir (inode);
      if (close_fd < 0)
        return close_fd;

      *close_fd_out = close_fd;
      return close_fd;
    }
}

static gboolean
open_flags_has_write (int open_flags)
{
  return
    (open_flags & O_ACCMODE) == O_WRONLY ||
    (open_flags & O_ACCMODE) == O_RDWR ||
    open_flags & O_TRUNC;
}

static void
gen_temp_name (gchar *tmpl)
{
  g_return_if_fail (tmpl != NULL);
  const size_t len = strlen (tmpl);
  g_return_if_fail (len >= 6);

  static const char letters[] =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  static const int NLETTERS = sizeof (letters) - 1;

  char *XXXXXX = tmpl + (len - 6);
  for (int i = 0; i < 6; i++)
    XXXXXX[i] = letters[g_random_int_range(0, NLETTERS)];
}

static int
open_temp_at (int          dirfd,
              const char  *orig_name,
              char       **name_out,
              mode_t       mode)
{
  g_autofree char *tmp = g_strconcat (".xdp-", orig_name, "-XXXXXX", NULL);
  const size_t count_max = 100;
  int errsv;
  int fd;

  for (size_t count = 0; count < count_max; count++)
    {
      gen_temp_name (tmp);

      fd = openat (dirfd, tmp, O_CREAT | O_EXCL | O_NOFOLLOW | O_NOCTTY | O_RDWR, mode);
      errsv = errno;
      if (fd < 0)
        {
          if (errsv == EEXIST)
            continue;
          else
            return -errsv;
        }

      *name_out = g_steal_pointer (&tmp);
      return fd;
    }

  return -EEXIST;
}

/* allocates tempfile for existing file,
   Called with tempfile lock held, sets errno */
static int
get_tempfile_for (XdpInode     *parent,
                  XdpDomain    *domain,
                  const char   *name,
                  int           dirfd,
                  const char   *tmpname,
                  XdpTempfile **tempfile_out)
{
  g_autoptr(XdpTempfile) tempfile = NULL;
  g_autoptr(XdpInode) inode = NULL;
  g_autofd int o_path_fd = -1;
  int res;

  if (tempfile_out != NULL)
    *tempfile_out = NULL;

  o_path_fd = openat (dirfd, tmpname, O_PATH, 0);
  if (o_path_fd == -1)
    return -errno;

  res = ensure_docdir_inode (parent, g_steal_fd (&o_path_fd), NULL, &inode); /* passed ownership of o_path_fd */
  if (res != 0)
    return res;

  tempfile = xdp_tempfile_new (inode, name, tmpname);

  /* This is atomic, because we're called with the lock held */
  g_hash_table_replace (domain->tempfiles, tempfile->name, xdp_tempfile_ref (tempfile));

  queue_invalidate_dentry (parent, tempfile->name);

  if (tempfile_out)
    *tempfile_out = g_steal_pointer (&tempfile);
  return 0;
}

/* Creates a new file on disk,
   Called with tempfile lock held, sets errno */
static int
create_tempfile (XdpInode     *parent,
                 XdpDomain    *domain,
                 const char   *name,
                 int           dirfd,
                 mode_t        mode,
                 XdpTempfile **tempfile_out)
{
  g_autoptr(XdpInode) inode = NULL;
  g_autofree char *real_fd_path = NULL;
  g_autofd int real_fd = -1;
  g_autofd int o_path_fd = -1;
  g_autoptr(XdpTempfile) tempfile = NULL;
  g_autofree char *tmpname = NULL;
  int res;

  if (tempfile_out != NULL)
    *tempfile_out = NULL;

  real_fd = open_temp_at (dirfd, name, &tmpname, mode);
  if (real_fd < 0)
    return real_fd;

  real_fd_path = fd_to_path (real_fd);
  o_path_fd = open (real_fd_path, O_PATH, 0);
  if (o_path_fd == -1)
    return -errno;

  /* We can close the tmpfd early */
  close (g_steal_fd (&real_fd));

  res = ensure_docdir_inode (parent, g_steal_fd (&o_path_fd), NULL, &inode); /* passed ownership of o_path_fd */
  if (res != 0)
    return res;

  tempfile = xdp_tempfile_new (inode, name, tmpname);

  /* This is atomic, because we're called with the lock held */
  g_hash_table_replace (domain->tempfiles, tempfile->name, xdp_tempfile_ref (tempfile));

  queue_invalidate_dentry (parent, tempfile->name);

  if (tempfile_out)
    *tempfile_out = g_steal_pointer (&tempfile);
  return 0;
}

static int
xdp_document_inode_open_child_fd (XdpInode   *inode,
                                  const char *name,
                                  int         open_flags,
                                  mode_t      mode)
{
  XdpDomain *domain = inode->domain;
  XdpTempfile *tempfile_lookup = NULL;
  g_autoptr(XdpTempfile) tempfile = NULL;
  int tempfile_res = 0;
  g_autofd int fd = -1;

  g_assert (domain->type == XDP_DOMAIN_DOCUMENT);

  if (!xdp_document_domain_can_write (domain) &&
      (open_flags_has_write (open_flags) ||
       (open_flags & O_CREAT) != 0))
      return -EACCES;

  if (inode->physical)
    {
      fd = openat (inode->physical->fd, name, open_flags, mode);
      if (fd == -1)
        return -errno;

      return g_steal_fd (&fd);
    }
  else
    {
      g_autofd int dirfd = -1;

      if (xdp_document_domain_is_dir (domain))
        {
          if (strcmp (name, domain->doc_file) == 0)
            {
              /* Ensure toplevel dir exist and is right */
              dirfd = xdp_nonphysical_document_inode_opendir (inode);
              if (dirfd < 0)
                return dirfd;

              fd = openat (dirfd, ".", open_flags, mode);
              if (fd == -1)
                return -errno;
              return g_steal_fd (&fd);
            }
        }
      else
        {
          /* Ensure parent dir exist and is right */
          dirfd = xdp_nonphysical_document_inode_opendir (inode);
          if (dirfd < 0)
            return dirfd;

          if (strcmp (name, domain->doc_file) == 0)
            {
              fd = openat (dirfd, name, open_flags, mode);
              if (fd == -1)
                return -errno;
              return g_steal_fd (&fd);
            }

          /* Not main file, maybe a temporary file? */

          g_mutex_lock (&domain->tempfile_mutex);

          tempfile_lookup = g_hash_table_lookup (domain->tempfiles, name);
          if (tempfile_lookup)
            {
              if ((open_flags & O_CREAT) && (open_flags & O_EXCL))
                tempfile_res = -EEXIST;
              else
                tempfile = xdp_tempfile_ref (tempfile_lookup);
            }
          else if (open_flags & O_CREAT)
            {
              tempfile_res = create_tempfile (inode, domain, name, dirfd, mode, &tempfile);
            }

          g_mutex_unlock (&domain->tempfile_mutex);

          if (tempfile)
            {
              g_autofree char *fd_path = fd_to_path (tempfile->inode->physical->fd);
              fd = open (fd_path, open_flags & ~(O_CREAT|O_EXCL|O_NOFOLLOW), mode);
              if (fd == -1)
                return -errno;

              return g_steal_fd (&fd);
            }
          else
            {
              if (tempfile_res != 0)
                return tempfile_res;
            }
        }
    }

  return -ENOENT;
}

/* Returns /proc/self/fds/$fd path for O_PATH fd or toplevel path */
static char *
xdp_document_inode_get_self_as_path (XdpInode *inode)
{
  g_assert (inode->domain->type == XDP_DOMAIN_DOCUMENT);

  if (inode->physical)
    return fd_to_path (inode->physical->fd);
  else
    {
      if (xdp_document_domain_is_dir (inode->domain))
        return NULL;
      return g_strdup (inode->domain->doc_path);
    }
}

static void
tweak_statbuf_for_document_inode (XdpInode    *inode,
                                  struct stat *buf)
{
  XdpDomain   *domain = inode->domain;

  g_assert (domain->type == XDP_DOMAIN_DOCUMENT);

  buf->st_ino = xdp_inode_to_ino (inode);

  /* Remove setuid/setgid/sticky flags */
  buf->st_mode &= ~(S_ISUID|S_ISGID|S_ISVTX);

  if (!xdp_document_domain_can_write (domain))
    buf->st_mode &= ~(0222);
}

static void
xdp_reply_err (const char *op,
               fuse_req_t  req,
               int         err)
{
  if (err != 0)
    {
      const char *errname = NULL;
      switch (err)
        {
        case ESTALE:
          errname = "ESTALE";
          break;
        case EEXIST:
          errname = "EEXIST";
          break;
        case ENOENT:
          errname = "ENOENT";
          break;
        case EPERM:
          errname = "EPERM";
          break;
        case EACCES:
          errname = "EACCES";
          break;
        case EINVAL:
          errname = "EINVAL";
          break;
        default:
          errname = NULL;
        }
      if (errname != NULL)
        g_debug ("%s -> error %s", op, errname);
      else
        g_debug ("%s -> error %d", op, err);
    }
  fuse_reply_err (req, err);
}

static inline void
xdp_reply_ok (const char *op,
              fuse_req_t  req)
{
  xdp_reply_err (op, req, 0);
}

typedef enum {
      CHECK_CAN_WRITE = 1 << 0,
      CHECK_IS_DIRECTORY = 1 << 1,
      CHECK_IS_PHYSICAL = 1 << 2,
      CHECK_IS_PHYSICAL_IF_DIR = 1 << 3,
} XdpDocumentChecks;

static gboolean
xdp_document_inode_checks (const char        *op,
                           fuse_req_t         req,
                           XdpInode          *inode,
                           XdpDocumentChecks  checks)
{
  XdpDomain *domain = inode->domain;
  gboolean check_is_directory = (checks & CHECK_IS_DIRECTORY) != 0;
  gboolean check_can_write = (checks & CHECK_CAN_WRITE) != 0;
  gboolean check_is_physical = (checks & CHECK_IS_PHYSICAL) != 0;
  gboolean check_is_physical_if_dir = (checks & CHECK_IS_PHYSICAL_IF_DIR) != 0;

  if (domain->type != XDP_DOMAIN_DOCUMENT)
    {
      xdp_reply_err (op, req, EPERM);
      return FALSE;
    }

  /* We allowed the inode lookup to succeed, but maybe the permissions changed since then */
  if (!xdp_document_domain_can_see (domain))
    {
      xdp_reply_err (op, req, EACCES);
      return FALSE;
    }

  if (check_is_directory && !xdp_document_domain_is_dir (domain))
    {
      xdp_reply_err (op, req, EPERM);
      return FALSE;
    }

  if (check_can_write && !xdp_document_domain_can_write (domain))
    {
      xdp_reply_err (op, req, EACCES);
      return FALSE;
    }

  if (check_is_physical_if_dir && xdp_document_domain_is_dir (domain))
    check_is_physical = TRUE;

  if (check_is_physical && inode->physical == NULL)
    {
      xdp_reply_err (op, req, EPERM);
      return FALSE;
    }

  return TRUE;
}

static void
stat_virtual_inode (XdpInode    *inode,
                    struct stat *buf)
{
  memset (buf, 0, sizeof (struct stat));
  buf->st_ino = xdp_inode_to_ino (inode);
  buf->st_uid = my_uid;
  buf->st_gid = my_gid;

  switch (inode->domain->type)
    {
    case XDP_DOMAIN_ROOT:
    case XDP_DOMAIN_BY_APP:
    case XDP_DOMAIN_APP:
      buf->st_mode = S_IFDIR | NON_DOC_DIR_PERMS;
      buf->st_nlink = 2;
      break;
    case XDP_DOMAIN_DOCUMENT:
      if (xdp_document_domain_is_dir (inode->domain))
        buf->st_mode = S_IFDIR | DOC_DIR_PERMS_DIR;
      else
        buf->st_mode = S_IFDIR | DOC_DIR_PERMS_FILE;
      buf->st_nlink = 2;

      /* Remove perms if not writable */
      if (inode->domain->app_id != NULL)
        {
          g_autoptr(PermissionDbEntry) entry = xdp_lookup_doc (inode->domain->doc_id);
          if (entry == NULL || !app_can_write_doc (entry, inode->domain->app_id))
            buf->st_mode &= ~(0222);
        }
      break;

    default:
      g_assert_not_reached ();
      break;
    }
}

static void
xdp_fuse_getattr (fuse_req_t             req,
                  fuse_ino_t             ino,
                  struct fuse_file_info *fi)
{
  g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino);
  XdpDomain *domain = inode->domain;
  double attr_valid_time = 0.0;/* Time in secs for attribute validation */
  struct stat buf;
  int res;
  const char *op = "GETATTR";

  g_debug ("GETATTR %" G_GINT64_MODIFIER "x", ino);

  if (xdp_domain_is_virtual_type (domain))
    {
      stat_virtual_inode (inode, &buf);
      fuse_reply_attr (req, &buf, attr_valid_time);
      return;
    }

  g_assert (domain->type == XDP_DOMAIN_DOCUMENT);

  if (inode->physical)
    {
      res = fstatat (inode->physical->fd, "", &buf, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW);
    }
  else
    {
      stat_virtual_inode (inode, &buf);
      res = 0;
    }

  if (res == -1)
    return xdp_reply_err (op, req, errno);

  tweak_statbuf_for_document_inode (inode, &buf);

  fuse_reply_attr (req, &buf, attr_valid_time);
}

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_autoptr(XdpInode) inode = xdp_inode_from_ino (ino);
  g_autofree char *to_set_string = setattr_flags_to_string (to_set);
  struct stat buf;
  double attr_valid_time = 0.0;/* Time in secs for attribute validation */
  int res;
  const char *op = "SETATTR";

  g_debug ("SETATTR %" G_GINT64_MODIFIER "x %s", ino, to_set_string);

  if (!xdp_document_inode_checks (op, req, inode,
                                  CHECK_CAN_WRITE | CHECK_IS_PHYSICAL))
    return;

  /* Truncate */
  if (to_set & FUSE_SET_ATTR_SIZE)
    {
      g_autofree char *path = NULL;
      XdpFile *file = (XdpFile *)fi->fh;

      if (file)
        {
          res = ftruncate (file->fd, attr->st_size);
          if (res == -1)
            res = -errno;
        }
      else if (inode->physical)
        {
          path = fd_to_path (inode->physical->fd);
          res = truncate (path, attr->st_size);
          if (res == -1)
            res = -errno;
        }
      else
        {
          res = -EISDIR;
        }

      if (res != 0)
        return xdp_reply_err (op, req, -res);
    }

  if (to_set & (FUSE_SET_ATTR_ATIME | FUSE_SET_ATTR_MTIME))
    {
      struct timespec times[2] = { {0, UTIME_OMIT}, {0, UTIME_OMIT} }; /* 0 = atime, 1 = mtime */
      g_autofree char *path = NULL;

      if (to_set & FUSE_SET_ATTR_ATIME_NOW)
        times[0].tv_nsec = UTIME_NOW;
      else if (to_set & FUSE_SET_ATTR_ATIME)
        times[0] = attr->st_atim;

      if (to_set & FUSE_SET_ATTR_MTIME_NOW)
        times[1].tv_nsec = UTIME_NOW;
      else if (to_set & FUSE_SET_ATTR_MTIME)
        times[1] = attr->st_mtim;

      if (inode->physical)
        {
          path = fd_to_path (inode->physical->fd);
          res = utimensat (AT_FDCWD, path, times, 0);
        }
      else
        res = utimensat (AT_FDCWD, inode->domain->doc_path, times, 0); /* follow symlink here */

      if (res != 0)
        return xdp_reply_err (op, req, errno);
    }

  if (to_set & (FUSE_SET_ATTR_UID | FUSE_SET_ATTR_GID))
    {
      g_autofree char *path = NULL;
      uid_t uid = -1;
      gid_t gid = -1;

      if (to_set & FUSE_SET_ATTR_UID)
        uid = attr->st_uid;

      if (to_set & FUSE_SET_ATTR_GID)
        gid = attr->st_gid;

      if (inode->physical)
        {
          path = fd_to_path (inode->physical->fd);
          res = chown (path, uid, gid);
          if (res == -1)
            res = -errno;
        }
      else
        {
          res = -EACCES;
        }

      if (res != 0)
        return xdp_reply_err (op, req, -res);
    }

  if (to_set & (FUSE_SET_ATTR_MODE))
    {
      g_autofree char *path = NULL;

      if (inode->physical)
        {
          path = fd_to_path (inode->physical->fd);
          res = chmod (path, attr->st_mode);
          if (res == -1)
            res = -errno;
        }
      else
        {
          res = -EACCES;
        }

      if (res != 0)
        return xdp_reply_err (op, req, -res);
    }

  if (inode->physical)
    res = fstatat (inode->physical->fd, "", &buf, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW);
  else
    res = stat (inode->domain->doc_path, &buf); /* Follow symlinks here */

  if (res != 0)
    return xdp_reply_err (op, req, errno);

  tweak_statbuf_for_document_inode (inode, &buf);

  fuse_reply_attr (req, &buf, attr_valid_time);
}

static void
prepare_reply_entry (XdpInode                *inode,
                     struct stat             *buf,
                     struct fuse_entry_param *e)
{
  xdp_inode_kernel_ref (inode); /* Ref given to the kernel, returned in xdp_forget() */
  e->ino = xdp_inode_to_ino (inode);
  e->generation = 1;
  e->attr = *buf;
  e->attr_timeout = 0.0; /* attribute timeout */
  e->entry_timeout = 0.0; /* dentry timeout */
}

static void
prepare_reply_virtual_entry (XdpInode                *inode,
                             struct fuse_entry_param *e)
{
  stat_virtual_inode (inode, &e->attr);

  xdp_inode_kernel_ref (inode); /* Ref given to the kernel, returned in xdp_forget() */
  e->ino = xdp_inode_to_ino (inode);
  e->generation = 1;

  /* Cache virtual dirs */
  e->attr_timeout = 60.0; /* attribute timeout */
  e->entry_timeout = 60.0; /* dentry timeout */
}

static void
abort_reply_entry (struct fuse_entry_param *e)
{
  XdpInode *inode = xdp_inode_from_ino (e->ino);
  xdp_inode_kernel_unref (inode, 1);
}

static int
ensure_docdir_inode (XdpInode                 *parent,
                     int                       o_path_fd_in,
                     struct fuse_entry_param  *e,
                     XdpInode                **inode_out)
{
  XdpDomain *domain = parent->domain;
  g_autoptr(XdpPhysicalInode) physical = NULL;
  g_autoptr(XdpInode) inode = NULL;
  g_autofd int o_path_fd = -1;
  struct stat buf;
  int res;

  /* Take ownership */
  o_path_fd = g_steal_fd (&o_path_fd_in);

  res = fstatat (o_path_fd, "", &buf, AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW);
  if (res == -1)
    return -errno;

  /* non-directory documents only support regular files */
  if (!xdp_document_domain_is_dir (domain) &&  !S_ISREG(buf.st_mode))
    return -ENOENT;

  physical = ensure_physical_inode (buf.st_dev, buf.st_ino, g_steal_fd (&o_path_fd)); /* passed ownership of fd */

  G_LOCK (domain_inodes);
  inode = g_hash_table_lookup (domain->inodes, physical);
  if (inode != NULL)
    inode = xdp_inode_ref (inode);
  else
    {
      inode = xdp_inode_new (domain, physical);
      if (parent->domain_root_inode)
        inode->domain_root_inode = xdp_inode_ref (parent->domain_root_inode);
      else
        inode->domain_root_inode = xdp_inode_ref (parent);
      g_hash_table_insert (domain->inodes, physical, inode);
    }
  G_UNLOCK (domain_inodes);

  if (e)
    {
      tweak_statbuf_for_document_inode (inode, &buf);
      prepare_reply_entry (inode, &buf, e);
    }

  if (inode_out)
    *inode_out = g_steal_pointer (&inode);

  return 0;
}

static int
ensure_docdir_inode_by_name (XdpInode                *parent,
                             int                      dirfd,
                             const char              *name,
                             struct fuse_entry_param *e)
{
  int o_path_fd;

  o_path_fd = openat (dirfd, name, O_PATH|O_NOFOLLOW, 0);
  if (o_path_fd == -1)
      return -errno;

  return ensure_docdir_inode (parent, o_path_fd, e, NULL); /* Takes ownership of o_path_fd */
}


static XdpInode *
ensure_by_app_inode (XdpInode   *by_app_inode,
                     const char *app_id)
{
  XdpDomain *by_app_domain = by_app_inode->domain;
  g_autoptr(XdpInode) inode = NULL;

  if (!xdp_is_valid_app_id (app_id))
    return NULL;

  G_LOCK (domain_inodes);
  inode = g_hash_table_lookup (by_app_domain->inodes, app_id);
  if (inode != NULL)
    inode = xdp_inode_ref (inode);
  else
    {
      g_autoptr(XdpDomain) app_domain = xdp_domain_new_app (by_app_inode, app_id);
      inode = xdp_inode_new (app_domain, NULL);
      g_hash_table_insert (by_app_domain->inodes, app_domain->app_id, inode);
    }
  G_UNLOCK (domain_inodes);

  return g_steal_pointer (&inode);
}

static XdpInode *
ensure_doc_inode (XdpInode   *parent,
                  const char *doc_id)
{
  g_autoptr(PermissionDbEntry) doc_entry = NULL;
  g_autoptr(XdpInode) inode = NULL;
  XdpDomain *parent_domain = parent->domain;

  doc_entry = xdp_lookup_doc (doc_id);

  if (doc_entry == NULL ||
      (parent_domain->app_id &&
       !app_can_see_doc (doc_entry, parent_domain->app_id)))
    return NULL;

  G_LOCK (domain_inodes);
  inode = g_hash_table_lookup (parent_domain->inodes, doc_id);
  if (inode != NULL)
    inode = xdp_inode_ref (inode);
  else
    {
      g_autoptr(XdpDomain) doc_domain = xdp_domain_new_document (parent_domain, doc_id, doc_entry);
      doc_domain->parent_inode = xdp_inode_ref (parent);
      inode = xdp_inode_new (doc_domain, NULL);
      g_hash_table_insert (parent_domain->inodes, doc_domain->doc_id, inode);
    }
  G_UNLOCK (domain_inodes);

  return g_steal_pointer (&inode);
}

static gboolean
invalidate_dentry_cb (gpointer user_data)
{
  GList *to_invalidate = NULL;
  {
    XDP_AUTOLOCK (invalidate_list);
    to_invalidate = g_steal_pointer (&invalidate_list);
  }
  to_invalidate = g_list_reverse (to_invalidate);

  XDP_AUTOLOCK (session);
  for (GList *l = to_invalidate; l != NULL; l = l->next)
    {
      XdpInvalidateData *data = l->data;
      if (session)
        fuse_lowlevel_notify_inval_entry (session, data->parent_ino, data->name,
                                          strlen (data->name));
      g_free (data);
    }

  g_list_free (to_invalidate);

  return FALSE;
}

/* Queue an inval_dentry, thereby freeing unused inodes in the dcache
 * which will free up a bunch of O_PATH fds in the fuse implementation.
 */
static void
queue_invalidate_dentry (XdpInode   *parent,
                         const char *name)
{
  XDP_AUTOLOCK (invalidate_list);

  for (GList *l = invalidate_list; l != NULL; l = l->next)
    {
      XdpInvalidateData *data = l->data;
      if (data->parent_ino == parent->ino && strcmp (name, data->name) == 0)
        return;
    }

  XdpInvalidateData *data = g_malloc0 (sizeof (XdpInvalidateData) + strlen (name) + 1);
  data->parent_ino = parent->ino;
  strcpy (data->name, name);

  if (invalidate_list == NULL)
    g_timeout_add (10, invalidate_dentry_cb, NULL);

  invalidate_list = g_list_append (invalidate_list, data);
}

static void
xdp_fuse_lookup (fuse_req_t  req,
                 fuse_ino_t  parent_ino,
                 const char *name)
{
  g_autoptr(XdpInode) parent = xdp_inode_from_ino (parent_ino);
  XdpDomain *parent_domain = parent->domain;
  g_autoptr(XdpInode) inode = NULL;
  struct fuse_entry_param e;
  int res, fd;
  int open_flags = O_PATH|O_NOFOLLOW;
  const char *op = "LOOKUP";

  g_debug ("LOOKUP %" G_GINT64_MODIFIER "x:%s", parent_ino, name);

  if (strcmp (name, ".") == 0 || strcmp (name, "..") == 0)
    {
      /* We don't set FUSE_CAP_EXPORT_SUPPORT, so should not get
       * here. But lets make sure we never ever resolve them as that
       * could be a security issue by escaping the root. */
      return xdp_reply_err (op, req, ESTALE);
    }

  if (xdp_domain_is_virtual_type (parent_domain))
    {
      switch (parent_domain->type)
        {
        case XDP_DOMAIN_ROOT:
          if (strcmp (name, BY_APP_NAME) == 0)
            inode = xdp_inode_ref (by_app_inode);
          else
            inode = ensure_doc_inode (parent, name);
          break;
        case XDP_DOMAIN_BY_APP:
          inode = ensure_by_app_inode (parent, name);
          break;
        case XDP_DOMAIN_APP:
          inode = ensure_doc_inode (parent, name);
          break;
        default:
          g_assert_not_reached ();
        }

      if (inode == NULL)
        return xdp_reply_err (op, req, ENOENT);

      prepare_reply_virtual_entry (inode, &e);
    }
  else
    {
      g_assert (parent_domain->type == XDP_DOMAIN_DOCUMENT);

      fd = xdp_document_inode_open_child_fd (parent, name, open_flags, 0);
      if (fd < 0)
        return xdp_reply_err (op, req, -fd);

      res = ensure_docdir_inode (parent, fd, &e, NULL); /* Takes ownership of fd */
      if (res != 0)
        return xdp_reply_err (op, req, -res);

      queue_invalidate_dentry (parent, name);
    }

  g_debug ("LOOKUP %" G_GINT64_MODIFIER "x:%s => %" G_GINT64_MODIFIER "x", parent_ino, name, e.ino);

  if (fuse_reply_entry (req, &e) == -ENOENT)
    abort_reply_entry (&e);
}

static XdpFile *
xdp_file_new (int fd)
{
  XdpFile *file = g_new0 (XdpFile, 1);
  file->fd = fd;

  XDP_AUTOLOCK (open_files);
  open_files = g_list_prepend (open_files, file);
  file->link = open_files;

  return file;
}

static void
xdp_file_free (XdpFile *file)
{
  GList *link = g_steal_pointer (&file->link);

  XDP_AUTOLOCK (open_files);
  open_files = g_list_delete_link (open_files, link);

  close (file->fd);
  g_free (file);
}

static void
xdp_fuse_open (fuse_req_t             req,
               fuse_ino_t             ino,
               struct fuse_file_info *fi)
{
  g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino);
  int open_flags = fi->flags;
  g_autofree char *open_flags_string = open_flags_to_string (open_flags);
  int fd;
  g_autofree char *path = NULL;
  XdpFile *file = NULL;
  XdpDocumentChecks checks;
  const char *op = "OPEN";

  g_debug ("OPEN %" G_GINT64_MODIFIER "x %s", ino, open_flags_string);

  checks = CHECK_IS_PHYSICAL;
  if (open_flags_has_write (open_flags))
    checks |= CHECK_CAN_WRITE;

  /* Note: open_flags guaranteed to exclude O_CREAT, O_EXCL */
  if (!xdp_document_inode_checks (op, req, inode, checks))
    return;

  path = fd_to_path (inode->physical->fd);

  /*
   * `path` is a path to the fd entry in `/proc`, which is a symlink
   * to the actual file. Opening it directly with `O_NOFOLLOW` will
   * fail. So we should resolve it first then we can honour the no
   * follow flag.
   */
  if (open_flags & O_NOFOLLOW)
    {
      char resolved_path[PATH_MAX] = { 0, };
      ssize_t res;

      res = readlink (path, resolved_path, sizeof (resolved_path));

      if (res == sizeof (resolved_path))
        return xdp_reply_err (op, req, ENAMETOOLONG);
      if (res < 0)
        return xdp_reply_err (op, req, errno);

      g_clear_pointer (&path, g_free);
      path = g_strdup (resolved_path);
    }

  fd = open (path, open_flags, 0);
  if (fd == -1)
    return xdp_reply_err (op, req, errno);

  file = xdp_file_new (fd);

  fi->fh = (gsize)file;
  if (fuse_reply_open (req, fi) == -ENOENT)
    {
      /* The open syscall was interrupted, so it must be cancelled */
      xdp_file_free (file);
    }
}

static void
xdp_fuse_create (fuse_req_t             req,
                 fuse_ino_t             parent_ino,
                 const char            *filename,
                 mode_t                 mode,
                 struct fuse_file_info *fi)
{
  g_autoptr(XdpInode) parent = xdp_inode_from_ino (parent_ino);
  int open_flags = fi->flags;
  g_autofree char *open_flags_string = open_flags_to_string (open_flags);
  struct fuse_entry_param e;
  int res;
  g_autofd int fd = -1;
  g_autofd int o_path_fd = -1;
  g_autofree char *fd_path = NULL;
  XdpFile *file = NULL;
  const char *op = "CREATE";

  g_debug ("CREATE %" G_GINT64_MODIFIER "x %s %s, 0%o", parent_ino, filename, open_flags_string, mode);

  if (!xdp_document_inode_checks (op, req, parent,
                                  CHECK_CAN_WRITE |
                                  CHECK_IS_PHYSICAL_IF_DIR))
    return;

  fd = xdp_document_inode_open_child_fd (parent, filename, open_flags, mode);
  if (fd < 0)
    return xdp_reply_err (op, req, -fd);

  fd_path = fd_to_path (fd);
  o_path_fd = open (fd_path, O_PATH, 0);
  if (o_path_fd < 0)
    return xdp_reply_err (op, req, errno);

  res = ensure_docdir_inode (parent, g_steal_fd (&o_path_fd), &e, NULL); /* Takes ownership of o_path_fd */
  if (res != 0)
    return xdp_reply_err (op, req, -res);

  file = xdp_file_new (g_steal_fd (&fd)); /* Takes ownership of fd */

  fi->fh = (gsize)file;
  if (fuse_reply_create (req, &e, fi) == -ENOENT)
    {
      /* The open syscall was interrupted, so it must be cancelled */
      xdp_file_free (file);
      abort_reply_entry (&e);
    }

  queue_invalidate_dentry (parent, filename);
}

static void
xdp_fuse_read (fuse_req_t             req,
               fuse_ino_t             ino,
               size_t                 size,
               off_t                  off,
               struct fuse_file_info *fi)
{
  struct fuse_bufvec buf = FUSE_BUFVEC_INIT(size);
  XdpFile *file = (XdpFile *)fi->fh;
  enum fuse_buf_copy_flags reply_flags = FUSE_BUF_SPLICE_MOVE;
  XdpFuseOptions *fuse_opts = fuse_req_userdata (req);

  g_debug ("READ %" G_GINT64_MODIFIER "x size %" G_GSIZE_FORMAT " off %" G_GOFFSET_FORMAT, ino, size, (goffset)off);

  buf.buf[0].flags = FUSE_BUF_IS_FD | FUSE_BUF_FD_SEEK;
  buf.buf[0].fd = file->fd;
  buf.buf[0].pos = off;

  if (!fuse_opts->use_splice)
    reply_flags = FUSE_BUF_NO_SPLICE;

  fuse_reply_data (req, &buf, reply_flags);
}


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)
{
  XdpFile *file = (XdpFile *)fi->fh;
  ssize_t res;
  const char *op = "WRITE";

  g_debug ("WRITE %" G_GINT64_MODIFIER "x size %" G_GSIZE_FORMAT " off %" G_GOFFSET_FORMAT, ino, size, (goffset)off);

  res = pwrite (file->fd, buf, size, off);

  if (res >= 0)
    fuse_reply_write (req, res);
  else
    xdp_reply_err (op, req, errno);
}

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)
{
  XdpFile *file = (XdpFile *)fi->fh;
  struct fuse_bufvec dst = FUSE_BUFVEC_INIT(fuse_buf_size(bufv));
  ssize_t res;
  const char *op = "WRITEBUF";
  enum fuse_buf_copy_flags copy_flags = FUSE_BUF_SPLICE_NONBLOCK;
  XdpFuseOptions *fuse_opts = fuse_req_userdata (req);

  g_debug ("WRITEBUF %" G_GINT64_MODIFIER "x off %" G_GOFFSET_FORMAT, ino, (goffset)off);

  dst.buf[0].flags = FUSE_BUF_IS_FD | FUSE_BUF_FD_SEEK;
  dst.buf[0].fd = file->fd;
  dst.buf[0].pos = off;

  if (!fuse_opts->use_splice)
    copy_flags = FUSE_BUF_NO_SPLICE;

  res = fuse_buf_copy (&dst, bufv, copy_flags);
  if (res >= 0)
    fuse_reply_write (req, res);
  else
    xdp_reply_err (op, req, errno);
}

static void
xdp_fuse_fsync (fuse_req_t             req,
                fuse_ino_t             ino,
                int                    datasync,
                struct fuse_file_info *fi)
{
  XdpFile *file = (XdpFile *)fi->fh;
  int res;
  const char *op = "FSYNC";

  g_debug ("FSYNC %" G_GINT64_MODIFIER "x", ino);

  if (datasync)
    res = fdatasync (file->fd);
  else
    res = fsync (file->fd);
  if (res == 0)
    xdp_reply_ok (op, req);
  else
    xdp_reply_err (op, req, errno);
}

static void
xdp_fuse_fallocate (fuse_req_t             req,
                    fuse_ino_t             ino,
                    int                    mode,
                    off_t                  offset,
                    off_t                  length,
                    struct fuse_file_info *fi)
{
  XdpFile *file = (XdpFile *)fi->fh;
  int res;
  const char *op = "FALLOCATE";

  g_debug ("FALLOCATE %" G_GINT64_MODIFIER "x", ino);

#ifdef __linux__
  res = fallocate (file->fd, mode, offset, length);
#else
  res = posix_fallocate (file->fd, offset, length);
#endif

  if (res == 0)
    xdp_reply_ok (op, req);
  else
    xdp_reply_err (op, req, errno);
}

static void
xdp_fuse_flush (fuse_req_t             req,
                fuse_ino_t             ino,
                struct fuse_file_info *fi)
{
  const char *op = "FLUSH";

  g_debug ("FLUSH %" G_GINT64_MODIFIER "x", ino);
  xdp_reply_ok (op, req);
}

static void
xdp_fuse_release (fuse_req_t             req,
                  fuse_ino_t             ino,
                  struct fuse_file_info *fi)
{
  XdpFile *file = (XdpFile *)fi->fh;
  const char *op = "RELEASE";

  g_debug ("RELEASE %" G_GINT64_MODIFIER "x", ino);

  xdp_file_free (file);

  xdp_reply_ok (op, req);
}

static void
forget_one (fuse_ino_t ino,
            unsigned long nlookup)
{
  g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino);

  g_debug ("FORGET %" G_GINT64_MODIFIER "x %ld", ino, nlookup);
  xdp_inode_kernel_unref (inode, nlookup);
}

static void
xdp_fuse_forget (fuse_req_t req,
                 fuse_ino_t ino,
                 uint64_t   nlookup)
{
  forget_one (ino, nlookup);
  fuse_reply_none (req);
}

static void
xdp_fuse_forget_multi (fuse_req_t               req,
                       size_t                   count,
                       struct fuse_forget_data *forgets)
{
  size_t i;

  g_debug ("FORGET_MULTI %" G_GSIZE_FORMAT, count);

  for (i = 0; i < count; i++)
    forget_one (forgets[i].ino, forgets[i].nlookup);

  fuse_reply_none (req);
}

static void
xdp_dir_free (XdpDir *d)
{
  if (d->dir)
    closedir (d->dir);
  g_free (d->dirbuf);
  g_free (d);
}

static void
xdp_dir_add (XdpDir     *d,
             fuse_req_t  req,
             const char *name,
             mode_t      mode)
{
  struct stat stbuf;

  size_t oldsize = d->dirbuf_size;

  d->dirbuf_size += fuse_add_direntry (req, NULL, 0, name, NULL, 0);
  d->dirbuf = (char *) g_realloc (d->dirbuf, d->dirbuf_size);
  memset (&stbuf, 0, sizeof (stbuf));
  stbuf.st_ino = FUSE_UNKNOWN_INO;
  stbuf.st_mode = mode;
  fuse_add_direntry (req, d->dirbuf + oldsize,
                     d->dirbuf_size - oldsize,
                     name, &stbuf,
                     d->dirbuf_size);
}

static XdpDir *
xdp_dir_new_physical (DIR *dir)
{
  XdpDir *d = g_new0 (XdpDir, 1);
  d->dir = dir;
  d->offset = 0;
  d->entry = NULL;
  return d;
}

static XdpDir *
xdp_dir_new_buffered (fuse_req_t  req)
{
  XdpDir *d = g_new0 (XdpDir, 1);
  xdp_dir_add (d, req, ".", S_IFDIR);
  xdp_dir_add (d, req, "..", S_IFDIR);
  return d;
}

static void
xdp_dir_add_docs (XdpDir     *d,
                  fuse_req_t  req,
                  const char *for_app_id)
{
  g_auto(GStrv) docs = NULL;
  int i;

  docs = xdp_list_docs ();
  for (i = 0; docs[i] != NULL; i++)
    {
      if (for_app_id)
        {
          g_autoptr(PermissionDbEntry) entry = xdp_lookup_doc (docs[i]);
          if (entry == NULL ||
              !app_can_see_doc (entry, for_app_id))
            continue;
        }

      xdp_dir_add (d, req, docs[i], S_IFDIR);
    }
}

static void
xdp_dir_add_apps (XdpDir     *d,
                  XdpDomain  *domain,
                  fuse_req_t  req,
                  const char *for_app_id)
{
  g_auto(GStrv) apps = NULL;
  g_auto(GStrv) names = NULL;
  int i;

  /* First all pre-used apps as these can be created on demand */
  names = xdp_domain_get_inode_keys_as_string (domain);
  for (i = 0; names[i] != NULL; i++)
    xdp_dir_add (d, req, names[i], S_IFDIR);

  /* Then all in the db (that don't already have inodes) */
  apps = xdp_list_apps ();
  for (i = 0; apps[i] != NULL; i++)
    {
      const char *app = apps[i];
      if (!g_strv_contains ((const gchar * const *)names, app))
        xdp_dir_add (d, req, app, S_IFDIR);
    }
}

static void
xdp_fuse_opendir (fuse_req_t             req,
                  fuse_ino_t             ino,
                  struct fuse_file_info *fi)
{
  g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino);
  XdpDomain *domain = inode->domain;
  XdpDir *d = NULL;
  const char *op = "OPENDIR";

  g_debug ("OPENDIR %" G_GINT64_MODIFIER "x domain %d", ino, inode->domain->type);

  if (xdp_domain_is_virtual_type (domain))
    {
      d = xdp_dir_new_buffered (req);
      switch (domain->type)
        {
        case XDP_DOMAIN_ROOT:
          xdp_dir_add (d, req, BY_APP_NAME, S_IFDIR);
          xdp_dir_add_docs (d, req, NULL);
          break;
        case XDP_DOMAIN_APP:
          xdp_dir_add_docs (d, req, domain->app_id);
          break;
        case XDP_DOMAIN_BY_APP:
          xdp_dir_add_apps (d, inode->domain, req, NULL);
          break;
        default:
          g_assert_not_reached ();
        }
    }
  else
    {
      g_assert (domain->type == XDP_DOMAIN_DOCUMENT);

      if (xdp_document_domain_is_dir (domain))
        {
          if (inode->physical)
            {
              DIR *dir;
              int fd;

              fd = openat (inode->physical->fd, ".", O_RDONLY | O_DIRECTORY, 0);
              if (fd < 0)
                return xdp_reply_err (op, req, errno);

              dir = fdopendir (fd);
              if (dir == NULL)
                {
                  xdp_reply_err (op, req, errno);
                  close (fd);
                  return;
                }

              d = xdp_dir_new_physical (dir);
            }
          else /* Nonphysical, i.e. toplevel */
            {
              struct stat buf;

              d = xdp_dir_new_buffered (req);

              if (stat (domain->doc_path, &buf) == 0 &&
                  buf.st_ino == domain->doc_dir_inode &&
                  buf.st_dev == domain->doc_dir_device)
                {
                  xdp_dir_add (d, req, domain->doc_file, buf.st_mode);
                }
            }
        }
      else
        {
          g_autofree char *main_path = g_build_filename (domain->doc_path, domain->doc_file, NULL);
          struct stat buf;
          GHashTableIter iter;
          gpointer key, value;

          d = xdp_dir_new_buffered (req);

          if (stat (main_path, &buf) == 0)
            xdp_dir_add (d, req, domain->doc_file, buf.st_mode);

          g_mutex_lock (&domain->tempfile_mutex);

          g_hash_table_iter_init (&iter, domain->tempfiles);
          while (g_hash_table_iter_next (&iter, &key, &value))
            {
              const char *tempname = key;
              xdp_dir_add (d, req, tempname, S_IFREG);
            }

          g_mutex_unlock (&domain->tempfile_mutex);
        }
    }

  fi->fh = (gsize)d;

  if (fuse_reply_open (req, fi) == -ENOENT)
    {
      /* The opendir syscall was interrupted, so it must be cancelled */
      xdp_dir_free (d);
    }
}

static void
xdp_fuse_readdir (fuse_req_t             req,
                  fuse_ino_t             ino,
                  size_t                 size,
                  off_t                  off,
                  struct fuse_file_info *fi)
{
  XdpDir *d = (XdpDir *)fi->fh;
  const char *op = "READDIR";

  g_debug ("READDIR %" G_GINT64_MODIFIER "x %" G_GSIZE_FORMAT " %" G_GOFFSET_FORMAT, ino, size, (goffset)off);

  if (d->dir)
    {
      g_autofree char *buf = g_try_malloc (size);
      size_t rem;
      char *p;

      if (buf == NULL)
        {
          xdp_reply_err (op, req, ENOMEM);
          return;
        }

      /* If offset is not same, need to seek it */
      if (off != d->offset)
        {
          seekdir (d->dir, off);
          d->entry = NULL;
          d->offset = off;
        }

      p = buf;
      rem = size;
      while (TRUE)
        {
          size_t entsize;
          off_t nextoff;

          if (!d->entry)
            {
              errno = 0;
              d->entry = readdir (d->dir);
              if (!d->entry)
                {
                  if (errno && rem == size)
                    {
                      xdp_reply_err (op, req, errno);
                      return;
                    }
                  break;
                }
            }
          nextoff = telldir (d->dir);

          struct stat st = {
            .st_ino = FUSE_UNKNOWN_INO,
            .st_mode = d->entry->d_type << 12,
          };
          entsize = fuse_add_direntry (req, p, rem,
                                       d->entry->d_name, &st, nextoff);
          /* The above function returns the size of the entry size even though
           * the copy failed due to smaller buf size, so I'm checking after this
           * function and breaking out in case we exceed the size.
           */
          if (entsize > rem)
            break;

          p += entsize;
          rem -= entsize;

          d->entry = NULL;
          d->offset = nextoff;
        }

      fuse_reply_buf(req, buf, size - rem);
    }
  else
    {
      if (off < d->dirbuf_size)
        {
          gsize reply_size = MIN (d->dirbuf_size - off, size);
          g_autofree char *buf = g_memdup2 (d->dirbuf + off, reply_size);
          fuse_reply_buf (req, buf, reply_size);
        }
      else
        {
          fuse_reply_buf (req, NULL, 0);
        }
    }
}

static void
xdp_fuse_releasedir (fuse_req_t             req,
                     fuse_ino_t             ino,
                     struct fuse_file_info *fi)
{
  XdpDir *d = (XdpDir *)fi->fh;
  const char *op = "RELEASEDIR";

  g_debug ("RELEASEDIR %" G_GINT64_MODIFIER "x", ino);

  xdp_dir_free (d);

  xdp_reply_ok (op, req);
}

static void
xdp_fuse_fsyncdir (fuse_req_t             req,
                   fuse_ino_t             ino,
                   int                    datasync,
                   struct fuse_file_info *fi)
{
  XdpDir *dir = (XdpDir *)fi->fh;
  int fd, res;
  const char *op = "FSYNCDIR";

  g_debug ("FSYNCDIR %" G_GINT64_MODIFIER "x", ino);

  if (dir->dir)
    {
      fd = dirfd (dir->dir);
      if (datasync)
        res = fdatasync (fd);
      else
        res = fsync (fd);
    }
  else
    res = 0;

  if (res == 0)
    xdp_reply_ok (op, req);
  else
    xdp_reply_err (op, req, errno);
}

static void
xdp_fuse_mkdir (fuse_req_t  req,
                fuse_ino_t  parent_ino,
                const char *name,
                mode_t      mode)
{
  g_autoptr(XdpInode) parent = xdp_inode_from_ino (parent_ino);
  struct fuse_entry_param e;
  int res;
  g_autofd int close_fd = -1;
  int dirfd;
  const char *op = "MKDIR";

  g_debug ("MKDIR %" G_GINT64_MODIFIER "x %s", parent_ino, name);

  if (!xdp_document_inode_checks (op, req, parent,
                                  CHECK_CAN_WRITE |
                                  CHECK_IS_DIRECTORY |
                                  CHECK_IS_PHYSICAL))
    return;

  dirfd = xdp_document_inode_ensure_dirfd (parent, &close_fd);
  if (dirfd < 0)
    return xdp_reply_err (op, req, -dirfd);

  res = mkdirat (dirfd, name, mode);
  if (res != 0)
    return xdp_reply_err (op, req, errno);

  res = ensure_docdir_inode_by_name (parent, dirfd, name, &e); /* Takes ownership of o_path_fd */
  if (res != 0)
    return xdp_reply_err (op, req, -res);

  if (fuse_reply_entry (req, &e) == -ENOENT)
    abort_reply_entry (&e);
}

static void
xdp_fuse_unlink (fuse_req_t  req,
                 fuse_ino_t  parent_ino,
                 const char *filename)
{
  g_autoptr(XdpInode) parent = xdp_inode_from_ino (parent_ino);
  XdpDomain *parent_domain = parent->domain;
  int res = -1;
  const char * op = "UNLINK";

  g_debug ("UNLINK %" G_GINT64_MODIFIER "x %s", parent_ino, filename);

  if (!xdp_document_inode_checks (op, req, parent,
                                  CHECK_CAN_WRITE |
                                  CHECK_IS_PHYSICAL_IF_DIR))
    return;

  if (parent->physical)
    {
      res = unlinkat (parent->physical->fd, filename, 0);
      if (res != 0)
        return xdp_reply_err (op, req, errno);
    }
  else
    {
      g_autofd int dirfd = -1;

      /* Only reached for non-directory inodes */

      dirfd = xdp_nonphysical_document_inode_opendir (parent);
      if (dirfd < 0)
        xdp_reply_err (op, req, -dirfd);

      if (strcmp (filename, parent_domain->doc_file) == 0)
        {
          res = unlinkat (dirfd, filename, 0);
          if (res != 0)
            return xdp_reply_err (op, req, errno);
        }
      else
        {
          gboolean removed = FALSE;

          /* Not directory and not main file, maybe a temporary file? */
          g_mutex_lock (&parent_domain->tempfile_mutex);
          removed = g_hash_table_remove (parent_domain->tempfiles, filename);
          g_mutex_unlock (&parent_domain->tempfile_mutex);

          if (!removed)
            return xdp_reply_err (op, req, ENOENT);
        }
    }

  xdp_reply_ok (op, req);
}

static int
try_renameat (int           olddirfd,
              const char   *oldpath,
              int           newdirfd,
              const char   *newpath,
              unsigned int  flags)
{
#if HAVE_RENAMEAT2
  return renameat2 (olddirfd, oldpath, newdirfd, newpath, flags);
#else
  if (flags)
    {
      g_warning ("renameat2 is not supported by this system and rename flags are set");
      errno = EINVAL;
      return -1;
    }

  return renameat (olddirfd, oldpath, newdirfd, newpath);
#endif
}


static void
xdp_fuse_rename (fuse_req_t    req,
                 fuse_ino_t    parent_ino,
                 const char   *name,
                 fuse_ino_t    newparent_ino,
                 const char   *newname,
                 unsigned int  flags)
{
  g_autoptr(XdpInode) parent = xdp_inode_from_ino (parent_ino);
  g_autoptr(XdpInode) newparent = xdp_inode_from_ino (newparent_ino);
  g_autofree char *rename_flags_string = renameat2_flags_to_string (flags);
  XdpDomain *domain;
  int res, errsv;
  int olddirfd, newdirfd, dirfd;
  g_autofd int close_fd1 = -1;
  g_autofd int close_fd2 = -1;
  const char *op = "RENAME";

  g_debug ("RENAME %" G_GINT64_MODIFIER "x %s -> %" G_GINT64_MODIFIER "x %s (flags: %s)", parent_ino, name,
           newparent_ino, newname, rename_flags_string);

  if (!xdp_document_inode_checks (op, req, parent,
                                  CHECK_CAN_WRITE |
                                  CHECK_IS_PHYSICAL_IF_DIR))
    return;

  /* Don't allow cross-domain renames */
  if (parent->domain != newparent->domain)
    return xdp_reply_err (op, req, EXDEV);

  domain = parent->domain;
  if (xdp_document_domain_is_dir (domain))
    {
      olddirfd = xdp_document_inode_ensure_dirfd (parent, &close_fd1);
      if (olddirfd < 0)
        return xdp_reply_err (op, req, -olddirfd);

      newdirfd = xdp_document_inode_ensure_dirfd (newparent, &close_fd2);
      if (newdirfd < 0)
        return xdp_reply_err (op, req, -newdirfd);

      res = try_renameat (olddirfd, name, newdirfd, newname, flags);
      if (res != 0)
        return xdp_reply_err (op, req, errno);

      xdp_reply_ok (op, req);
    }
  else
    {
      /* For non-directories, only allow renames in toplevel (nonphysical) dir */
      if (parent != newparent || parent->physical != NULL)
        return xdp_reply_err (op, req, EACCES);

      /* Early exit for same file */
      if (strcmp (name, newname) == 0)
        return xdp_reply_ok (op, req);

      dirfd = xdp_nonphysical_document_inode_opendir (parent);
      if (dirfd < 0)
        return xdp_reply_err (op, req, -dirfd);
      close_fd1 = dirfd;

      if (strcmp (name, domain->doc_file) == 0)
        {
          /* Source is (maybe) main file, destination is tempfile */
          g_autofree char *tmpname = NULL;
          int tmp_fd;

          /* Just use this to get an exclusive name, we will later replace its content */
          tmp_fd = open_temp_at (dirfd, newname, &tmpname, 0600);
          if (tmp_fd < 0)
            return xdp_reply_err (op, req, -tmp_fd);
          close (tmp_fd);

          g_mutex_lock (&domain->tempfile_mutex);
          res = try_renameat (dirfd, name, dirfd, tmpname, flags);
          if (res == -1)
            {
              res = -errno;
              /* Remove the temporary file if the move failed */
              (void) unlinkat (dirfd, tmpname, 0);
            }
          else
            {
              res = get_tempfile_for (parent, domain, newname, dirfd, tmpname, NULL);
            }

          g_mutex_unlock (&domain->tempfile_mutex);

          if (res != 0)
            return xdp_reply_err (op, req, -res);

          xdp_reply_ok (op, req);
        }
      else if (strcmp (newname, domain->doc_file) == 0)
        {
          gpointer stolen_value;

          /* source is (maybe) tempfile, Destination is main file */

          g_mutex_lock (&domain->tempfile_mutex);
          if (g_hash_table_steal_extended (domain->tempfiles, name,
                                           NULL, &stolen_value))
            {
              XdpTempfile *tempfile = stolen_value;

              res = try_renameat (dirfd, tempfile->tempname, dirfd, newname, flags);
              errsv = errno;

              if (res == -1) /* Revert tempfile steal */
                g_hash_table_replace (domain->tempfiles, tempfile->name, tempfile);
              else
                {
                  /* Steal the old tempname so we don't unlink it */
                  g_free (g_steal_pointer (&tempfile->tempname));
                  xdp_tempfile_unref (tempfile);
                }
            }
          else
            {
              res = -1;
              errsv = ENOENT;
            }

          g_mutex_unlock (&domain->tempfile_mutex);

          if (res != 0)
            return xdp_reply_err (op, req, errsv);

          xdp_reply_ok (op, req);
        }
      else
        {
          /* Source and destinations are both tempfiles, no need to change anything on disk */
          gboolean found_tempfile = FALSE;
          gpointer stolen_value;

          /* Renaming temp file to temp file */
          g_mutex_lock (&domain->tempfile_mutex);
          if (g_hash_table_steal_extended (domain->tempfiles, name,
                                            NULL, &stolen_value))
            {
              XdpTempfile *tempfile = stolen_value;

              found_tempfile = TRUE;

              g_free (tempfile->name);
              tempfile->name = g_strdup (newname);

              /* This destroys any pre-existing tempfile with this name */
              g_hash_table_replace (domain->tempfiles, tempfile->name, tempfile);
          }
          g_mutex_unlock (&domain->tempfile_mutex);

          if (!found_tempfile)
            return xdp_reply_err (op, req, ENOENT);

          xdp_reply_ok (op, req);
        }
    }
}

static void
xdp_fuse_access (fuse_req_t req,
                 fuse_ino_t ino,
                 int        mask)
{
  g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino);
  g_autofree char *path = NULL;
  int res;
  const char *op = "ACCESS";

  g_debug ("ACCESS %" G_GINT64_MODIFIER "x", ino);

  if (inode->domain->type != XDP_DOMAIN_DOCUMENT)
    {
      if (mask & W_OK)
        return xdp_reply_err (op, req, EPERM);
      else
        return xdp_reply_ok (op, req);
    }

  if ((mask & W_OK) != 0 &&
      !xdp_document_domain_can_write (inode->domain))
    return xdp_reply_err (op, req, EPERM);

  if (inode->physical)
    {
      path = fd_to_path (inode->physical->fd);
      res = access (path, mask);
    }
  else
    {
      if (xdp_document_domain_is_dir (inode->domain))
        {
          if (mask & W_OK)
            res = EPERM;
          else
            res = 0;
        }
      else
        res = access (inode->domain->doc_path, mask);
    }

  if (res == -1)
    xdp_reply_err (op, req, errno);
  else
    xdp_reply_ok (op, req);
}

static void
xdp_fuse_rmdir (fuse_req_t  req,
                fuse_ino_t  parent_ino,
                const char *filename)
{
  g_autoptr(XdpInode) parent = xdp_inode_from_ino (parent_ino);
  g_autofd int close_fd = -1;
  int dirfd;
  int res;
  const char *op = "RMDIR";

  g_debug ("RMDIR %" G_GINT64_MODIFIER "x %s", parent_ino, filename);

  if (!xdp_document_inode_checks (op, req, parent,
                                  CHECK_CAN_WRITE |
                                  CHECK_IS_DIRECTORY |
                                  CHECK_IS_PHYSICAL))
    return;

  dirfd = xdp_document_inode_ensure_dirfd (parent, &close_fd);
  if (dirfd < 0)
    return xdp_reply_err (op, req, -dirfd);

  res = unlinkat (dirfd, filename, AT_REMOVEDIR);
  if (res != 0)
    return xdp_reply_err (op, req, errno);

  xdp_reply_ok (op, req);
}

static void
xdp_fuse_readlink (fuse_req_t req,
                   fuse_ino_t ino)
{
  g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino);
  char linkname[PATH_MAX + 1];
  const char *op = "READLINK";
  ssize_t res;

  g_debug ("READLINK %" G_GINT64_MODIFIER "x", ino);

  if (!xdp_document_inode_checks (op, req, inode,
                                  CHECK_IS_DIRECTORY |
                                  CHECK_IS_PHYSICAL))
    return;

  if (inode->physical == NULL)
    return xdp_reply_err (op, req, EINVAL);

  res = readlinkat (inode->physical->fd, "", linkname, sizeof(linkname));
  if (res < 0)
    return xdp_reply_err (op, req, errno);

  linkname[res] = '\0';
  fuse_reply_readlink (req, linkname);
}

static void
xdp_fuse_symlink (fuse_req_t  req,
                  const char *link,
                  fuse_ino_t  parent_ino,
                  const char *name)
{
  g_autoptr(XdpInode) parent = xdp_inode_from_ino (parent_ino);
  g_autofd int close_fd = -1;
  struct fuse_entry_param e;
  const char * op = "SYMLINK";
  int dirfd;
  int res;

  g_debug ("SYMLINK %s %" G_GINT64_MODIFIER "x %s", link, parent_ino, name);

  if (!xdp_document_inode_checks (op, req, parent,
                                  CHECK_CAN_WRITE |
                                  CHECK_IS_DIRECTORY |
                                  CHECK_IS_PHYSICAL))
    return;

  dirfd = xdp_document_inode_ensure_dirfd (parent, &close_fd);
  if (dirfd < 0)
    return xdp_reply_err (op, req, -dirfd);

  res = symlinkat (link, dirfd, name);
  if (res != 0)
    return xdp_reply_err (op, req, errno);

  res = ensure_docdir_inode_by_name (parent, dirfd, name, &e); /* Takes ownership of o_path_fd */
  if (res != 0)
    return xdp_reply_err (op, req, -res);

  if (fuse_reply_entry (req, &e) == -ENOENT)
    abort_reply_entry (&e);
}

static void
xdp_fuse_link (fuse_req_t  req,
               fuse_ino_t  ino,
               fuse_ino_t  newparent_ino,
               const char *newname)
{
  g_autoptr(XdpInode) newparent = xdp_inode_from_ino (newparent_ino);
  g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino);
  g_autofree char *proc_path = NULL;
  g_autofd int close_fd = -1;
  struct fuse_entry_param e;
  const char * op = "LINK";
  int newparent_dirfd;
  int res;

  g_debug ("LINK %" G_GINT64_MODIFIER "x %" G_GINT64_MODIFIER "x %s", ino, newparent_ino, newname);

  /* hardlinks only supported in docdirs, and only physical files */
  if (!xdp_document_inode_checks (op, req, inode,
                                  CHECK_CAN_WRITE |
                                  CHECK_IS_DIRECTORY |
                                  CHECK_IS_PHYSICAL))
    return;

  /* Don't allow linking between domains */
  if (inode->domain != newparent->domain)
    return xdp_reply_err (op, req, EXDEV);

  proc_path = fd_to_path (inode->physical->fd);
  newparent_dirfd = xdp_document_inode_ensure_dirfd (newparent, &close_fd);
  if (newparent_dirfd < 0)
    return xdp_reply_err (op, req, -newparent_dirfd);

  res = linkat (AT_FDCWD, proc_path, newparent_dirfd, newname, AT_SYMLINK_FOLLOW);
  if (res != 0)
    return xdp_reply_err (op, req, errno);

  res = ensure_docdir_inode_by_name (inode, newparent_dirfd, newname, &e); /* Takes ownership of o_path_fd */
  if (res != 0)
    return xdp_reply_err (op, req, -res);

  if (fuse_reply_entry (req, &e) == -ENOENT)
    abort_reply_entry (&e);
}


static void
xdp_fuse_statfs (fuse_req_t req,
                 fuse_ino_t ino)
{
  g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino);
  struct statvfs buf;
  int res;
  const char *op = "STATFS";

  g_debug ("STATFS %" G_GINT64_MODIFIER "x", ino);

  if (xdp_domain_is_virtual_type (inode->domain))
    {
      /* statfs data doesn't make much sense for a virtual filesystem, so we reply with
       * a mostly empty buffer, mimicking an empty .statfs. This is consistent with what
       * libfuse does if we don't supply a statfs implementation. */
      buf = (struct statvfs) {
        .f_namemax = 255,
        .f_bsize = 512,
      };
      fuse_reply_statfs (req, &buf);
      return;
    }

  if (!xdp_document_inode_checks (op, req, inode, 0))
    return;

  if (inode->physical)
    res = fstatvfs (inode->physical->fd, &buf);
  else
    res = statvfs (inode->domain->doc_path, &buf);

  if (!res)
    fuse_reply_statfs (req, &buf);
  else
    xdp_reply_err (op, req, errno);
}

static gboolean
xdp_fuse_get_real_path (XdpPhysicalInode  *physical,
                        char             **real_path_out)
{
  g_autofree char *fd_path = fd_to_path (physical->fd);
  char path_buffer[PATH_MAX + 1];
  DevIno file_devino = physical->backing_devino;
  ssize_t symlink_size;
  struct stat buf;

  /* Try to extract a real path to the file
   * (and verify it goes to the same place as the fd) */
  symlink_size = readlink (fd_path, path_buffer, PATH_MAX);
  if (symlink_size < 1)
    return FALSE;

  path_buffer[symlink_size] = 0;

  if (lstat (path_buffer, &buf) != 0 ||
      buf.st_dev != file_devino.dev ||
      buf.st_ino != file_devino.ino)
    return FALSE;

  *real_path_out = g_strdup (path_buffer);
  return TRUE;
}

static ssize_t
xdp_fuse_set_host_path_xattr (XdpInode   *inode,
                              const char *value,
                              size_t      size)
{
  errno = EPERM;
  return -1;
}

static ssize_t
xdp_fuse_get_host_path_xattr (XdpInode *inode,
                              char     *buf,
                              size_t    size)
{
  const char *path = NULL;
  size_t path_size;
  g_autofree char *real_path = NULL;

  path = inode->domain->doc_path;
  if (!path)
    {
      errno = ENODATA;
      return -1;
    }

  if (inode->physical)
    {
      if (!xdp_fuse_get_real_path (inode->physical, &real_path))
        {
          errno = ENODATA;
          return -1;
        }

      if (!g_str_has_prefix (real_path, path))
        {
          errno = ENODATA;
          return -1;
        }

      path = real_path;
    }

  path_size = strlen (path);

  if (size == 0)
    return path_size;

  if (size < path_size)
    {
      errno = ERANGE;
      return -1;
    }

  memcpy (buf, path, path_size);
  return path_size;
}

static ssize_t
xdp_fuse_remove_host_path_xattr (XdpInode *inode)
{
  errno = EPERM;
  return -1;
}

static void
xdp_fuse_setxattr (fuse_req_t  req,
                   fuse_ino_t  ino,
                   const char *name,
                   const char *value,
                   size_t      size,
                   int         flags)
{
  g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino);
  ssize_t res;
  g_autofree char *path = NULL;
  const char *op = "SETXATTR";

  g_debug ("SETXATTR %" G_GINT64_MODIFIER "x %s", ino, name);

  if (!xdp_document_inode_checks (op, req, inode,
                                  CHECK_CAN_WRITE |
                                  CHECK_IS_DIRECTORY |
                                  CHECK_IS_PHYSICAL))
    return;

  if (g_strcmp0 (name, XDP_XATTR_HOST_PATH) == 0)
    {
      res = xdp_fuse_set_host_path_xattr (inode, value, size);
    }
  else
    {
      path = fd_to_path (inode->physical->fd);
#if HAVE_SYS_XATTR_H
      res = setxattr (path, name, value, size, flags);
#elif HAVE_SYS_EXTATTR_H
      res = extattr_set_file (path, EXTATTR_NAMESPACE_USER, name, value, size);
#else
#error "Not implemented for your platform"
#endif
    }

  if (res < 0)
    return xdp_reply_err (op, req, errno);

  xdp_reply_ok (op, req);
}

static void
xdp_fuse_getxattr (fuse_req_t  req,
                   fuse_ino_t  ino,
                   const char *name,
                   size_t      size)
{
  g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino);
  ssize_t res;
  g_autofree char *buf = NULL;
  g_autofree char *path = NULL;
  const char *op = "GETXATTR";

  g_debug ("GETXATTR %" G_GINT64_MODIFIER "x %s %" G_GSIZE_FORMAT, ino, name, size);

  if (inode->domain->type != XDP_DOMAIN_DOCUMENT)
    return xdp_reply_err (op, req, ENODATA);

  if (size != 0)
    buf = g_malloc (size);

  path = xdp_document_inode_get_self_as_path (inode);
  if (path == NULL)
    return xdp_reply_err (op, req, ENODATA);

  if (g_strcmp0 (name, XDP_XATTR_HOST_PATH) == 0)
    {
      res = xdp_fuse_get_host_path_xattr (inode, buf, size);
    }
  else
    {
#if HAVE_SYS_XATTR_H
      res = getxattr (path, name, buf, size);
#elif HAVE_SYS_EXTATTR_H
      res = extattr_get_file (path, EXTATTR_NAMESPACE_USER, name, buf, size);
#else
#error "Not implemented for your platform"
#endif
    }

  if (res < 0)
    return xdp_reply_err (op, req, errno);

  if (size == 0)
    fuse_reply_xattr (req, res);
  else
    fuse_reply_buf (req, buf, res);
}

static ssize_t
xdp_fuse_listxattr_xdp_attrs (XdpInode *inode,
                              ssize_t   res,
                              char     *buf,
                              size_t    size)
{
  size_t attr_size = sizeof (XDP_XATTR_ATTRIBUTE_NAME_LIST);

  if (size == 0)
    return res + attr_size;

  if (size < res + attr_size)
    {
      errno = ERANGE;
      return -1;
    }

  memcpy (buf + res, XDP_XATTR_ATTRIBUTE_NAME_LIST, attr_size);
  return res + attr_size;
}

static void
xdp_fuse_listxattr (fuse_req_t req,
                    fuse_ino_t ino,
                    size_t     size)
{
  g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino);
  g_autofree char *path = NULL;
  g_autofree char *buf = NULL;
  const char *op = "LISTXATTR";
  ssize_t res;

  g_debug ("LISTXATTR %" G_GINT64_MODIFIER "x %" G_GSIZE_FORMAT, ino, size);

  if (inode->domain->type != XDP_DOMAIN_DOCUMENT)
    return xdp_reply_err (op, req, ENOTSUP);

  if (size != 0)
    buf = g_malloc (size);

  path = xdp_document_inode_get_self_as_path (inode);

  if (path == NULL)
    {
      res = 0;
    }
  else
    {
#if HAVE_SYS_XATTR_H
      res = listxattr (path, buf, size);
#elif HAVE_SYS_EXTATTR_H
      res = extattr_list_file (path, EXTATTR_NAMESPACE_USER, buf, size);
#else
#error "Not implemented for your platform"
#endif

      if (res >= 0)
        res = xdp_fuse_listxattr_xdp_attrs (inode, res, buf, size);
    }

  if (res < 0)
    return xdp_reply_err (op, req, errno);

  if (size == 0)
    fuse_reply_xattr (req, res);
  else
    fuse_reply_buf (req, buf, res);
}

static void
xdp_fuse_removexattr (fuse_req_t  req,
                      fuse_ino_t  ino,
                      const char *name)
{
  g_autoptr(XdpInode) inode = xdp_inode_from_ino (ino);
  g_autofree char *path = NULL;
  ssize_t res;
  const char *op = "REMOVEXATTR";

  g_debug ("REMOVEXATTR %" G_GINT64_MODIFIER "x %s", ino, name);

  if (!xdp_document_inode_checks (op, req, inode,
                                  CHECK_CAN_WRITE |
                                  CHECK_IS_DIRECTORY |
                                  CHECK_IS_PHYSICAL))
    return;

  if (g_strcmp0 (name, XDP_XATTR_HOST_PATH) == 0)
    {
      res = xdp_fuse_remove_host_path_xattr (inode);
    }
  else
    {
      path = fd_to_path (inode->physical->fd);
#if HAVE_SYS_XATTR_H
      res = removexattr (path, name);
#elif HAVE_SYS_EXTATTR_H
      res = extattr_delete_file (path, EXTATTR_NAMESPACE_USER, name);
#else
#error "Not implemented for your platform"
#endif
    }

  if (res < 0)
    xdp_reply_err (op, req, errno);
  else
    xdp_reply_ok (op, req);
}

static void
xdp_fuse_getlk (fuse_req_t             req,
                fuse_ino_t             ino,
                struct fuse_file_info *fi,
                struct flock          *lock)
{
  const char *op = "GETLK";
  XdpFile *file = (XdpFile *)fi->fh;
  int res;

  g_debug ("GETLK %" G_GINT64_MODIFIER "x", ino);

  res = fcntl (file->fd, F_GETLK, lock);
  if (res < 0)
    return xdp_reply_err (op, req, errno);

  return xdp_reply_ok (op, req);
}

static void
xdp_fuse_setlk (fuse_req_t             req,
                fuse_ino_t             ino,
                struct fuse_file_info *fi,
                struct flock          *lock,
                int                    sleep)
{
  const char *op = "SETLK";
  XdpFile *file = (XdpFile *)fi->fh;
  int res;

  g_debug ("SETLK %" G_GINT64_MODIFIER "x", ino);

  res = fcntl (file->fd, F_SETLK, lock);
  if (res < 0)
    return xdp_reply_err (op, req, errno);

  return xdp_reply_ok (op, req);
}

static void
xdp_fuse_flock (fuse_req_t             req,
                fuse_ino_t             ino,
                struct fuse_file_info *fi,
                int                    lock_op)
{
  const char *op = "FLOCK";

  g_debug ("FLOCK %" G_GINT64_MODIFIER "x", ino);

  xdp_reply_err (op, req, ENOSYS);
}

static void
xdp_fuse_init_cb (void                  *userdata,
                  struct fuse_conn_info *conn)
{
  XdpFuseOptions *fuse_opts = userdata;

  g_debug ("INIT");

  /* atomic_o_trunc: We handle O_TRUNC in create() */
  conn->want |= FUSE_CAP_ATOMIC_O_TRUNC;

  if (fuse_opts->use_splice)
    {
      /* splice_read: use splice() to read from fuse pipe */
      conn->want |= FUSE_CAP_SPLICE_READ;
      /* splice_write: use splice() to write to fuse pipe */
      conn->want |= FUSE_CAP_SPLICE_WRITE;
      /* splice_move: move buffers from writing app to kernel during splice write */
      conn->want |= FUSE_CAP_SPLICE_MOVE;
    }
}

extern gboolean on_fuse_unmount (void *);

static void
xdp_fuse_destroy_cb (void *userdata)
{
  XdpFuseOptions *fuse_opts = userdata;

  g_debug ("DESTROY");

  /* Ensure we call this on the main thread */
  g_idle_add ((GSourceFunc) on_fuse_unmount, NULL);

  g_clear_pointer (&fuse_opts, g_free);
}

static struct fuse_lowlevel_ops xdp_fuse_oper = {
 .init         = xdp_fuse_init_cb,
 .destroy      = xdp_fuse_destroy_cb,
 .lookup       = xdp_fuse_lookup,
 .getattr      = xdp_fuse_getattr,
 .setattr      = xdp_fuse_setattr,
 .readdir      = xdp_fuse_readdir,
 .open         = xdp_fuse_open,
 .read         = xdp_fuse_read,
 .write        = xdp_fuse_write,
 .write_buf    = xdp_fuse_write_buf,
 .fsync        = xdp_fuse_fsync,
 .forget       = xdp_fuse_forget,
 .forget_multi = xdp_fuse_forget_multi,
 .releasedir   = xdp_fuse_releasedir,
 .release      = xdp_fuse_release,
 .opendir      = xdp_fuse_opendir,
 .fsyncdir     = xdp_fuse_fsyncdir,
 .create       = xdp_fuse_create,
 .unlink       = xdp_fuse_unlink,
 .rename       = xdp_fuse_rename,
 .access       = xdp_fuse_access,
 .readlink     = xdp_fuse_readlink,
 .rmdir        = xdp_fuse_rmdir,
 .mkdir        = xdp_fuse_mkdir,
 .symlink      = xdp_fuse_symlink,
 .link         = xdp_fuse_link,
 .flush        = xdp_fuse_flush,
 .statfs       = xdp_fuse_statfs,
 .setxattr     = xdp_fuse_setxattr,
 .getxattr     = xdp_fuse_getxattr,
 .listxattr    = xdp_fuse_listxattr,
 .removexattr  = xdp_fuse_removexattr,
 .getlk        = xdp_fuse_getlk,
 .setlk        = xdp_fuse_setlk,
 .flock        = xdp_fuse_flock,
 .fallocate    = xdp_fuse_fallocate,
};

typedef struct {
  GMutex lock;
  GCond cond;
  GError *error;
} XdpFuseThreadData;

static void
xdp_fuse_mainloop (struct fuse_session     *se,
                   struct fuse_loop_config *loop_config)
{
  const char *status;

  fuse_session_loop_mt (se, loop_config);

  status = getenv ("TEST_DOCUMENT_PORTAL_FUSE_STATUS");
  if (status)
    {
      GError *error = NULL;
      g_autoptr(GString) s = g_string_new ("");

      g_string_append (s, "ok");

      g_file_set_contents (status, s->str, -1, &error);
      g_assert_no_error (error);
    }
}

typedef struct fuse_args XdpAutoFuseArgs;
G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC (XdpAutoFuseArgs, fuse_opt_free_args);

static gpointer
xdp_fuse_thread (gpointer data)
{
  /* Options:
   *  auto_unmount: Tell fusermount to auto unmount if we die.
   */
  static char *fusermount_argv[] = {
    "xdp-fuse", "-osubtype=portal,fsname=portal,auto_unmount",
  };
  g_auto(XdpAutoFuseArgs) args =
    FUSE_ARGS_INIT (G_N_ELEMENTS (fusermount_argv), fusermount_argv);
  g_autoptr(GMutexLocker) session_locker = NULL;
  g_autoptr(GMutexLocker) locker = NULL;
  struct fuse_cmdline_opts opts = {0};
  struct fuse_loop_config loop_config = {0};
  XdpFuseThreadData *thread_data = data;
  XdpFuseOptions *fuse_opts = NULL;
  struct fuse_session *se;
  const char *path;

  locker = g_mutex_locker_new (&thread_data->lock);

  g_cond_signal (&thread_data->cond);

  if (fuse_parse_cmdline (&args, &opts) != 0)
    {
      g_set_error (&thread_data->error, XDG_DESKTOP_PORTAL_ERROR,
                   XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT,
                   "Impossible to parse command line");
      return NULL;
    }

  fuse_opts = g_new0 (XdpFuseOptions, 1);
#if HAVE_SPLICE
  fuse_opts->use_splice = TRUE;
#endif

  se = fuse_session_new (&args, &xdp_fuse_oper,
                         sizeof (xdp_fuse_oper), fuse_opts);
  if (se == NULL)
    {
      g_set_error (&thread_data->error, XDG_DESKTOP_PORTAL_ERROR,
                   XDG_DESKTOP_PORTAL_ERROR_FAILED,
                   "Can't create fuse session");
      g_clear_pointer (&fuse_opts, g_free);
      return NULL;
    }

  path = xdp_fuse_get_mountpoint ();
  if (fuse_session_mount (se, path) != 0)
    {
      fuse_session_destroy (se);
      g_set_error (&thread_data->error, XDG_DESKTOP_PORTAL_ERROR,
                   XDG_DESKTOP_PORTAL_ERROR_FAILED,
                   "Can't mount path %s", path);
      return NULL;
    }

  session = se;
  thread_data = NULL;
  g_clear_pointer (&locker, g_mutex_locker_free);

  loop_config.clone_fd = opts.clone_fd;
  loop_config.max_idle_threads = opts.max_idle_threads;
  thread_data = NULL;

  session_locker = g_mutex_locker_new (&G_LOCK_NAME (session));
  g_clear_pointer (&session_locker, g_mutex_locker_free);
  xdp_fuse_mainloop (session, &loop_config);

  session_locker = g_mutex_locker_new (&G_LOCK_NAME (session));
  /* unmount is called from xdp_fuse_exit() to wake up this thread */
  fuse_session_destroy (se);
  session = NULL;

  return NULL;
}

gboolean
xdp_fuse_init (GError **error)
{
  g_autoptr(XdpDomain) by_app_domain = NULL;
  g_autoptr(XdpDomain) root_domain = NULL;
  XdpFuseThreadData thread_data = {0};
  struct statfs stfs;
  struct rlimit rl;
  struct stat st;
  const char *path;
  int statfs_res;

  my_uid = getuid ();
  my_gid = getgid ();

  all_inodes = g_hash_table_new_full (g_int64_hash, g_int64_equal, NULL, NULL);
  g_assert (open_files == NULL);

  root_domain = xdp_domain_new_root ();
  root_inode = xdp_inode_new (root_domain, NULL);
  by_app_domain = xdp_domain_new_by_app (root_inode);
  by_app_inode = xdp_inode_new (by_app_domain, NULL);

  physical_inodes =
    g_hash_table_new_full (devino_hash, devino_equal, NULL, NULL);

    /* Bump nr of filedescriptor limit to max */
  if (getrlimit (RLIMIT_NOFILE , &rl) == 0 &&
      rl.rlim_cur != rl.rlim_max)
    {
      rl.rlim_cur = rl.rlim_max;
      setrlimit (RLIMIT_NOFILE, &rl);
    }

  path = xdp_fuse_get_mountpoint ();

  if ((stat (path, &st) == -1 && errno == ENOTCONN) ||
      ((statfs_res = statfs (path, &stfs)) == -1 && errno == ENOTCONN) ||
      (statfs_res == 0 && stfs.f_type == 0x65735546 /* fuse */))
    {
      int count = 0;
      char *umount_argv[] = { "fusermount3", "-u", "-z", (char *) path, NULL };

      g_spawn_sync (NULL, umount_argv, NULL, G_SPAWN_SEARCH_PATH,
                    NULL, NULL, NULL, NULL, NULL, NULL);

      g_usleep (10000); /* 10ms */
      while (stat (path, &st) == -1 && count++ < 10)
        g_usleep (10000); /* 10ms */
    }

  if (g_mkdir_with_parents (path, 0700))
    {
      g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_FAILED,
                   "Unable to create dir %s", path);
      return FALSE;
    }

  g_mutex_init (&thread_data.lock);
  g_cond_init (&thread_data.cond);

  g_mutex_lock (&thread_data.lock);
  XDP_AUTOLOCK (session);
  fuse_thread = g_thread_new ("fuse mainloop", xdp_fuse_thread, &thread_data);

  while (session == NULL && thread_data.error == NULL)
    g_cond_wait (&thread_data.cond, &thread_data.lock);

  g_mutex_unlock (&thread_data.lock);
  g_cond_clear (&thread_data.cond);
  g_mutex_clear (&thread_data.lock);

  if (thread_data.error != NULL)
    {
      g_propagate_error (error, g_steal_pointer (&thread_data.error));
      return FALSE;
    }

  g_assert (session != NULL);

  XDP_AUTOLOCK (open_files);
  while (open_files)
    xdp_file_free (open_files->data);

  return TRUE;
}

void
xdp_fuse_exit (void)
{
  {
    XDP_AUTOLOCK (session);

    if (session)
      {
        fuse_session_exit (session);
        /* Unmount to force the FUSE kernel interface to disconnect,
         * which will cause blocking read() in fuse_session_loop_mt()
         * to fail and the loop to exit. */
        fuse_session_unmount (session);
      }
  }

  g_clear_pointer (&fuse_thread, g_thread_join);
  g_assert (session == NULL);
}

const char *
xdp_fuse_get_mountpoint (void)
{
  if (mount_path == NULL)
    mount_path = g_build_filename (g_get_user_runtime_dir (), "doc", NULL);
  return mount_path;
}

typedef struct {
  fuse_ino_t ino;
  char *filename;
} Invalidate;

/* Called with domain_inodes lock held, don't block */
static void
invalidate_doc_inode (XdpInode   *parent_inode,
                      const char *doc_id,
                      GArray     *invalidates)
{
  XdpInode *doc_inode = g_hash_table_lookup (parent_inode->domain->inodes, doc_id);
  Invalidate inval;

  if (doc_inode == NULL)
    return;

  inval.ino = xdp_inode_to_ino (doc_inode);
  inval.filename = NULL;
  g_array_append_val (invalidates, inval);

  inval.ino = xdp_inode_to_ino (parent_inode);
  inval.filename = g_strdup (doc_id);
  g_array_append_val (invalidates, inval);

  /* No need to invalidate doc children, we don't cache them */
}


/* Called when a apps permissions to see a document is changed,
   and with null opt_app_id when the doc is created/removed */
void
xdp_fuse_invalidate_doc_app (const char *doc_id,
                             const char *opt_app_id)
{
  g_autoptr(GArray) invalidates = NULL;
  XDP_AUTOLOCK (session);
  int i;

  /* This can happen if fuse is not initialized yet for the very
     first dbus message that activated the service */
  if (session == NULL)
    return;

  g_debug ("invalidate %s/%s", doc_id, opt_app_id ? opt_app_id : "*");

  invalidates = g_array_new (FALSE, FALSE, sizeof (Invalidate));

  G_LOCK (domain_inodes);
  if (opt_app_id != NULL)
    {
      XdpInode *app_inode = g_hash_table_lookup (by_app_inode->domain->inodes, opt_app_id);
      if (app_inode)
        invalidate_doc_inode (app_inode, doc_id, invalidates);
    }
  else
    {
      GHashTableIter iter;
      gpointer key, value;

      invalidate_doc_inode (root_inode, doc_id, invalidates);
      g_hash_table_iter_init (&iter, by_app_inode->domain->inodes);
      while (g_hash_table_iter_next (&iter, &key, &value))
        invalidate_doc_inode ((XdpInode *)value, doc_id, invalidates);
    }

  G_UNLOCK (domain_inodes);

  for (i = 0; i < invalidates->len; i++)
    {
      Invalidate *invalidate = &g_array_index (invalidates, Invalidate, i);

      if (invalidate->filename)
        {
          fuse_lowlevel_notify_inval_entry (session, invalidate->ino,
                                            invalidate->filename, strlen (invalidate->filename));
          g_free (invalidate->filename);
        }
      else
        fuse_lowlevel_notify_inval_inode (session, invalidate->ino, 0, 0);
    }
}

char *
xdp_fuse_lookup_id_for_inode (ino_t      ino,
                              gboolean   directory,
                              char     **real_path_out)
{
  g_autoptr(XdpDomain) domain = NULL;
  g_autoptr(XdpPhysicalInode) physical = NULL;
  DevIno file_devino;
  struct stat buf;

  if (real_path_out)
    *real_path_out = NULL;

  G_LOCK (all_inodes);
  {
    XdpInode *inode = g_hash_table_lookup (all_inodes, &ino);
    if (inode)
      {
        /* We're not allowed to resurrect the inode here, but we can get the data while in the lock */
        domain = xdp_domain_ref (inode->domain);
        if (inode->physical)
          physical = xdp_physical_inode_ref (inode->physical);
      }
  }
  G_UNLOCK (all_inodes);

  if (domain == NULL)
    return NULL;

  if (domain->type != XDP_DOMAIN_DOCUMENT)
    return NULL;

  if (physical == NULL)
    return NULL;

  file_devino = physical->backing_devino;

  if (!xdp_document_domain_is_dir (domain))
    {
      g_autofree char *main_path = g_build_filename (domain->doc_path, domain->doc_file, NULL);

      /* file document */

      if (directory)
        return NULL;

      /* Only return for main file */
      if (lstat (main_path, &buf) == 0 &&
          buf.st_dev == file_devino.dev &&
          buf.st_ino == file_devino.ino)
        return g_strdup (domain->doc_id);
    }
  else
    {
      /* directory document */

      /* Only return entire doc for main dir */
      if (file_devino.dev == domain->doc_dir_device &&
          file_devino.ino == domain->doc_dir_inode)
        return g_strdup (domain->doc_id);

      /* But maybe its a subfile of the document */
      if (real_path_out && xdp_fuse_get_real_path (physical, real_path_out))
        return g_strdup (domain->doc_id);
    }

  return NULL;
}
