/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
 *
 * Copyright (C) 2014,2015 Colin Walters <walters@verbum.org>.
 * SPDX-License-Identifier: LGPL-2.0-or-later
 *
 * 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 "libglnx-config.h"

#include <string.h>
#include <stdio.h>

#include <glnx-macros.h>
#include <glnx-xattrs.h>
#include <glnx-errors.h>
#include <glnx-local-alloc.h>

static GVariant *
variant_new_ay_bytes (GBytes *bytes)
{
  gsize size;
  gconstpointer data;
  data = g_bytes_get_data (bytes, &size);
  g_bytes_ref (bytes);
  return g_variant_new_from_data (G_VARIANT_TYPE ("ay"), data, size,
                                  TRUE, (GDestroyNotify)g_bytes_unref, bytes);
}

static char *
canonicalize_xattrs (char    *xattr_string,
                     size_t   len)
{
  char *p;
  GSList *xattrs = NULL;
  GSList *iter;
  GString *result;

  result = g_string_new (0);

  p = xattr_string;
  while (p < xattr_string+len)
    {
      xattrs = g_slist_prepend (xattrs, p);
      p += strlen (p) + 1;
    }

  xattrs = g_slist_sort (xattrs, (GCompareFunc) strcmp);
  for (iter = xattrs; iter; iter = iter->next) {
    g_string_append (result, iter->data);
    g_string_append_c (result, '\0');
  }

  g_slist_free (xattrs);
  return g_string_free (result, FALSE);
}

static gboolean
read_xattr_name_array (const char *path,
                       int         fd,
                       const char *xattrs,
                       size_t      len,
                       GVariantBuilder *builder,
                       GError  **error)
{
  gboolean ret = FALSE;
  const char *p;
  int r;
  const char *funcstr;

  g_assert (path != NULL || fd != -1);

  funcstr = fd != -1 ? "fgetxattr" : "lgetxattr";

  for (p = xattrs; p < xattrs+len; p = p + strlen (p) + 1)
    {
      ssize_t bytes_read;
      g_autofree char *buf = NULL;
      g_autoptr(GBytes) bytes = NULL;

    again:
      if (fd != -1)
        bytes_read = fgetxattr (fd, p, NULL, 0);
      else
        bytes_read = lgetxattr (path, p, NULL, 0);
      if (bytes_read < 0)
        {
          if (errno == ENODATA)
            continue;

          glnx_set_prefix_error_from_errno (error, "%s", funcstr);
          goto out;
        }
      if (bytes_read == 0)
        continue;

      buf = g_malloc (bytes_read);
      if (fd != -1)
        r = fgetxattr (fd, p, buf, bytes_read);
      else
        r = lgetxattr (path, p, buf, bytes_read);
      if (r < 0)
        {
          if (errno == ERANGE)
            {
              g_free (g_steal_pointer (&buf));
              goto again;
            }
          else if (errno == ENODATA)
            continue;

          glnx_set_prefix_error_from_errno (error, "%s", funcstr);
          goto out;
        }

      bytes = g_bytes_new_take (g_steal_pointer (&buf), bytes_read);
      g_variant_builder_add (builder, "(@ay@ay)",
                             g_variant_new_bytestring (p),
                             variant_new_ay_bytes (bytes));
    }

  ret = TRUE;
 out:
  return ret;
}

static gboolean
get_xattrs_impl (const char      *path,
                 int              fd,
                 GVariant       **out_xattrs,
                 G_GNUC_UNUSED GCancellable *cancellable,
                 GError         **error)
{
  gboolean ret = FALSE;
  ssize_t bytes_read, real_size;
  g_autofree char *xattr_names = NULL;
  g_autofree char *xattr_names_canonical = NULL;
  GVariantBuilder builder;
  gboolean builder_initialized = FALSE;
  g_autoptr(GVariant) ret_xattrs = NULL;

  g_assert (path != NULL || fd != -1);

  g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(ayay)"));
  builder_initialized = TRUE;

 again:
  if (path)
    bytes_read = llistxattr (path, NULL, 0);
  else
    bytes_read = flistxattr (fd, NULL, 0);

  if (bytes_read < 0)
    {
      if (errno != ENOTSUP)
        {
          glnx_set_prefix_error_from_errno (error, "%s", "llistxattr");
          goto out;
        }
    }
  else if (bytes_read > 0)
    {
      xattr_names = g_malloc (bytes_read);
      if (path)
        real_size = llistxattr (path, xattr_names, bytes_read);
      else
        real_size = flistxattr (fd, xattr_names, bytes_read);
      if (real_size < 0)
        {
          if (errno == ERANGE)
            {
              g_free (g_steal_pointer (&xattr_names));
              goto again;
            }
          glnx_set_prefix_error_from_errno (error, "%s", "llistxattr");
          goto out;
        }
      else if (real_size > 0)
        {
          xattr_names_canonical = canonicalize_xattrs (xattr_names, real_size);

          if (!read_xattr_name_array (path, fd, xattr_names_canonical, real_size, &builder, error))
            goto out;
        }
    }

  ret_xattrs = g_variant_builder_end (&builder);
  builder_initialized = FALSE;
  g_variant_ref_sink (ret_xattrs);
  
  ret = TRUE;
  if (out_xattrs)
    *out_xattrs = g_steal_pointer (&ret_xattrs);
 out:
  if (!builder_initialized)
    g_variant_builder_clear (&builder);
  return ret;
}

