/* vi:set et sw=2 sts=2 cin cino=t0,f0,(0,{s,>2s,n-s,^-s,e-s:
 * Copyright © 1995-1998 Free Software Foundation, Inc.
 * Copyright © 2014-2019 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 "flatpak-uri-private.h"

#if !GLIB_CHECK_VERSION (2, 66, 0)

struct _GUri {
  gchar     *scheme;
  gchar     *userinfo;
  gchar     *host;
  gint       port;
  gchar     *path;
  gchar     *query;
  gchar     *fragment;

  gchar     *user;
  gchar     *password;
  gchar     *auth_params;

  GUriFlags  flags;
  int ref_count;
};

GUri *
flatpak_g_uri_ref (GUri *uri)
{
  g_return_val_if_fail (uri != NULL, NULL);
  g_atomic_int_inc (&uri->ref_count);
  return uri;
}

void
flatpak_g_uri_unref (GUri *uri)
{
  g_return_if_fail (uri != NULL);

  if (g_atomic_int_dec_and_test (&uri->ref_count))
    {
      g_free (uri->scheme);
      g_free (uri->userinfo);
      g_free (uri->host);
      g_free (uri->path);
      g_free (uri->query);
      g_free (uri->fragment);
      g_free (uri->user);
      g_free (uri->password);
      g_free (uri->auth_params);
      g_free (uri);
    }
}

static gboolean
flatpak_g_uri_char_is_unreserved (gchar ch)
{
  if (g_ascii_isalnum (ch))
    return TRUE;
  return ch == '-' || ch == '.' || ch == '_' || ch == '~';
}

#define XDIGIT(c) ((c) <= '9' ? (c) - '0' : ((c) & 0x4F) - 'A' + 10)
#define HEXCHAR(s) ((XDIGIT (s[1]) << 4) + XDIGIT (s[2]))

static gssize
uri_decoder (gchar       **out,
             const gchar  *illegal_chars,
             const gchar  *start,
             gsize         length,
             gboolean      just_normalize,
             gboolean      www_form,
             GUriFlags     flags,
             GError      **error)
{
  gchar c;
  GString *decoded;
  const gchar *invalid, *s, *end;
  gssize len;

  if (!(flags & G_URI_FLAGS_ENCODED))
    just_normalize = FALSE;

  decoded = g_string_sized_new (length + 1);
  for (s = start, end = s + length; s < end; s++)
    {
      if (*s == '%')
        {
          if (s + 2 >= end ||
              !g_ascii_isxdigit (s[1]) ||
              !g_ascii_isxdigit (s[2]))
            {
              /* % followed by non-hex or the end of the string; this is an error */
              if (!(flags & G_URI_FLAGS_PARSE_RELAXED))
                {
                  g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
                                       /* xgettext: no-c-format */
                                       _("Invalid %-encoding in URI"));
                  g_string_free (decoded, TRUE);
                  return -1;
                }

              /* In non-strict mode, just let it through; we *don't*
               * fix it to "%25", since that might change the way that
               * the URI's owner would interpret it.
               */
              g_string_append_c (decoded, *s);
              continue;
            }

          c = HEXCHAR (s);
          if (illegal_chars && strchr (illegal_chars, c))
            {
              g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
                                   _("Illegal character in URI"));
              g_string_free (decoded, TRUE);
              return -1;
            }
          if (just_normalize && !flatpak_g_uri_char_is_unreserved (c))
            {
              /* Leave the % sequence there but normalize it. */
              g_string_append_c (decoded, *s);
              g_string_append_c (decoded, g_ascii_toupper (s[1]));
              g_string_append_c (decoded, g_ascii_toupper (s[2]));
              s += 2;
            }
          else
            {
              g_string_append_c (decoded, c);
              s += 2;
            }
        }
      else if (www_form && *s == '+')
        g_string_append_c (decoded, ' ');
      /* Normalize any illegal characters. */
      else if (just_normalize && (!g_ascii_isgraph (*s)))
        g_string_append_printf (decoded, "%%%02X", (guchar)*s);
      else
        g_string_append_c (decoded, *s);
    }

  len = decoded->len;
  g_assert (len >= 0);

  if (!(flags & G_URI_FLAGS_ENCODED) &&
      !g_utf8_validate (decoded->str, len, &invalid))
    {
      g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
                           _("Non-UTF-8 characters in URI"));
      g_string_free (decoded, TRUE);
      return -1;
    }

  if (out)
    *out = g_string_free (decoded, FALSE);
  else
    g_string_free (decoded, TRUE);

  return len;
}

static gboolean
uri_decode (gchar       **out,
            const gchar  *illegal_chars,
            const gchar  *start,
            gsize         length,
            gboolean      www_form,
            GUriFlags     flags,
            GError      **error)
{
  return uri_decoder (out, illegal_chars, start, length, FALSE, www_form, flags,
                      error) != -1;
}

static gboolean
uri_normalize (gchar       **out,
               const gchar  *start,
               gsize         length,
               GUriFlags     flags,
               GError      **error)
{
  return uri_decoder (out, NULL, start, length, TRUE, FALSE, flags,
                      error) != -1;
}

