/* vi:set et sw=2 sts=2 cin cino=t0,f0,(0,{s,>2s,n-s,^-s,e-s:
 * Copyright © 2014-2020 Red Hat, Inc
 *
 * 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/>.
 *
 * Authors:
 *       Alexander Larsson <alexl@redhat.com>
 */

#include "config.h"

#include <glib/gi18n-lib.h>

#include <string.h>
#include <stdlib.h>

#include <glib.h>
#include "flatpak-ref-utils-private.h"

#include <ostree.h>

#include "flatpak-error.h"
#include "flatpak-metadata-private.h"
#include "flatpak-utils-private.h"

FlatpakKinds
flatpak_kinds_from_kind (FlatpakRefKind kind)
{
  if (kind == FLATPAK_REF_KIND_RUNTIME)
    return FLATPAK_KINDS_RUNTIME;
  return FLATPAK_KINDS_APP;
}

static gboolean
is_valid_initial_name_character (gint c, gboolean allow_dash)
{
  return
    (c >= 'A' && c <= 'Z') ||
    (c >= 'a' && c <= 'z') ||
    (c == '_') || (allow_dash && c == '-');
}

static gboolean
is_valid_name_character (gint c, gboolean allow_dash)
{
  return
    is_valid_initial_name_character (c, allow_dash) ||
    (c >= '0' && c <= '9');
}

static const char *
find_last_char (const char *str, gsize len, int c)
{
  const char *p = str + len - 1;
  while (p >= str)
    {
      if (*p == c)
        return p;
      p--;
    }
  return NULL;
}

/**
 * flatpak_is_valid_name:
 * @string: The string to check
 * @len: The string length, or -1 for null-terminated
 * @error: Return location for an error
 *
 * Checks if @string is a valid application name.
 *
 * App names are composed of 3 or more elements separated by a period
 * ('.') character. All elements must contain at least one character.
 *
 * Each element must only contain the ASCII characters
 * "[A-Z][a-z][0-9]_-". Elements may not begin with a digit.
 * Additionally "-" is only allowed in the last element.
 *
 * App names must not begin with a '.' (period) character.
 *
 * App names must not exceed 255 characters in length.
 *
 * The above means that any app name is also a valid DBus well known
 * bus name, but not all DBus names are valid app names. The difference are:
 * 1) DBus name elements may contain '-' in the non-last element.
 * 2) DBus names require only two elements
 *
 * Returns: %TRUE if valid, %FALSE otherwise.
 */
gboolean
flatpak_is_valid_name (const char *string,
                       gssize      len,
                       GError    **error)
{
  gboolean ret;
  const gchar *s;
  const gchar *end;
  const gchar *last_dot;
  int dot_count;
  gboolean last_element;

  g_return_val_if_fail (string != NULL, FALSE);

  ret = FALSE;

  if (len < 0)
    len = strlen (string);
  if (G_UNLIKELY (len == 0))
    {
      flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME,
                          _("Name can't be empty"));
      goto out;
    }

  if (G_UNLIKELY (len > 255))
    {
      flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME,
                          _("Name can't be longer than 255 characters"));
      goto out;
    }

  end = string + len;

  last_dot = find_last_char (string, len, '.');
  last_element = FALSE;

  s = string;
  if (G_UNLIKELY (*s == '.'))
    {
      flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME,
                          _("Name can't start with a period"));
      goto out;
    }
  else if (G_UNLIKELY (!is_valid_initial_name_character (*s, last_element)))
    {
      flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME,
                          _("Name can't start with %c"), *s);
      goto out;
    }

  s += 1;
  dot_count = 0;
  while (s != end)
    {
      if (*s == '.')
        {
          if (s == last_dot)
            last_element = TRUE;
          s += 1;
          if (G_UNLIKELY (s == end))
            {
              flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME,
                                  _("Name can't end with a period"));
              goto out;
            }
          if (!is_valid_initial_name_character (*s, last_element))
            {
              if (*s == '-')
                flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME,
                                    _("Only last name segment can contain -"));
              else
                flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME,
                                    _("Name segment can't start with %c"), *s);
              goto out;
            }
          dot_count++;
        }
      else if (G_UNLIKELY (!is_valid_name_character (*s, last_element)))
        {
          if (*s == '-')
            flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME,
                                _("Only last name segment can contain -"));
          else
            flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME,
                                _("Name can't contain %c"), *s);
          goto out;
        }
      s += 1;
    }

  if (G_UNLIKELY (dot_count < 2))
    {
      flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME,
                          _("Names must contain at least 2 periods"));
      goto out;
    }

  ret = TRUE;

out:
  return ret;
}

gboolean
flatpak_has_name_prefix (const char *string,
                         const char *name)
{
  const char *rest;

  if (!g_str_has_prefix (string, name))
    return FALSE;

  rest = string + strlen (name);
  return
    *rest == 0 ||
    *rest == '.' ||
    !is_valid_name_character (*rest, FALSE);
}


