/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
 *
 * Copyright (C) 2014,2015 Colin Walters <walters@verbum.org>.
 *
 * Portions derived from systemd:
 *  Copyright 2010 Lennart Poettering
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

#include "config.h"

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <sys/ioctl.h>
#include <sys/sendfile.h>
#include <errno.h>
/* See linux.git/fs/btrfs/ioctl.h */
#define BTRFS_IOCTL_MAGIC 0x94
#define BTRFS_IOC_CLONE _IOW(BTRFS_IOCTL_MAGIC, 9, int)

#include <glnx-fdio.h>
#include <glnx-dirfd.h>
#include <glnx-alloca.h>
#include <glnx-errors.h>
#include <glnx-xattrs.h>
#include <glnx-backport-autoptr.h>
#include <glnx-local-alloc.h>
#include <glnx-missing.h>

/* Returns the number of chars needed to format variables of the
 * specified type as a decimal string. Adds in extra space for a
 * negative '-' prefix (hence works correctly on signed
 * types). Includes space for the trailing NUL. */
#define DECIMAL_STR_MAX(type)                                           \
        (2+(sizeof(type) <= 1 ? 3 :                                     \
            sizeof(type) <= 2 ? 5 :                                     \
            sizeof(type) <= 4 ? 10 :                                    \
            sizeof(type) <= 8 ? 20 : sizeof(int[-2*(sizeof(type) > 8)])))

static gboolean
rename_file_noreplace_at (int olddirfd, const char *oldpath,
                          int newdirfd, const char *newpath,
                          gboolean ignore_eexist,
                          GError **error)
{
#ifndef ENABLE_WRPSEUDO_COMPAT
  if (renameat2 (olddirfd, oldpath, newdirfd, newpath, RENAME_NOREPLACE) < 0)
    {
      if (errno == EINVAL || errno == ENOSYS)
        {
          /* Fall through */
          ;
        }
      else if (errno == EEXIST && ignore_eexist)
        {
          (void) unlinkat (olddirfd, oldpath, 0);
          return TRUE;
        }
      else
        {
          glnx_set_error_from_errno (error);
          return FALSE;
        }
    }
  else
    return TRUE;
#endif

  if (linkat (olddirfd, oldpath, newdirfd, newpath, 0) < 0)
    {
      if (errno == EEXIST && ignore_eexist)
        /* Fall through */
        ;
      else
        {
          glnx_set_error_from_errno (error);
          return FALSE;
        }
    }
  
  if (unlinkat (olddirfd, oldpath, 0) < 0)
    {
      glnx_set_error_from_errno (error);
      return FALSE;
    }

  return TRUE;
}

gboolean
glnx_open_tmpfile_linkable_at (int dfd,
                               const char *subpath,
                               int flags,
                               int *out_fd,
                               char **out_path,
                               GError **error)
{
  glnx_fd_close int fd = -1;
  int count;

  /* Don't allow O_EXCL, as that has a special meaning for O_TMPFILE */
  g_return_val_if_fail ((flags & O_EXCL) == 0, FALSE);

  /* Creates a temporary file, that shall be renamed to "target"
   * later. If possible, this uses O_TMPFILE – in which case
   * "ret_path" will be returned as NULL. If not possible a the
   * tempoary path name used is returned in "ret_path". Use
   * link_tmpfile() below to rename the result after writing the file
   * in full. */
#if defined(O_TMPFILE) && !defined(DISABLE_OTMPFILE) && !defined(ENABLE_WRPSEUDO_COMPAT)
  fd = openat (dfd, subpath, O_TMPFILE|flags, 0600);
  if (fd == -1 && !(errno == ENOSYS || errno == EISDIR || errno == EOPNOTSUPP))
    {
      glnx_set_prefix_error_from_errno (error, "%s", "open(O_TMPFILE)");
      return FALSE;
    }
  if (fd != -1)
    {
      *out_fd = fd;
      fd = -1;
      *out_path = NULL;
      return TRUE;
    }
  /* Fallthrough */
#endif

  { g_autofree char *tmp = g_strconcat (subpath, "/tmp.XXXXXX", NULL);
    const guint count_max = 100;
  
    for (count = 0; count < count_max; count++)
      {
        glnx_gen_temp_name (tmp);

        fd = openat (dfd, tmp, O_CREAT|O_EXCL|O_NOFOLLOW|O_NOCTTY|flags, 0600);
        if (fd < 0)
          {
            if (errno == EEXIST)
              continue;
            else
              {
                glnx_set_prefix_error_from_errno (error, "%s", "Creating temp file");
                return FALSE;
              }
          }
        else
          {
            *out_fd = fd;
            fd = -1;
            *out_path = g_steal_pointer (&tmp);
            return TRUE;
          }
      }
  }
  g_set_error (error, G_IO_ERROR, G_IO_ERROR_EXISTS,
               "Exhausted %u attempts to create temporary file", count);
  return FALSE;
}