static gboolean
parse_ip_literal (const gchar  *start,
                  gsize         length,
                  GUriFlags     flags,
                  gchar       **out,
                  GError      **error)
{
  gchar *pct, *zone_id = NULL;
  gchar *addr = NULL;
  gsize addr_length = 0;
  gsize zone_id_length = 0;
  gchar *decoded_zone_id = NULL;

  if (start[length - 1] != ']')
    goto bad_ipv6_literal;

  /* Drop the square brackets */
  addr = g_strndup (start + 1, length - 2);
  addr_length = length - 2;

  /* If there's an IPv6 scope ID, split out the zone. */
  pct = strchr (addr, '%');
  if (pct != NULL)
    {
      *pct = '\0';

      if (addr_length - (pct - addr) >= 4 &&
          *(pct + 1) == '2' && *(pct + 2) == '5')
        {
          zone_id = pct + 3;
          zone_id_length = addr_length - (zone_id - addr);
        }
      else if (flags & G_URI_FLAGS_PARSE_RELAXED &&
               addr_length - (pct - addr) >= 2)
        {
          zone_id = pct + 1;
          zone_id_length = addr_length - (zone_id - addr);
        }
      else
        goto bad_ipv6_literal;

      g_assert (zone_id_length >= 1);
    }

  /* addr must be an IPv6 address */
  if (!g_hostname_is_ip_address (addr) || !strchr (addr, ':'))
    goto bad_ipv6_literal;

  /* Zone ID must be valid. It can contain %-encoded characters. */
  if (zone_id != NULL &&
      !uri_decode (&decoded_zone_id, NULL, zone_id, zone_id_length, FALSE,
                   flags, NULL))
    goto bad_ipv6_literal;

  /* Success */
  if (out != NULL && decoded_zone_id != NULL)
    *out = g_strconcat (addr, "%", decoded_zone_id, NULL);
  else if (out != NULL)
    *out = g_steal_pointer (&addr);

  g_free (addr);
  g_free (decoded_zone_id);

  return TRUE;

bad_ipv6_literal:
  g_free (addr);
  g_free (decoded_zone_id);
  g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
               _("Invalid IPv6 address ‘%.*s’ in URI"),
               (gint)length, start);

  return FALSE;
}

static gboolean
parse_host (const gchar  *start,
            gsize         length,
            GUriFlags     flags,
            gchar       **out,
            GError      **error)
{
  gchar *decoded = NULL, *host;
  gchar *addr = NULL;

  if (*start == '[')
    {
      if (!parse_ip_literal (start, length, flags, &host, error))
        return FALSE;
      goto ok;
    }

  if (g_ascii_isdigit (*start))
    {
      addr = g_strndup (start, length);
      if (g_hostname_is_ip_address (addr))
        {
          host = addr;
          goto ok;
        }
      g_free (addr);
    }

  if (flags & G_URI_FLAGS_NON_DNS)
    {
      if (!uri_normalize (&decoded, start, length, flags,
                          error))
        return FALSE;
      host = g_steal_pointer (&decoded);
      goto ok;
    }

  flags &= ~G_URI_FLAGS_ENCODED;
  if (!uri_decode (&decoded, NULL, start, length, FALSE, flags,
                   error))
    return FALSE;

  /* You're not allowed to %-encode an IP address, so if it wasn't
   * one before, it better not be one now.
   */
  if (g_hostname_is_ip_address (decoded))
    {
      g_free (decoded);
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
                   _("Illegal encoded IP address ‘%.*s’ in URI"),
                   (gint)length, start);
      return FALSE;
    }

  if (g_hostname_is_non_ascii (decoded))
    {
      host = g_hostname_to_ascii (decoded);
      if (host == NULL)
        {
          g_free (decoded);
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
                       _("Illegal internationalized hostname ‘%.*s’ in URI"),
                       (gint) length, start);
          return FALSE;
        }
    }
  else
    {
      host = g_steal_pointer (&decoded);
    }

 ok:
  if (out)
    *out = g_steal_pointer (&host);
  g_free (host);
  g_free (decoded);

  return TRUE;
}

static gboolean
parse_port (const gchar  *start,
            gsize         length,
            gint         *out,
            GError      **error)
{
  gchar *end;
  gulong parsed_port;

  /* strtoul() allows leading + or -, so we have to check this first. */
  if (!g_ascii_isdigit (*start))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
                   _("Could not parse port ‘%.*s’ in URI"),
                   (gint)length, start);
      return FALSE;
    }

  /* We know that *(start + length) is either '\0' or a non-numeric
   * character, so strtoul() won't scan beyond it.
   */
  parsed_port = strtoul (start, &end, 10);
  if (end != start + length)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
                   _("Could not parse port ‘%.*s’ in URI"),
                   (gint)length, start);
      return FALSE;
    }
  else if (parsed_port > 65535)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
                   _("Port ‘%.*s’ in URI is out of range"),
                   (gint)length, start);
      return FALSE;
    }

  if (out)
    *out = parsed_port;
  return TRUE;
}

static gboolean
parse_userinfo (const gchar  *start,
                gsize         length,
                GUriFlags     flags,
                gchar       **user,
                gchar       **password,
                gchar       **auth_params,
                GError      **error)
{
  const gchar *user_end = NULL, *password_end = NULL, *auth_params_end;

  auth_params_end = start + length;
  if (flags & G_URI_FLAGS_HAS_AUTH_PARAMS)
    password_end = memchr (start, ';', auth_params_end - start);
  if (!password_end)
    password_end = auth_params_end;
  if (flags & G_URI_FLAGS_HAS_PASSWORD)
    user_end = memchr (start, ':', password_end - start);
  if (!user_end)
    user_end = password_end;

  if (!uri_normalize (user, start, user_end - start, flags,
                      error))
    return FALSE;

  if (*user_end == ':')
    {
      start = user_end + 1;
      if (!uri_normalize (password, start, password_end - start, flags,
                          error))
        {
          if (user)
            g_clear_pointer (user, g_free);
          return FALSE;
        }
    }
  else if (password)
    *password = NULL;

  if (*password_end == ';')
    {
      start = password_end + 1;
      if (!uri_normalize (auth_params, start, auth_params_end - start, flags,
                          error))
        {
          if (user)
            g_clear_pointer (user, g_free);
          if (password)
            g_clear_pointer (password, g_free);
          return FALSE;
        }
    }
  else if (auth_params)
    *auth_params = NULL;

  return TRUE;
}

