Skip to content
Snippets Groups Projects
utils.c 14.8 KiB
Newer Older
/*
 * Contains code taken from Flatpak.
 *
 * Copyright © 2014-2019 Red Hat, Inc
 * Copyright © 2017-2019 Collabora Ltd.
 *
 * 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/>.
 */

#include "utils.h"

#include <glib.h>
#include <glib/gstdio.h>
#include <gio/gio.h>

#include "libglnx/libglnx.h"

#include "glib-backports.h"
#include "flatpak-bwrap-private.h"
#include "flatpak-utils-base-private.h"
#include "flatpak-utils-private.h"

static void
child_setup_cb (gpointer user_data)
{
  flatpak_close_fds_workaround (3);
}

/**
 * pv_avoid_gvfs:
 *
 * Disable gvfs. This function must be called from main() before
 * starting any threads.
 */
void
pv_avoid_gvfs (void)
{
  g_autofree gchar *old_env = NULL;

  /* avoid gvfs (http://bugzilla.gnome.org/show_bug.cgi?id=526454) */
  old_env = g_strdup (g_getenv ("GIO_USE_VFS"));
  g_setenv ("GIO_USE_VFS", "local", TRUE);
  g_vfs_get_default ();
  if (old_env)
    g_setenv ("GIO_USE_VFS", old_env, TRUE);
  else
    g_unsetenv ("GIO_USE_VFS");
}

/**
 * pv_envp_cmp:
 * @p1: a `const char * const *`
 * @p2: a `const char * const *`
 *
 * Compare two environment variables, given as pointers to pointers
 * to the actual `KEY=value` string.
 *
 * In particular this is suitable for sorting a #GStrv using `qsort`.
 *
 * Returns: negative, 0 or positive if `*p1` compares before, equal to
 *  or after `*p2`
 */
int
pv_envp_cmp (const void *p1,
             const void *p2)
{
  const char * const * s1 = p1;
  const char * const * s2 = p2;
  size_t l1 = strlen (*s1);
  size_t l2 = strlen (*s2);
  size_t min;
  const char *tmp;
  int ret;

  tmp = strchr (*s1, '=');

  if (tmp != NULL)
    l1 = tmp - *s1;

  tmp = strchr (*s2, '=');

  if (tmp != NULL)
    l2 = tmp - *s2;

  min = MIN (l1, l2);
  ret = strncmp (*s1, *s2, min);

  /* If they differ before the first '=' (if any) in either s1 or s2,
   * then they are certainly different */
  ret = strcmp (*s1, *s2);

  /* If they do not differ at all, then they are equal */
  if (ret == 0)
    return ret;

  /* FOO < FOO=..., and FOO < FOOBAR */
  if ((*s1)[min] == '\0')
    return -1;

  /* FOO=... > FOO, and FOOBAR > FOO */
  if ((*s2)[min] == '\0')
    return 1;

  /* FOO= < FOOBAR */
  if ((*s1)[min] == '=' && (*s2)[min] != '=')
  /* FOOBAR > FOO= */
  if ((*s2)[min] == '=' && (*s1)[min] != '=')
  /* Fall back to plain string comparison */
  return ret;
}

/**
 * pv_get_current_dirs:
 * @cwd_p: (out) (transfer full) (optional): Used to return the
 *  current physical working directory, equivalent to `$(pwd -P)`
 *  in a shell
 * @cwd_l: (out) (transfer full) (optional): Used to return the
 *  current logical working directory, equivalent to `$(pwd -L)`
 *  in a shell
 *
 * Return the physical and/or logical working directory.
 *
 * Equivalent to `$(pwd -L)` in a shell.
 */