gboolean
glnx_link_tmpfile_at (int dfd,
                      GLnxLinkTmpfileReplaceMode mode,
                      int fd,
                      const char *tmpfile_path,
                      int target_dfd,
                      const char *target,
                      GError **error)
{
  const gboolean replace = (mode == GLNX_LINK_TMPFILE_REPLACE);
  const gboolean ignore_eexist = (mode == GLNX_LINK_TMPFILE_NOREPLACE_IGNORE_EXIST);

  g_return_val_if_fail (fd >= 0, FALSE);

  /* Unlike the original systemd code, this function also supports
   * replacing existing files.
   */

  /* We have `tmpfile_path` for old systems without O_TMPFILE. */
  if (tmpfile_path)
    {
      if (replace)
        {
          /* We have a regular tempfile, we're overwriting - this is a
           * simple renameat().
           */
          if (renameat (dfd, tmpfile_path, target_dfd, target) < 0)
            {
              (void) unlinkat (dfd, tmpfile_path, 0);
              glnx_set_error_from_errno (error);
              return FALSE;
            }
        }
      else
        {
          /* We need to use renameat2(..., NOREPLACE) or emulate it */
          if (!rename_file_noreplace_at (dfd, tmpfile_path, target_dfd, target,
                                         ignore_eexist,
                                         error))
            {
              (void) unlinkat (dfd, tmpfile_path, 0);
              return FALSE;
            }
        }
    }
  else
    {
      /* This case we have O_TMPFILE, so our reference to it is via /proc/self/fd */
      char proc_fd_path[strlen("/proc/self/fd/") + DECIMAL_STR_MAX(fd) + 1];

      sprintf (proc_fd_path, "/proc/self/fd/%i", fd);

      if (replace)
        {
          /* In this case, we had our temp file atomically hidden, but now
           * we need to make it visible in the FS so we can do a rename.
           * Ideally, linkat() would gain AT_REPLACE or so.
           */
          /* TODO - avoid double alloca, we can just alloca a copy of
           * the pathname plus space for tmp.XXXXX */
          char *dnbuf = strdupa (target);
          const char *dn = dirname (dnbuf);
          char *tmpname_buf = glnx_strjoina (dn, "/tmp.XXXXXX");
          guint count;
          const guint count_max = 100;

          for (count = 0; count < count_max; count++)
            {
              glnx_gen_temp_name (tmpname_buf);

              if (linkat (AT_FDCWD, proc_fd_path, target_dfd, tmpname_buf, AT_SYMLINK_FOLLOW) < 0)
                {
                  if (errno == EEXIST)
                    continue;
                  else
                    {
                      glnx_set_error_from_errno (error);
                      return FALSE;
                    }
                }
              else
                break;
            }
          if (count == count_max)
            {
              g_set_error (error, G_IO_ERROR, G_IO_ERROR_EXISTS,
               "Exhausted %u attempts to create temporary file", count);
            }
          if (renameat (target_dfd, tmpname_buf, target_dfd, target) < 0)
            {
              /* This is currently the only case where we need to have
               * a cleanup unlinkat() still with O_TMPFILE.
               */
              (void) unlinkat (target_dfd, tmpname_buf, 0);
              glnx_set_error_from_errno (error);
              return FALSE;
            }
        }
      else
        {
          if (linkat (AT_FDCWD, proc_fd_path, target_dfd, target, AT_SYMLINK_FOLLOW) < 0)
            {
              if (errno == EEXIST && mode == GLNX_LINK_TMPFILE_NOREPLACE_IGNORE_EXIST)
                ;
              else
                {
                  glnx_set_error_from_errno (error);
                  return FALSE;
                }
            }
        }

    }
  return TRUE;
}

