/*
 * Copyright (C) 2020 Sebastian Wick
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program 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
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses/>.
 *
 * Author: Sebastian Wick <sebastian@sebastianwick.net>
 */

#include "config.h"

#include <errno.h>
#include <fcntl.h>
#include <sys/mman.h>

#include "core/meta-anonymous-file.h"

struct _MetaAnonymousFile
{
  int fd;
  size_t size;
};

#define READONLY_SEALS (F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE)

static int
create_tmpfile_cloexec (char *tmpname)
{
  int fd;

#if defined(HAVE_MKOSTEMP)
  fd = mkostemp (tmpname, O_CLOEXEC);
  if (fd >= 0)
    unlink (tmpname);
#else
  fd = mkstemp (tmpname);
  if (fd >= 0)
    {
      long flags;

      unlink (tmpname);

      flags = fcntl (fd, F_GETFD);
      if (flags == -1 ||
          fcntl (fd, F_SETFD, flags | FD_CLOEXEC) == -1)
        {
          close (fd);
          return -1;
        }
    }
#endif

  return fd;
}

/*
 * Create a new, unique, anonymous file of the given size, and
 * return the file descriptor for it. The file descriptor is set
 * CLOEXEC. The file is immediately suitable for mmap()'ing
 * the given size at offset zero.
 *
 * The file should not have a permanent backing store like a disk,
 * but may have if XDG_RUNTIME_DIR is not properly implemented in OS.
 *
 * The file name is deleted from the file system.
 *
 * The file is suitable for buffer sharing between processes by
 * transmitting the file descriptor over Unix sockets using the
 * SCM_RIGHTS methods.
 *
 * If the C library implements posix_fallocate(), it is used to
 * guarantee that disk space is available for the file at the
 * given size. If disk space is insufficient, errno is set to ENOSPC.
 * If posix_fallocate() is not supported, program may receive
 * SIGBUS on accessing mmap()'ed file contents instead.
 *
 * If the C library implements memfd_create(), it is used to create the
 * file purely in memory, without any backing file name on the file
 * system, and then sealing off the possibility of shrinking it. This
 * can then be checked before accessing mmap()'ed file contents, to make
 * sure SIGBUS can't happen. It also avoids requiring XDG_RUNTIME_DIR.
 */
static int
create_anonymous_file (off_t size)
{
  int fd, ret;

#if defined(HAVE_MEMFD_CREATE)
  fd = memfd_create ("mutter-shared", MFD_CLOEXEC | MFD_ALLOW_SEALING);
  if (fd >= 0)
    {
      /* We can add this seal before calling posix_fallocate(), as
       * the file is currently zero-sized anyway.
       *
       * There is also no need to check for the return value, we
       * couldn't do anything with it anyway.
       */
      fcntl (fd, F_ADD_SEALS, F_SEAL_SHRINK);
    }
  else
#endif
    {
      static const char template[] = "/mutter-shared-XXXXXX";
      const char *path;
      char *name;

      path = getenv ("XDG_RUNTIME_DIR");
      if (!path)
        {
          errno = ENOENT;
          return -1;
        }

      name = g_malloc (strlen (path) + sizeof (template));
      if (!name)
        return -1;

      strcpy (name, path);
      strcat (name, template);

      fd = create_tmpfile_cloexec (name);

      g_free (name);

      if (fd < 0)
        return -1;
    }

#if defined(HAVE_POSIX_FALLOCATE)
  do
    {
      ret = posix_fallocate (fd, 0, size);
    }
  while (ret == EINTR);

  if (ret != 0)
    {
      close (fd);
      errno = ret;
      return -1;
    }
#else
  do
    {
      ret = ftruncate (fd, size);
    }
  while (ret < 0 && errno == EINTR);

  if (ret < 0)
    {
      close (fd);
      return -1;
    }
#endif

  return fd;
}

/**
 * meta_anonymous_file_new: (skip)
 * @size: The size of @data
 * @data: The data of the file with the size @size
 *
 * Create a new anonymous read-only file of the given size and the given data
 * The intended use-case is for sending mid-sized data from the compositor
 * to clients.
 *
 * When done, free the data using meta_anonymous_file_free().
 *
 * If this function fails errno is set.
 *
 * Returns: The newly created #MetaAnonymousFile, or NULL on failure. Use
 *   meta_anonymous_file_free() to free the resources when done.
 */