/**
 * glnx_fd_get_all_xattrs:
 * @fd: a file descriptor
 * @out_xattrs: (out): A new #GVariant containing the extended attributes
 * @cancellable: Cancellable
 * @error: Error
 *
 * Read all extended attributes from @fd in a canonical sorted order, and
 * set @out_xattrs with the result.
 *
 * If the filesystem does not support extended attributes, @out_xattrs
 * will have 0 elements, and this function will return successfully.
 */
gboolean
glnx_fd_get_all_xattrs (int            fd,
                        GVariant     **out_xattrs,
                        GCancellable  *cancellable,
                        GError       **error)
{
  return get_xattrs_impl (NULL, fd, out_xattrs,
                          cancellable, error);
}

/**
 * glnx_dfd_name_get_all_xattrs:
 * @dfd: Parent directory file descriptor
 * @name: File name
 * @out_xattrs: (out): Extended attribute set
 * @cancellable: Cancellable
 * @error: Error
 *
 * Load all extended attributes for the file named @name residing in
 * directory @dfd.
 */
gboolean
glnx_dfd_name_get_all_xattrs (int            dfd,
                              const char    *name,
                              GVariant     **out_xattrs,
                              GCancellable  *cancellable,
                              GError       **error)
{
  if (G_IN_SET(dfd, AT_FDCWD, -1))
    {
      return get_xattrs_impl (name, -1, out_xattrs, cancellable, error);
    }
  else
    {
      char buf[PATH_MAX];
      /* A workaround for the lack of lgetxattrat(), thanks to Florian Weimer:
       * https://mail.gnome.org/archives/ostree-list/2014-February/msg00017.html
       */
      snprintf (buf, sizeof (buf), "/proc/self/fd/%d/%s", dfd, name);
      return get_xattrs_impl (buf, -1, out_xattrs, cancellable, error);
    }
}

static gboolean
set_all_xattrs_for_path (const char    *path,
                         GVariant      *xattrs,
                         G_GNUC_UNUSED GCancellable *cancellable,
                         GError       **error)
{
  const guint n = g_variant_n_children (xattrs);
  for (guint i = 0; i < n; i++)
    {
      const guint8* name;
      g_autoptr(GVariant) value = NULL;
      g_variant_get_child (xattrs, i, "(^&ay@ay)",
                           &name, &value);

      gsize value_len;
      const guint8* value_data = g_variant_get_fixed_array (value, &value_len, 1);

      if (lsetxattr (path, (char*)name, (char*)value_data, value_len, 0) < 0)
        return glnx_throw_errno_prefix (error, "lsetxattr(%s)", name);
    }

  return TRUE;
}

/**
 * glnx_dfd_name_set_all_xattrs:
 * @dfd: Parent directory file descriptor
 * @name: File name
 * @xattrs: Extended attribute set
 * @cancellable: Cancellable
 * @error: Error
 *
 * Set all extended attributes for the file named @name residing in
 * directory @dfd.
 */
gboolean
glnx_dfd_name_set_all_xattrs (int            dfd,
                              const char    *name,
                              GVariant      *xattrs,
                              GCancellable  *cancellable,
                              GError       **error)
{
  if (G_IN_SET(dfd, AT_FDCWD, -1))
    {
      return set_all_xattrs_for_path (name, xattrs, cancellable, error);
    }
  else
    {
      char buf[PATH_MAX];
      /* A workaround for the lack of lsetxattrat(), thanks to Florian Weimer:
       * https://mail.gnome.org/archives/ostree-list/2014-February/msg00017.html
       */
      snprintf (buf, sizeof (buf), "/proc/self/fd/%d/%s", dfd, name);
      return set_all_xattrs_for_path (buf, xattrs, cancellable, error);
    }
}