static guint8*
glnx_fd_readall_malloc (int               fd,
                        gsize            *out_len,
                        gboolean          nul_terminate,
                        GCancellable     *cancellable,
                        GError          **error)
{
  gboolean success = FALSE;
  const guint maxreadlen = 4096;
  int res;
  struct stat stbuf;
  guint8* buf = NULL;
  gsize buf_allocated;
  gsize buf_size = 0;
  gssize bytes_read;

  do
    res = fstat (fd, &stbuf);
  while (G_UNLIKELY (res == -1 && errno == EINTR));
  if (res == -1)
    {
      glnx_set_error_from_errno (error);
      goto out;
    }

  if (S_ISREG (stbuf.st_mode) && stbuf.st_size > 0)
    buf_allocated = stbuf.st_size;
  else
    buf_allocated = 16;
        
  buf = g_malloc (buf_allocated);

  while (TRUE)
    {
      gsize readlen = MIN (buf_allocated - buf_size, maxreadlen);
      
      if (g_cancellable_set_error_if_cancelled (cancellable, error))
        goto out;

      do
        bytes_read = read (fd, buf + buf_size, readlen);
      while (G_UNLIKELY (bytes_read == -1 && errno == EINTR));
      if (G_UNLIKELY (bytes_read == -1))
        {
          glnx_set_error_from_errno (error);
          goto out;
        }
      if (bytes_read == 0)
        break;
      
      buf_size += bytes_read;
      if (buf_allocated - buf_size < maxreadlen)
        buf = g_realloc (buf, buf_allocated *= 2);
    }

  if (nul_terminate)
    {
      if (buf_allocated - buf_size == 0)
        buf = g_realloc (buf, buf_allocated + 1);
      buf[buf_size] = '\0';
    }

  success = TRUE;
 out:
  if (success)
    {
      *out_len = buf_size;
      return buf;
    }
  g_free (buf);
  return NULL;
}

/**
 * glnx_fd_readall_bytes:
 * @fd: A file descriptor
 * @cancellable: Cancellable:
 * @error: Error
 *
 * Read all data from file descriptor @fd into a #GBytes.  It's
 * recommended to only use this for small files.
 *
 * Returns: (transfer full): A newly allocated #GBytes
 */
GBytes *
glnx_fd_readall_bytes (int               fd,
                       GCancellable     *cancellable,
                       GError          **error)
{
  guint8 *buf;
  gsize len;
  
  buf = glnx_fd_readall_malloc (fd, &len, FALSE, cancellable, error);
  if (!buf)
    return NULL;
  
  return g_bytes_new_take (buf, len);
}

/**
 * glnx_fd_readall_utf8:
 * @fd: A file descriptor
 * @out_len: (out): Returned length
 * @cancellable: Cancellable:
 * @error: Error
 *
 * Read all data from file descriptor @fd, validating
 * the result as UTF-8.
 *
 * Returns: (transfer full): A string validated as UTF-8, or %NULL on error.
 */
char *
glnx_fd_readall_utf8 (int               fd,
                      gsize            *out_len,
                      GCancellable     *cancellable,
                      GError          **error)
{
  gboolean success = FALSE;
  guint8 *buf;
  gsize len;
  
  buf = glnx_fd_readall_malloc (fd, &len, TRUE, cancellable, error);
  if (!buf)
    goto out;

  if (!g_utf8_validate ((char*)buf, len, NULL))
    {
      g_set_error (error,
                   G_IO_ERROR,
                   G_IO_ERROR_INVALID_DATA,
                   "Invalid UTF-8");
      goto out;
    }

  success = TRUE;
 out:
  if (success)
    {
      if (out_len)
        *out_len = len;
      return (char*)buf;
    }
  g_free (buf);
  return NULL;
}