gboolean
flatpak_name_matches_one_wildcard_prefix (const char         *name,
                                          const char * const *wildcarded_prefixes,
                                          gboolean            require_exact_match)
{
  const char * const *iter = wildcarded_prefixes;
  const char *remainder;
  gsize longest_match_len = 0;

  /* Find longest valid match */
  for (; *iter != NULL; ++iter)
    {
      const char *prefix = *iter;
      gsize prefix_len = strlen (prefix);
      gsize match_len;
      gboolean has_wildcard = FALSE;
      const char *end_of_match;

      if (g_str_has_suffix (prefix, ".*"))
        {
          has_wildcard = TRUE;
          prefix_len -= 2;
        }

      if (strncmp (name, prefix, prefix_len) != 0)
        continue;

      end_of_match = name + prefix_len;

      if (has_wildcard &&
          end_of_match[0] == '.' &&
          is_valid_initial_name_character (end_of_match[1], TRUE))
        {
          end_of_match += 2;
          while (*end_of_match != 0 &&
                 (is_valid_name_character (*end_of_match, TRUE) ||
                  (end_of_match[0] == '.' &&
                   is_valid_initial_name_character (end_of_match[1], TRUE))))
            end_of_match++;
        }

      match_len = end_of_match - name;

      if (match_len > longest_match_len)
        longest_match_len = match_len;
    }

  if (longest_match_len == 0)
    return FALSE;

  if (require_exact_match)
    return name[longest_match_len] == 0;

  /* non-exact matches can be exact, or can be followed by characters that would make
   * not be part of the last element in the matched prefix, due to being invalid or
   * a new element. As a special case we explicitly disallow dash here, even though
   * it iss typically allowed in the final element of a name, this allows you too sloppily
   * match org.the.App with org.the.App-symbolic[.png] or org.the.App-settings[.desktop].
   */
  remainder = name + longest_match_len;
  return
    *remainder == 0 ||
    *remainder == '.' ||
    !is_valid_name_character (*remainder, FALSE);
}


static gboolean
is_valid_arch_character (char c)
{
  return
    (c >= 'A' && c <= 'Z') ||
    (c >= 'a' && c <= 'z') ||
    (c >= '0' && c <= '9') ||
    (c == '_');
}

gboolean
flatpak_is_valid_arch (const char *string,
                       gssize      len,
                       GError    **error)
{
  const gchar *end;

  if (len < 0)
    len = strlen (string);

  if (G_UNLIKELY (len == 0))
    {
      flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME,
                          _("Arch can't be empty"));
      return FALSE;
    }

  end = string + len;

  while (string != end)
    {
      if (G_UNLIKELY (!is_valid_arch_character (*string)))
        {
          flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME,
                              _("Arch can't contain %c"), *string);
          return FALSE;
        }
      string += 1;
    }

  return TRUE;
}


static gboolean
is_valid_initial_branch_character (gint c)
{
  return
    (c >= '0' && c <= '9') ||
    (c >= 'A' && c <= 'Z') ||
    (c >= 'a' && c <= 'z') ||
    (c == '_') ||
    (c == '-');
}

static gboolean
is_valid_branch_character (gint c)
{
  return
    is_valid_initial_branch_character (c) ||
    (c == '.');
}

/**
 * flatpak_is_valid_branch:
 * @string: The string to check
 * @len: The string length, or -1 for null-terminated
 * @error: return location for an error
 *
 * Checks if @string is a valid branch name.
 *
 * Branch names must only contain the ASCII characters
 * "[A-Z][a-z][0-9]_-.".
 * Branch names may not begin with a period.
 * Branch names must contain at least one character.
 *
 * Returns: %TRUE if valid, %FALSE otherwise.
 */
gboolean
flatpak_is_valid_branch (const char *string,
                         gssize      len,
                         GError    **error)
{
  gboolean ret;
  const gchar *s;
  const gchar *end;

  g_return_val_if_fail (string != NULL, FALSE);

  ret = FALSE;

  if (len < 0)
    len = strlen (string);
  if (G_UNLIKELY (len == 0))
    {
      flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME,
                          _("Branch can't be empty"));
      goto out;
    }

  end = string + len;

  s = string;
  if (G_UNLIKELY (!is_valid_initial_branch_character (*s)))
    {
      flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME,
                          _("Branch can't start with %c"), *s);
      goto out;
    }

  s += 1;
  while (s != end)
    {
      if (G_UNLIKELY (!is_valid_branch_character (*s)))
        {
          flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME,
                              _("Branch can't contain %c"), *s);
          goto out;
        }
      s += 1;
    }

  ret = TRUE;

out:
  return ret;
}


/* Dashes are only valid in the last part of the app id, so
   we replace them with underscore so we can suffix the id */
char *
flatpak_make_valid_id_prefix (const char *orig_id)
{
  char *id, *t;

  id = g_strdup (orig_id);
  t = id;
  while (*t != 0 && *t != '/')
    {
      if (*t == '-')
        *t = '_';

      t++;
    }

  return id;
}

static gboolean
str_has_suffix (const gchar *str,
                gsize        str_len,
                const gchar *suffix)
{
  gsize suffix_len;

  suffix_len = strlen (suffix);
  if (str_len < suffix_len)
    return FALSE;

  return strncmp (str + str_len - suffix_len, suffix, suffix_len) == 0;
}


gboolean
flatpak_id_has_subref_suffix (const char *id,
                              gssize id_len)
{
  if (id_len < 0)
    id_len = strlen (id);

  return
    str_has_suffix (id, id_len, ".Locale") ||
    str_has_suffix (id, id_len, ".Debug") ||
    str_has_suffix (id, id_len, ".Sources");
}


static gboolean
str_has_prefix (const gchar *str,
                gsize        str_len,
                const gchar *prefix)
{
  gsize prefix_len;

  prefix_len = strlen (prefix);
  if (str_len < prefix_len)
    return FALSE;

  return strncmp (str, prefix, prefix_len) == 0;
}

static const char *
skip_segment (const char *s)
{
  const char *slash;

  slash = strchr (s, '/');
  if (slash)
    return slash + 1;
  return s + strlen (s);
}

static int
compare_segment (const char *s1, const char *s2)
{
  gint c1, c2;

  while (*s1 && *s1 != '/' &&
         *s2 && *s2 != '/')
    {
      c1 = *s1;
      c2 = *s2;
      if (c1 != c2)
        return c1 - c2;
      s1++;
      s2++;
    }

  c1 = *s1;
  if (c1 == '/')
    c1 = 0;
  c2 = *s2;
  if (c2 == '/')
    c2 = 0;

  return c1 - c2;
}