static gchar *
uri_cleanup (const gchar *uri_string)
{
  GString *copy;
  const gchar *end;

  /* Skip leading whitespace */
  while (g_ascii_isspace (*uri_string))
    uri_string++;

  /* Ignore trailing whitespace */
  end = uri_string + strlen (uri_string);
  while (end > uri_string && g_ascii_isspace (*(end - 1)))
    end--;

  /* Copy the rest, encoding unencoded spaces and stripping other whitespace */
  copy = g_string_sized_new (end - uri_string);
  while (uri_string < end)
    {
      if (*uri_string == ' ')
        g_string_append (copy, "%20");
      else if (g_ascii_isspace (*uri_string))
        ;
      else
        g_string_append_c (copy, *uri_string);
      uri_string++;
    }

  return g_string_free (copy, FALSE);
}

static gboolean
should_normalize_empty_path (const char *scheme)
{
  const char * const schemes[] = { "https", "http", "wss", "ws" };
  gsize i;
  for (i = 0; i < G_N_ELEMENTS (schemes); ++i)
    {
      if (!strcmp (schemes[i], scheme))
        return TRUE;
    }
  return FALSE;
}

static int
normalize_port (const char *scheme,
                int         port)
{
  const char *default_schemes[3] = { NULL };
  int i;

  switch (port)
    {
    case 21:
      default_schemes[0] = "ftp";
      break;
    case 80:
      default_schemes[0] = "http";
      default_schemes[1] = "ws";
      break;
    case 443:
      default_schemes[0] = "https";
      default_schemes[1] = "wss";
      break;
    default:
      break;
    }

  for (i = 0; default_schemes[i]; ++i)
    {
      if (!strcmp (scheme, default_schemes[i]))
        return -1;
    }

  return port;
}

static int
default_scheme_port (const char *scheme)
{
  if (strcmp (scheme, "http") == 0 || strcmp (scheme, "ws") == 0)
    return 80;

  if (strcmp (scheme, "https") == 0 || strcmp (scheme, "wss") == 0)
    return 443;

  if (strcmp (scheme, "ftp") == 0)
    return 21;

  return -1;
}

static gboolean
flatpak_g_uri_split_internal (const gchar  *uri_string,
                      GUriFlags     flags,
                      gchar       **scheme,
                      gchar       **userinfo,
                      gchar       **user,
                      gchar       **password,
                      gchar       **auth_params,
                      gchar       **host,
                      gint         *port,
                      gchar       **path,
                      gchar       **query,
                      gchar       **fragment,
                      GError      **error)
{
  const gchar *end, *colon, *at, *path_start, *semi, *question;
  const gchar *p, *bracket, *hostend;
  gchar *cleaned_uri_string = NULL;
  gchar *normalized_scheme = NULL;

  if (scheme)
    *scheme = NULL;
  if (userinfo)
    *userinfo = NULL;
  if (user)
    *user = NULL;
  if (password)
    *password = NULL;
  if (auth_params)
    *auth_params = NULL;
  if (host)
    *host = NULL;
  if (port)
    *port = -1;
  if (path)
    *path = NULL;
  if (query)
    *query = NULL;
  if (fragment)
    *fragment = NULL;

  if ((flags & G_URI_FLAGS_PARSE_RELAXED) && strpbrk (uri_string, " \t\n\r"))
    {
      cleaned_uri_string = uri_cleanup (uri_string);
      uri_string = cleaned_uri_string;
    }

  /* Find scheme */
  p = uri_string;
  while (*p && (g_ascii_isalpha (*p) ||
               (p > uri_string && (g_ascii_isdigit (*p) ||
                                   *p == '.' || *p == '+' || *p == '-'))))
    p++;

  if (p > uri_string && *p == ':')
    {
      normalized_scheme = g_ascii_strdown (uri_string, p - uri_string);
      if (scheme)
        *scheme = g_steal_pointer (&normalized_scheme);
      p++;
    }
  else
    {
      if (scheme)
        *scheme = NULL;
      p = uri_string;
    }

  /* Check for authority */
  if (strncmp (p, "//", 2) == 0)
    {
      p += 2;

      path_start = p + strcspn (p, "/?#");
      at = memchr (p, '@', path_start - p);
      if (at)
        {
          if (flags & G_URI_FLAGS_PARSE_RELAXED)
            {
              gchar *next_at;

              /* Any "@"s in the userinfo must be %-encoded, but
               * people get this wrong sometimes. Since "@"s in the
               * hostname are unlikely (and also wrong anyway), assume
               * that if there are extra "@"s, they belong in the
               * userinfo.
               */
              do
                {
                  next_at = memchr (at + 1, '@', path_start - (at + 1));
                  if (next_at)
                    at = next_at;
                }
              while (next_at);
            }

          if (user || password || auth_params ||
              (flags & (G_URI_FLAGS_HAS_PASSWORD|G_URI_FLAGS_HAS_AUTH_PARAMS)))
            {
              if (!parse_userinfo (p, at - p, flags,
                                   user, password, auth_params,
                                   error))
                goto fail;
            }

          if (!uri_normalize (userinfo, p, at - p, flags,
                              error))
            goto fail;

          p = at + 1;
        }

      if (flags & G_URI_FLAGS_PARSE_RELAXED)
        {
          semi = strchr (p, ';');
          if (semi && semi < path_start)
            {
              /* Technically, semicolons are allowed in the "host"
               * production, but no one ever does this, and some
               * schemes mistakenly use semicolon as a delimiter
               * marking the start of the path. We have to check this
               * after checking for userinfo though, because a
               * semicolon before the "@" must be part of the
               * userinfo.
               */
              path_start = semi;
            }
        }

      /* Find host and port. The host may be a bracket-delimited IPv6
       * address, in which case the colon delimiting the port must come
       * (immediately) after the close bracket.
       */
      if (*p == '[')
        {
          bracket = memchr (p, ']', path_start - p);
          if (bracket && *(bracket + 1) == ':')
            colon = bracket + 1;
          else
            colon = NULL;
        }
      else
        colon = memchr (p, ':', path_start - p);

      hostend = colon ? colon : path_start;
      if (!parse_host (p, hostend - p, flags, host, error))
        goto fail;

      if (colon && colon != path_start - 1)
        {
          p = colon + 1;
          if (!parse_port (p, path_start - p, port, error))
            goto fail;
        }

      p = path_start;
    }

  /* Find fragment. */
  end = p + strcspn (p, "#");
  if (*end == '#')
    {
      if (!uri_normalize (fragment, end + 1, strlen (end + 1),
                          flags | (flags & G_URI_FLAGS_ENCODED_FRAGMENT ? G_URI_FLAGS_ENCODED : 0),
                          error))
        goto fail;
    }

  /* Find query */
  question = memchr (p, '?', end - p);
  if (question)
    {
      if (!uri_normalize (query, question + 1, end - (question + 1),
                          flags | (flags & G_URI_FLAGS_ENCODED_QUERY ? G_URI_FLAGS_ENCODED : 0),
                          error))
        goto fail;
      end = question;
    }

  if (!uri_normalize (path, p, end - p,
                      flags | (flags & G_URI_FLAGS_ENCODED_PATH ? G_URI_FLAGS_ENCODED : 0),
                      error))
    goto fail;

  /* Scheme-based normalization */
  if (flags & G_URI_FLAGS_SCHEME_NORMALIZE && ((scheme && *scheme) || normalized_scheme))
    {
      const char *scheme_str = scheme && *scheme ? *scheme : normalized_scheme;

      if (should_normalize_empty_path (scheme_str) && path && !**path)
        {
          g_free (*path);
          *path = g_strdup ("/");
        }

      if (port && *port == -1)
        *port = default_scheme_port (scheme_str);
    }

  g_free (normalized_scheme);
  g_free (cleaned_uri_string);
  return TRUE;

 fail:
  if (scheme)
    g_clear_pointer (scheme, g_free);
  if (userinfo)
    g_clear_pointer (userinfo, g_free);
  if (host)
    g_clear_pointer (host, g_free);
  if (port)
    *port = -1;
  if (path)
    g_clear_pointer (path, g_free);
  if (query)
    g_clear_pointer (query, g_free);
  if (fragment)
    g_clear_pointer (fragment, g_free);

  g_free (normalized_scheme);
  g_free (cleaned_uri_string);
  return FALSE;
}