/**
 * glnx_file_get_contents_utf8_at:
 * @dfd: Directory file descriptor
 * @subpath: Path relative to @dfd
 * @out_len: (out) (allow-none): Optional length
 * @cancellable: Cancellable
 * @error: Error
 *
 * Read the entire contents of the file referred
 * to by @dfd and @subpath, validate the result as UTF-8.
 * The length is optionally stored in @out_len.
 *
 * Returns: (transfer full): UTF-8 validated text, or %NULL on error
 */
char *
glnx_file_get_contents_utf8_at (int                   dfd,
                                const char           *subpath,
                                gsize                *out_len,
                                GCancellable         *cancellable,
                                GError              **error)
{
  gboolean success = FALSE;
  glnx_fd_close int fd = -1;
  char *buf = NULL;
  gsize len;

  dfd = glnx_dirfd_canonicalize (dfd);

  do
    fd = openat (dfd, subpath, O_RDONLY | O_NOCTTY | O_CLOEXEC);
  while (G_UNLIKELY (fd == -1 && errno == EINTR));
  if (G_UNLIKELY (fd == -1))
    {
      glnx_set_error_from_errno (error);
      goto out;
    }

  buf = glnx_fd_readall_utf8 (fd, &len, cancellable, error);
  if (G_UNLIKELY(!buf))
    goto out;
  
  success = TRUE;
 out:
  if (success)
    {
      if (out_len)
        *out_len = len;
      return buf;
    }
  g_free (buf);
  return NULL;
}

/**
 * glnx_readlinkat_malloc:
 * @dfd: Directory file descriptor
 * @subpath: Subpath
 * @cancellable: Cancellable
 * @error: Error
 *
 * Read the value of a symlink into a dynamically
 * allocated buffer.
 */
char *
glnx_readlinkat_malloc (int            dfd,
                        const char    *subpath,
                        GCancellable  *cancellable,
                        GError       **error)
{
  size_t l = 100;

  dfd = glnx_dirfd_canonicalize (dfd);

  for (;;)
    {
      char *c;
      ssize_t n;

      c = g_malloc (l);
      n = TEMP_FAILURE_RETRY (readlinkat (dfd, subpath, c, l-1));
      if (n < 0)
        {
          glnx_set_error_from_errno (error);
          g_free (c);
          return FALSE;
        }

      if ((size_t) n < l-1)
        {
          c[n] = 0;
          return c;
        }

      g_free (c);
      l *= 2;
    }

  g_assert_not_reached ();
}

static gboolean
copy_symlink_at (int                   src_dfd,
                 const char           *src_subpath,
                 const struct stat    *src_stbuf,
                 int                   dest_dfd,
                 const char           *dest_subpath,
                 GLnxFileCopyFlags     copyflags,
                 GCancellable         *cancellable,
                 GError              **error)
{
  gboolean ret = FALSE;
  g_autofree char *buf = NULL;

  buf = glnx_readlinkat_malloc (src_dfd, src_subpath, cancellable, error);
  if (!buf)
    goto out;

  if (TEMP_FAILURE_RETRY (symlinkat (buf, dest_dfd, dest_subpath)) != 0)
    {
      glnx_set_error_from_errno (error);
      goto out;
    }
  
  if (!(copyflags & GLNX_FILE_COPY_NOXATTRS))
    {
      g_autoptr(GVariant) xattrs = NULL;

      if (!glnx_dfd_name_get_all_xattrs (src_dfd, src_subpath, &xattrs,
                                         cancellable, error))
        goto out;

      if (!glnx_dfd_name_set_all_xattrs (dest_dfd, dest_subpath, xattrs,
                                         cancellable, error))
        goto out;
    }
  
  if (TEMP_FAILURE_RETRY (fchownat (dest_dfd, dest_subpath,
                                    src_stbuf->st_uid, src_stbuf->st_gid,
                                    AT_SYMLINK_NOFOLLOW)) != 0)
    {
      glnx_set_error_from_errno (error);
      goto out;
    }

  ret = TRUE;
 out:
  return ret;
}

#define COPY_BUFFER_SIZE (16*1024)

/* From systemd */