int
flatpak_compare_ref (const char *ref1, const char *ref2)
{
  int res;
  int i;

  /* Skip first element and do per-segment compares for rest */
  for (i = 0; i < 3; i++)
    {
      ref1 = skip_segment (ref1);
      ref2 = skip_segment (ref2);

      res = compare_segment (ref1, ref2);
      if (res != 0)
        return res;
    }
  return 0;
}

struct _FlatpakDecomposed {
  int ref_count;
  guint16 ref_offset;
  guint16 id_offset;
  guint16 arch_offset;
  guint16 branch_offset;
  char *data;

  /* This is only used when we're directly manipulating sideload repos, by giving
   * a file:// uri as the remote name. Typically we don't really care about collection ids
   * internally in flatpak as we use refs tied to a remote. */
  char *collection_id;
};

static gboolean
is_valid_initial_remote_name_character (gint c)
{
  return
    (c >= 'A' && c <= 'Z') ||
    (c >= 'a' && c <= 'z') ||
    (c >= '0' && c <= '9') ||
    (c == '_');
}

static gboolean
is_valid_remote_name_character (gint c)
{
  return
    is_valid_initial_remote_name_character (c) ||
    c == '-' ||
    c == '.';
}

static gboolean
is_valid_remote_name (const char *remote,
                      gsize len)
{
  const char *end;

  if (len == 0)
    return FALSE;

  end = remote + len;

  if (!is_valid_initial_remote_name_character (*remote++))
    return FALSE;

  while (remote < end)
    {
      char c = *remote++;
      if (!is_valid_remote_name_character (c))
        return FALSE;
    }

  return TRUE;
}


static FlatpakDecomposed *
_flatpak_decomposed_new (char            *ref,
                         gboolean         allow_refspec,
                         gboolean         take,
                         GError         **error)
{
  g_autoptr(GError) local_error = NULL;
  const char *p;
  const char *slash;
  gsize ref_offset;
  gsize id_offset;
  gsize arch_offset;
  gsize branch_offset;
  gsize len;
  FlatpakDecomposed *decomposed;

  /* We want to use uint16 to store offset, so fail on uselessly large refs */
  len = strlen (ref);
  if (len > 0xffff)
    {
      flatpak_fail_error (error, FLATPAK_ERROR_INVALID_REF, _("Ref too long"));
      return NULL;
    }

  p = ref;
  if (allow_refspec)
    {
      const char *colon = strchr (p, ':');
      if (colon != NULL)
        {
          if (!is_valid_remote_name (ref, colon - ref))
            {
              flatpak_fail_error (error, FLATPAK_ERROR_INVALID_REF, _("Invalid remote name"));
              return NULL;
            }
          p = colon + 1;
        }
    }
  ref_offset = p - ref;

  if (g_str_has_prefix (p, "app/"))
    p += strlen ("app/");
  else if (g_str_has_prefix (p, "runtime/"))
    p += strlen ("runtime/");
  else
    {
      flatpak_fail_error (error, FLATPAK_ERROR_INVALID_REF, _("%s is not application or runtime"), ref);
      return NULL;
    }

  id_offset = p - ref;

  slash = strchr (p, '/');
  if (slash == NULL)
    {
      flatpak_fail_error (error, FLATPAK_ERROR_INVALID_REF, _("Wrong number of components in %s"), ref);
      return NULL;
    }

  if (!flatpak_is_valid_name (p, slash - p, &local_error))
    {
      flatpak_fail_error (error, FLATPAK_ERROR_INVALID_REF, _("Invalid name %.*s: %s"), (int)(slash - p), p, local_error->message);
      return NULL;
    }

  p = slash + 1;

  arch_offset = p - ref;

  slash = strchr (p, '/');
  if (slash == NULL)
    {
      flatpak_fail_error (error, FLATPAK_ERROR_INVALID_REF, _("Wrong number of components in %s"), ref);
      return NULL;
    }

  if (!flatpak_is_valid_arch (p, slash - p, &local_error))
    {
      flatpak_fail_error (error, FLATPAK_ERROR_INVALID_REF, _("Invalid arch: %.*s: %s"), (int)(slash - p), p, local_error->message);
      return NULL;
    }

  p = slash + 1;
  branch_offset = p - ref;

  slash = strchr (p, '/');
  if (slash != NULL)
    {
      flatpak_fail_error (error, FLATPAK_ERROR_INVALID_REF, _("Wrong number of components in %s"), ref);
      return NULL;
    }

  if (!flatpak_is_valid_branch (p, -1, &local_error))
    {
      flatpak_fail_error (error, FLATPAK_ERROR_INVALID_REF, _("Invalid branch %s: %s"), p, local_error->message);
      return NULL;
    }

  if (take)
    {
      decomposed = g_malloc (sizeof (FlatpakDecomposed));
      decomposed->data = ref;
    }
  else
    {
      char *inline_data;

      /* Store the dup:ed ref inline */
      decomposed = g_malloc (sizeof (FlatpakDecomposed) + strlen (ref) + 1);
      inline_data = (char *)decomposed + sizeof (FlatpakDecomposed);

      strcpy (inline_data, ref);
      decomposed->data = inline_data;
    }
  decomposed->ref_count = 1;
  decomposed->collection_id = NULL;
  decomposed->ref_offset = (guint16)ref_offset;
  decomposed->id_offset = (guint16)id_offset;
  decomposed->arch_offset = (guint16)arch_offset;
  decomposed->branch_offset = (guint16)branch_offset;

  return decomposed;
}

FlatpakDecomposed *
flatpak_decomposed_new_from_ref (const char         *ref,
                                 GError            **error)
{
  return _flatpak_decomposed_new ((char *)ref, FALSE, FALSE, error);
}

FlatpakDecomposed *
flatpak_decomposed_new_from_refspec (const char         *refspec,
                                     GError            **error)
{
  return _flatpak_decomposed_new ((char *)refspec, TRUE, FALSE, error);
}

