Skip to content
Snippets Groups Projects
utils.c 22.69 KiB
/*
 * Copyright © 2019 Collabora Ltd.
 *
 * SPDX-License-Identifier: MIT
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#include "steam-runtime-tools/utils.h"
#include "steam-runtime-tools/utils-internal.h"

#include <dlfcn.h>
#include <elf.h>
#include <errno.h>
#include <ftw.h>
#include <link.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

#ifdef HAVE_SYS_AUXV_H
#include <sys/auxv.h>
#endif

#include <glib-object.h>
#include <gio/gio.h>
#include "steam-runtime-tools/glib-compat.h"

#ifdef HAVE_GETAUXVAL
#define getauxval_AT_SECURE() getauxval (AT_SECURE)
#else
/*
 * This implementation assumes that auxv entries are pointer-sized on
 * all architectures.
 *
 * Note that this implementation doesn't special-case AT_HWCAP and
 * AT_HWCAP like glibc does, so it is only suitable for other types
 * (but in practice we only need AT_SECURE here).
 */
static long
getauxval_AT_SECURE (void)
{
  uintptr_t buf[2] = { 0 /* type */, 0 /* value */ };
  FILE *auxv;
  gboolean found = FALSE;

  if ((auxv = fopen("/proc/self/auxv", "r")) == NULL)
    return 0;

  while ((fread (buf, sizeof (buf), 1, auxv)) == 1)
    {
      if (buf[0] == AT_SECURE)
        {
          found = TRUE;
          break;
        }
      else
        {
          buf[0] = buf[1] = 0;
        }
    }

  fclose(auxv);

  if (!found)
    errno = ENOENT;

  return (long) buf[1];
}
#endif

/* Return TRUE if setuid, setgid, setcap or otherwise running with
 * elevated privileges. "setuid" in the name is shorthand for this. */
static gboolean
check_for_setuid_once (void)
{
  errno = 0;

  /* If the kernel says we are running with elevated privileges,
   * believe it */
  if (getauxval_AT_SECURE ())
    return TRUE;

  /* If the kernel specifically told us we are not running with
   * elevated privileges, believe it (as opposed to the kernel not
   * having told us either way, which sets errno to ENOENT) */
  if (errno == 0)
    return FALSE;

  /* Otherwise resort to comparing (e)uid and (e)gid */
  if (geteuid () != getuid ())
    return TRUE;

  if (getegid () != getgid ())
    return TRUE;

  return FALSE;
}

static int is_setuid = -1;

/*
 * _srt_check_not_setuid:
 *
 * Check that the process containing this library is not setuid, setgid,
 * setcap or otherwise running with elevated privileges. The word
 * "setuid" in the function name is not completely accurate, but is used
 * as a shorthand term since it is the most common way for a process
 * to be more privileged than its parent.
 *
 * This library trusts environment variables and other aspects of the
 * execution environment, and is not designed to be used with elevated
 * privileges, so this should normally be done as a precondition check:
 *
 * |[<!-- language="C" -->
 * g_return_if_fail (_srt_check_not_setuid ());
 * // or in functions that return a value
 * g_return_val_if_fail (_srt_check_not_setuid (), SOME_ERROR_CONSTANT);
 * ]|
 *
 * Returns: %TRUE under normal circumstances
 */
G_GNUC_INTERNAL gboolean
_srt_check_not_setuid (void)
{
  if (is_setuid >= 0)
    return !is_setuid;

  is_setuid = check_for_setuid_once ();
  return !is_setuid;
}

#define MULTIARCH_LIBDIR \
  "/lib/" _SRT_MULTIARCH
#define RELOCATABLE_PKGLIBDIR \
  MULTIARCH_LIBDIR "/steam-runtime-tools-" _SRT_API_MAJOR

/**
 * _srt_process_timeout_wait_status:
 *
 * @wait_status: The wait_status from g_spawn_sync to process
 * @exit_status: (not optional): The exit_status to populate
 * @terminating_signal: (not optional): The terminating signal if any, 0 otherwise
 *
 * Check given wait_status and populate given exit_status and terminating_signal
 *
 * Returns: True if timeout, false otherwise
 */