void
pv_get_current_dirs (gchar **cwd_p,
                     gchar **cwd_l)
{
  g_autofree gchar *cwd = NULL;
  const gchar *pwd;

  g_return_if_fail (cwd_p == NULL || *cwd_p == NULL);
  g_return_if_fail (cwd_l == NULL || *cwd_l == NULL);

  cwd = g_get_current_dir ();

  if (cwd_p != NULL)
    *cwd_p = flatpak_canonicalize_filename (cwd);

  if (cwd_l != NULL)
    {
      pwd = g_getenv ("PWD");

      if (pwd != NULL && pv_is_same_file (pwd, cwd))
        *cwd_l = g_strdup (pwd);
      else
        *cwd_l = g_strdup (cwd);
    }
}

/**
 * pv_is_same_file:
 * @a: a path
 * @b: a path
 *
 * Returns: %TRUE if a and b are names for the same inode.
 */
gboolean
pv_is_same_file (const gchar *a,
                 const gchar *b)
{
  GStatBuf a_buffer, b_buffer;

  g_return_val_if_fail (a != NULL, FALSE);
  g_return_val_if_fail (b != NULL, FALSE);

  if (strcmp (a, b) == 0)
    return TRUE;

  return (stat (a, &a_buffer) == 0
          && stat (b, &b_buffer) == 0
          && a_buffer.st_dev == b_buffer.st_dev
          && a_buffer.st_ino == b_buffer.st_ino);
}

void
pv_search_path_append (GString *search_path,
                       const gchar *item)
{
  g_return_if_fail (search_path != NULL);

  if (item == NULL || item[0] == '\0')
    return;

  if (search_path->len != 0)
    g_string_append (search_path, ":");

  g_string_append (search_path, item);
}

gchar *
pv_capture_output (const char * const * argv,
                   GError **error)
{
  gsize len;
  gint wait_status;
  g_autofree gchar *output = NULL;
  g_autofree gchar *errors = NULL;
  gsize i;
  g_autoptr(GString) command = g_string_new ("");

  g_return_val_if_fail (argv != NULL, NULL);
  g_return_val_if_fail (argv[0] != NULL, NULL);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  for (i = 0; argv[i] != NULL; i++)
    {
      g_autofree gchar *quoted = g_shell_quote (argv[i]);

      g_string_append_printf (command, " %s", quoted);
    }

  g_debug ("run:%s", command->str);

  /* We use LEAVE_DESCRIPTORS_OPEN to work around a deadlock in older GLib,
   * see flatpak_close_fds_workaround */
  if (!g_spawn_sync (NULL,  /* cwd */
                     (char **) argv,
                     NULL,  /* env */
                     (G_SPAWN_SEARCH_PATH |
                      G_SPAWN_LEAVE_DESCRIPTORS_OPEN),
                     child_setup_cb, NULL,
                     &output,
                     &errors,
                     &wait_status,
                     error))
    return NULL;

  g_printerr ("%s", errors);

  if (!g_spawn_check_exit_status (wait_status, error))
    return NULL;

  len = strlen (output);

  /* Emulate shell $() */
  if (len > 0 && output[len - 1] == '\n')
    output[len - 1] = '\0';

  g_debug ("-> %s", output);

  return g_steal_pointer (&output);
}

/*
 * Returns: (transfer none): The first key in @table in iteration order,
 *  or %NULL if @table is empty.
 */
gpointer
pv_hash_table_get_arbitrary_key (GHashTable *table)
{
  GHashTableIter iter;
  gpointer key = NULL;

  g_hash_table_iter_init (&iter, table);
  if (g_hash_table_iter_next (&iter, &key, NULL))
    return key;
  else
    return NULL;
}

/* nftw() doesn't have a user_data argument so we need to use a global
 * variable :-( */
static struct
{
  gchar *source_root;
  gchar *dest_root;
  GError *error;
} nftw_data;

static int
copy_tree_helper (const char *fpath,
                  const struct stat *sb,
                  int typeflag,
                  struct FTW *ftwbuf)
{
  size_t len;
  const char *suffix;
  g_autofree gchar *dest = NULL;
  g_autofree gchar *target = NULL;
  GError **error = &nftw_data.error;

  g_return_val_if_fail (g_str_has_prefix (fpath, nftw_data.source_root), 1);