FlatpakDecomposed *
flatpak_decomposed_new_from_ref_take (char         *ref,
                                      GError      **error)
{
  return _flatpak_decomposed_new (ref, FALSE, TRUE, error);
}

FlatpakDecomposed *
flatpak_decomposed_new_from_refspec_take (char         *refspec,
                                          GError       **error)
{
  return _flatpak_decomposed_new (refspec, TRUE, TRUE, error);
}

FlatpakDecomposed *
flatpak_decomposed_new_from_col_ref      (const char         *ref,
                                          const char         *collection_id,
                                          GError            **error)
{
  g_autoptr(FlatpakDecomposed) decomposed = NULL;

  if (collection_id != NULL &&
      !ostree_validate_collection_id (collection_id, error))
    return FALSE;

  decomposed = flatpak_decomposed_new_from_ref (ref, error);
  if (decomposed == NULL)
    return FALSE;

  decomposed->collection_id = g_strdup (collection_id);

  return g_steal_pointer (&decomposed);
}

static FlatpakDecomposed *
_flatpak_decomposed_new_from_decomposed (FlatpakDecomposed  *old,
                                         FlatpakKinds        opt_kind,
                                         const char         *opt_id,
                                         gssize              opt_id_len,
                                         const char         *opt_arch,
                                         gssize              opt_arch_len,
                                         const char         *opt_branch,
                                         gssize              opt_branch_len,
                                         GError            **error)
{
  FlatpakDecomposed *decomposed;
  g_autoptr(GError) local_error = NULL;
  char *inline_data;
  const char *kind_str;
  gsize kind_len;
  gsize id_len;
  gsize arch_len;
  gsize branch_len;
  gsize ref_len;
  char *ref;
  gsize offset;

  if (old == NULL)
    {
      g_assert (opt_kind != 0);
      g_assert (opt_id != NULL);
      g_assert (opt_arch != NULL);
      g_assert (opt_branch != NULL);
    }

  if (opt_kind == 0)
    kind_str = flatpak_decomposed_get_kind_str (old);
  else if (opt_kind == FLATPAK_KINDS_APP)
    kind_str = "app";
  else
    kind_str = "runtime";

  kind_len = strlen (kind_str);

  if (opt_id)
    {
      if (opt_id_len == -1)
        id_len = strlen (opt_id);
      else
        id_len = opt_id_len;

      if (!flatpak_is_valid_name (opt_id, id_len, &local_error))
        {
          flatpak_fail_error (error, FLATPAK_ERROR_INVALID_REF, _("Invalid name %s: %s"), opt_id, local_error->message);
          return NULL;
        }
    }
  else
    {
      opt_id = flatpak_decomposed_peek_id (old, &id_len);
    }

  if (opt_arch)
    {
      if (opt_arch_len == -1)
        arch_len = strlen (opt_arch);
      else
        arch_len = opt_arch_len;

      if (!flatpak_is_valid_arch (opt_arch, arch_len, &local_error))
        {
          flatpak_fail_error (error, FLATPAK_ERROR_INVALID_REF, _("Invalid arch: %s: %s"), opt_arch, local_error->message);
          return NULL;
        }

    }
  else
    {
      opt_arch = flatpak_decomposed_peek_arch (old, &arch_len);
    }

  if (opt_branch)
    {
      if (opt_branch_len == -1)
        branch_len = strlen (opt_branch);
      else
        branch_len = opt_branch_len;

      if (!flatpak_is_valid_branch (opt_branch, branch_len, &local_error))
        {
          flatpak_fail_error (error, FLATPAK_ERROR_INVALID_REF, _("Invalid branch: %s: %s"), opt_branch, local_error->message);
          return NULL;
        }
    }
  else
    {
      opt_branch = flatpak_decomposed_peek_branch (old, &branch_len);
    }

  ref_len = kind_len + 1 + id_len + 1 + arch_len + 1 + branch_len;
  if (ref_len > 0xffff)
    {
      flatpak_fail_error (error, FLATPAK_ERROR_INVALID_REF, _("Ref too long"));
      return NULL;
    }

  /* Store the ref inline */
  decomposed = g_malloc (sizeof (FlatpakDecomposed) + ref_len + 1);
  inline_data = (char *)decomposed + sizeof (FlatpakDecomposed);

  decomposed->ref_count = 1;
  decomposed->data = inline_data;
  decomposed->collection_id = NULL;

  ref = inline_data;
  offset = 0;

  decomposed->ref_offset = (guint16)offset;
  memcpy (ref + offset, kind_str, kind_len);
  offset += kind_len;

  memcpy (ref + offset, "/", 1);
  offset += 1;

  decomposed->id_offset = (guint16)offset;
  memcpy (ref + offset, opt_id, id_len);
  offset += id_len;

  memcpy (ref + offset, "/", 1);
  offset += 1;

  decomposed->arch_offset = (guint16)offset;
  memcpy (ref + offset, opt_arch, arch_len);
  offset += arch_len;

  memcpy (ref + offset, "/", 1);
  offset += 1;

  decomposed->branch_offset = (guint16)offset;
  memcpy (ref + offset, opt_branch, branch_len);
  offset += branch_len;

  g_assert (offset == ref_len);
  *(ref + offset) = 0;

  return decomposed;
}

FlatpakDecomposed *
flatpak_decomposed_new_from_decomposed (FlatpakDecomposed  *old,
                                        FlatpakKinds        opt_kind,
                                        const char         *opt_id,
                                        const char         *opt_arch,
                                        const char         *opt_branch,
                                        GError            **error)
{
  return _flatpak_decomposed_new_from_decomposed (old, opt_kind,
                                                  opt_id, -1,
                                                  opt_arch, -1,
                                                  opt_branch, -1,
                                                  error);
}