/* Implements the "Remove Dot Segments" algorithm from section 5.2.4 of
 * RFC 3986.
 *
 * See https://tools.ietf.org/html/rfc3986#section-5.2.4
 */
static void
remove_dot_segments (gchar *path)
{
  /* The output can be written to the same buffer that the input
   * is read from, as the output pointer is only ever increased
   * when the input pointer is increased as well, and the input
   * pointer is never decreased. */
  gchar *input = path;
  gchar *output = path;

  if (!*path)
    return;

  while (*input)
    {
      /*  A.  If the input buffer begins with a prefix of "../" or "./",
       *      then remove that prefix from the input buffer; otherwise,
       */
      if (strncmp (input, "../", 3) == 0)
        input += 3;
      else if (strncmp (input, "./", 2) == 0)
        input += 2;

      /*  B.  if the input buffer begins with a prefix of "/./" or "/.",
       *      where "." is a complete path segment, then replace that
       *      prefix with "/" in the input buffer; otherwise,
       */
      else if (strncmp (input, "/./", 3) == 0)
        input += 2;
      else if (strcmp (input, "/.") == 0)
        input[1] = '\0';

      /*  C.  if the input buffer begins with a prefix of "/../" or "/..",
       *      where ".." is a complete path segment, then replace that
       *      prefix with "/" in the input buffer and remove the last
       *      segment and its preceding "/" (if any) from the output
       *      buffer; otherwise,
       */
      else if (strncmp (input, "/../", 4) == 0)
        {
          input += 3;
          if (output > path)
            {
              do
                {
                  output--;
                }
              while (*output != '/' && output > path);
            }
        }
      else if (strcmp (input, "/..") == 0)
        {
          input[1] = '\0';
          if (output > path)
            {
              do
                 {
                   output--;
                 }
              while (*output != '/' && output > path);
            }
        }

      /*  D.  if the input buffer consists only of "." or "..", then remove
       *      that from the input buffer; otherwise,
       */
      else if (strcmp (input, "..") == 0 || strcmp (input, ".") == 0)
        input[0] = '\0';

      /*  E.  move the first path segment in the input buffer to the end of
       *      the output buffer, including the initial "/" character (if
       *      any) and any subsequent characters up to, but not including,
       *      the next "/" character or the end of the input buffer.
       */
      else
        {
          *output++ = *input++;
          while (*input && *input != '/')
            *output++ = *input++;
        }
    }
  *output = '\0';
}

GUri *
flatpak_g_uri_parse (const gchar  *uri_string,
             GUriFlags     flags,
             GError      **error)
{
  g_return_val_if_fail (uri_string != NULL, NULL);
  g_return_val_if_fail (error == NULL || *error == NULL, NULL);

  return flatpak_g_uri_parse_relative (NULL, uri_string, flags, error);
}