G_GNUC_INTERNAL gboolean
_srt_process_timeout_wait_status (int wait_status, int *exit_status, int *terminating_signal)
{
  gboolean timed_out = FALSE;

  g_return_val_if_fail (exit_status != NULL, FALSE);
  g_return_val_if_fail (terminating_signal != NULL, FALSE);

  *exit_status = -1;
  *terminating_signal = 0;

  if (WIFEXITED (wait_status))
    {
      *exit_status = WEXITSTATUS (wait_status);

      if (*exit_status > 128 && *exit_status <= 128 + SIGRTMAX)
        {
          g_debug ("-> killed by signal %d", (*exit_status - 128));
          *terminating_signal = (*exit_status - 128);
        }
      else if (*exit_status == 124)
        {
          g_debug ("-> timed out");
          timed_out = TRUE;
        }
    }
  else if (WIFSIGNALED (wait_status))
    {
      g_debug ("-> timeout killed by signal %d", WTERMSIG (wait_status));
      *terminating_signal = WTERMSIG (wait_status);
    }
  else
    {
      g_critical ("Somehow got a wait_status that was neither exited nor signaled");
      g_return_val_if_reached (FALSE);
    }

  return timed_out;
}

G_GNUC_INTERNAL const char *
_srt_find_myself (const char **helpers_path_out,
                  GError **error)
{
  static gchar *saved_prefix = NULL;
  static gchar *saved_helpers_path = NULL;
  Dl_info ignored;
  struct link_map *map = NULL;
  gchar *dir = NULL;

  g_return_val_if_fail (_srt_check_not_setuid (), NULL);
  g_return_val_if_fail (error == NULL || *error == NULL, NULL);
  g_return_val_if_fail (helpers_path_out == NULL || *helpers_path_out == NULL,
                        NULL);

  if (saved_prefix != NULL && saved_helpers_path != NULL)
    goto out;

  if (dladdr1 (_srt_find_myself, &ignored, (void **) &map,
               RTLD_DL_LINKMAP) == 0 ||
      map == NULL)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                   "Unable to locate shared library containing "
                   "_srt_find_myself()");
      goto out;
    }

  g_debug ("Found _srt_find_myself() in %s", map->l_name);
  dir = g_path_get_dirname (map->l_name);

  if (g_str_has_suffix (dir, RELOCATABLE_PKGLIBDIR))
    dir[strlen (dir) - strlen (RELOCATABLE_PKGLIBDIR)] = '\0';
  else if (g_str_has_suffix (dir, MULTIARCH_LIBDIR))
    dir[strlen (dir) - strlen (MULTIARCH_LIBDIR)] = '\0';
  else if (g_str_has_suffix (dir, "/lib64"))
    dir[strlen (dir) - strlen ("/lib64")] = '\0';
  else if (g_str_has_suffix (dir, "/lib"))
    dir[strlen (dir) - strlen ("/lib")] = '\0';

  /* If the library was found in /lib/MULTIARCH, /lib64 or /lib on a
   * merged-/usr system, assume --prefix=/usr (/libexec doesn't
   * normally exist) */
  if (dir[0] == '\0')
    {
      g_free (dir);
      dir = g_strdup ("/usr");
    }

  saved_prefix = g_steal_pointer (&dir);
  /* deliberate one-per-process leak */
  saved_helpers_path = g_build_filename (
      saved_prefix, "libexec", "steam-runtime-tools-" _SRT_API_MAJOR,
      NULL);

out:
  if (helpers_path_out != NULL)
    *helpers_path_out = saved_helpers_path;

  g_free (dir);
  return saved_prefix;
}