static int btrfs_reflink(int infd, int outfd) {
        int r;

        g_return_val_if_fail(infd >= 0, -1);
        g_return_val_if_fail(outfd >= 0, -1);

        r = ioctl(outfd, BTRFS_IOC_CLONE, infd);
        if (r < 0)
                return -errno;

        return 0;
}

int glnx_loop_write(int fd, const void *buf, size_t nbytes) {
        const uint8_t *p = buf;

        g_return_val_if_fail(fd >= 0, -1);
        g_return_val_if_fail(buf, -1);

        errno = 0;

        while (nbytes > 0) {
                ssize_t k;

                k = write(fd, p, nbytes);
                if (k < 0) {
                        if (errno == EINTR)
                                continue;

                        return -errno;
                }

                if (k == 0) /* Can't really happen */
                        return -EIO;

                p += k;
                nbytes -= k;
        }

        return 0;
}

static int copy_bytes(int fdf, int fdt, off_t max_bytes, bool try_reflink) {
        bool try_sendfile = true;
        int r;

        g_return_val_if_fail (fdf >= 0, -1);
        g_return_val_if_fail (fdt >= 0, -1);

        /* Try btrfs reflinks first. */
        if (try_reflink && max_bytes == (off_t) -1) {
                r = btrfs_reflink(fdf, fdt);
                if (r >= 0)
                        return r;
        }

        for (;;) {
                size_t m = COPY_BUFFER_SIZE;
                ssize_t n;

                if (max_bytes != (off_t) -1) {

                        if (max_bytes <= 0)
                                return -EFBIG;

                        if ((off_t) m > max_bytes)
                                m = (size_t) max_bytes;
                }

                /* First try sendfile(), unless we already tried */
                if (try_sendfile) {

                        n = sendfile(fdt, fdf, NULL, m);
                        if (n < 0) {
                                if (errno != EINVAL && errno != ENOSYS)
                                        return -errno;

                                try_sendfile = false;
                                /* use fallback below */
                        } else if (n == 0) /* EOF */
                                break;
                        else if (n > 0)
                                /* Succcess! */
                                goto next;
                }

                /* As a fallback just copy bits by hand */
                {
                        char buf[m];

                        n = read(fdf, buf, m);
                        if (n < 0)
                                return -errno;
                        if (n == 0) /* EOF */
                                break;

                        r = glnx_loop_write(fdt, buf, (size_t) n);
                        if (r < 0)
                                return r;
                }

        next:
                if (max_bytes != (off_t) -1) {
                        g_assert(max_bytes >= n);
                        max_bytes -= n;
                }
        }

        return 0;
}

/**
 * glnx_file_copy_at:
 * @src_dfd: Source directory fd
 * @src_subpath: Subpath relative to @src_dfd
 * @dest_dfd: Target directory fd
 * @dest_subpath: Destination name
 * @copyflags: Flags
 * @cancellable: cancellable
 * @error: Error
 *
 * Perform a full copy of the regular file or
 * symbolic link from @src_subpath to @dest_subpath.
 *
 * If @src_subpath is anything other than a regular
 * file or symbolic link, an error will be returned.
 */