GUri *
flatpak_g_uri_parse_relative (GUri         *base_uri,
                      const gchar  *uri_ref,
                      GUriFlags     flags,
                      GError      **error)
{
  GUri *uri = NULL;

  g_return_val_if_fail (uri_ref != NULL, NULL);
  g_return_val_if_fail (error == NULL || *error == NULL, NULL);
  g_return_val_if_fail (base_uri == NULL || base_uri->scheme != NULL, NULL);

  /* Use GUri struct to construct the return value: there is no guarantee it is
   * actually correct within the function body. */
  uri = g_new0 (GUri, 1);
  uri->ref_count = 1;
  uri->flags = flags;

  if (!flatpak_g_uri_split_internal (uri_ref, flags,
                             &uri->scheme, &uri->userinfo,
                             &uri->user, &uri->password, &uri->auth_params,
                             &uri->host, &uri->port,
                             &uri->path, &uri->query, &uri->fragment,
                             error))
    {
      flatpak_g_uri_unref (uri);
      return NULL;
    }

  if (!uri->scheme && !base_uri)
    {
      g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
                           _("URI is not absolute, and no base URI was provided"));
      flatpak_g_uri_unref (uri);
      return NULL;
    }

  if (base_uri)
    {
      /* This is section 5.2.2 of RFC 3986, except that we're doing
       * it in place in @uri rather than copying from R to T.
       *
       * See https://tools.ietf.org/html/rfc3986#section-5.2.2
       */
      if (uri->scheme)
        remove_dot_segments (uri->path);
      else
        {
          uri->scheme = g_strdup (base_uri->scheme);
          if (uri->host)
            remove_dot_segments (uri->path);
          else
            {
              if (!*uri->path)
                {
                  g_free (uri->path);
                  uri->path = g_strdup (base_uri->path);
                  if (!uri->query)
                    uri->query = g_strdup (base_uri->query);
                }
              else
                {
                  if (*uri->path == '/')
                    remove_dot_segments (uri->path);
                  else
                    {
                      gchar *newpath, *last;

                      last = strrchr (base_uri->path, '/');
                      if (last)
                        {
                          newpath = g_strdup_printf ("%.*s/%s",
                                                     (gint)(last - base_uri->path),
                                                     base_uri->path,
                                                     uri->path);
                        }
                      else
                        newpath = g_strdup_printf ("/%s", uri->path);

                      g_free (uri->path);
                      uri->path = g_steal_pointer (&newpath);

                      remove_dot_segments (uri->path);
                    }
                }

              uri->userinfo = g_strdup (base_uri->userinfo);
              uri->user = g_strdup (base_uri->user);
              uri->password = g_strdup (base_uri->password);
              uri->auth_params = g_strdup (base_uri->auth_params);
              uri->host = g_strdup (base_uri->host);
              uri->port = base_uri->port;
            }
        }

      /* Scheme normalization couldn't have been done earlier
       * as the relative URI may not have had a scheme */
      if (flags & G_URI_FLAGS_SCHEME_NORMALIZE)
        {
          if (should_normalize_empty_path (uri->scheme) && !*uri->path)
            {
              g_free (uri->path);
              uri->path = g_strdup ("/");
            }

          uri->port = normalize_port (uri->scheme, uri->port);
        }
    }
  else
    {
      remove_dot_segments (uri->path);
    }

  return g_steal_pointer (&uri);
}

/* userinfo as a whole can contain sub-delims + ":", but split-out
 * user can't contain ":" or ";", and split-out password can't contain
 * ";".
 */
#define USERINFO_ALLOWED_CHARS G_URI_RESERVED_CHARS_ALLOWED_IN_USERINFO
#define USER_ALLOWED_CHARS "!$&'()*+,="
#define PASSWORD_ALLOWED_CHARS "!$&'()*+,=:"
#define AUTH_PARAMS_ALLOWED_CHARS USERINFO_ALLOWED_CHARS
#define IP_ADDR_ALLOWED_CHARS ":"
#define HOST_ALLOWED_CHARS G_URI_RESERVED_CHARS_SUBCOMPONENT_DELIMITERS
#define PATH_ALLOWED_CHARS G_URI_RESERVED_CHARS_ALLOWED_IN_PATH
#define QUERY_ALLOWED_CHARS G_URI_RESERVED_CHARS_ALLOWED_IN_PATH "?"
#define FRAGMENT_ALLOWED_CHARS G_URI_RESERVED_CHARS_ALLOWED_IN_PATH "?"

static gchar *
flatpak_g_uri_join_internal (GUriFlags    flags,
                     const gchar *scheme,
                     gboolean     userinfo,
                     const gchar *user,
                     const gchar *password,
                     const gchar *auth_params,
                     const gchar *host,
                     gint         port,
                     const gchar *path,
                     const gchar *query,
                     const gchar *fragment)
{
  gboolean encoded = (flags & G_URI_FLAGS_ENCODED);
  GString *str;
  char *normalized_scheme = NULL;

  /* Restrictions on path prefixes. See:
   * https://tools.ietf.org/html/rfc3986#section-3
   */
  g_return_val_if_fail (path != NULL, NULL);
  g_return_val_if_fail (host == NULL || (path[0] == '\0' || path[0] == '/'), NULL);
  g_return_val_if_fail (host != NULL || (path[0] != '/' || path[1] != '/'), NULL);

  str = g_string_new (scheme);
  if (scheme)
    g_string_append_c (str, ':');

  if (flags & G_URI_FLAGS_SCHEME_NORMALIZE && scheme && ((host && port != -1) || path[0] == '\0'))
    normalized_scheme = g_ascii_strdown (scheme, -1);

  if (host)
    {
      g_string_append (str, "//");

      if (user)
        {
          if (encoded)
            g_string_append (str, user);
          else
            {
              if (userinfo)
                g_string_append_uri_escaped (str, user, USERINFO_ALLOWED_CHARS, TRUE);
              else
                /* Encode ':' and ';' regardless of whether we have a
                 * password or auth params, since it may be parsed later
                 * under the assumption that it does.
                 */
                g_string_append_uri_escaped (str, user, USER_ALLOWED_CHARS, TRUE);
            }

          if (password)
            {
              g_string_append_c (str, ':');
              if (encoded)
                g_string_append (str, password);
              else
                g_string_append_uri_escaped (str, password,
                                             PASSWORD_ALLOWED_CHARS, TRUE);
            }

          if (auth_params)
            {
              g_string_append_c (str, ';');
              if (encoded)
                g_string_append (str, auth_params);
              else
                g_string_append_uri_escaped (str, auth_params,
                                             AUTH_PARAMS_ALLOWED_CHARS, TRUE);
            }

          g_string_append_c (str, '@');
        }

      if (strchr (host, ':') && g_hostname_is_ip_address (host))
        {
          g_string_append_c (str, '[');
          if (encoded)
            g_string_append (str, host);
          else
            g_string_append_uri_escaped (str, host, IP_ADDR_ALLOWED_CHARS, TRUE);
          g_string_append_c (str, ']');
        }
      else
        {
          if (encoded)
            g_string_append (str, host);
          else
            g_string_append_uri_escaped (str, host, HOST_ALLOWED_CHARS, TRUE);
        }

      if (port != -1 && (!normalized_scheme || normalize_port (normalized_scheme, port) != -1))
        g_string_append_printf (str, ":%d", port);
    }

  if (path[0] == '\0' && normalized_scheme && should_normalize_empty_path (normalized_scheme))
    g_string_append (str, "/");
  else if (encoded || flags & G_URI_FLAGS_ENCODED_PATH)
    g_string_append (str, path);
  else
    g_string_append_uri_escaped (str, path, PATH_ALLOWED_CHARS, TRUE);

  g_free (normalized_scheme);

  if (query)
    {
      g_string_append_c (str, '?');
      if (encoded || flags & G_URI_FLAGS_ENCODED_QUERY)
        g_string_append (str, query);
      else
        g_string_append_uri_escaped (str, query, QUERY_ALLOWED_CHARS, TRUE);
    }
  if (fragment)
    {
      g_string_append_c (str, '#');
      if (encoded || flags & G_URI_FLAGS_ENCODED_FRAGMENT)
        g_string_append (str, fragment);
      else
        g_string_append_uri_escaped (str, fragment, FRAGMENT_ALLOWED_CHARS, TRUE);
    }

  return g_string_free (str, FALSE);
}