/*
 * _srt_get_helper:
 * @helpers_path: (nullable): Directory to search for helper executables,
 *  or %NULL for default behaviour
 * @multiarch: (nullable): A multiarch tuple like %SRT_ABI_I386 to prefix
 *  to the executable name, or %NULL
 * @base: (not nullable): Base name of the executable
 * @flags: Flags affecting how we set up the helper
 * @error: Used to raise an error if %NULL is returned
 *
 * Find a helper executable. We return an array of arguments so that the
 * helper can be wrapped by an "adverb" like `env`, `timeout` or a
 * specific `ld.so` implementation if required.
 *
 * Returns: (nullable) (element-type filename) (transfer container): The
 *  initial `argv` for the helper, with g_free() set as the free-function, and
 *  no %NULL terminator. Free with g_ptr_array_unref() or g_ptr_array_free().
 */
G_GNUC_INTERNAL GPtrArray *
_srt_get_helper (const char *helpers_path,
                 const char *multiarch,
                 const char *base,
                 SrtHelperFlags flags,
                 GError **error)
{
  GPtrArray *argv = NULL;
  gchar *path;
  gchar *prefixed;

  g_return_val_if_fail (_srt_check_not_setuid (), NULL);
  g_return_val_if_fail (base != NULL, NULL);
  g_return_val_if_fail (error == NULL || *error == NULL, NULL);

  argv = g_ptr_array_new_with_free_func (g_free);

  if (flags & SRT_HELPER_FLAGS_TIME_OUT)
    {
      g_ptr_array_add (argv, g_strdup ("timeout"));
      g_ptr_array_add (argv, g_strdup ("--signal=TERM"));

      if (flags & SRT_HELPER_FLAGS_TIME_OUT_SOONER)
        {
          /* Speed up the failing case in automated testing */
          g_ptr_array_add (argv, g_strdup ("--kill-after=1"));
          g_ptr_array_add (argv, g_strdup ("1"));
        }
      else
        {
          /* Kill the helper (if still running) 3 seconds after the TERM
           * signal */
          g_ptr_array_add (argv, g_strdup ("--kill-after=3"));
          /* Send TERM signal after 10 seconds */
          g_ptr_array_add (argv, g_strdup ("10"));
        }
    }

  if (helpers_path == NULL)
    helpers_path = g_getenv ("SRT_HELPERS_PATH");

  if (helpers_path == NULL
      && _srt_find_myself (&helpers_path, error) == NULL)
    {
      g_ptr_array_unref (argv);
      return NULL;
    }

  /* Prefer a helper from ${SRT_HELPERS_PATH} or
   * ${libexecdir}/steam-runtime-tools-${_SRT_API_MAJOR}
   * if it exists */
  path = g_strdup_printf ("%s/%s%s%s",
                          helpers_path,
                          multiarch == NULL ? "" : multiarch,
                          multiarch == NULL ? "" : "-",
                          base);

  g_debug ("Looking for %s", path);

  if (g_file_test (path, G_FILE_TEST_IS_EXECUTABLE))
    {
      g_ptr_array_add (argv, g_steal_pointer (&path));
      return argv;
    }

  if ((flags & SRT_HELPER_FLAGS_SEARCH_PATH) == 0)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
                   "%s not found", path);
      g_free (path);
      g_ptr_array_unref (argv);
      return NULL;
    }

  /* For helpers that are not part of steam-runtime-tools, such as
   * *-wflinfo and *-vulkaninfo, we fall back to searching $PATH */
  g_free (path);

  if (multiarch == NULL)
    prefixed = g_strdup (base);
  else
    prefixed = g_strdup_printf ("%s-%s", multiarch, base);

  g_ptr_array_add (argv, g_steal_pointer (&prefixed));
  return argv;
}

/**
 * _srt_filter_gameoverlayrenderer:
 * @input: The environment variable value that needs to be filtered.
 *  Usually retrieved with g_environ_getenv ()
 *
 * Filter the @input paths list from every path containing `gameoverlayrenderer.so`
 *
 * Returns: A newly-allocated string containing all the paths from @input
 *  except for the ones with `gameoverlayrenderer.so`.
 *  Free with g_free ().
 */