FlatpakDecomposed *
flatpak_decomposed_new_from_parts (FlatpakKinds        kind,
                                   const char         *id,
                                   const char         *arch,
                                   const char         *branch,
                                   GError            **error)
{
  g_assert (kind == FLATPAK_KINDS_APP || kind == FLATPAK_KINDS_RUNTIME);
  g_assert (id != NULL);

  if (branch == NULL)
    branch = "master";

  if (arch == NULL)
    arch = flatpak_get_arch ();

  return flatpak_decomposed_new_from_decomposed (NULL, kind, id, arch, branch, error);
}

FlatpakDecomposed *
flatpak_decomposed_new_from_pref (FlatpakKinds        kind,
                                  const char         *pref,
                                  GError            **error)
{
  const char *slash;
  const char *id;
  const char *arch;
  const char *branch;

  g_assert (kind == FLATPAK_KINDS_APP || kind == FLATPAK_KINDS_RUNTIME);
  g_assert (pref != NULL);

  id = pref;
  slash = strchr (id, '/');
  if (slash == NULL)
    {
      flatpak_fail_error (error, FLATPAK_ERROR_INVALID_REF, _("Wrong number of components in partial ref %s"), pref);
      return NULL;
    }

  arch = slash + 1;
  slash = strchr (arch, '/');
  if (slash == NULL)
    {
      flatpak_fail_error (error, FLATPAK_ERROR_INVALID_REF, _("Wrong number of components in partial ref %s"), pref);
      return NULL;
    }

  branch = slash + 1;
  slash = strchr (branch, '/');
  if (slash != NULL)
    {
      flatpak_fail_error (error, FLATPAK_ERROR_INVALID_REF, _("Wrong number of components in partial ref %s"), pref);
      return NULL;
    }

  return _flatpak_decomposed_new_from_decomposed (NULL, kind,
                                                  id, arch - id - 1,
                                                  arch, branch - arch - 1,
                                                  branch, -1,
                                                  error);
}

FlatpakDecomposed *
flatpak_decomposed_ref (FlatpakDecomposed  *ref)
{
  g_atomic_int_inc (&ref->ref_count);
  return ref;
}

void
flatpak_decomposed_unref (FlatpakDecomposed  *ref)
{
  if (g_atomic_int_dec_and_test (&ref->ref_count))
    {
      char *inline_data = (char *)ref + sizeof (FlatpakDecomposed);
      if (ref->data != inline_data)
        g_free (ref->data);
      g_free (ref->collection_id);
      g_free (ref);
    }
}

const char *
flatpak_decomposed_get_ref (FlatpakDecomposed  *ref)
{
  return (const char *)&ref->data[ref->ref_offset];
}

char *
flatpak_decomposed_dup_ref (FlatpakDecomposed  *ref)
{
  return g_strdup (flatpak_decomposed_get_ref (ref));
}

const char *
flatpak_decomposed_get_refspec (FlatpakDecomposed  *ref)
{
  return (const char *)&ref->data[0];
}

char *
flatpak_decomposed_dup_refspec (FlatpakDecomposed  *ref)
{
  return g_strdup (flatpak_decomposed_get_refspec (ref));
}

char *
flatpak_decomposed_dup_remote (FlatpakDecomposed  *ref)
{
  if (ref->ref_offset == 0)
    return NULL;

  return g_strndup (ref->data, ref->ref_offset - 1);
}

/* Note: These are always NULL for regular refs, as they generally
 * tied to a remote and uses the collection_id from that.
 * The only case this is set is if we enumerate a remote
 * of the form `file:///path/to/repo`, as we don't then know
 * which remote name it is from.
 */
const char *
flatpak_decomposed_get_collection_id (FlatpakDecomposed  *ref)
{
  return ref->collection_id;
}

/* Note: These are always NULL for regular refs, as they generally
 * tied to a remote and uses the collection_id from that.
 * The only case this is set is if we enumerate a remote
 * of the form `file:///path/to/repo`, as we don't then know
 * which remote name it is from.
 */
char *
flatpak_decomposed_dup_collection_id (FlatpakDecomposed  *ref)
{
  return g_strdup (ref->collection_id);
}

gboolean
flatpak_decomposed_equal (FlatpakDecomposed  *ref_a,
                          FlatpakDecomposed  *ref_b)
{
  return strcmp (ref_a->data, ref_b->data) == 0 &&
    g_strcmp0 (ref_a->collection_id, ref_b->collection_id) == 0;
}

gint
flatpak_decomposed_strcmp (FlatpakDecomposed  *ref_a,
                           FlatpakDecomposed  *ref_b)
{
  int res = strcmp (ref_a->data, ref_b->data);
  if (res != 0)
    return res;

  return g_strcmp0 (ref_a->collection_id, ref_b->collection_id);
}

gint
flatpak_decomposed_strcmp_p (FlatpakDecomposed  **ref_a,
                             FlatpakDecomposed  **ref_b)
{
  return flatpak_decomposed_strcmp (*ref_a, *ref_b);
}

gboolean
flatpak_decomposed_equal_except_branch (FlatpakDecomposed  *ref_a,
                                        FlatpakDecomposed  *ref_b)
{
  return
    ref_a->branch_offset == ref_b->branch_offset &&
    strncmp (ref_a->data, ref_b->data, ref_a->branch_offset) == 0 &&
    g_strcmp0 (ref_a->collection_id, ref_b->collection_id) == 0;
}

gboolean
flatpak_decomposed_equal_except_arch (FlatpakDecomposed  *ref_a,
                                      FlatpakDecomposed  *ref_b)
{
  return
    ref_a->arch_offset == ref_b->arch_offset &&
    strncmp (ref_a->data, ref_b->data, ref_a->arch_offset) == 0 &&
    strcmp (&ref_a->data[ref_a->branch_offset], &ref_b->data[ref_b->branch_offset]) == 0 &&
    g_strcmp0 (ref_a->collection_id, ref_b->collection_id) == 0;
}