gboolean
glnx_file_copy_at (int                   src_dfd,
                   const char           *src_subpath,
                   struct stat          *src_stbuf,
                   int                   dest_dfd,
                   const char           *dest_subpath,
                   GLnxFileCopyFlags     copyflags,
                   GCancellable         *cancellable,
                   GError              **error)
{
  gboolean ret = FALSE;
  int r;
  int dest_open_flags;
  struct timespec ts[2];
  glnx_fd_close int src_fd = -1;
  glnx_fd_close int dest_fd = -1;
  struct stat local_stbuf;

  if (g_cancellable_set_error_if_cancelled (cancellable, error))
    goto out;

  src_dfd = glnx_dirfd_canonicalize (src_dfd);
  dest_dfd = glnx_dirfd_canonicalize (dest_dfd);

  /* Automatically do stat() if no stat buffer was supplied */
  if (!src_stbuf)
    {
      if (fstatat (src_dfd, src_subpath, &local_stbuf, AT_SYMLINK_NOFOLLOW) != 0)
        {
          glnx_set_error_from_errno (error);
          goto out;
        }
      src_stbuf = &local_stbuf;
    }

  if (S_ISLNK (src_stbuf->st_mode))
    {
      return copy_symlink_at (src_dfd, src_subpath, src_stbuf,
                              dest_dfd, dest_subpath,
                              copyflags,
                              cancellable, error);
    }
  else if (!S_ISREG (src_stbuf->st_mode))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
                   "Cannot copy non-regular/non-symlink file: %s", src_subpath);
      goto out;
    }

  src_fd = TEMP_FAILURE_RETRY (openat (src_dfd, src_subpath, O_RDONLY | O_CLOEXEC | O_NOCTTY | O_NOFOLLOW));
  if (src_fd == -1)
    {
      glnx_set_error_from_errno (error);
      goto out;
    }

  dest_open_flags = O_WRONLY | O_CREAT | O_CLOEXEC | O_NOCTTY;
  if (!(copyflags & GLNX_FILE_COPY_OVERWRITE))
    dest_open_flags |= O_EXCL;
  else
    dest_open_flags |= O_TRUNC;

  dest_fd = TEMP_FAILURE_RETRY (openat (dest_dfd, dest_subpath, dest_open_flags, src_stbuf->st_mode));
  if (dest_fd == -1)
    {
      glnx_set_error_from_errno (error);
      goto out;
    }

  r = copy_bytes (src_fd, dest_fd, (off_t) -1, TRUE);
  if (r < 0)
    {
      errno = -r;
      glnx_set_error_from_errno (error);
      goto out;
    }

  if (fchown (dest_fd, src_stbuf->st_uid, src_stbuf->st_gid) != 0)
    {
      glnx_set_error_from_errno (error);
      goto out;
    }

  if (fchmod (dest_fd, src_stbuf->st_mode & 07777) != 0)
    {
      glnx_set_error_from_errno (error);
      goto out;
    }

  ts[0] = src_stbuf->st_atim;
  ts[1] = src_stbuf->st_mtim;
  (void) futimens (dest_fd, ts);

  if (!(copyflags & GLNX_FILE_COPY_NOXATTRS))
    {
      g_autoptr(GVariant) xattrs = NULL;

      if (!glnx_fd_get_all_xattrs (src_fd, &xattrs,
                                   cancellable, error))
        goto out;

      if (!glnx_fd_set_all_xattrs (dest_fd, xattrs,
                                   cancellable, error))
        goto out;
    }

  if (copyflags & GLNX_FILE_COPY_DATASYNC)
    {
      if (fdatasync (dest_fd) < 0)
        {
          glnx_set_error_from_errno (error);
          goto out;
        }
    }
  
  r = close (dest_fd);
  dest_fd = -1;
  if (r < 0)
    {
      glnx_set_error_from_errno (error);
      goto out;
    }

  ret = TRUE;
 out:
  if (!ret)
    (void) unlinkat (dest_dfd, dest_subpath, 0);
  return ret;
}

/**
 * glnx_file_replace_contents_at:
 * @dfd: Directory fd
 * @subpath: Subpath
 * @buf: (array len=len) (element-type guint8): File contents
 * @len: Length (if `-1`, assume @buf is `NUL` terminated)
 * @flags: Flags
 * @cancellable: Cancellable
 * @error: Error
 *
 * Create a new file, atomically replacing the contents of @subpath
 * (relative to @dfd) with @buf.  By default, if the file already
 * existed, fdatasync() will be used before rename() to ensure stable
 * contents.  This and other behavior can be controlled via @flags.
 *
 * Note that no metadata from the existing file is preserved, such as
 * uid/gid or extended attributes.  The default mode will be `0666`,
 * modified by umask.
 */ 
gboolean
glnx_file_replace_contents_at (int                   dfd,
                               const char           *subpath,
                               const guint8         *buf,
                               gsize                 len,
                               GLnxFileReplaceFlags  flags,
                               GCancellable         *cancellable,
                               GError              **error)
{
  return glnx_file_replace_contents_with_perms_at (dfd, subpath, buf, len,
                                                   (mode_t) -1, (uid_t) -1, (gid_t) -1,
                                                   flags, cancellable, error);
                                                   
}