static gchar *
flatpak_g_uri_join (GUriFlags    flags,
            const gchar *scheme,
            const gchar *userinfo,
            const gchar *host,
            gint         port,
            const gchar *path,
            const gchar *query,
            const gchar *fragment)
{
  g_return_val_if_fail (port >= -1 && port <= 65535, NULL);
  g_return_val_if_fail (path != NULL, NULL);

  return flatpak_g_uri_join_internal (flags,
                              scheme,
                              TRUE, userinfo, NULL, NULL,
                              host,
                              port,
                              path,
                              query,
                              fragment);
}

static gchar *
flatpak_g_uri_join_with_user (GUriFlags    flags,
                      const gchar *scheme,
                      const gchar *user,
                      const gchar *password,
                      const gchar *auth_params,
                      const gchar *host,
                      gint         port,
                      const gchar *path,
                      const gchar *query,
                      const gchar *fragment)
{
  g_return_val_if_fail (port >= -1 && port <= 65535, NULL);
  g_return_val_if_fail (path != NULL, NULL);

  return flatpak_g_uri_join_internal (flags,
                              scheme,
                              FALSE, user, password, auth_params,
                              host,
                              port,
                              path,
                              query,
                              fragment);
}

GUri *
flatpak_g_uri_build (GUriFlags    flags,
             const gchar *scheme,
             const gchar *userinfo,
             const gchar *host,
             gint         port,
             const gchar *path,
             const gchar *query,
             const gchar *fragment)
{
  GUri *uri;

  g_return_val_if_fail (scheme != NULL, NULL);
  g_return_val_if_fail (port >= -1 && port <= 65535, NULL);
  g_return_val_if_fail (path != NULL, NULL);

  uri = g_new0 (GUri, 1);
  uri->ref_count = 1;
  uri->flags = flags;
  uri->scheme = g_ascii_strdown (scheme, -1);
  uri->userinfo = g_strdup (userinfo);
  uri->host = g_strdup (host);
  uri->port = port;
  uri->path = g_strdup (path);
  uri->query = g_strdup (query);
  uri->fragment = g_strdup (fragment);

  return g_steal_pointer (&uri);
}

gchar *
flatpak_g_uri_to_string_partial (GUri          *uri,
                         GUriHideFlags  flags)
{
  gboolean hide_user = (flags & G_URI_HIDE_USERINFO);
  gboolean hide_password = (flags & (G_URI_HIDE_USERINFO | G_URI_HIDE_PASSWORD));
  gboolean hide_auth_params = (flags & (G_URI_HIDE_USERINFO | G_URI_HIDE_AUTH_PARAMS));
  gboolean hide_query = (flags & G_URI_HIDE_QUERY);
  gboolean hide_fragment = (flags & G_URI_HIDE_FRAGMENT);

  g_return_val_if_fail (uri != NULL, NULL);

  if (uri->flags & (G_URI_FLAGS_HAS_PASSWORD | G_URI_FLAGS_HAS_AUTH_PARAMS))
    {
      return flatpak_g_uri_join_with_user (uri->flags,
                                   uri->scheme,
                                   hide_user ? NULL : uri->user,
                                   hide_password ? NULL : uri->password,
                                   hide_auth_params ? NULL : uri->auth_params,
                                   uri->host,
                                   uri->port,
                                   uri->path,
                                   hide_query ? NULL : uri->query,
                                   hide_fragment ? NULL : uri->fragment);
    }

  return flatpak_g_uri_join (uri->flags,
                     uri->scheme,
                     hide_user ? NULL : uri->userinfo,
                     uri->host,
                     uri->port,
                     uri->path,
                     hide_query ? NULL : uri->query,
                     hide_fragment ? NULL : uri->fragment);
}

const gchar *
flatpak_g_uri_get_scheme (GUri *uri)
{
  g_return_val_if_fail (uri != NULL, NULL);

  return uri->scheme;
}

const gchar *
flatpak_g_uri_get_userinfo (GUri *uri)
{
  g_return_val_if_fail (uri != NULL, NULL);

  return uri->userinfo;
}

const gchar *
flatpak_g_uri_get_user (GUri *uri)
{
  g_return_val_if_fail (uri != NULL, NULL);

  return uri->user;
}

const gchar *
flatpak_g_uri_get_password (GUri *uri)
{
  g_return_val_if_fail (uri != NULL, NULL);

  return uri->password;
}

