soup-address: fix proxy enumerator implementation
[platform/upstream/libsoup.git] / libsoup / soup-uri.c
index 876b175..26ec24a 100644 (file)
@@ -5,13 +5,12 @@
  * Copyright 1999-2003 Ximian, Inc.
  */
 
-#include <ctype.h>
 #include <string.h>
 #include <stdlib.h>
 
 #include "soup-uri.h"
-#include "soup-form.h"
-#include "soup-misc.h"
+#include "soup.h"
+#include "soup-misc-private.h"
 
 /**
  * SECTION:soup-uri
@@ -68,7 +67,7 @@
  * #SoupURI will have a @path of "/".
  *
  * @query and @fragment are optional for all URI types.
- * soup_form_decode_urlencoded() may be useful for parsing @query.
+ * soup_form_decode() may be useful for parsing @query.
  *
  * Note that @path, @query, and @fragment may contain
  * %<!-- -->-encoded characters. soup_uri_new() calls
  **/
 
 /**
+ * SOUP_URI_IS_VALID:
+ * @uri: a #SoupURI
+ *
+ * Tests whether @uri is a valid #SoupURI; that is, that it is non-%NULL
+ * and its @scheme and @path members are also non-%NULL.
+ *
+ * This macro does not check whether http and https URIs have a non-%NULL
+ * @host member.
+ *
+ * Return value: %TRUE if @uri is valid for use.
+ *
+ * Since: 2.38
+ **/
+
+/**
  * SOUP_URI_VALID_FOR_HTTP:
  * @uri: a #SoupURI
  *
  * Since: 2.24
  **/
 
+/**
+ * SOUP_URI_SCHEME_HTTP:
+ *
+ * "http" as an interned string; you can compare this directly to a
+ * #SoupURI's <literal>scheme</literal> field using
+ * <literal>==</literal>.
+ */
+/**
+ * SOUP_URI_SCHEME_HTTPS:
+ *
+ * "https" as an interned string; you can compare this directly to a
+ * #SoupURI's <literal>scheme</literal> field using
+ * <literal>==</literal>.
+ */
+/**
+ * SOUP_URI_SCHEME_FTP:
+ *
+ * "ftp" as an interned string; you can compare this directly to a
+ * #SoupURI's <literal>scheme</literal> field using
+ * <literal>==</literal>.
+ *
+ * Since: 2.30
+ */
+/**
+ * SOUP_URI_SCHEME_FILE:
+ *
+ * "file" as an interned string; you can compare this directly to a
+ * #SoupURI's <literal>scheme</literal> field using
+ * <literal>==</literal>.
+ *
+ * Since: 2.30
+ */
+/**
+ * SOUP_URI_SCHEME_DATA:
+ *
+ * "data" as an interned string; you can compare this directly to a
+ * #SoupURI's <literal>scheme</literal> field using
+ * <literal>==</literal>.
+ *
+ * Since: 2.30
+ */
+/**
+ * SOUP_URI_SCHEME_RESOURCE:
+ *
+ * "data" as an interned string; you can compare this directly to a
+ * #SoupURI's <literal>scheme</literal> field using
+ * <literal>==</literal>.
+ *
+ * Since: 2.42
+ */
+
 static void append_uri_encoded (GString *str, const char *in, const char *extra_enc_chars);
-static char *uri_decoded_copy (const char *str, int length);
-static char *uri_normalized_copy (const char *str, int length, const char *unescape_extra, gboolean fixup);
+static char *uri_normalized_copy (const char *str, int length, const char *unescape_extra);
 
-const char *_SOUP_URI_SCHEME_HTTP, *_SOUP_URI_SCHEME_HTTPS;
+gpointer _SOUP_URI_SCHEME_HTTP, _SOUP_URI_SCHEME_HTTPS;
+gpointer _SOUP_URI_SCHEME_FTP;
+gpointer _SOUP_URI_SCHEME_FILE, _SOUP_URI_SCHEME_DATA, _SOUP_URI_SCHEME_RESOURCE;
 
 static inline const char *