gchar *
_srt_filter_gameoverlayrenderer (const gchar *input)
{
  gchar **entries;
  gchar **entry;
  gchar *ret = NULL;
  GPtrArray *filtered;

  g_return_val_if_fail (input != NULL, NULL);

  entries = g_strsplit (input, ":", 0);
  filtered = g_ptr_array_new ();

  for (entry = entries; entry != NULL && *entry != NULL; entry++)
    {
      if (!g_str_has_suffix (*entry, "/gameoverlayrenderer.so"))
        g_ptr_array_add (filtered, *entry);
    }

  g_ptr_array_add (filtered, NULL);
  ret = g_strjoinv (":", (gchar **) filtered->pdata);

  g_ptr_array_free (filtered, TRUE);
  g_strfreev (entries);

  return ret;
}

/**
 * srt_enum_value_to_nick
 * @enum_type: The type of the enumeration.
 * @value: The enumeration value to stringify.
 *
 * Get the #GEnumValue.value-nick of a given enumeration value.
 * For example, `srt_enum_value_to_nick (SRT_TYPE_WINDOW_SYSTEM, SRT_WINDOW_SYSTEM_EGL_X11)`
 * returns `"egl-x11"`.
 *
 * Returns: (transfer none): A string representation
 *  of the given enumeration value.
 */
const char *
srt_enum_value_to_nick (GType enum_type,
                        int value)
{
  GEnumClass *class;
  GEnumValue *enum_value;
  const char *result;

  g_return_val_if_fail (G_TYPE_IS_ENUM (enum_type), NULL);

  class = g_type_class_ref (enum_type);
  enum_value = g_enum_get_value (class, value);

  if (enum_value != NULL)
    result = enum_value->value_nick;
  else
    result = NULL;

  g_type_class_unref (class);
  return result;
}

/**
 * srt_enum_from_nick:
 * @enum_type: The type of the enumeration
 * @nick: The nickname to look up
 * @value_out: (not nullable): Used to return the enumeration that has been
 *  found from the provided @nick
 * @error: Used to raise an error on failure
 *
 * Get the enumeration from a given #GEnumValue.value-nick.
 * For example:
 * `srt_enum_from_nick (SRT_TYPE_GRAPHICS_LIBRARY_VENDOR, "glvnd", (gint *) &library_vendor, NULL)`
 * will set `library_vendor` to SRT_GRAPHICS_LIBRARY_VENDOR_GLVND.
 *
 * Returns: %TRUE if no errors have been found.
 */
gboolean
srt_enum_from_nick (GType enum_type,
                    const gchar *nick,
                    gint *value_out,
                    GError **error)
{
  GEnumClass *class;
  GEnumValue *enum_value;
  gboolean result = TRUE;

  g_return_val_if_fail (G_TYPE_IS_ENUM (enum_type), FALSE);
  g_return_val_if_fail (nick != NULL, FALSE);
  g_return_val_if_fail (value_out != NULL, FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  class = g_type_class_ref (enum_type);

  enum_value = g_enum_get_value_by_nick (class, nick);
  if (enum_value)
    {
      *value_out = enum_value->value;
    }
  else
    {
      if (error != NULL)
        g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                     "\"%s\" is not a known member of %s",
                     nick, g_type_name (enum_type));
      result = FALSE;
    }

  g_type_class_unref (class);
  return result;
}

/**
 * srt_add_flag_from_nick:
 * @flags_type: The type of the flag
 * @nick: The nickname to look up
 * @value_out: (not nullable) (inout): The flag, from the provided @nick,
 *  will be added to @value_out
 * @error: Used to raise an error on failure
 *
 * Get the flag from a given #GEnumValue.value-nick.
 * For example:
 * `srt_add_flag_from_nick (SRT_TYPE_STEAM_ISSUES, "cannot-find", &issues, error)`
 * will add SRT_STEAM_ISSUES_CANNOT_FIND to `issues`.
 *
 * Returns: %TRUE if no errors have been found.
 */