const gchar *
flatpak_g_uri_get_auth_params (GUri *uri)
{
  g_return_val_if_fail (uri != NULL, NULL);

  return uri->auth_params;
}

const gchar *
flatpak_g_uri_get_host (GUri *uri)
{
  g_return_val_if_fail (uri != NULL, NULL);

  return uri->host;
}

gint
flatpak_g_uri_get_port (GUri *uri)
{
  g_return_val_if_fail (uri != NULL, -1);

  if (uri->port == -1 && uri->flags & G_URI_FLAGS_SCHEME_NORMALIZE)
    return default_scheme_port (uri->scheme);

  return uri->port;
}

const gchar *
flatpak_g_uri_get_path (GUri *uri)
{
  g_return_val_if_fail (uri != NULL, NULL);

  return uri->path;
}

const gchar *
flatpak_g_uri_get_query (GUri *uri)
{
  g_return_val_if_fail (uri != NULL, NULL);

  return uri->query;
}

const gchar *
flatpak_g_uri_get_fragment (GUri *uri)
{
  g_return_val_if_fail (uri != NULL, NULL);

  return uri->fragment;
}

GUriFlags
flatpak_g_uri_get_flags (GUri *uri)
{
  g_return_val_if_fail (uri != NULL, G_URI_FLAGS_NONE);

  return uri->flags;
}

#endif /* GLIB_CHECK_VERSION (2, 66, 0) */


static void
append_form_encoded (GString *str, const char *in)
{
  const unsigned char *s = (const unsigned char *)in;

  while (*s)
    {
      if (*s == ' ')
        {
          g_string_append_c (str, '+');
          s++;
        }
      else if (!g_ascii_isalnum (*s) && (*s != '-') && (*s != '_')
               && (*s != '.'))
        g_string_append_printf (str, "%%%02X", (int)*s++);
      else
        g_string_append_c (str, *s++);
    }
}

void
flatpak_uri_encode_query_arg (GString *str,
                              const char *key,
                              const char *value)
{
  if (str->len)
    g_string_append_c (str, '&');
  append_form_encoded (str, key);

  g_string_append_c (str, '=');
  append_form_encoded (str, value);
}


/* This is a simplified copy of soup_header_parse_param_list() to avoid a soup dependency */

static const char *
skip_lws (const char *s)
{
  while (g_ascii_isspace (*s))
    s++;
  return s;
}

static const char *
unskip_lws (const char *s, const char *start)
{
  while (s > start && g_ascii_isspace (*(s - 1)))
    s--;
  return s;
}

static const char *
skip_delims (const char *s, char delim)
{
  /* The grammar allows for multiple delimiters */
  while (g_ascii_isspace (*s) || *s == delim)
    s++;
  return s;
}

static const char *
skip_item (const char *s, char delim)
{
  gboolean quoted = FALSE;
  const char *start = s;

  /* A list item ends at the last non-whitespace character
   * before a delimiter which is not inside a quoted-string. Or
   * at the end of the string.
   */

  while (*s)
    {
      if (*s == '"')
        quoted = !quoted;
      else if (quoted)
        {
          if (*s == '\\' && *(s + 1))
            s++;
        }
      else
        {
          if (*s == delim)
            break;
        }
      s++;
    }

  return unskip_lws (s, start);
}

static GSList *
parse_list (const char *header, char delim)
{
  GSList *list = NULL;
  const char *end;

  header = skip_delims (header, delim);
  while (*header)
    {
      end = skip_item (header, delim);
      list = g_slist_prepend (list, g_strndup (header, end - header));
      header = skip_delims (end, delim);
    }

  return g_slist_reverse (list);
}

static void
decode_quoted_string (char *quoted_string)
{
  char *src, *dst;

  src = quoted_string + 1;
  dst = quoted_string;
  while (*src && *src != '"')
    {
      if (*src == '\\' && *(src + 1))
        src++;
      *dst++ = *src++;
    }
  *dst = '\0';
}

GHashTable *
flatpak_parse_http_header_param_list (const char *header)
{
  GHashTable *params;
  GSList *list, *iter;
  char *eq, *name_end, *value;

  params = g_hash_table_new_full (g_str_hash,
                                  g_str_equal,
                                  g_free, g_free);

  list = parse_list (header, ',');
  for (iter = list; iter; iter = iter->next)
    {
      g_autofree char *item = iter->data;

      eq = strchr (item, '=');
      if (eq)
        {
          name_end = (char *)unskip_lws (eq, item);
          if (name_end == item)
            continue;

          *name_end = '\0';

          value = (char *)skip_lws (eq + 1);
          if (*value == '"')
            decode_quoted_string (value);
        }
      else
        value = NULL;

      g_autofree char *key =  g_ascii_strdown (item, -1);
      if (!g_hash_table_contains (params, key))
        g_hash_table_replace (params, g_steal_pointer (&key), g_strdup (value));
    }

  g_slist_free (list);
  return params;
}

/* Do not internationalize */
static const char *const months[] = {
  "Jan", "Feb", "Mar", "Apr", "May", "Jun",
  "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
};

/* Do not internationalize */
static const char *const days[] = {
  "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
};

char *
flatpak_format_http_date (GDateTime *date)
{
  g_autoptr(GDateTime) utcdate = g_date_time_to_utc (date);
  g_autofree char *date_format = NULL;

  /* "Sun, 06 Nov 1994 08:49:37 GMT" */

  date_format = g_strdup_printf ("%s, %%d %s %%Y %%T GMT",
                                 days[g_date_time_get_day_of_week (utcdate) - 1],
                                 months[g_date_time_get_month (utcdate) - 1]);

  return g_date_time_format (utcdate, (const char*)date_format);
}


static inline gboolean
parse_day (int *day, const char **date_string)
{
  char *end;

  *day = strtoul (*date_string, &end, 10);
  if (end == (char *)*date_string)
    return FALSE;

  while (*end == ' ' || *end == '-')
    end++;
  *date_string = end;
  return TRUE;
}