guint
flatpak_decomposed_hash (FlatpakDecomposed  *ref)
{
  guint h = g_str_hash (ref->data);

  if (ref->collection_id)
    h |= g_str_hash (ref->collection_id);

  return h;
}

gboolean
flatpak_decomposed_is_app (FlatpakDecomposed  *ref)
{
  const char *r = flatpak_decomposed_get_ref (ref);
  return r[0] == 'a';
}

gboolean
flatpak_decomposed_is_runtime (FlatpakDecomposed  *ref)
{
  const char *r = flatpak_decomposed_get_ref (ref);
  return r[0] == 'r';
}

FlatpakKinds
flatpak_decomposed_get_kinds (FlatpakDecomposed  *ref)
{
  if (flatpak_decomposed_is_app (ref))
    return FLATPAK_KINDS_APP;
  else
    return FLATPAK_KINDS_RUNTIME;
}

FlatpakRefKind
flatpak_decomposed_get_kind (FlatpakDecomposed  *ref)
{
  if (flatpak_decomposed_is_app (ref))
    return FLATPAK_REF_KIND_APP;
  else
    return FLATPAK_REF_KIND_RUNTIME;
}

const char *
flatpak_decomposed_get_kind_str (FlatpakDecomposed  *ref)
{
  if (flatpak_decomposed_is_app (ref))
    return "app";
  else
    return "runtime";
}

const char *
flatpak_decomposed_get_kind_metadata_group (FlatpakDecomposed  *ref)
{
  if (flatpak_decomposed_is_app (ref))
    return FLATPAK_METADATA_GROUP_APPLICATION;
  else
    return FLATPAK_METADATA_GROUP_RUNTIME;
}

/* A slashed string ends at '/' instead of nul */
static gboolean
slashed_str_equal (const char *slashed_str, const char *str)
{
  char c;
  while ((c = *str) != 0)
    {
      char s_c = *slashed_str;
      if (s_c == '/')
        return FALSE; /* slashed_str stopped early */

      if (s_c != c)
        return FALSE; /* slashed_str not same */

      str++;
      slashed_str++;
    }

  if (*slashed_str != '/') /* str stopped early */
    return FALSE;

  return TRUE;
}

/* These are for refs, so ascii case only */
static gboolean
slashed_str_strcasestr (const char *haystack,
                        gsize haystack_len,
                        const char *needle)
{
  gssize needle_len = strlen (needle);

  if (needle_len > haystack_len)
    return FALSE;

  if (needle_len == 0)
    return TRUE;

  for (gssize i = 0; i <= haystack_len - needle_len; i++)
    {
      if (g_ascii_strncasecmp (haystack + i, needle, needle_len) == 0)
        return TRUE;
    }

  return FALSE;
}

const char *
flatpak_decomposed_get_pref (FlatpakDecomposed  *ref)
{
  return &ref->data[ref->id_offset];
}

char *
flatpak_decomposed_dup_pref (FlatpakDecomposed  *ref)
{
  return g_strdup (flatpak_decomposed_get_pref (ref));
}

const char *
flatpak_decomposed_peek_id (FlatpakDecomposed  *ref,
                            gsize              *out_len)
{
  if (out_len)
    *out_len = ref->arch_offset - ref->id_offset - 1;
  return &ref->data[ref->id_offset];
}

char *
flatpak_decomposed_dup_id (FlatpakDecomposed  *ref)
{
  gsize len;
  const char *ref_id = flatpak_decomposed_peek_id (ref, &len);

  return g_strndup (ref_id, len);
}

char *
flatpak_decomposed_dup_readable_id (FlatpakDecomposed  *ref)
{
  gsize len;
  const char *ref_id = flatpak_decomposed_peek_id (ref, &len);
  const char *start;
  gboolean is_debug, is_sources, is_locale, is_docs, is_sdk, is_platform, is_baseapp;
  GString *s;

  is_debug = str_has_suffix (ref_id, len, ".Debug");
  if (is_debug)
    len -= strlen (".Debug");

  is_sources = str_has_suffix (ref_id, len, ".Sources");
  if (is_sources)
    len -= strlen (".Sources");

  is_locale = str_has_suffix (ref_id, len, ".Locale");
  if (is_locale)
    len -= strlen (".Locale");

  is_docs = str_has_suffix (ref_id, len, ".Docs");
  if (is_docs)
    len -= strlen (".Docs");

  is_baseapp = str_has_suffix (ref_id, len, ".BaseApp");
  if (is_baseapp)
    len -= strlen (".BaseApp");

  is_platform = str_has_suffix (ref_id, len, ".Platform");
  if (is_platform)
    len -= strlen (".Platform");

  is_sdk = str_has_suffix (ref_id, len, ".Sdk");
  if (is_sdk)
    len -= strlen (".Sdk");

  start = ref_id + len;

  while (start > ref_id && start[-1] != '.')
    start--;

  len -= (start - ref_id);

  s = g_string_new ("");
  g_string_append_len (s, start, len);
  if (is_sdk)
    g_string_append (s, _(" development platform"));
  if (is_platform)
    g_string_append (s, _(" platform"));
  if (is_baseapp)
    g_string_append (s, _(" application base"));

  if (is_debug)
    g_string_append (s, _(" debug symbols"));
  if (is_sources)
    g_string_append (s, _(" sourcecode"));
  if (is_locale)
    g_string_append (s, _(" translations"));
  if (is_docs)
    g_string_append (s, _(" docs"));

  return g_string_free (s, FALSE);
}

gboolean
flatpak_decomposed_is_id (FlatpakDecomposed  *ref,
                          const char         *id)
{
  const char *ref_id = flatpak_decomposed_peek_id (ref, NULL);

  return slashed_str_equal (ref_id, id);
}