/**
 * glnx_fd_set_all_xattrs:
 * @fd: File descriptor
 * @xattrs: Extended attributes
 * @cancellable: Cancellable
 * @error: Error
 *
 * For each attribute in @xattrs, set its value on the file or
 * directory referred to by @fd.  This function does not remove any
 * attributes not in @xattrs.
 */
gboolean
glnx_fd_set_all_xattrs (int            fd,
                        GVariant      *xattrs,
                        G_GNUC_UNUSED GCancellable *cancellable,
                        GError       **error)
{
  const guint n = g_variant_n_children (xattrs);
  for (guint i = 0; i < n; i++)
    {
      const guint8* name;
      g_autoptr(GVariant) value = NULL;
      g_variant_get_child (xattrs, i, "(^&ay@ay)",
                           &name, &value);

      gsize value_len;
      const guint8* value_data = g_variant_get_fixed_array (value, &value_len, 1);

      if (TEMP_FAILURE_RETRY (fsetxattr (fd, (char*)name, (char*)value_data, value_len, 0)) < 0)
        return glnx_throw_errno_prefix (error, "Setting xattrs: fsetxattr(%s)", name);
    }

  return TRUE;
}

/**
 * glnx_lgetxattrat:
 * @dfd: Directory file descriptor
 * @subpath: Subpath
 * @attribute: Extended attribute to retrieve
 * @error: Error
 *
 * Retrieve an extended attribute value, relative to a directory file
 * descriptor.
 */
GBytes *
glnx_lgetxattrat (int            dfd,
                  const char    *subpath,
                  const char    *attribute,
                  GError       **error)
{
  char pathbuf[PATH_MAX];
  snprintf (pathbuf, sizeof (pathbuf), "/proc/self/fd/%d/%s", dfd, subpath);

  ssize_t bytes_read, real_size;
  if (TEMP_FAILURE_RETRY (bytes_read = lgetxattr (pathbuf, attribute, NULL, 0)) < 0)
    return glnx_null_throw_errno_prefix (error, "lgetxattr(%s)", attribute);

  g_autofree guint8 *buf = g_malloc (bytes_read);
  if (TEMP_FAILURE_RETRY (real_size = lgetxattr (pathbuf, attribute, buf, bytes_read)) < 0)
    return glnx_null_throw_errno_prefix (error, "lgetxattr(%s)", attribute);

  return g_bytes_new_take (g_steal_pointer (&buf), real_size);
}

/**
 * glnx_fgetxattr_bytes:
 * @fd: Directory file descriptor
 * @attribute: Extended attribute to retrieve
 * @error: Error
 *
 * Returns: (transfer full): An extended attribute value, or %NULL on error
 */
GBytes *
glnx_fgetxattr_bytes (int            fd,
                      const char    *attribute,
                      GError       **error)
{
  ssize_t bytes_read, real_size;

  if (TEMP_FAILURE_RETRY (bytes_read = fgetxattr (fd, attribute, NULL, 0)) < 0)
    return glnx_null_throw_errno_prefix (error, "fgetxattr(%s)", attribute);

  g_autofree guint8 *buf = g_malloc (bytes_read);
  if (TEMP_FAILURE_RETRY (real_size = fgetxattr (fd, attribute, buf, bytes_read)) < 0)
    return glnx_null_throw_errno_prefix (error, "fgetxattr(%s)", attribute);

  return g_bytes_new_take (g_steal_pointer (&buf), real_size);
}

/**
 * glnx_lsetxattrat:
 * @dfd: Directory file descriptor
 * @subpath: Path
 * @attribute: An attribute name
 * @value: (array length=len) (element-type guint8): Attribute value
 * @len: Length of @value
 * @flags: Flags, containing either XATTR_CREATE or XATTR_REPLACE
 * @error: Error
 *
 * Set an extended attribute, relative to a directory file descriptor.
 */
gboolean
glnx_lsetxattrat (int            dfd,
                  const char    *subpath,
                  const char    *attribute,
                  const guint8  *value,
                  gsize          len,
                  int            flags,
                  GError       **error)
{
  char pathbuf[PATH_MAX];
  snprintf (pathbuf, sizeof (pathbuf), "/proc/self/fd/%d/%s", dfd, subpath);

  if (TEMP_FAILURE_RETRY (lsetxattr (pathbuf, attribute, value, len, flags)) < 0)
    return glnx_throw_errno_prefix (error, "lsetxattr(%s)", attribute);

  return TRUE;
}