static inline gboolean
parse_month (int *month, const char **date_string)
{
  int i;

  for (i = 0; i < G_N_ELEMENTS (months); i++)
    {
      if (!g_ascii_strncasecmp (*date_string, months[i], 3))
        {
          *month = i + 1;
          *date_string += 3;
          while (**date_string == ' ' || **date_string == '-')
            (*date_string)++;
          return TRUE;
        }
    }

  return FALSE;
}

static inline gboolean
parse_year (int *year, const char **date_string)
{
  char *end;

  *year = strtoul (*date_string, &end, 10);
  if (end == (char *)*date_string)
    return FALSE;

  if (end == (char *)*date_string + 2) {
    if (*year < 70)
      *year += 2000;
    else
      *year += 1900;
  } else if (end == (char *)*date_string + 3)
    *year += 1900;

  while (*end == ' ' || *end == '-')
    end++;
  *date_string = end;

  return TRUE;
}

static inline gboolean
parse_time (int *hour, int *minute, int *second, const char **date_string)
{
  char *p, *end;

  *hour = strtoul (*date_string, &end, 10);
  if (end == (char *)*date_string || *end++ != ':')
    return FALSE;
  p = end;
  *minute = strtoul (p, &end, 10);
  if (end == p || *end++ != ':')
    return FALSE;
  p = end;
  *second = strtoul (p, &end, 10);
  if (end == p)
    return FALSE;
  p = end;

  while (*p == ' ')
    p++;
  *date_string = p;

  return TRUE;
}

static inline GTimeZone *
time_zone_new_offset (gint32 offset)
{
#if GLIB_CHECK_VERSION (2, 58, 0)
  return g_time_zone_new_offset (offset);
#else
  g_autofree char *id = NULL;
  gint hours, minutes;
  gint seconds = offset;
  GTimeZone *tz;
  char sign = '+';

  if (seconds == 0)
    return g_time_zone_new_utc ();

  if (seconds < 0)
    {
      seconds = -seconds;
      sign = '-';
    }

  hours = seconds / 3600;
  seconds = seconds % 3600;
  minutes = seconds / 60;
  seconds = seconds % 60;

  id = g_strdup_printf ("%c%02d:%02d:%02d", sign, hours, minutes, seconds);
  tz = g_time_zone_new (id);
  /* If this assertion fails, we'll log a critical but still return tz,
   * which is documented to be UTC if the time zone could not be parsed */
  g_return_val_if_fail (g_time_zone_get_offset (tz, 0) == offset, tz);
  return tz;
#endif
}

static inline gboolean
parse_timezone (GTimeZone **timezone_out, const char **date_string)
{
  gint32 offset_minutes;
  gboolean utc;

  if (!**date_string)
    {
      utc = FALSE;
      offset_minutes = 0;
    }
  else if (**date_string == '+' || **date_string == '-')
    {
      gulong val;
      int sign = (**date_string == '+') ? 1 : -1;
      val = strtoul (*date_string + 1, (char **)date_string, 10);
      if (**date_string == ':')
        val = 60 * val + strtoul (*date_string + 1, (char **)date_string, 10);
      else
        val =  60 * (val / 100) + (val % 100);
      offset_minutes = sign * val;
      utc = (sign == -1) && !val;
    }
  else if (**date_string == 'Z')
    {
      offset_minutes = 0;
      utc = TRUE;
      (*date_string)++;
    }
  else if (!strcmp (*date_string, "GMT") ||
           !strcmp (*date_string, "UTC"))
    {
      offset_minutes = 0;
      utc = TRUE;
      (*date_string) += 3;
    }
  else if (strchr ("ECMP", **date_string) &&
           ((*date_string)[1] == 'D' || (*date_string)[1] == 'S') &&
           (*date_string)[2] == 'T') {
    offset_minutes = -60 * (5 * strcspn ("ECMP", *date_string));
    if ((*date_string)[1] == 'D')
      offset_minutes += 60;
    utc = FALSE;
  }
  else
    return FALSE;

  if (utc)
    *timezone_out = g_time_zone_new_utc ();
  else
    *timezone_out = time_zone_new_offset (offset_minutes * 60);

  return TRUE;
}

GDateTime *
flatpak_parse_http_time (const char *date_string)
{
  int month, day, year, hour, minute, second;
  g_autoptr(GTimeZone) tz = NULL;

  g_return_val_if_fail (date_string != NULL, NULL);

  while (g_ascii_isspace (*date_string))
    date_string++;

  /* If it starts with a word, it must be a weekday, which we skip */
  if (g_ascii_isalpha (*date_string))
    {
      while (g_ascii_isalpha (*date_string))
        date_string++;
      if (*date_string == ',')
        date_string++;
      while (g_ascii_isspace (*date_string))
        date_string++;
    }

  /* If there's now another word, this must be an asctime-date */
  if (g_ascii_isalpha (*date_string))
    {
      /* (Sun) Nov  6 08:49:37 1994 */
      if (!parse_month (&month, &date_string) ||
          !parse_day (&day, &date_string) ||
          !parse_time (&hour, &minute, &second, &date_string) ||
          !parse_year (&year, &date_string))
        return NULL;

      /* There shouldn't be a timezone, but check anyway */
      parse_timezone (&tz, &date_string);
    }
  else
    {
      /* Non-asctime date, so some variation of
       * (Sun,) 06 Nov 1994 08:49:37 GMT
       */
      if (!parse_day (&day, &date_string) ||
          !parse_month (&month, &date_string) ||
          !parse_year (&year, &date_string) ||
          !parse_time (&hour, &minute, &second, &date_string))
        return NULL;

      /* This time there *should* be a timezone, but we
       * survive if there isn't.
       */
      parse_timezone (&tz, &date_string);
    }

  if (!tz)
    tz = g_time_zone_new_utc ();

  return g_date_time_new (tz, year, month, day, hour, minute, second);
}