  if (strcmp (fpath, nftw_data.source_root) == 0)
    {
      if (typeflag != FTW_D)
        {
          glnx_throw (error, "\"%s\" is not a directory", fpath);
          return 1;
        }

      if (!glnx_shutil_mkdir_p_at (-1, nftw_data.dest_root,
                                   sb->st_mode & 07777, NULL, error))
        return 1;

      return 0;
    }

  len = strlen (nftw_data.source_root);
  g_return_val_if_fail (fpath[len] == '/', 1);
  suffix = &fpath[len + 1];
  dest = g_build_filename (nftw_data.dest_root, suffix, NULL);

  switch (typeflag)
    {
      case FTW_D:
        if (!glnx_shutil_mkdir_p_at (-1, dest, sb->st_mode & 07777,
                                     NULL, error))
          return 1;
        break;

      case FTW_SL:
        target = glnx_readlinkat_malloc (-1, fpath, NULL, error);

        if (target == NULL)
          return 1;

        if (symlink (target, dest) != 0)
          {
            glnx_throw_errno_prefix (error,
                                     "Unable to create symlink at \"%s\"",
                                     dest);
            return 1;
          }
        break;

      case FTW_F:
        /* Fast path: try to make a hard link. */
        if (link (fpath, dest) == 0)
          break;

        /* Slow path: fall back to copying.
         *
         * This does a FICLONE or copy_file_range to get btrfs reflinks
         * if possible, making the copy as cheap as cp --reflink=auto.
         *
         * Rather than second-guessing which errno values would result
         * in link() failing but a copy succeeding, we just try it
         * unconditionally - the worst that can happen is that this
         * fails too. */
        if (!glnx_file_copy_at (AT_FDCWD, fpath, sb,
                                AT_FDCWD, dest,
                                GLNX_FILE_COPY_OVERWRITE,
                                NULL, error))
            glnx_prefix_error (error, "Unable to copy \"%s\" to \"%s\"",
                               fpath, dest);
            return 1;
          }
        break;

      default:
        glnx_throw (&nftw_data.error,
                    "Don't know how to handle ftw type flag %d at %s",
                    typeflag, fpath);
        return 1;
    }

  return 0;
}