gboolean
flatpak_decomposed_id_has_suffix (FlatpakDecomposed  *ref,
                                  const char         *suffix)
{
  gsize id_len;
  const char *ref_id = flatpak_decomposed_peek_id (ref, &id_len);
  return str_has_suffix (ref_id, id_len, suffix);
}

gboolean
flatpak_decomposed_id_has_prefix (FlatpakDecomposed  *ref,
                                  const char         *prefix)
{
  gsize id_len;
  const char *ref_id = flatpak_decomposed_peek_id (ref, &id_len);
  return str_has_prefix (ref_id, id_len, prefix);
}


/* See if the given id looks similar to this ref. The
 * Levenshtein distance constant was chosen pretty arbitrarily. */
gboolean
flatpak_decomposed_is_id_fuzzy (FlatpakDecomposed  *ref,
                                const char         *id)
{
  gsize ref_id_len;
  const char *ref_id = flatpak_decomposed_peek_id (ref, &ref_id_len);

  if (slashed_str_strcasestr (ref_id, ref_id_len, id))
    return TRUE;

  return flatpak_levenshtein_distance (id, -1, ref_id, ref_id_len) <= 2;
}

gboolean
flatpak_decomposed_id_is_subref (FlatpakDecomposed  *ref)
{
  gsize ref_id_len;
  const char *ref_id = flatpak_decomposed_peek_id (ref, &ref_id_len);

  if (!flatpak_decomposed_is_runtime (ref))
    return FALSE;

  return flatpak_id_has_subref_suffix (ref_id, ref_id_len);
}

gboolean
flatpak_decomposed_id_is_subref_of (FlatpakDecomposed  *ref,
                                    FlatpakDecomposed  *parent_ref)
{
  gsize ref_id_len;
  const char *ref_id = flatpak_decomposed_peek_id (ref, &ref_id_len);
  gsize parent_id_len;
  const char *parent_id = flatpak_decomposed_peek_id (parent_ref, &parent_id_len);

  /* All subrefs are runtimes, even for apps */
  if (!flatpak_decomposed_is_runtime (ref))
    return FALSE;

  if (!flatpak_id_has_subref_suffix (ref_id, ref_id_len))
    return FALSE;

  /* Guaranteed to have a dot in it from the above check, so strip last element  */
  while (ref_id[ref_id_len-1] != '.')
    ref_id_len--;
  ref_id_len--; /* And strip dot */

  /* Any dashes in the last element of parent_id got converted to
     underscores to make it a valid prefix, so custom compare here. */

  if (ref_id_len != parent_id_len)
    return FALSE;

  for (int i = 0; i < ref_id_len; i++)
    {
      char c = parent_id[i];
      if (c == '-')
        c = '_';

      if (c != ref_id[i])
        return FALSE;
    }

  /* Check the rest of the ref */
  return strcmp (flatpak_decomposed_peek_arch (ref, NULL),
                 flatpak_decomposed_peek_arch (parent_ref, NULL)) == 0;
}


const char *
flatpak_decomposed_peek_arch (FlatpakDecomposed  *ref,
                              gsize *out_len)
{
  if (out_len)
    *out_len = ref->branch_offset - ref->arch_offset - 1;
  return &ref->data[ref->arch_offset];
}

char *
flatpak_decomposed_dup_arch (FlatpakDecomposed  *ref)
{
  gsize len;
  const char *ref_arch = flatpak_decomposed_peek_arch (ref, &len);

  return g_strndup (ref_arch, len);
}

gboolean
flatpak_decomposed_is_arch (FlatpakDecomposed  *ref,
                            const char         *arch)
{
  const char *ref_arch = flatpak_decomposed_peek_arch (ref, NULL);

  return slashed_str_equal (ref_arch, arch);
}

gboolean
flatpak_decomposed_is_arches (FlatpakDecomposed  *ref,
                              gssize              len,
                              const char        **arches)
{
  const char *ref_arch = flatpak_decomposed_peek_arch (ref, NULL);

  if (len < 0)
    {
      len = 0;
      while (arches[len] != NULL)
        len++;
    }

  for (int i = 0; i < len; i++)
    {
      if (slashed_str_equal (ref_arch, arches[i]))
        return TRUE;
    }

  return FALSE;
}

/* We can add a getter for this, because the branch is last so guaranteed to be null-terminated */
const char *
flatpak_decomposed_get_branch (FlatpakDecomposed  *ref)
{
  return &ref->data[ref->branch_offset];
}

const char *
flatpak_decomposed_peek_branch (FlatpakDecomposed  *ref,
                                gsize              *out_len)
{
  const char *branch = flatpak_decomposed_get_branch (ref);
  if (out_len)
    *out_len = strlen (branch);
  return branch;
}

char *
flatpak_decomposed_dup_branch (FlatpakDecomposed  *ref)
{
  const char *ref_branch = flatpak_decomposed_peek_branch (ref, NULL);

  return g_strdup (ref_branch);
}

gboolean
flatpak_decomposed_is_branch (FlatpakDecomposed  *ref,
                              const char         *branch)
{
  const char *ref_branch = flatpak_decomposed_get_branch (ref);

  return strcmp (ref_branch, branch) == 0;
}

static const char *
next_element (const char **partial_ref)
{
  const char *slash;
  const char *end;

  slash = (const char *) strchr (*partial_ref, '/');
  if (slash != NULL)
    {
      end = slash;
      *partial_ref = slash + 1;
    }
  else
    {
      end = *partial_ref + strlen (*partial_ref);
      *partial_ref = end;
    }

  return end;
}

FlatpakKinds
flatpak_kinds_from_bools (gboolean app, gboolean runtime)
{
  FlatpakKinds kinds = 0;

  if (app)
    kinds |= FLATPAK_KINDS_APP;

  if (runtime)
    kinds |= FLATPAK_KINDS_RUNTIME;

  if (kinds == 0)
    kinds = FLATPAK_KINDS_APP | FLATPAK_KINDS_RUNTIME;

  return kinds;
}