/**
 * glnx_file_replace_contents_with_perms_at:
 * @dfd: Directory fd
 * @subpath: Subpath
 * @buf: (array len=len) (element-type guint8): File contents
 * @len: Length (if `-1`, assume @buf is `NUL` terminated)
 * @mode: File mode; if `-1`, use `0666 - umask`
 * @flags: Flags
 * @cancellable: Cancellable
 * @error: Error
 *
 * Like glnx_file_replace_contents_at(), but also supports
 * setting mode, and uid/gid.
 */ 
gboolean
glnx_file_replace_contents_with_perms_at (int                   dfd,
                                          const char           *subpath,
                                          const guint8         *buf,
                                          gsize                 len,
                                          mode_t                mode,
                                          uid_t                 uid,
                                          gid_t                 gid,
                                          GLnxFileReplaceFlags  flags,
                                          GCancellable         *cancellable,
                                          GError              **error)
{
  int r;
  char *dnbuf = strdupa (subpath);
  const char *dn = dirname (dnbuf);
  g_autofree char *tmpfile_path = NULL;
  glnx_fd_close int fd = -1;

  dfd = glnx_dirfd_canonicalize (dfd);

  /* With O_TMPFILE we can't use umask, and we can't sanely query the
   * umask...let's assume something relatively standard.
   */
  if (mode == (mode_t) -1)
    mode = 0644;

  if (!glnx_open_tmpfile_linkable_at (dfd, dn, O_WRONLY | O_CLOEXEC,
                                      &fd, &tmpfile_path,
                                      error))
    return FALSE;

  if (len == -1)
    len = strlen ((char*)buf);

  /* Note that posix_fallocate does *not* set errno but returns it. */
  if (len > 0)
    {
      r = posix_fallocate (fd, 0, len);
      if (r != 0)
        {
          errno = r;
          glnx_set_error_from_errno (error);
          return FALSE;
        }
    }

  if ((r = glnx_loop_write (fd, buf, len)) != 0)
    {
      errno = -r;
      glnx_set_error_from_errno (error);
      return FALSE;
    }
    
  if (!(flags & GLNX_FILE_REPLACE_NODATASYNC))
    {
      struct stat stbuf;
      gboolean do_sync;
      
      if (fstatat (dfd, subpath, &stbuf, AT_SYMLINK_NOFOLLOW) != 0)
        {
          if (errno != ENOENT)
            {
              glnx_set_error_from_errno (error);
              return FALSE;
            }
          do_sync = (flags & GLNX_FILE_REPLACE_DATASYNC_NEW) > 0;
        }
      else
        do_sync = TRUE;

      if (do_sync)
        {
          if (fdatasync (fd) != 0)
            {
              glnx_set_error_from_errno (error);
              return FALSE;
            }
        }
    }

  if (uid != (uid_t) -1)
    {
      if (fchown (fd, uid, gid) != 0)
        {
          glnx_set_error_from_errno (error);
          return FALSE;
        }
    }

  if (fchmod (fd, mode) != 0)
    {
      glnx_set_error_from_errno (error);
      return FALSE;
    }

  if (!glnx_link_tmpfile_at (dfd, GLNX_LINK_TMPFILE_REPLACE,
                             fd, tmpfile_path, dfd, subpath, error))
    return FALSE;

  return TRUE;
}

/**
 * glnx_stream_fstat:
 * @stream: A stream containing a Unix file descriptor
 * @stbuf: Memory location to write stat buffer
 * @error:
 *
 * Some streams created via libgsystem are #GUnixInputStream; these do
 * not support e.g. g_file_input_stream_query_info().  This function
 * allows dropping to the raw unix fstat() call for these types of
 * streams, while still conveniently wrapped with the normal GLib
 * handling of @error.
 */
gboolean
glnx_stream_fstat (GFileDescriptorBased *stream,
                   struct stat          *stbuf,
                   GError              **error)
{
  int fd = g_file_descriptor_based_get_fd (stream);

  if (fstat (fd, stbuf) == -1)
    {
      glnx_set_prefix_error_from_errno (error, "%s", "fstat");
      return FALSE;
    }

  return TRUE;
}