-soup_uri_get_scheme (const char *scheme, int len)
+soup_uri_parse_scheme (const char *scheme, int len)
 {
        if (len == 4 && !g_ascii_strncasecmp (scheme, "http", len)) {
                return SOUP_URI_SCHEME_HTTP;
        } else if (len == 5 && !g_ascii_strncasecmp (scheme, "https", len)) {
                return SOUP_URI_SCHEME_HTTPS;
+       } else if (len == 8 && !g_ascii_strncasecmp (scheme, "resource", len)) {
+               return SOUP_URI_SCHEME_RESOURCE;
        } else {
                char *lower_scheme;
 
@@ -122,6 +190,8 @@ soup_scheme_default_port (const char *scheme)
                return 80;
        else if (scheme == SOUP_URI_SCHEME_HTTPS)
                return 443;
+       else if (scheme == SOUP_URI_SCHEME_FTP)
+               return 21;
        else
                return 0;
 }
@@ -138,73 +208,97 @@ soup_scheme_default_port (const char *scheme)
 SoupURI *
 soup_uri_new_with_base (SoupURI *base, const char *uri_string)
 {
-       SoupURI *uri;
+       SoupURI *uri, fixed_base;
        const char *end, *hash, *colon, *at, *path, *question;
        const char *p, *hostend;
        gboolean remove_dot_segments = TRUE;
+       int len;
 
-       uri = g_slice_new0 (SoupURI);
+       g_return_val_if_fail (uri_string != NULL, NULL);
+
+       /* Allow a %NULL path in @base, for compatibility */
+       if (base && base->scheme && !base->path) {
+               g_warn_if_fail (SOUP_URI_IS_VALID (base));
+
+               memcpy (&fixed_base, base, sizeof (SoupURI));
+               fixed_base.path = "";
+               base = &fixed_base;
+       }
+
+       g_return_val_if_fail (base == NULL || SOUP_URI_IS_VALID (base), NULL);
 
-       /* See RFC 3986 for details. IF YOU CHANGE ANYTHING IN THIS
-        * FUNCTION, RUN tests/uri-parsing AFTERWARDS.
+       /* First some cleanup steps (which are supposed to all be no-ops,
+        * but...). Skip initial whitespace, strip out internal tabs and
+        * line breaks, and ignore trailing whitespace.
         */
+       while (g_ascii_isspace (*uri_string))
+               uri_string++;
 
-       /* Find fragment. */
-       end = hash = strchr (uri_string, '#');
-       if (hash && hash[1]) {
-               uri->fragment = uri_normalized_copy (hash + 1, strlen (hash + 1),
-                                                    NULL, FALSE);
-               if (!uri->fragment) {
-                       soup_uri_free (uri);
-                       return NULL;
+       len = strcspn (uri_string, "\t\n\r");
+       if (uri_string[len]) {
+               char *clean = g_malloc (strlen (uri_string) + 1), *d;
+               const char *s;
+
+               for (s = uri_string, d = clean; *s; s++) {
+                       if (*s != '\t' && *s != '\n' && *s != '\r')
+                               *d++ = *s;
                }
-       } else
-               end = uri_string + strlen (uri_string);
+               *d = '\0';
+
+               uri = soup_uri_new_with_base (base, clean);
+               g_free (clean);
+               return uri;
+       }
+       end = uri_string + len;
+       while (end > uri_string && g_ascii_isspace (end[-1]))
+               end--;
+
+       uri = g_slice_new0 (SoupURI);
+
+       /* Find fragment. */
+       hash = strchr (uri_string, '#');
+       if (hash) {
+               uri->fragment = uri_normalized_copy (hash + 1, end - hash + 1,
+                                                    NULL);
+               end = hash;
+       }
 
        /* Find scheme: initial [a-z+.-]* substring until ":" */
        p = uri_string;
-       while (p < end && (g_ascii_isalnum (*p) ||
+       while (p < end && (g_ascii_isalpha (*p) ||
                           *p == '.' || *p == '+' || *p == '-'))
                p++;
 
        if (p > uri_string && *p == ':') {
-               uri->scheme = soup_uri_get_scheme (uri_string, p - uri_string);
-               if (!uri->scheme) {
-                       soup_uri_free (uri);
-                       return NULL;
-               }
+               uri->scheme = soup_uri_parse_scheme (uri_string, p - uri_string);
                uri_string = p + 1;
        }
 
-       if (!*uri_string && !base)
+       if (uri_string == end && !base && !uri->fragment) {
+               uri->path = g_strdup ("");
                return uri;
+        }
 
        /* Check for authority */
        if (strncmp (uri_string, "//", 2) == 0) {
                uri_string += 2;
 
                path = uri_string + strcspn (uri_string, "/?#");
+               if (path > end)
+                       path = end;
                at = strchr (uri_string, '@');
                if (at && at < path) {
                        colon = strchr (uri_string, ':');
                        if (colon && colon < at) {
                                uri->password = uri_decoded_copy (colon + 1,
-                                                                 at - colon - 1);
-                               if (!uri->password) {
-                                       soup_uri_free (uri);
-                                       return NULL;
-                               }
+                                                                 at - colon - 1, NULL);
                        } else {
                                uri->password = NULL;
                                colon = at;
                        }
 
                        uri->user = uri_decoded_copy (uri_string,
-                                                     colon - uri_string);
-                       if (!uri->user) {
-                               soup_uri_free (uri);
-                               return NULL;
-                       }
+                                                     colon - uri_string, NULL);
                        uri_string = at + 1;
                } else
                        uri->user = uri->password = NULL;
@@ -226,11 +320,7 @@ soup_uri_new_with_base (SoupURI *base, const char *uri_string)
                        hostend = colon ? colon : path;
                }
 
-               uri->host = uri_decoded_copy (uri_string, hostend - uri_string);
-               if (!uri->host) {
-                       soup_uri_free (uri);
-                       return NULL;
-               }
+               uri->host = uri_decoded_copy (uri_string, hostend - uri_string, NULL);
 
                if (colon && colon != path - 1) {
                        char *portend;
@@ -247,28 +337,18 @@ soup_uri_new_with_base (SoupURI *base, const char *uri_string)
        /* Find query */
        question = memchr (uri_string, '?', end - uri_string);
        if (question) {
-               if (question[1]) {
-                       uri->query = uri_normalized_copy (question + 1,
-                                                         end - (question + 1),
-                                                         NULL, TRUE);
-                       if (!uri->query) {
-                               soup_uri_free (uri);
-                               return NULL;
-                       }
-               }
+               uri->query = uri_normalized_copy (question + 1,
+                                                 end - (question + 1),
+                                                 NULL);
                end = question;
        }
 
        if (end != uri_string) {
                uri->path = uri_normalized_copy (uri_string, end - uri_string,
-                                                NULL, TRUE);
-               if (!uri->path) {
-                       soup_uri_free (uri);
-                       return NULL;
-               }
+                                                NULL);
        }
 
-       /* Apply base URI. Again, this is spelled out in RFC 3986. */
+       /* Apply base URI. This is spelled out in RFC 3986. */
        if (base && !uri->scheme && uri->host)
                uri->scheme = base->scheme;
        else if (base && !uri->scheme) {
@@ -288,8 +368,8 @@ soup_uri_new_with_base (SoupURI *base, const char *uri_string)
 
                        last = strrchr (base->path, '/');
                        if (last) {
-                               newpath = g_strdup_printf ("%.*s/%s",
-                                                          (int)(last - base->path),
+                               newpath = g_strdup_printf ("%.*s%s",
+                                                          (int)(last + 1 - base->path),
                                                           base->path,
                                                           uri->path);
                        } else
@@ -301,7 +381,7 @@ soup_uri_new_with_base (SoupURI *base, const char *uri_string)
        }
 
        if (remove_dot_segments && uri->path && *uri->path) {
-               char *p = uri->path, *q;
+               char *p, *q;
 
                /* Remove "./" where "." is a complete segment. */
                for (p = uri->path + 1; *p; ) {
@@ -352,12 +432,19 @@ soup_uri_new_with_base (SoupURI *base, const char *uri_string)
        /* HTTP-specific stuff */
        if (uri->scheme == SOUP_URI_SCHEME_HTTP ||
            uri->scheme == SOUP_URI_SCHEME_HTTPS) {
+               if (!uri->path)
+                       uri->path = g_strdup ("/");
                if (!SOUP_URI_VALID_FOR_HTTP (uri)) {
                        soup_uri_free (uri);
                        return NULL;
                }
-               if (!uri->path)
-                       uri->path = g_strdup ("/");
+       }
+
+       if (uri->scheme == SOUP_URI_SCHEME_FTP) {
+               if (!uri->host) {
+                       soup_uri_free (uri);
+                       return NULL;
+               }
        }
 
        if (!uri->port)
@@ -370,7 +457,7 @@ soup_uri_new_with_base (SoupURI *base, const char *uri_string)
 
 /**
  * soup_uri_new:
- * @uri_string: a URI
+ * @uri_string: (allow-none): a URI
  *
  * Parses an absolute URI.
  *
@@ -379,7 +466,8 @@ soup_uri_new_with_base (SoupURI *base, const char *uri_string)
  * call at least soup_uri_set_scheme() and soup_uri_set_path(), since
  * those fields are required.)
  *
- * Return value: a #SoupURI, or %NULL.
+ * Return value: a #SoupURI, or %NULL if the given string was found to be
+ *  invalid.
  **/
 SoupURI *
 soup_uri_new (const char *uri_string)
@@ -392,7 +480,7 @@ soup_uri_new (const char *uri_string)
        uri = soup_uri_new_with_base (NULL, uri_string);
        if (!uri)
                return NULL;
-       if (!uri->scheme) {
+       if (!SOUP_URI_IS_VALID (uri)) {
                soup_uri_free (uri);
                return NULL;
        }
@@ -401,32 +489,17 @@ soup_uri_new (const char *uri_string)
 }
 
 
-/**
- * soup_uri_to_string:
- * @uri: a #SoupURI
- * @just_path_and_query: if %TRUE, output just the path and query portions
- *
- * Returns a string representing @uri.
- *
- * If @just_path_and_query is %TRUE, this concatenates the path and query
- * together. That is, it constructs the string that would be needed in
- * the Request-Line of an HTTP request for @uri.
- *
- * Return value: a string representing @uri, which the caller must free.
- **/
 char *
-soup_uri_to_string (SoupURI *uri, gboolean just_path_and_query)
+soup_uri_to_string_internal (SoupURI *uri, gboolean just_path_and_query,
+                            gboolean force_port)
 {
        GString *str;
        char *return_result;
 
        g_return_val_if_fail (uri != NULL, NULL);
+       g_warn_if_fail (SOUP_URI_IS_VALID (uri));
 
-       /* IF YOU CHANGE ANYTHING IN THIS FUNCTION, RUN
-        * tests/uri-parsing AFTERWARD.
-        */
-
-       str = g_string_sized_new (20);
+       str = g_string_sized_new (40);
 
        if (uri->scheme && !just_path_and_query)
                g_string_append_printf (str, "%s:", uri->scheme);
@@ -442,14 +515,20 @@ soup_uri_to_string (SoupURI *uri, gboolean just_path_and_query)
                        g_string_append_c (str, ']');
                } else
                        append_uri_encoded (str, uri->host, ":/");
-               if (uri->port && uri->port != soup_scheme_default_port (uri->scheme))
-                       g_string_append_printf (str, ":%d", uri->port);
+               if (uri->port && (force_port || uri->port != soup_scheme_default_port (uri->scheme)))
+                       g_string_append_printf (str, ":%u", uri->port);
                if (!uri->path && (uri->query || uri->fragment))
                        g_string_append_c (str, '/');
+               else if ((!uri->path || !*uri->path) &&
+                        (uri->scheme == SOUP_URI_SCHEME_HTTP ||
+                         uri->scheme == SOUP_URI_SCHEME_HTTPS))
+                       g_string_append_c (str, '/');
        }
 
        if (uri->path && *uri->path)
                g_string_append (str, uri->path);
+       else if (just_path_and_query)
+               g_string_append_c (str, '/');
 
        if (uri->query) {
                g_string_append_c (str, '?');
@@ -467,6 +546,25 @@ soup_uri_to_string (SoupURI *uri, gboolean just_path_and_query)
 }
 
 /**
+ * soup_uri_to_string:
+ * @uri: a #SoupURI
+ * @just_path_and_query: if %TRUE, output just the path and query portions
+ *
+ * Returns a string representing @uri.
+ *
+ * If @just_path_and_query is %TRUE, this concatenates the path and query
+ * together. That is, it constructs the string that would be needed in
+ * the Request-Line of an HTTP request for @uri.
+ *
+ * Return value: a string representing @uri, which the caller must free.
+ **/
+char *
+soup_uri_to_string (SoupURI *uri, gboolean just_path_and_query)
+{
+       return soup_uri_to_string_internal (uri, just_path_and_query, FALSE);
+}
+
+/**
  * soup_uri_copy:
  * @uri: a #SoupURI
  *
@@ -480,6 +578,7 @@ soup_uri_copy (SoupURI *uri)
        SoupURI *dup;
 
        g_return_val_if_fail (uri != NULL, NULL);
+       g_warn_if_fail (SOUP_URI_IS_VALID (uri));
 
        dup = g_slice_new0 (SoupURI);
        dup->scheme   = uri->scheme;
@@ -516,6 +615,11 @@ parts_equal (const char *one, const char *two, gboolean insensitive)
 gboolean 
 soup_uri_equal (SoupURI *uri1, SoupURI *uri2)
 {
+       g_return_val_if_fail (uri1 != NULL, FALSE);
+       g_return_val_if_fail (uri2 != NULL, FALSE);
+       g_warn_if_fail (SOUP_URI_IS_VALID (uri1));
+       g_warn_if_fail (SOUP_URI_IS_VALID (uri2));
+
        if (uri1->scheme != uri2->scheme                         ||
            uri1->port   != uri2->port                           ||
            !parts_equal (uri1->user, uri2->user, FALSE)         ||
@@ -550,37 +654,14 @@ soup_uri_free (SoupURI *uri)
        g_slice_free (SoupURI, uri);
 }
 
-/* From RFC 3986 */
-#define SOUP_URI_UNRESERVED  0
-#define SOUP_URI_PCT_ENCODED 1
-#define SOUP_URI_GEN_DELIMS  2
-#define SOUP_URI_SUB_DELIMS  4
-static const char uri_encoded_char[] = {
-       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,  /* 0x00 - 0x0f */
-       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,  /* 0x10 - 0x1f */
-       1, 4, 1, 2, 4, 1, 4, 4, 4, 4, 4, 4, 4, 0, 0, 2,  /*  !"#$%&'()*+,-./ */
-       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 4, 1, 4, 1, 2,  /* 0123456789:;<=>? */
-       2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  /* @ABCDEFGHIJKLMNO */
-       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 1, 2, 1, 0,  /* PQRSTUVWXYZ[\]^_ */
-       1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  /* `abcdefghijklmno */
-       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1,  /* pqrstuvwxyz{|}~  */
-       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
-       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
-       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
-       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
-       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
-       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
-       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
-       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
-};
-
 static void
 append_uri_encoded (GString *str, const char *in, const char *extra_enc_chars)
 {
        const unsigned char *s = (const unsigned char *)in;
 
        while (*s) {
-               if ((uri_encoded_char[*s] & (SOUP_URI_PCT_ENCODED | SOUP_URI_GEN_DELIMS)) ||
+               if (soup_char_is_uri_percent_encoded (*s) ||
+                   soup_char_is_uri_gen_delims (*s) ||
                    (extra_enc_chars && strchr (extra_enc_chars, *s)))
                        g_string_append_printf (str, "%%%02X", (int)*s++);
                else
@@ -591,7 +672,8 @@ append_uri_encoded (GString *str, const char *in, const char *extra_enc_chars)
 /**
  * soup_uri_encode:
  * @part: a URI part
- * @escape_extra: additional reserved characters to escape (or %NULL)
+ * @escape_extra: (allow-none): additional reserved characters to
+ * escape (or %NULL)
  *
  * This %<!-- -->-encodes the given URI part and returns the escaped
  * version in allocated memory, which the caller must free when it is
@@ -605,6 +687,8 @@ soup_uri_encode (const char *part, const char *escape_extra)
        GString *str;
        char *encoded;
 
+       g_return_val_if_fail (part != NULL, NULL);
+
        str = g_string_new (NULL);
        append_uri_encoded (str, part, escape_extra);
        encoded = str->str;
@@ -616,19 +700,21 @@ soup_uri_encode (const char *part, const char *escape_extra)
 #define XDIGIT(c) ((c) <= '9' ? (c) - '0' : ((c) & 0x4F) - 'A' + 10)
 #define HEXCHAR(s) ((XDIGIT (s[1]) << 4) + XDIGIT (s[2]))
 
-static char *
-uri_decoded_copy (const char *part, int length)
+char *
+uri_decoded_copy (const char *part, int length, int *decoded_length)
 {
        unsigned char *s, *d;
        char *decoded = g_strndup (part, length);
 
+       g_return_val_if_fail (part != NULL, NULL);
+
        s = d = (unsigned char *)decoded;
        do {
                if (*s == '%') {
                        if (!g_ascii_isxdigit (s[1]) ||
                            !g_ascii_isxdigit (s[2])) {
-                               g_free (decoded);
-                               return NULL;
+                               *d++ = *s;
+                               continue;
                        }
                        *d++ = HEXCHAR (s);
                        s += 2;
@@ -636,6 +722,9 @@ uri_decoded_copy (const char *part, int length)
                        *d++ = *s;
        } while (*s++);
 
+       if (decoded_length)
+               *decoded_length = d - (unsigned char *)decoded - 1;
+
        return decoded;
 }
 
@@ -645,61 +734,78 @@ uri_decoded_copy (const char *part, int length)
  *
  * Fully %<!-- -->-decodes @part.
  *
- * Return value: the decoded URI part, or %NULL if an invalid percent
- * code was encountered.
+ * In the past, this would return %NULL if @part contained invalid
+ * percent-encoding, but now it just ignores the problem (as
+ * soup_uri_new() already did).
+ *
+ * Return value: the decoded URI part.
  */
 char *
 soup_uri_decode (const char *part)
 {
-       return uri_decoded_copy (part, strlen (part));
+       g_return_val_if_fail (part != NULL, NULL);
+
+       return uri_decoded_copy (part, strlen (part), NULL);
 }
 
 static char *
 uri_normalized_copy (const char *part, int length,
-                    const char *unescape_extra, gboolean fixup)
+                    const char *unescape_extra)
 {
        unsigned char *s, *d, c;
        char *normalized = g_strndup (part, length);
        gboolean need_fixup = FALSE;
 
+       if (!unescape_extra)
+               unescape_extra = "";
+
        s = d = (unsigned char *)normalized;
-       do {
+       while (*s) {
                if (*s == '%') {
                        if (!g_ascii_isxdigit (s[1]) ||
                            !g_ascii_isxdigit (s[2])) {
-                               g_free (normalized);
-                               return NULL;
+                               *d++ = *s++;
+                               continue;
                        }
 
                        c = HEXCHAR (s);
-                       if (uri_encoded_char[c] == SOUP_URI_UNRESERVED ||
-                           (unescape_extra && strchr (unescape_extra, c))) {
+                       if (soup_char_is_uri_unreserved (c) ||
+                           (c && strchr (unescape_extra, c))) {
                                *d++ = c;
-                               s += 2;
+                               s += 3;
                        } else {
+                               /* We leave it unchanged. We used to uppercase percent-encoded
+                                * triplets but we do not do it any more as RFC3986 Section 6.2.2.1
+                                * says that they only SHOULD be case normalized.
+                                */
+                               *d++ = *s++;
+                               *d++ = *s++;
                                *d++ = *s++;
-                               *d++ = g_ascii_toupper (*s++);
-                               *d++ = g_ascii_toupper (*s);
                        }
                } else {
-                       if (*s == ' ')
+                       if (!g_ascii_isgraph (*s) &&
+                           !strchr (unescape_extra, *s))
                                need_fixup = TRUE;
-                       *d++ = *s;
+                       *d++ = *s++;
                }
-       } while (*s++);
+       }
+       *d = '\0';
+
+       if (need_fixup) {
+               GString *fixed;
 
-       if (fixup && need_fixup) {
-               char *tmp, *sp;
-               /* This code is lame, but so are people who put
-                * unencoded spaces in URLs!
-                */
-               while ((sp = strchr (normalized, ' '))) {
-                       tmp = g_strdup_printf ("%.*s%%20%s",
-                                              (int)(sp - normalized),
-                                              normalized, sp + 1);
-                       g_free (normalized);
-                       normalized = tmp;
-               };
+               fixed = g_string_new (NULL);
+               s = (guchar *)normalized;
+               while (*s) {
+                       if (g_ascii_isgraph (*s) ||
+                           strchr (unescape_extra, *s))
+                               g_string_append_c (fixed, *s);
+                       else
+                               g_string_append_printf (fixed, "%%%02X", (int)*s);
+                       s++;
+               }
+               g_free (normalized);
+               normalized = g_string_free (fixed, FALSE);
        }
 
        return normalized;
@@ -723,13 +829,18 @@ uri_normalized_copy (const char *part, int length,
  * be changed, because it might mean something different to the
  * server.
  *
- * Return value: the normalized URI part, or %NULL if an invalid percent
- * code was encountered.
+ * In the past, this would return %NULL if @part contained invalid
+ * percent-encoding, but now it just ignores the problem (as
+ * soup_uri_new() already did).
+ *
+ * Return value: the normalized URI part
  */
 char *
 soup_uri_normalize (const char *part, const char *unescape_extra)
 {
-       return uri_normalized_copy (part, strlen (part), unescape_extra, FALSE);
+       g_return_val_if_fail (part != NULL, NULL);
+
+       return uri_normalized_copy (part, strlen (part), unescape_extra);
 }
 
 
@@ -738,7 +849,7 @@ soup_uri_normalize (const char *part, const char *unescape_extra)
  * @uri: a #SoupURI
  *
  * Tests if @uri uses the default port for its scheme. (Eg, 80 for
- * http.) (This only works for http and https; libsoup does not know
+ * http.) (This only works for http, https and ftp; libsoup does not know
  * the default ports of other protocols.)
  *
  * Return value: %TRUE or %FALSE
@@ -746,8 +857,8 @@ soup_uri_normalize (const char *part, const char *unescape_extra)
 gboolean
 soup_uri_uses_default_port (SoupURI *uri)
 {
-       g_return_val_if_fail (uri->scheme == SOUP_URI_SCHEME_HTTP ||
-                             uri->scheme == SOUP_URI_SCHEME_HTTPS, FALSE);
+       g_return_val_if_fail (uri != NULL, FALSE);
+       g_warn_if_fail (SOUP_URI_IS_VALID (uri));
 
        return uri->port == soup_scheme_default_port (uri->scheme);
 }
@@ -767,6 +878,33 @@ soup_uri_uses_default_port (SoupURI *uri)
  **/
 
 /**
+ * SOUP_URI_SCHEME_RESOURCE:
+ *
+ * "resource" as an interned string. This can be compared directly
+ * against the value of a #SoupURI's <structfield>scheme</structfield>
+ *
+ * Since: 2.42
+ **/
+
+/**
+ * soup_uri_get_scheme:
+ * @uri: a #SoupURI
+ *
+ * Gets @uri's scheme.
+ *
+ * Return value: @uri's scheme.
+ *
+ * Since: 2.32
+ **/
+const char *
+soup_uri_get_scheme (SoupURI *uri)
+{
+       g_return_val_if_fail (uri != NULL, NULL);
+
+       return uri->scheme;
+}
+
+/**
  * soup_uri_set_scheme:
  * @uri: a #SoupURI
  * @scheme: the URI scheme
@@ -777,57 +915,140 @@ soup_uri_uses_default_port (SoupURI *uri)
 void
 soup_uri_set_scheme (SoupURI *uri, const char *scheme)
 {
-       uri->scheme = soup_uri_get_scheme (scheme, strlen (scheme));
+       g_return_if_fail (uri != NULL);
+       g_return_if_fail (scheme != NULL);
+
+       uri->scheme = soup_uri_parse_scheme (scheme, strlen (scheme));
        uri->port = soup_scheme_default_port (uri->scheme);
 }
 
 /**
+ * soup_uri_get_user:
+ * @uri: a #SoupURI
+ *
+ * Gets @uri's user.
+ *
+ * Return value: @uri's user.
+ *
+ * Since: 2.32
+ **/
+const char *
+soup_uri_get_user (SoupURI *uri)
+{
+       g_return_val_if_fail (uri != NULL, NULL);
+
+       return uri->user;
+}
+
+/**
  * soup_uri_set_user:
  * @uri: a #SoupURI
- * @user: the username, or %NULL
+ * @user: (allow-none): the username, or %NULL
  *
  * Sets @uri's user to @user.
  **/
 void
 soup_uri_set_user (SoupURI *uri, const char *user)
 {
+       g_return_if_fail (uri != NULL);
+
        g_free (uri->user);
        uri->user = g_strdup (user);
 }
 
 /**
+ * soup_uri_get_password:
+ * @uri: a #SoupURI
+ *
+ * Gets @uri's password.
+ *
+ * Return value: @uri's password.
+ *
+ * Since: 2.32
+ **/
+const char *
+soup_uri_get_password (SoupURI *uri)
+{
+       g_return_val_if_fail (uri != NULL, NULL);
+
+       return uri->password;
+}
+
+/**
  * soup_uri_set_password:
  * @uri: a #SoupURI
- * @password: the password, or %NULL
+ * @password: (allow-none): the password, or %NULL
  *
  * Sets @uri's password to @password.
  **/
 void
 soup_uri_set_password (SoupURI *uri, const char *password)
 {
+       g_return_if_fail (uri != NULL);
+
        g_free (uri->password);
        uri->password = g_strdup (password);
 }
 
 /**
+ * soup_uri_get_host:
+ * @uri: a #SoupURI
+ *
+ * Gets @uri's host.
+ *
+ * Return value: @uri's host.
+ *
+ * Since: 2.32
+ **/
+const char *
+soup_uri_get_host (SoupURI *uri)
+{
+       g_return_val_if_fail (uri != NULL, NULL);
+
+       return uri->host;
+}
+
+/**
  * soup_uri_set_host:
  * @uri: a #SoupURI
- * @host: the hostname or IP address, or %NULL
+ * @host: (allow-none): the hostname or IP address, or %NULL
  *
  * Sets @uri's host to @host.
  *
  * If @host is an IPv6 IP address, it should not include the brackets
  * required by the URI syntax; they will be added automatically when
  * converting @uri to a string.
+ *
+ * http and https URIs should not have a %NULL @host.
  **/
 void
 soup_uri_set_host (SoupURI *uri, const char *host)
 {
+       g_return_if_fail (uri != NULL);
+
        g_free (uri->host);
        uri->host = g_strdup (host);
 }
 
 /**
+ * soup_uri_get_port:
+ * @uri: a #SoupURI
+ *
+ * Gets @uri's port.
+ *
+ * Return value: @uri's port.
+ *
+ * Since: 2.32
+ **/
+guint
+soup_uri_get_port (SoupURI *uri)
+{
+       g_return_val_if_fail (uri != NULL, 0);
+
+       return uri->port;
+}
+
+/**
  * soup_uri_set_port:
  * @uri: a #SoupURI
  * @port: the port, or 0
@@ -838,33 +1059,81 @@ soup_uri_set_host (SoupURI *uri, const char *host)
 void
 soup_uri_set_port (SoupURI *uri, guint port)
 {
+       g_return_if_fail (uri != NULL);
+
        uri->port = port;
 }
 
 /**
+ * soup_uri_get_path:
+ * @uri: a #SoupURI
+ *
+ * Gets @uri's path.
+ *
+ * Return value: @uri's path.
+ *
+ * Since: 2.32
+ **/
+const char *
+soup_uri_get_path (SoupURI *uri)
+{
+       g_return_val_if_fail (uri != NULL, NULL);
+
+       return uri->path;
+}
+
+/**
  * soup_uri_set_path:
  * @uri: a #SoupURI
- * @path: the path
+ * @path: the non-%NULL path
  *
  * Sets @uri's path to @path.
  **/
 void
 soup_uri_set_path (SoupURI *uri, const char *path)
 {
+       g_return_if_fail (uri != NULL);
+
+       /* We allow a NULL path for compatibility, but warn about it. */
+       if (!path) {
+               g_warn_if_fail (path != NULL);
+               path = "";
+       }
+
        g_free (uri->path);
        uri->path = g_strdup (path);
 }
 
 /**
+ * soup_uri_get_query:
+ * @uri: a #SoupURI
+ *
+ * Gets @uri's query.
+ *
+ * Return value: @uri's query.
+ *
+ * Since: 2.32
+ **/
+const char *
+soup_uri_get_query (SoupURI *uri)
+{
+       g_return_val_if_fail (uri != NULL, NULL);
+
+       return uri->query;
+}
+
+/**
  * soup_uri_set_query:
  * @uri: a #SoupURI
- * @query: the query
+ * @query: (allow-none): the query
  *
  * Sets @uri's query to @query.
  **/
 void
 soup_uri_set_query (SoupURI *uri, const char *query)
 {
+       g_return_if_fail (uri != NULL);
+
        g_free (uri->query);
        uri->query = g_strdup (query);
 }
@@ -872,7 +1141,8 @@ soup_uri_set_query (SoupURI *uri, const char *query)
 /**
  * soup_uri_set_query_from_form:
  * @uri: a #SoupURI
- * @form: a #GHashTable containing HTML form information
+ * @form: (element-type utf8 utf8): a #GHashTable containing HTML form
+ * information
  *
  * Sets @uri's query to the result of encoding @form according to the
  * HTML form rules. See soup_form_encode_hash() for more information.
@@ -880,8 +1150,10 @@ soup_uri_set_query (SoupURI *uri, const char *query)
 void
 soup_uri_set_query_from_form (SoupURI *uri, GHashTable *form)
 {
+       g_return_if_fail (uri != NULL);
+
        g_free (uri->query);
-       uri->query = soup_form_encode_urlencoded (form);
+       uri->query = soup_form_encode_hash (form);
 }
 
 /**
@@ -902,6 +1174,8 @@ soup_uri_set_query_from_fields (SoupURI    *uri,
 {
        va_list args;
 
+       g_return_if_fail (uri != NULL);
+
        g_free (uri->query);
        va_start (args, first_field);
        uri->query = soup_form_encode_valist (first_field, args);
@@ -909,28 +1183,48 @@ soup_uri_set_query_from_fields (SoupURI    *uri,
 }
 
 /**
+ * soup_uri_get_fragment:
+ * @uri: a #SoupURI
+ *
+ * Gets @uri's fragment.
+ *
+ * Return value: @uri's fragment.
+ *
+ * Since: 2.32
+ **/
+const char *
+soup_uri_get_fragment (SoupURI *uri)
+{
+       g_return_val_if_fail (uri != NULL, NULL);
+
+       return uri->fragment;
+}
+
+/**
  * soup_uri_set_fragment:
  * @uri: a #SoupURI
- * @fragment: the fragment
+ * @fragment: (allow-none): the fragment
  *
  * Sets @uri's fragment to @fragment.
  **/
 void
 soup_uri_set_fragment (SoupURI *uri, const char *fragment)
 {
+       g_return_if_fail (uri != NULL);
+
        g_free (uri->fragment);
        uri->fragment = g_strdup (fragment);
 }
 
 /**
  * soup_uri_copy_host:
- * @uri: a #SoupUri
+ * @uri: a #SoupURI
  *
  * Makes a copy of @uri, considering only the protocol, host, and port
  *
- * Return value: the new #SoupUri
+ * Return value: the new #SoupURI
  *
- * Since: 2.26.3
+ * Since: 2.28
  **/
 SoupURI *
 soup_uri_copy_host (SoupURI *uri)
@@ -938,27 +1232,26 @@ soup_uri_copy_host (SoupURI *uri)
        SoupURI *dup;
 
        g_return_val_if_fail (uri != NULL, NULL);
+       g_warn_if_fail (SOUP_URI_IS_VALID (uri));
 
        dup = soup_uri_new (NULL);
        dup->scheme = uri->scheme;
        dup->host   = g_strdup (uri->host);
        dup->port   = uri->port;
-       if (dup->scheme == SOUP_URI_SCHEME_HTTP ||
-           dup->scheme == SOUP_URI_SCHEME_HTTPS)
-               dup->path = g_strdup ("");
+       dup->path   = g_strdup ("");
 
        return dup;
 }
 
 /**
  * soup_uri_host_hash:
- * @key: a #SoupURI
+ * @key: (type Soup.URI): a #SoupURI with a non-%NULL @host member
  *
  * Hashes @key, considering only the scheme, host, and port.
  *
  * Return value: a hash
  *
- * Since: 2.26.3
+ * Since: 2.28
  **/
 guint
 soup_uri_host_hash (gconstpointer key)
@@ -966,6 +1259,7 @@ soup_uri_host_hash (gconstpointer key)
        const SoupURI *uri = key;
 
        g_return_val_if_fail (uri != NULL && uri->host != NULL, 0);
+       g_warn_if_fail (SOUP_URI_IS_VALID (uri));
 
        return GPOINTER_TO_UINT (uri->scheme) + uri->port +
                soup_str_case_hash (uri->host);
@@ -973,15 +1267,15 @@ soup_uri_host_hash (gconstpointer key)
 
 /**
  * soup_uri_host_equal:
- * @v1: a #SoupURI
- * @v2: a #SoupURI
+ * @v1: (type Soup.URI): a #SoupURI with a non-%NULL @host member
+ * @v2: (type Soup.URI): a #SoupURI with a non-%NULL @host member
  *
  * Compares @v1 and @v2, considering only the scheme, host, and port.
  *
  * Return value: whether or not the URIs are equal in scheme, host,
  * and port.
  *
- * Since: 2.26.3
+ * Since: 2.28
  **/
 gboolean
 soup_uri_host_equal (gconstpointer v1, gconstpointer v2)
@@ -991,6 +1285,8 @@ soup_uri_host_equal (gconstpointer v1, gconstpointer v2)
 
        g_return_val_if_fail (one != NULL && two != NULL, one == two);
        g_return_val_if_fail (one->host != NULL && two->host != NULL, one->host == two->host);
+       g_warn_if_fail (SOUP_URI_IS_VALID (one));
+       g_warn_if_fail (SOUP_URI_IS_VALID (two));
 
        if (one->scheme != two->scheme)
                return FALSE;
@@ -1000,18 +1296,4 @@ soup_uri_host_equal (gconstpointer v1, gconstpointer v2)
        return g_ascii_strcasecmp (one->host, two->host) == 0;
 }
 
-
-GType
-soup_uri_get_type (void)
-{
-       static volatile gsize type_volatile = 0;
-
-       if (g_once_init_enter (&type_volatile)) {
-               GType type = g_boxed_type_register_static (
-                       g_intern_static_string ("SoupURI"),
-                       (GBoxedCopyFunc) soup_uri_copy,
-                       (GBoxedFreeFunc) soup_uri_free);
-               g_once_init_leave (&type_volatile, type);
-       }
-       return type_volatile;
-}
+G_DEFINE_BOXED_TYPE (SoupURI, soup_uri, soup_uri_copy, soup_uri_free)