MetaAnonymousFile *
meta_anonymous_file_new (size_t         size,
                         const uint8_t *data)
{
  MetaAnonymousFile *file;
  void *map;

  file = g_malloc0 (sizeof *file);
  if (!file)
    {
      errno = ENOMEM;
      return NULL;
    }

  file->size = size;
  file->fd = create_anonymous_file (size);
  if (file->fd == -1)
    goto err_free;

  map = mmap (NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, file->fd, 0);
  if (map == MAP_FAILED)
    goto err_close;

  memcpy (map, data, size);

  munmap (map, size);

#if defined(HAVE_MEMFD_CREATE)
  /* try to put seals on the file to make it read-only so that we can
   * return the fd later directly when MAPMODE_SHARED is not set.
   * meta_anonymous_file_open_fd can handle the fd even if it is not
   * sealed read-only and will instead create a new anonymous file on
   * each invocation.
   */
  fcntl (file->fd, F_ADD_SEALS, READONLY_SEALS);
#endif

  return file;

err_close:
  close (file->fd);
err_free:
  g_free (file);
  return NULL;
}


/**
 * meta_anonymous_file_free: (skip)
 * @file: the #MetaAnonymousFile
 *
 * Free the resources used by an anonymous read-only file.
 */
void
meta_anonymous_file_free (MetaAnonymousFile *file)
{
  close (file->fd);
  g_free (file);
}

/**
 * meta_anonymous_file_size: (skip)
 * @file: the #MetaAnonymousFile
 *
 * Get the size of an anonymous read-only file.
 *
 * Returns: The size of the anonymous read-only file.
 */
size_t
meta_anonymous_file_size (MetaAnonymousFile *file)
{
  return file->size;
}

/**
 * meta_anonymous_file_open_fd: (skip)
 * @file: the #MetaAnonymousFile to get a file descriptor for
 * @mapmode: describes the ways in which the returned file descriptor can
 *   be used with mmap
 *
 * Returns a file descriptor for the given file, ready to be sent to a client.
 * The returned file descriptor must not be shared between multiple clients.
 * If @mapmode is %META_ANONYMOUS_FILE_MAPMODE_PRIVATE the file descriptor is
 * only guaranteed to be mmapable with MAP_PRIVATE. If @mapmode is
 * %META_ANONYMOUS_FILE_MAPMODE_SHARED the file descriptor can be mmaped with
 * either MAP_PRIVATE or MAP_SHARED.
 *
 * In case %META_ANONYMOUS_FILE_MAPMODE_PRIVATE is used, it is important to
 * only read the returned fd using mmap() since using read() will move the
 * read cursor of the fd and thus may cause read() calls on other returned
 * fds to fail.
 *
 * When done using the fd, it is required to call meta_anonymous_file_close_fd()
 * instead of close().
 *
 * If this function fails errno is set.
 *
 * Returns: A file descriptor for the given file that can be sent to a client
 *   or -1 on failure. Use meta_anonymous_file_close_fd() to release the fd
 *   when done.
 */
int
meta_anonymous_file_open_fd (MetaAnonymousFile        *file,
                             MetaAnonymousFileMapmode  mapmode)
{
  void *src, *dst;
  int fd;

#if defined(HAVE_MEMFD_CREATE)
  int seals;

  seals = fcntl (file->fd, F_GET_SEALS);

  /* file was sealed for read-only and we don't have to support MAP_SHARED
   * so we can simply pass the memfd fd
   */
  if (seals != -1 && mapmode == META_ANONYMOUS_FILE_MAPMODE_PRIVATE &&
      (seals & READONLY_SEALS) == READONLY_SEALS)
    return file->fd;
#endif

  /* for all other cases we create a new anonymous file that can be mapped
   * with MAP_SHARED and copy the contents to it and return that instead
   */
  fd = create_anonymous_file (file->size);
  if (fd == -1)
    return fd;

  src = mmap (NULL, file->size, PROT_READ, MAP_PRIVATE, file->fd, 0);
  if (src == MAP_FAILED)
    {
      close (fd);
      return -1;
    }

  dst = mmap (NULL, file->size, PROT_WRITE, MAP_SHARED, fd, 0);
  if (dst == MAP_FAILED)
    {
      close (fd);
      munmap (src, file->size);
      return -1;
    }

  memcpy (dst, src, file->size);
  munmap (src, file->size);
  munmap (dst, file->size);

  return fd;
}

/**
 * meta_anonymous_file_close_fd: (skip)
 * @fd: A file descriptor obtained using meta_anonymous_file_open_fd()
 *
 * Release a file descriptor returned by meta_anonymous_file_open_fd().
 * This function must be called for every file descriptor created with
 * meta_anonymous_file_open_fd() to not leak any resources.
 *
 * If this function fails errno is set.
 */
void
meta_anonymous_file_close_fd (int fd)
{
#if defined(HAVE_MEMFD_CREATE)
  int seals;

  seals = fcntl (fd, F_GET_SEALS);
  if (seals == -1 && errno != EINVAL)
    {
      g_warning ("Reading seals of anonymous file %d failed", fd);
      return;
    }

  /* The only case in which we do NOT have to close the file is when the file
   * was sealed for read-only
   */
  if (seals != -1 && (seals & READONLY_SEALS) == READONLY_SEALS)
    return;
#endif

  close (fd);
}