gboolean
pv_cheap_tree_copy (const char *source_root,
                    const char *dest_root,
                    GError **error)
{
  int res;

  g_return_val_if_fail (source_root != NULL, FALSE);
  g_return_val_if_fail (dest_root != NULL, FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
  /* Can't run concurrently */
  g_return_val_if_fail (nftw_data.source_root == NULL, FALSE);

  nftw_data.source_root = flatpak_canonicalize_filename (source_root);
  nftw_data.dest_root = flatpak_canonicalize_filename (dest_root);
  nftw_data.error = NULL;

  res = nftw (nftw_data.source_root, copy_tree_helper, 100, FTW_PHYS);

  if (res == -1)
    {
      g_assert (nftw_data.error == NULL);
      glnx_throw_errno_prefix (error, "Unable to copy \"%s\" to \"%s\"",
                               source_root, dest_root);
    }
  else if (res != 0)
    {
      g_propagate_error (error, g_steal_pointer (&nftw_data.error));
    }

  g_clear_pointer (&nftw_data.source_root, g_free);
  g_clear_pointer (&nftw_data.dest_root, g_free);
  g_assert (nftw_data.error == NULL);
  return (res == 0);
}

static gint
ftw_remove (const gchar *path,
            const struct stat *sb,
            gint typeflags,
            struct FTW *ftwbuf)
{
  if (remove (path) < 0)
    return -1;

  return 0;
}

/*
 * @directory: (type filename): The directory to remove.
 *
 * Recursively delete @directory within the same file system and
 * without following symbolic links.
 *
 * Returns: %TRUE if the removal was successful
 */
gboolean
pv_rm_rf (const char *directory)
{
  g_return_val_if_fail (directory != NULL, FALSE);

  if (nftw (directory, ftw_remove, 10, FTW_DEPTH|FTW_MOUNT|FTW_PHYS) < 0)
    return FALSE;

  return TRUE;
}
gboolean
pv_boolean_environment (const gchar *name,
                        gboolean def)
{
  const gchar *value = g_getenv (name);

  if (g_strcmp0 (value, "1") == 0)
    return TRUE;

  if (g_strcmp0 (value, "") == 0 || g_strcmp0 (value, "0") == 0)
    return FALSE;

  if (value != NULL)
    g_warning ("Unrecognised value \"%s\" for $%s", value, name);

  return def;
}

/**
 * pv_divert_stdout_to_stderr:
 * @error: Used to raise an error on failure
 *
 * Duplicate file descriptors so that functions that would write to
 * `stdout` instead write to a copy of the original `stderr`. Return
 * a file handle that can be used to print structured output to the
 * original `stdout`.
 *
 * Returns: (transfer full): A libc file handle for the original `stdout`,
 *  or %NULL on error. Free with `fclose()`.
 */
FILE *
pv_divert_stdout_to_stderr (GError **error)
{
  g_autoptr(FILE) original_stdout = NULL;
  glnx_autofd int original_stdout_fd = -1;
  int flags;

  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  /* Duplicate the original stdout so that we still have a way to write
   * machine-readable output. */
  original_stdout_fd = dup (STDOUT_FILENO);

  if (original_stdout_fd < 0)
    return glnx_null_throw_errno_prefix (error,
                                         "Unable to duplicate fd %d",
                                         STDOUT_FILENO);

  flags = fcntl (original_stdout_fd, F_GETFD, 0);

  if (flags < 0)
    return glnx_null_throw_errno_prefix (error,
                                         "Unable to get flags of new fd");

  fcntl (original_stdout_fd, F_SETFD, flags|FD_CLOEXEC);

  /* If something like g_debug writes to stdout, make it come out of
   * our original stderr. */
  if (dup2 (STDERR_FILENO, STDOUT_FILENO) != STDOUT_FILENO)
    return glnx_null_throw_errno_prefix (error,
                                         "Unable to make fd %d a copy of fd %d",
                                         STDOUT_FILENO, STDERR_FILENO);

  original_stdout = fdopen (original_stdout_fd, "w");

  if (original_stdout == NULL)
    return glnx_null_throw_errno_prefix (error,
                                         "Unable to create a stdio wrapper for fd %d",
                                         original_stdout_fd);
  else
    original_stdout_fd = -1;    /* ownership taken, do not close */

  return g_steal_pointer (&original_stdout);
}

/**
 * pv_async_signal_safe_error:
 * @message: A human-readable message
 * @exit_status: Call `_exit` with this status
 *
 * Exit with a fatal error, like g_error(), but async-signal-safe
 * (see signal-safety(7)).
 */
void
pv_async_signal_safe_error (const char *message,
                            int exit_status)
{
  if (write (2, message, strlen (message)) < 0)
    {
      /* Ignore - there's nothing we can do about it anyway - but
       * suppress -Wunused-result. */
    }

  _exit (exit_status);
}

#define PROC_SYS_KERNEL_RANDOM_UUID "/proc/sys/kernel/random/uuid"

/**
 * pv_get_random_uuid:
 * @error: Used to raise an error on failure
 *
 * Return a random UUID (RFC 4122 version 4) as a string.
 * It is a 128-bit quantity, with 122 bits of entropy, and 6 fixed bits
 * indicating the "variant" (type, 0b10) and "version" (subtype, 0b0100).
 *
 * Returns: (transfer full): A random UUID, or %NULL on error
 */
gchar *
pv_get_random_uuid (GError **error)
{
  g_autofree gchar *contents = NULL;

  if (!g_file_get_contents (PROC_SYS_KERNEL_RANDOM_UUID,
                            &contents, NULL, error))
    return NULL;

  g_strchomp (contents);    /* delete trailing newline */

  /* Format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx */
  if (strlen (contents) != 36)
    return glnx_null_throw (error, "%s not in expected format",
                            PROC_SYS_KERNEL_RANDOM_UUID);

  return g_steal_pointer (&contents);
}