gboolean
srt_add_flag_from_nick (GType flags_type,
                        const gchar *nick,
                        guint *value_out,
                        GError **error)
{
  GFlagsClass *class;
  GFlagsValue *flags_value;
  gboolean result = TRUE;

  g_return_val_if_fail (G_TYPE_IS_FLAGS (flags_type), FALSE);
  g_return_val_if_fail (nick != NULL, FALSE);
  g_return_val_if_fail (value_out != NULL, FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  class = g_type_class_ref (flags_type);

  flags_value = g_flags_get_value_by_nick (class, nick);
  if (flags_value)
    {
      *value_out |= flags_value->value;
    }
  else
    {
      if (error != NULL)
        g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                     "\"%s\" is not a known member of %s",
                     nick, g_type_name (flags_type));
      result = FALSE;
    }

  g_type_class_unref (class);
  return result;
}

/**
 * srt_get_flags_from_json_array:
 * @flags_type: The type of the flag
 * @json_obj: (not nullable): A JSON Object used to search for
 *  @array_member property
 * @array_member: (not nullable): The JSON member to look up
 * @flag_if_unknown: flag to use in case of parsing error
 *
 * Get the flags from a given JSON object array member.
 * If @json_obj doesn't have the provided @member, or it is malformed, the
 * @flag_if_unknown will be returned.
 * If the parsed JSON array_member has some elements that we can't parse,
 * @flag_if_unknown will be added to the returned flags.
 *
 * Returns: the found flags from the provided @json_obj
 */
guint
srt_get_flags_from_json_array (GType flags_type,
                               JsonObject *json_obj,
                               const gchar *array_member,
                               guint flag_if_unknown)
{
  JsonArray *array;
  guint ret = flag_if_unknown;

  g_return_val_if_fail (G_TYPE_IS_FLAGS (flags_type), 0);
  g_return_val_if_fail (json_obj != NULL, 0);
  g_return_val_if_fail (array_member != NULL, 0);

  if (json_object_has_member (json_obj, array_member))
    {
      array = json_object_get_array_member (json_obj, array_member);

      if (array == NULL)
        goto out;

      /* We reset the value out because we found the member we were looking for */
      ret = 0;

      for (guint j = 0; j < json_array_get_length (array); j++)
        {
          const gchar *issue_string = json_array_get_string_element (array, j);
          if (!srt_add_flag_from_nick (flags_type, issue_string, &ret, NULL))
            ret |= flag_if_unknown;
        }
    }

out:
  return ret;
}

static void _srt_constructor (void) __attribute__((__constructor__));
static void
_srt_constructor (void)
{
#if !GLIB_CHECK_VERSION(2, 36, 0)
  g_type_init ();
#endif
  g_return_if_fail (_srt_check_not_setuid ());
}

static const int signals_blocked_by_steam[] =
  {
    SIGALRM,
    SIGCHLD,
    SIGPIPE,
    SIGTRAP
  };

/*
 * _srt_child_setup_unblock_signals:
 * @ignored: Ignored, for compatibility with #GSpawnChildSetupFunc
 *
 * A child-setup function that unblocks all signals, and resets signals
 * known to be altered by the Steam client to their default dispositions.
 *
 * In particular, this can be used to work around versions of `timeout(1)`
 * that do not do configure `SIGCHLD` to make sure they receive it
 * (GNU coreutils >= 8.27, < 8.29 as seen in Ubuntu 18.04).
 *
 * This function is async-signal-safe.
 */
void
_srt_child_setup_unblock_signals (gpointer ignored)
{
  struct sigaction action = { .sa_handler = SIG_DFL };
  sigset_t new_set;
  gsize i;

  /* We ignore errors and don't even g_debug(), to avoid being
   * async-signal-unsafe */
  sigemptyset (&new_set);
  (void) pthread_sigmask (SIG_SETMASK, &new_set, NULL);

  for (i = 0; i < G_N_ELEMENTS (signals_blocked_by_steam); i++)
    (void) sigaction (signals_blocked_by_steam[i], &action, NULL);
}

/*
 * _srt_unblock_signals:
 *
 * Unblock all signals, and reset signals known to be altered by the
 * Steam client to their default dispositions.
 *
 * This function is not async-signal-safe.
 */