static gboolean
_flatpak_split_partial_ref_arg (const char   *partial_ref,
                                gboolean      validate,
                                FlatpakKinds  default_kinds,
                                const char   *default_arch,
                                const char   *default_branch,
                                FlatpakKinds *out_kinds,
                                char        **out_id,
                                char        **out_arch,
                                char        **out_branch,
                                GError      **error)
{
  const char *id_start = NULL;
  const char *id_end = NULL;
  g_autofree char *id = NULL;
  const char *arch_start = NULL;
  const char *arch_end = NULL;
  g_autofree char *arch = NULL;
  const char *branch_start = NULL;
  const char *branch_end = NULL;
  g_autofree char *branch = NULL;
  g_autoptr(GError) local_error = NULL;
  FlatpakKinds kinds = 0;

  if (g_str_has_prefix (partial_ref, "app/"))
    {
      partial_ref += strlen ("app/");
      kinds = FLATPAK_KINDS_APP;
    }
  else if (g_str_has_prefix (partial_ref, "runtime/"))
    {
      partial_ref += strlen ("runtime/");
      kinds = FLATPAK_KINDS_RUNTIME;
    }
  else
    kinds = default_kinds;

  id_start = partial_ref;
  id_end = next_element (&partial_ref);
  id = g_strndup (id_start, id_end - id_start);

  if (validate && !flatpak_is_valid_name (id, -1, &local_error))
    return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_REF, _("Invalid id %s: %s"), id, local_error->message);

  arch_start = partial_ref;
  arch_end = next_element (&partial_ref);
  if (arch_end != arch_start)
    arch = g_strndup (arch_start, arch_end - arch_start);
  else
    arch = g_strdup (default_arch);

  branch_start = partial_ref;
  branch_end = next_element (&partial_ref);
  if (branch_end != branch_start)
    branch = g_strndup (branch_start, branch_end - branch_start);
  else
    branch = g_strdup (default_branch);

  if (validate && branch != NULL && !flatpak_is_valid_branch (branch, -1, &local_error))
    return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_REF, _("Invalid branch %s: %s"), branch, local_error->message);

  if (out_kinds)
    *out_kinds = kinds;
  if (out_id != NULL)
    *out_id = g_steal_pointer (&id);
  if (out_arch != NULL)
    *out_arch = g_steal_pointer (&arch);
  if (out_branch != NULL)
    *out_branch = g_steal_pointer (&branch);

  return TRUE;
}

gboolean
flatpak_split_partial_ref_arg (const char   *partial_ref,
                               FlatpakKinds  default_kinds,
                               const char   *default_arch,
                               const char   *default_branch,
                               FlatpakKinds *out_kinds,
                               char        **out_id,
                               char        **out_arch,
                               char        **out_branch,
                               GError      **error)
{
  return _flatpak_split_partial_ref_arg (partial_ref,
                                         TRUE,
                                         default_kinds,
                                         default_arch,
                                         default_branch,
                                         out_kinds,
                                         out_id,
                                         out_arch,
                                         out_branch,
                                         error);
}

gboolean
flatpak_split_partial_ref_arg_novalidate (const char   *partial_ref,
                                          FlatpakKinds  default_kinds,
                                          const char   *default_arch,
                                          const char   *default_branch,
                                          FlatpakKinds *out_kinds,
                                          char        **out_id,
                                          char        **out_arch,
                                          char        **out_branch)
{
  return _flatpak_split_partial_ref_arg (partial_ref,
                                         FALSE,
                                         default_kinds,
                                         default_arch,
                                         default_branch,
                                         out_kinds,
                                         out_id,
                                         out_arch,
                                         out_branch,
                                         NULL);
}

char *
flatpak_build_untyped_ref (const char *runtime,
                           const char *branch,
                           const char *arch)
{
  if (arch == NULL)
    arch = flatpak_get_arch ();

  return g_build_filename (runtime, arch, branch, NULL);
}

char *
flatpak_build_runtime_ref (const char *runtime,
                           const char *branch,
                           const char *arch)
{
  if (branch == NULL)
    branch = "master";

  if (arch == NULL)
    arch = flatpak_get_arch ();

  return g_build_filename ("runtime", runtime, arch, branch, NULL);
}

char *
flatpak_build_app_ref (const char *app,
                       const char *branch,
                       const char *arch)
{
  if (branch == NULL)
    branch = "master";

  if (arch == NULL)
    arch = flatpak_get_arch ();

  return g_build_filename ("app", app, arch, branch, NULL);
}

gboolean
flatpak_is_app_runtime_or_appstream_ref (const char *ref)
{
  return
    g_str_has_prefix (ref, "appstream/") ||
    g_str_has_prefix (ref, "appstream2/") ||
    g_str_has_prefix (ref, "app/") ||
    g_str_has_prefix (ref, "runtime/");
}

char *
flatpak_get_arch_for_ref (const char *ref)
{
  if (g_str_has_prefix (ref, "appstream/") ||
      g_str_has_prefix (ref, "appstream2/"))
    {
      const char *rest = strchr (ref, '/') + 1; /* Guaranteed to exist per above check */
      const char *dash = strrchr (rest, '-'); /*  Subset appstream refs are appstream2/$subset-$arch */
      if (dash != NULL)
        rest = dash + 1;
      return g_strdup (rest);
    }
    else if (g_str_has_prefix (ref, "app/") ||
      g_str_has_prefix (ref, "runtime/"))
    {
      const char *slash;
      const char *arch;

      slash = strchr (ref, '/') + 1; /* Guaranteed to exist per above check */
      slash = strchr (slash, '/'); /* Skip id */
      if (slash == NULL)
        return NULL;
      arch = slash + 1;

      slash = strchr (arch, '/'); /* skip to end arch */
      if (slash == NULL)
        return NULL;

      return g_strndup (arch, slash - arch);
    }

  return NULL;
}