void
_srt_unblock_signals (void)
{
  struct sigaction old_action = { .sa_handler = SIG_DFL };
  struct sigaction new_action = { .sa_handler = SIG_DFL };
  sigset_t old_set;
  sigset_t new_set;
  gsize i;
  int sig;
  int saved_errno;

  sigemptyset (&old_set);
  sigfillset (&new_set);

  /* This returns an errno code instead of setting errno */
  saved_errno = pthread_sigmask (SIG_UNBLOCK, &new_set, &old_set);

  if (saved_errno != 0)
    {
      g_warning ("Unable to unblock signals: %s", g_strerror (saved_errno));
    }
  else
    {
      for (sig = 1; sig < 64; sig++)
        {
          /* sigismember returns -1 for non-signals, which we ignore */
          if (sigismember (&new_set, sig) == 1 &&
              sigismember (&old_set, sig) == 1)
            g_debug ("Unblocked signal %d (%s)", sig, g_strsignal (sig));
        }
    }

  for (i = 0; i < G_N_ELEMENTS (signals_blocked_by_steam); i++)
    {
      sig = signals_blocked_by_steam[i];

      if (sigaction (sig, &new_action, &old_action) != 0)
        {
          saved_errno = errno;
          g_warning ("Unable to reset handler for signal %d (%s): %s",
                     sig, g_strsignal (sig), g_strerror (saved_errno));
        }
      else if (old_action.sa_handler != SIG_DFL)
        {
          g_debug ("Reset signal %d (%s) from handler %p to SIG_DFL",
                   sig, g_strsignal (sig), old_action.sa_handler);
        }
    }
}

/*
 * _srt_indirect_strcmp0:
 * @left: A non-%NULL pointer to a (possibly %NULL) `const char *`
 * @right: A non-%NULL pointer to a (possibly %NULL) `const char *`
 *
 * A #GCompareFunc to sort pointers to strings in lexicographic
 * (g_strcmp0()) order.
 *
 * Returns: An integer < 0 if left < right, > 0 if left > right,
 *  or 0 if left == right or if they are not comparable
 */
int
_srt_indirect_strcmp0 (gconstpointer left,
                       gconstpointer right)
{
  const gchar * const *l = left;
  const gchar * const *r = right;

  g_return_val_if_fail (l != NULL, 0);
  g_return_val_if_fail (r != NULL, 0);
  return g_strcmp0 (*l, *r);
}

static gint
ftw_remove (const gchar *path,
            const struct stat *sb,
            gint typeflags,
            struct FTW *ftwbuf)
{
  if (remove (path) < 0)
    {
      g_debug ("Unable to remove %s: %s", path, g_strerror (errno));
      return -1;
    }

  return 0;
}

/**
 * _srt_rm_rf:
 * @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
_srt_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;
}

/**
 * _srt_json_array_to_strv:
 * @json_obj: (not nullable): A JSON Object used to search for
 *  @array_member property
 * @array_member: (not nullable): The JSON member to look up
 *
 * Every %NULL or non string members of the array are replaced with the value
 * "<invalid>".
 *
 * Returns: (transfer full) (array zero-terminated=1) (element-type utf8) (nullable):
 *  A string array from the given @json_obj, or %NULL if it doesn't have a
 *  property @array_member
 */
gchar **
_srt_json_array_to_strv (JsonObject *json_obj,
                         const gchar *array_member)
{
  JsonArray *array;
  guint length;
  const gchar *element;
  gchar **ret = NULL;

  g_return_val_if_fail (json_obj != NULL, NULL);
  g_return_val_if_fail (array_member != NULL, NULL);

  if (json_object_has_member (json_obj, array_member))
    {
      array = json_object_get_array_member (json_obj, array_member);
      if (array == NULL)
        return ret;

      length = json_array_get_length (array);
      ret = g_new0 (gchar *, length + 1);
      for (guint i = 0; i < length; i++)
        {
          element = json_array_get_string_element (array, i);
          if (element == NULL)
            element = "<invalid>";
          ret[i] = g_strdup (element);
        }
      ret[length] = NULL;
    }

  return ret;
}