]> sjero.net Git - wget/blobdiff - src/url.c
[svn] Attempt to quote '?' as "%3F" when linking to local files.
[wget] / src / url.c
index 3e19c83a7550beb7ec1f10604802857778c67093..26642e59d9e62f70f8c23f70c3cb22cd7fe0a873 100644 (file)
--- a/src/url.c
+++ b/src/url.c
@@ -37,6 +37,7 @@ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.  */
 #include "utils.h"
 #include "url.h"
 #include "host.h"
+#include "hash.h"
 
 #ifndef errno
 extern int errno;
@@ -49,75 +50,26 @@ extern int errno;
 
 static int urlpath_length PARAMS ((const char *));
 
-/* A NULL-terminated list of strings to be recognized as protocol
-   types (URL schemes).  Note that recognized doesn't mean supported
-   -- only HTTP, HTTPS and FTP are currently supported.
-
-   However, a string that does not match anything in the list will be
-   considered a relative URL.  Thus it's important that this list has
-   anything anyone could think of being legal.
-
-   #### This is probably broken.  Wget should use other means to
-   distinguish between absolute and relative URIs in HTML links.
-
-   Take a look at <http://www.w3.org/pub/WWW/Addressing/schemes.html>
-   for more.  */
-static char *protostrings[] =
-{
-  "cid:",
-  "clsid:",
-  "file:",
-  "finger:",
-  "ftp:",
-  "gopher:",
-  "hdl:",
-  "http:",
-  "https:",
-  "ilu:",
-  "ior:",
-  "irc:",
-  "java:",
-  "javascript:",
-  "lifn:",
-  "mailto:",
-  "mid:",
-  "news:",
-  "nntp:",
-  "path:",
-  "prospero:",
-  "rlogin:",
-  "service:",
-  "shttp:",
-  "snews:",
-  "stanf:",
-  "telnet:",
-  "tn3270:",
-  "wais:",
-  "whois++:",
-  NULL
-};
-
-struct proto
+struct scheme_data
 {
-  char *name;
-  uerr_t ind;
-  unsigned short port;
+  char *leading_string;
+  int default_port;
 };
 
-/* Similar to former, but for supported protocols: */
-static struct proto sup_protos[] =
+/* Supported schemes: */
+static struct scheme_data supported_schemes[] =
 {
-  { "http://", URLHTTP, DEFAULT_HTTP_PORT },
+  { "http://",  DEFAULT_HTTP_PORT },
 #ifdef HAVE_SSL
-  { "https://",URLHTTPS, DEFAULT_HTTPS_PORT},
+  { "https://", DEFAULT_HTTPS_PORT },
 #endif
-  { "ftp://", URLFTP, DEFAULT_FTP_PORT }
+  { "ftp://",   DEFAULT_FTP_PORT },
+
+  /* SCHEME_INVALID */
+  { NULL,       -1 }
 };
 
-static void parse_dir PARAMS ((const char *, char **, char **));
-static uerr_t parse_uname PARAMS ((const char *, char **, char **));
 static char *construct_relative PARAMS ((const char *, const char *));
-static char process_ftp_type PARAMS ((char *));
 
 \f
 /* Support for encoding and decoding of URL strings.  We determine
@@ -135,17 +87,11 @@ enum {
 
 #define urlchr_test(c, mask) (urlchr_table[(unsigned char)(c)] & (mask))
 
-/* rfc1738 reserved chars.  We don't use this yet; preservation of
-   reserved chars will be implemented when I integrate the new
-   `reencode_string' function.  */
+/* rfc1738 reserved chars, preserved from encoding.  */
 
 #define RESERVED_CHAR(c) urlchr_test(c, urlchr_reserved)
 
-/* Unsafe chars:
-   - anything <= 32;
-   - stuff from rfc1738 ("<>\"#%{}|\\^~[]`");
-   - '@' and ':'; needed for encoding URL username and password.
-   - anything >= 127. */
+/* rfc1738 unsafe chars, plus some more.  */
 
 #define UNSAFE_CHAR(c) urlchr_test(c, urlchr_unsafe)
 
@@ -155,10 +101,10 @@ const static unsigned char urlchr_table[256] =
   U,  U,  U,  U,   U,  U,  U,  U,   /* BS  HT  LF  VT   FF  CR  SO  SI  */
   U,  U,  U,  U,   U,  U,  U,  U,   /* DLE DC1 DC2 DC3  DC4 NAK SYN ETB */
   U,  U,  U,  U,   U,  U,  U,  U,   /* CAN EM  SUB ESC  FS  GS  RS  US  */
-  U,  0,  U,  U,   0,  U,  R,  0,   /* SP  !   "   #    $   %   &   '   */
+  U,  0,  U, RU,   0,  U,  R,  0,   /* SP  !   "   #    $   %   &   '   */
   0,  0,  0,  R,   0,  0,  0,  R,   /* (   )   *   +    ,   -   .   /   */
   0,  0,  0,  0,   0,  0,  0,  0,   /* 0   1   2   3    4   5   6   7   */
-  0,  0,  U,  R,   U,  R,  U,  R,   /* 8   9   :   ;    <   =   >   ?   */
+  0,  0, RU,  R,   U,  R,  U,  R,   /* 8   9   :   ;    <   =   >   ?   */
  RU,  0,  0,  0,   0,  0,  0,  0,   /* @   A   B   C    D   E   F   G   */
   0,  0,  0,  0,   0,  0,  0,  0,   /* H   I   J   K    L   M   N   O   */
   0,  0,  0,  0,   0,  0,  0,  0,   /* P   Q   R   S    T   U   V   W   */
@@ -237,7 +183,7 @@ encode_string_maybe (const char *s)
     {
       if (UNSAFE_CHAR (*p1))
        {
-         const unsigned char c = *p1++;
+         unsigned char c = *p1++;
          *p2++ = '%';
          *p2++ = XDIGIT_TO_XCHAR (c >> 4);
          *p2++ = XDIGIT_TO_XCHAR (c & 0xf);
@@ -277,535 +223,758 @@ encode_string (const char *s)
     }                                          \
 } while (0)
 \f
-/* Returns the protocol type if URL's protocol is supported, or
-   URLUNKNOWN if not.  */
-uerr_t
-urlproto (const char *url)
-{
-  int i;
+enum copy_method { CM_DECODE, CM_ENCODE, CM_PASSTHROUGH };
 
-  for (i = 0; i < ARRAY_SIZE (sup_protos); i++)
-    if (!strncasecmp (url, sup_protos[i].name, strlen (sup_protos[i].name)))
-      return sup_protos[i].ind;
-  for (i = 0; url[i] && url[i] != ':' && url[i] != '/'; i++);
-  if (url[i] == ':')
+/* Decide whether to encode, decode, or pass through the char at P.
+   This used to be a macro, but it got a little too convoluted.  */
+static inline enum copy_method
+decide_copy_method (const char *p)
+{
+  if (*p == '%')
     {
-      for (++i; url[i] && url[i] != '/'; i++)
-       if (!ISDIGIT (url[i]))
-         return URLBADPORT;
-      if (url[i - 1] == ':')
-       return URLFTP;
+      if (ISXDIGIT (*(p + 1)) && ISXDIGIT (*(p + 2)))
+       {
+         /* %xx sequence: decode it, unless it would decode to an
+            unsafe or a reserved char; in that case, leave it as
+            is. */
+         char preempt = (XCHAR_TO_XDIGIT (*(p + 1)) << 4) +
+           XCHAR_TO_XDIGIT (*(p + 2));
+
+         if (UNSAFE_CHAR (preempt) || RESERVED_CHAR (preempt))
+           return CM_PASSTHROUGH;
+         else
+           return CM_DECODE;
+       }
       else
-       return URLHTTP;
+       /* Garbled %.. sequence: encode `%'. */
+       return CM_ENCODE;
     }
+  else if (UNSAFE_CHAR (*p) && !RESERVED_CHAR (*p))
+    return CM_ENCODE;
   else
-    return URLHTTP;
+    return CM_PASSTHROUGH;
 }
 
-/* Skip the protocol part of the URL, e.g. `http://'.  If no protocol
-   part is found, returns 0.  */
+/* Translate a %-quoting (but possibly non-conformant) input string S
+   into a %-quoting (and conformant) output string.  If no characters
+   are encoded or decoded, return the same string S; otherwise, return
+   a freshly allocated string with the new contents.
+
+   After a URL has been run through this function, the protocols that
+   use `%' as the quote character can use the resulting string as-is,
+   while those that don't call decode_string() to get to the intended
+   data.  This function is also stable: after an input string is
+   transformed the first time, all further transformations of the
+   result yield the same result string.
+
+   Let's discuss why this function is needed.
+
+   Imagine Wget is to retrieve `http://abc.xyz/abc def'.  Since a raw
+   space character would mess up the HTTP request, it needs to be
+   quoted, like this:
+
+       GET /abc%20def HTTP/1.0
+
+   So it appears that the unsafe chars need to be quoted, as with
+   encode_string.  But what if we're requested to download
+   `abc%20def'?  Remember that %-encoding is valid URL syntax, so what
+   the user meant was a literal space, and he was kind enough to quote
+   it.  In that case, Wget should obviously leave the `%20' as is, and
+   send the same request as above.  So in this case we may not call
+   encode_string.
+
+   But what if the requested URI is `abc%20 def'?  If we call
+   encode_string, we end up with `/abc%2520%20def', which is almost
+   certainly not intended.  If we don't call encode_string, we are
+   left with the embedded space and cannot send the request.  What the
+   user meant was for Wget to request `/abc%20%20def', and this is
+   where reencode_string kicks in.
+
+   Wget used to solve this by first decoding %-quotes, and then
+   encoding all the "unsafe" characters found in the resulting string.
+   This was wrong because it didn't preserve certain URL special
+   (reserved) characters.  For instance, URI containing "a%2B+b" (0x2b
+   == '+') would get translated to "a%2B%2Bb" or "a++b" depending on
+   whether we considered `+' reserved (it is).  One of these results
+   is inevitable because by the second step we would lose information
+   on whether the `+' was originally encoded or not.  Both results
+   were wrong because in CGI parameters + means space, while %2B means
+   literal plus.  reencode_string correctly translates the above to
+   "a%2B+b", i.e. returns the original string.
+
+   This function uses an algorithm proposed by Anon Sricharoenchai:
+
+   1. Encode all URL_UNSAFE and the "%" that are not followed by 2
+      hexdigits.
+
+   2. Decode all "%XX" except URL_UNSAFE, URL_RESERVED (";/?:@=&") and
+      "+".
+
+   ...except that this code conflates the two steps, and decides
+   whether to encode, decode, or pass through each character in turn.
+   The function still uses two passes, but their logic is the same --
+   the first pass exists merely for the sake of allocation.  Another
+   small difference is that we include `+' to URL_RESERVED.
+
+   Anon's test case:
+
+   "http://abc.xyz/%20%3F%%36%31%25aa% a?a=%61+a%2Ba&b=b%26c%3Dc"
+   ->
+   "http://abc.xyz/%20%3F%2561%25aa%25%20a?a=a+a%2Ba&b=b%26c%3Dc"
+
+   Simpler test cases:
+
+   "foo bar"         -> "foo%20bar"
+   "foo%20bar"       -> "foo%20bar"
+   "foo %20bar"      -> "foo%20%20bar"
+   "foo%%20bar"      -> "foo%25%20bar"       (0x25 == '%')
+   "foo%25%20bar"    -> "foo%25%20bar"
+   "foo%2%20bar"     -> "foo%252%20bar"
+   "foo+bar"         -> "foo+bar"            (plus is reserved!)
+   "foo%2b+bar"      -> "foo%2b+bar"  */
+
+char *
+reencode_string (const char *s)
+{
+  const char *p1;
+  char *newstr, *p2;
+  int oldlen, newlen;
+
+  int encode_count = 0;
+  int decode_count = 0;
+
+  /* First, pass through the string to see if there's anything to do,
+     and to calculate the new length.  */
+  for (p1 = s; *p1; p1++)
+    {
+      switch (decide_copy_method (p1))
+       {
+       case CM_ENCODE:
+         ++encode_count;
+         break;
+       case CM_DECODE:
+         ++decode_count;
+         break;
+       case CM_PASSTHROUGH:
+         break;
+       }
+    }
+
+  if (!encode_count && !decode_count)
+    /* The string is good as it is. */
+    return (char *)s;          /* C const model sucks. */
+
+  oldlen = p1 - s;
+  /* Each encoding adds two characters (hex digits), while each
+     decoding removes two characters.  */
+  newlen = oldlen + 2 * (encode_count - decode_count);
+  newstr = xmalloc (newlen + 1);
+
+  p1 = s;
+  p2 = newstr;
+
+  while (*p1)
+    {
+      switch (decide_copy_method (p1))
+       {
+       case CM_ENCODE:
+         {
+           unsigned char c = *p1++;
+           *p2++ = '%';
+           *p2++ = XDIGIT_TO_XCHAR (c >> 4);
+           *p2++ = XDIGIT_TO_XCHAR (c & 0xf);
+         }
+         break;
+       case CM_DECODE:
+         *p2++ = ((XCHAR_TO_XDIGIT (*(p1 + 1)) << 4)
+                  + (XCHAR_TO_XDIGIT (*(p1 + 2))));
+         p1 += 3;              /* skip %xx */
+         break;
+       case CM_PASSTHROUGH:
+         *p2++ = *p1++;
+       }
+    }
+  *p2 = '\0';
+  assert (p2 - newstr == newlen);
+  return newstr;
+}
+
+/* Run PTR_VAR through reencode_string.  If a new string is consed,
+   free PTR_VAR and make it point to the new storage.  Obviously,
+   PTR_VAR needs to be an lvalue.  */
+
+#define REENCODE(ptr_var) do {                 \
+  char *rf_new = reencode_string (ptr_var);    \
+  if (rf_new != ptr_var)                       \
+    {                                          \
+      xfree (ptr_var);                         \
+      ptr_var = rf_new;                                \
+    }                                          \
+} while (0)
+\f
+/* Returns the scheme type if the scheme is supported, or
+   SCHEME_INVALID if not.  */
+enum url_scheme
+url_scheme (const char *url)
+{
+  int i;
+
+  for (i = 0; supported_schemes[i].leading_string; i++)
+    if (!strncasecmp (url, supported_schemes[i].leading_string,
+                     strlen (supported_schemes[i].leading_string)))
+      return (enum url_scheme)i;
+  return SCHEME_INVALID;
+}
+
+/* Return the number of characters needed to skip the scheme part of
+   the URL, e.g. `http://'.  If no scheme is found, returns 0.  */
 int
-skip_proto (const char *url)
+url_skip_scheme (const char *url)
 {
-  char **s;
-  int l;
+  const char *p = url;
 
-  for (s = protostrings; *s; s++)
-    if (!strncasecmp (*s, url, strlen (*s)))
-      break;
-  if (!*s)
+  /* Skip the scheme name.  We allow `-' and `+' because of `whois++',
+     etc. */
+  while (ISALNUM (*p) || *p == '-' || *p == '+')
+    ++p;
+  if (*p != ':')
     return 0;
-  l = strlen (*s);
-  /* HTTP and FTP protocols are expected to yield exact host names
-     (i.e. the `//' part must be skipped, too).  */
-  if (!strcmp (*s, "http:") || !strcmp (*s, "ftp:"))
-    l += 2;
-  return l;
+  /* Skip ':'. */
+  ++p;
+
+  /* Skip "//" if found. */
+  if (*p == '/' && *(p + 1) == '/')
+    p += 2;
+
+  return p - url;
 }
 
-/* Returns 1 if the URL begins with a protocol (supported or
+/* Returns 1 if the URL begins with a scheme (supported or
    unsupported), 0 otherwise.  */
 int
-has_proto (const char *url)
+url_has_scheme (const char *url)
 {
-  char **s;
+  const char *p = url;
+  while (ISALNUM (*p) || *p == '-' || *p == '+')
+    ++p;
+  return *p == ':';
+}
 
-  for (s = protostrings; *s; s++)
-    if (strncasecmp (url, *s, strlen (*s)) == 0)
-      return 1;
-  return 0;
+int
+scheme_default_port (enum url_scheme scheme)
+{
+  return supported_schemes[scheme].default_port;
 }
 
 /* Skip the username and password, if present here.  The function
    should be called *not* with the complete URL, but with the part
-   right after the protocol.
+   right after the scheme.
 
    If no username and password are found, return 0.  */
 int
-skip_uname (const char *url)
+url_skip_uname (const char *url)
 {
   const char *p;
-  const char *q = NULL;
-  for (p = url ; *p && *p != '/'; p++)
-    if (*p == '@') q = p;
-  /* If a `@' was found before the first occurrence of `/', skip
-     it.  */
-  if (q != NULL)
-    return q - url + 1;
-  else
+
+  /* Look for '@' that comes before '/' or '?'. */
+  p = (const char *)strpbrk (url, "/?@");
+  if (!p || *p != '@')
     return 0;
-}
-\f
-/* Allocate a new urlinfo structure, fill it with default values and
-   return a pointer to it.  */
-struct urlinfo *
-newurl (void)
-{
-  struct urlinfo *u;
 
-  u = (struct urlinfo *)xmalloc (sizeof (struct urlinfo));
-  memset (u, 0, sizeof (*u));
-  u->proto = URLUNKNOWN;
-  return u;
+  return p - url + 1;
 }
 
-/* Perform a "deep" free of the urlinfo structure.  The structure
-   should have been created with newurl, but need not have been used.
-   If free_pointer is non-0, free the pointer itself.  */
-void
-freeurl (struct urlinfo *u, int complete)
-{
-  assert (u != NULL);
-  FREE_MAYBE (u->url);
-  FREE_MAYBE (u->host);
-  FREE_MAYBE (u->path);
-  FREE_MAYBE (u->file);
-  FREE_MAYBE (u->dir);
-  FREE_MAYBE (u->user);
-  FREE_MAYBE (u->passwd);
-  FREE_MAYBE (u->local);
-  FREE_MAYBE (u->referer);
-  if (u->proxy)
-    freeurl (u->proxy, 1);
-  if (complete)
-    xfree (u);
-  return;
-}
-\f
-/* Extract the given URL of the form
-   (http:|ftp:)// (user (:password)?@)?hostname (:port)? (/path)?
-   1. hostname (terminated with `/' or `:')
-   2. port number (terminated with `/'), or chosen for the protocol
-   3. dirname (everything after hostname)
-   Most errors are handled.  No allocation is done, you must supply
-   pointers to allocated memory.
-   ...and a host of other stuff :-)
-
-   - Recognizes hostname:dir/file for FTP and
-     hostname (:portnum)?/dir/file for HTTP.
-   - Parses the path to yield directory and file
-   - Parses the URL to yield the username and passwd (if present)
-   - Decodes the strings, in case they contain "forbidden" characters
-   - Writes the result to struct urlinfo
-
-   If the argument STRICT is set, it recognizes only the canonical
-   form.  */
-uerr_t
-parseurl (const char *url, struct urlinfo *u, int strict)
+static int
+parse_uname (const char *str, int len, char **user, char **passwd)
 {
-  int i, l, abs_ftp;
-  int recognizable;            /* Recognizable URL is the one where
-                                 the protocol name was explicitly
-                                 named, i.e. it wasn't deduced from
-                                 the URL format.  */
-  uerr_t type;
-
-  DEBUGP (("parseurl (\"%s\") -> ", url));
-  recognizable = has_proto (url);
-  if (strict && !recognizable)
-    return URLUNKNOWN;
-  for (i = 0, l = 0; i < ARRAY_SIZE (sup_protos); i++)
+  char *colon;
+
+  if (len == 0)
+    /* Empty user name not allowed. */
+    return 0;
+
+  colon = memchr (str, ':', len);
+  if (colon == str)
+    /* Empty user name again. */
+    return 0;
+
+  if (colon)
     {
-      l = strlen (sup_protos[i].name);
-      if (!strncasecmp (sup_protos[i].name, url, l))
-       break;
+      int pwlen = len - (colon + 1 - str);
+      *passwd = xmalloc (pwlen + 1);
+      memcpy (*passwd, colon + 1, pwlen);
+      (*passwd)[pwlen] = '\0';
+      len -= pwlen + 1;
     }
-  /* If protocol is recognizable, but unsupported, bail out, else
-     suppose unknown.  */
-  if (recognizable && i == ARRAY_SIZE (sup_protos))
-    return URLUNKNOWN;
-  else if (i == ARRAY_SIZE (sup_protos))
-    type = URLUNKNOWN;
   else
-    u->proto = type = sup_protos[i].ind;
-
-  if (type == URLUNKNOWN)
-    l = 0;
-  /* Allow a username and password to be specified (i.e. just skip
-     them for now).  */
-  if (recognizable)
-    l += skip_uname (url + l);
-  for (i = l; url[i] && url[i] != ':' && url[i] != '/'; i++);
-  if (i == l)
-    return URLBADHOST;
-  /* Get the hostname.  */
-  u->host = strdupdelim (url + l, url + i);
-  DEBUGP (("host %s -> ", u->host));
-
-  /* Assume no port has been given.  */
-  u->port = 0;
-  if (url[i] == ':')
-    {
-      /* We have a colon delimiting the hostname.  It could mean that
-        a port number is following it, or a directory.  */
-      if (ISDIGIT (url[++i]))    /* A port number */
-       {
-         if (type == URLUNKNOWN)
-           u->proto = type = URLHTTP;
-         for (; url[i] && url[i] != '/'; i++)
-           if (ISDIGIT (url[i]))
-             u->port = 10 * u->port + (url[i] - '0');
-           else
-             return URLBADPORT;
-         if (!u->port)
-           return URLBADPORT;
-         DEBUGP (("port %hu -> ", u->port));
-       }
-      else if (type == URLUNKNOWN) /* or a directory */
-       u->proto = type = URLFTP;
-      else                      /* or just a misformed port number */
-       return URLBADPORT;
-    }
-  else if (type == URLUNKNOWN)
-    u->proto = type = URLHTTP;
-  if (!u->port)
+    *passwd = NULL;
+
+  *user = xmalloc (len + 1);
+  memcpy (*user, str, len);
+  (*user)[len] = '\0';
+
+  return 1;
+}
+
+/* Used by main.c: detect URLs written using the "shorthand" URL forms
+   popularized by Netscape and NcFTP.  HTTP shorthands look like this:
+
+   www.foo.com[:port]/dir/file   -> http://www.foo.com[:port]/dir/file
+   www.foo.com[:port]            -> http://www.foo.com[:port]
+
+   FTP shorthands look like this:
+
+   foo.bar.com:dir/file          -> ftp://foo.bar.com/dir/file
+   foo.bar.com:/absdir/file      -> ftp://foo.bar.com//absdir/file
+
+   If the URL needs not or cannot be rewritten, return NULL.  */
+char *
+rewrite_shorthand_url (const char *url)
+{
+  const char *p;
+
+  if (url_has_scheme (url))
+    return NULL;
+
+  /* Look for a ':' or '/'.  The former signifies NcFTP syntax, the
+     latter Netscape.  */
+  for (p = url; *p && *p != ':' && *p != '/'; p++)
+    ;
+
+  if (p == url)
+    return NULL;
+
+  if (*p == ':')
     {
-      int ind;
-      for (ind = 0; ind < ARRAY_SIZE (sup_protos); ind++)
-       if (sup_protos[ind].ind == type)
-         break;
-      if (ind == ARRAY_SIZE (sup_protos))
-       return URLUNKNOWN;
-      u->port = sup_protos[ind].port;
+      const char *pp, *path;
+      char *res;
+      /* If the characters after the colon and before the next slash
+        or end of string are all digits, it's HTTP.  */
+      int digits = 0;
+      for (pp = p + 1; ISDIGIT (*pp); pp++)
+       ++digits;
+      if (digits > 0
+         && (*pp == '/' || *pp == '\0'))
+       goto http;
+
+      /* Prepend "ftp://" to the entire URL... */
+      path = p + 1;
+      res = xmalloc (6 + strlen (url) + 1);
+      sprintf (res, "ftp://%s", url);
+      /* ...and replace ':' with '/'. */
+      res[6 + (p - url)] = '/';
+      return res;
     }
-  /* Some delimiter troubles...  */
-  if (url[i] == '/' && url[i - 1] != ':')
-    ++i;
-  if (type == URLHTTP)
-    while (url[i] && url[i] == '/')
-      ++i;
-  u->path = (char *)xmalloc (strlen (url + i) + 8);
-  strcpy (u->path, url + i);
-  if (type == URLFTP)
+  else
     {
-      u->ftp_type = process_ftp_type (u->path);
-      /* #### We don't handle type `d' correctly yet.  */
-      if (!u->ftp_type || TOUPPER (u->ftp_type) == 'D')
-       u->ftp_type = 'I';
-      DEBUGP (("ftp_type %c -> ", u->ftp_type));
+      char *res;
+    http:
+      /* Just prepend "http://" to what we have. */
+      res = xmalloc (7 + strlen (url) + 1);
+      sprintf (res, "http://%s", url);
+      return res;
     }
-  DEBUGP (("opath %s -> ", u->path));
-  /* Parse the username and password (if existing).  */
-  parse_uname (url, &u->user, &u->passwd);
-  /* Decode the strings, as per RFC 1738.  */
-  decode_string (u->host);
-  decode_string (u->path);
-  if (u->user)
-    decode_string (u->user);
-  if (u->passwd)
-    decode_string (u->passwd);
-  /* Parse the directory.  */
-  parse_dir (u->path, &u->dir, &u->file);
-  DEBUGP (("dir %s -> file %s -> ", u->dir, u->file));
-  /* Simplify the directory.  */
-  path_simplify (u->dir);
-  /* Remove the leading `/' in HTTP.  */
-  if (type == URLHTTP && *u->dir == '/')
-    strcpy (u->dir, u->dir + 1);
-  DEBUGP (("ndir %s\n", u->dir));
-  /* Strip trailing `/'.  */
-  l = strlen (u->dir);
-  if (l > 1 && u->dir[l - 1] == '/')
-    u->dir[l - 1] = '\0';
-  /* Re-create the path: */
-  abs_ftp = (u->proto == URLFTP && *u->dir == '/');
-  /*  sprintf (u->path, "%s%s%s%s", abs_ftp ? "%2F": "/",
-      abs_ftp ? (u->dir + 1) : u->dir, *u->dir ? "/" : "", u->file); */
-  strcpy (u->path, abs_ftp ? "%2F" : "/");
-  strcat (u->path, abs_ftp ? (u->dir + 1) : u->dir);
-  strcat (u->path, *u->dir ? "/" : "");
-  strcat (u->path, u->file);
-  ENCODE (u->path);
-  DEBUGP (("newpath: %s\n", u->path));
-  /* Create the clean URL.  */
-  u->url = str_url (u, 0);
-  return URLOK;
 }
 \f
-/* Special versions of DOTP and DDOTP for parse_dir().  They work like
-   DOTP and DDOTP, but they also recognize `?' as end-of-string
-   delimiter.  This is needed for correct handling of query
-   strings.  */
+static void parse_path PARAMS ((const char *, char **, char **));
 
-#define PD_DOTP(x)  ((*(x) == '.') && (!*((x) + 1) || *((x) + 1) == '?'))
-#define PD_DDOTP(x) ((*(x) == '.') && (*(x) == '.')            \
-                    && (!*((x) + 2) || *((x) + 2) == '?'))
+static char *
+strpbrk_or_eos (const char *s, const char *accept)
+{
+  char *p = strpbrk (s, accept);
+  if (!p)
+    p = (char *)s + strlen (s);
+  return p;
+}
 
-/* Build the directory and filename components of the path.  Both
-   components are *separately* malloc-ed strings!  It does not change
-   the contents of path.
+/* Turn STR into lowercase; return non-zero if a character was
+   actually changed. */
 
-   If the path ends with "." or "..", they are (correctly) counted as
-   directories.  */
-static void
-parse_dir (const char *path, char **dir, char **file)
+static int
+lowercase_str (char *str)
 {
-  int i, l;
+  int change = 0;
+  for (; *str; str++)
+    if (!ISLOWER (*str))
+      {
+       change = 1;
+       *str = TOLOWER (*str);
+      }
+  return change;
+}
+
+static char *parse_errors[] = {
+#define PE_NO_ERROR            0
+  "No error",
+#define PE_UNRECOGNIZED_SCHEME 1
+  "Unrecognized scheme",
+#define PE_EMPTY_HOST          2
+  "Empty host",
+#define PE_BAD_PORT_NUMBER     3
+  "Bad port number",
+#define PE_INVALID_USER_NAME   4
+  "Invalid user name"
+};
+
+#define SETERR(p, v) do {                      \
+  if (p)                                       \
+    *(p) = (v);                                        \
+} while (0)
+
+/* Parse a URL.
+
+   Return a new struct url if successful, NULL on error.  In case of
+   error, and if ERROR is not NULL, also set *ERROR to the appropriate
+   error code. */
+struct url *
+url_parse (const char *url, int *error)
+{
+  struct url *u;
+  const char *p;
+  int path_modified, host_modified;
+
+  enum url_scheme scheme;
+
+  const char *uname_b,     *uname_e;
+  const char *host_b,      *host_e;
+  const char *path_b,      *path_e;
+  const char *params_b,    *params_e;
+  const char *query_b,     *query_e;
+  const char *fragment_b,  *fragment_e;
 
-  l = urlpath_length (path);
-  for (i = l; i && path[i] != '/'; i--);
+  int port;
+  char *user = NULL, *passwd = NULL;
 
-  if (!i && *path != '/')   /* Just filename */
+  char *url_encoded;
+
+  scheme = url_scheme (url);
+  if (scheme == SCHEME_INVALID)
     {
-      if (PD_DOTP (path) || PD_DDOTP (path))
-       {
-         *dir = strdupdelim (path, path + l);
-         *file = xstrdup (path + l); /* normally empty, but could
-                                         contain ?... */
-       }
-      else
-       {
-         *dir = xstrdup ("");     /* This is required because of FTP */
-         *file = xstrdup (path);
-       }
+      SETERR (error, PE_UNRECOGNIZED_SCHEME);
+      return NULL;
     }
-  else if (!i)                 /* /filename */
+
+  url_encoded = reencode_string (url);
+  p = url_encoded;
+
+  p += strlen (supported_schemes[scheme].leading_string);
+  uname_b = p;
+  p += url_skip_uname (p);
+  uname_e = p;
+
+  /* scheme://user:pass@host[:port]... */
+  /*                    ^              */
+
+  /* We attempt to break down the URL into the components path,
+     params, query, and fragment.  They are ordered like this:
+
+       scheme://host[:port][/path][;params][?query][#fragment]  */
+
+  params_b   = params_e   = NULL;
+  query_b    = query_e    = NULL;
+  fragment_b = fragment_e = NULL;
+
+  host_b = p;
+  p = strpbrk_or_eos (p, ":/;?#");
+  host_e = p;
+
+  if (host_b == host_e)
     {
-      if (PD_DOTP (path + 1) || PD_DDOTP (path + 1))
-       {
-         *dir = strdupdelim (path, path + l);
-         *file = xstrdup (path + l); /* normally empty, but could
-                                         contain ?... */
-       }
-      else
-       {
-         *dir = xstrdup ("/");
-         *file = xstrdup (path + 1);
-       }
+      SETERR (error, PE_EMPTY_HOST);
+      return NULL;
     }
-  else /* Nonempty directory with or without a filename */
+
+  port = scheme_default_port (scheme);
+  if (*p == ':')
     {
-      if (PD_DOTP (path + i + 1) || PD_DDOTP (path + i + 1))
+      const char *port_b, *port_e, *pp;
+
+      /* scheme://host:port/tralala */
+      /*              ^             */
+      ++p;
+      port_b = p;
+      p = strpbrk_or_eos (p, "/;?#");
+      port_e = p;
+
+      if (port_b == port_e)
        {
-         *dir = strdupdelim (path, path + l);
-         *file = xstrdup (path + l); /* normally empty, but could
-                                         contain ?... */
+         /* http://host:/whatever */
+         /*             ^         */
+         SETERR (error, PE_BAD_PORT_NUMBER);
+         return NULL;
        }
-      else
+
+      for (port = 0, pp = port_b; pp < port_e; pp++)
        {
-         *dir = strdupdelim (path, path + i);
-         *file = xstrdup (path + i + 1);
+         if (!ISDIGIT (*pp))
+           {
+             /* http://host:12randomgarbage/blah */
+             /*               ^                  */
+             SETERR (error, PE_BAD_PORT_NUMBER);
+             return NULL;
+           }
+         port = 10 * port + (*pp - '0');
        }
     }
-}
 
-/* Find the optional username and password within the URL, as per
-   RFC1738.  The returned user and passwd char pointers are
-   malloc-ed.  */
-static uerr_t
-parse_uname (const char *url, char **user, char **passwd)
-{
-  int l;
-  const char *p, *q, *col;
-  char **where;
-
-  *user = NULL;
-  *passwd = NULL;
-
-  /* Look for the end of the protocol string.  */
-  l = skip_proto (url);
-  if (!l)
-    return URLUNKNOWN;
-  /* Add protocol offset.  */
-  url += l;
-  /* Is there an `@' character?  */
-  for (p = url; *p && *p != '/'; p++)
-    if (*p == '@')
-      break;
-  /* If not, return.  */
-  if (*p != '@')
-    return URLOK;
-  /* Else find the username and password.  */
-  for (p = q = col = url; *p && *p != '/'; p++)
+  if (*p == '/')
+    {
+      ++p;
+      path_b = p;
+      p = strpbrk_or_eos (p, ";?#");
+      path_e = p;
+    }
+  else
+    {
+      /* Path is not allowed not to exist. */
+      path_b = path_e = p;
+    }
+
+  if (*p == ';')
+    {
+      ++p;
+      params_b = p;
+      p = strpbrk_or_eos (p, "?#");
+      params_e = p;
+    }
+  if (*p == '?')
     {
-      if (*p == ':' && !*user)
+      ++p;
+      query_b = p;
+      p = strpbrk_or_eos (p, "#");
+      query_e = p;
+    }
+  if (*p == '#')
+    {
+      ++p;
+      fragment_b = p;
+      p += strlen (p);
+      fragment_e = p;
+    }
+  assert (*p == 0);
+
+  if (uname_b != uname_e)
+    {
+      /* http://user:pass@host */
+      /*        ^         ^    */
+      /*     uname_b   uname_e */
+      if (!parse_uname (uname_b, uname_e - uname_b - 1, &user, &passwd))
        {
-         *user = (char *)xmalloc (p - url + 1);
-         memcpy (*user, url, p - url);
-         (*user)[p - url] = '\0';
-         col = p + 1;
+         SETERR (error, PE_INVALID_USER_NAME);
+         return NULL;
        }
-      if (*p == '@') q = p;
     }
-  /* Decide whether you have only the username or both.  */
-  where = *user ? passwd : user;
-  *where = (char *)xmalloc (q - col + 1);
-  memcpy (*where, col, q - col);
-  (*where)[q - col] = '\0';
-  return URLOK;
-}
 
-/* If PATH ends with `;type=X', return the character X.  */
-static char
-process_ftp_type (char *path)
-{
-  int len = strlen (path);
+  u = (struct url *)xmalloc (sizeof (struct url));
+  memset (u, 0, sizeof (*u));
+
+  u->scheme = scheme;
+  u->host   = strdupdelim (host_b, host_e);
+  u->port   = port;
+  u->user   = user;
+  u->passwd = passwd;
+
+  u->path = strdupdelim (path_b, path_e);
+  path_modified = path_simplify (u->path);
+  parse_path (u->path, &u->dir, &u->file);
 
-  if (len >= 7
-      && !memcmp (path + len - 7, ";type=", 6))
+  host_modified = lowercase_str (u->host);
+
+  if (params_b)
+    u->params = strdupdelim (params_b, params_e);
+  if (query_b)
+    u->query = strdupdelim (query_b, query_e);
+  if (fragment_b)
+    u->fragment = strdupdelim (fragment_b, fragment_e);
+
+
+  if (path_modified || u->fragment || host_modified)
     {
-      path[len - 7] = '\0';
-      return path[len - 1];
+      /* If path_simplify modified the path, or if a fragment is
+        present, or if the original host name had caps in it, make
+        sure that u->url is equivalent to what would be printed by
+        url_string.  */
+      u->url = url_string (u, 0);
+
+      if (url_encoded != url)
+       xfree ((char *) url_encoded);
     }
   else
-    return '\0';
-}
-\f
-/* Return the URL as fine-formed string, with a proper protocol, optional port
-   number, directory and optional user/password.  If `hide' is non-zero (as it
-   is when we're calling this on a URL we plan to print, but not when calling it
-   to canonicalize a URL for use within the program), password will be hidden.
-   The forbidden characters in the URL will be cleansed.  */
-char *
-str_url (const struct urlinfo *u, int hide)
-{
-  char *res, *host, *user, *passwd, *proto_name, *dir, *file;
-  int i, l, ln, lu, lh, lp, lf, ld;
-  unsigned short proto_default_port;
-
-  /* Look for the protocol name.  */
-  for (i = 0; i < ARRAY_SIZE (sup_protos); i++)
-    if (sup_protos[i].ind == u->proto)
-      break;
-  if (i == ARRAY_SIZE (sup_protos))
-    return NULL;
-  proto_name = sup_protos[i].name;
-  proto_default_port = sup_protos[i].port;
-  host = encode_string (u->host);
-  dir = encode_string (u->dir);
-  file = encode_string (u->file);
-  user = passwd = NULL;
-  if (u->user)
-    user = encode_string (u->user);
-  if (u->passwd)
     {
-      if (hide)
-       /* Don't output the password, or someone might see it over the user's
-          shoulder (or in saved wget output).  Don't give away the number of
-          characters in the password, either, as we did in past versions of
-          this code, when we replaced the password characters with 'x's. */
-       passwd = xstrdup("<password>");
+      if (url_encoded == url)
+       u->url    = xstrdup (url);
       else
-       passwd = encode_string (u->passwd);
-    }
-  if (u->proto == URLFTP && *dir == '/')
-    {
-      char *tmp = (char *)xmalloc (strlen (dir) + 3);
-      /*sprintf (tmp, "%%2F%s", dir + 1);*/
-      tmp[0] = '%';
-      tmp[1] = '2';
-      tmp[2] = 'F';
-      strcpy (tmp + 3, dir + 1);
-      xfree (dir);
-      dir = tmp;
+       u->url    = url_encoded;
     }
+  url_encoded = NULL;
+
+  return u;
+}
 
-  ln = strlen (proto_name);
-  lu = user ? strlen (user) : 0;
-  lp = passwd ? strlen (passwd) : 0;
-  lh = strlen (host);
-  ld = strlen (dir);
-  lf = strlen (file);
-  res = (char *)xmalloc (ln + lu + lp + lh + ld + lf + 20); /* safe sex */
-  /* sprintf (res, "%s%s%s%s%s%s:%d/%s%s%s", proto_name,
-     (user ? user : ""), (passwd ? ":" : ""),
-     (passwd ? passwd : ""), (user ? "@" : ""),
-     host, u->port, dir, *dir ? "/" : "", file); */
-  l = 0;
-  memcpy (res, proto_name, ln);
-  l += ln;
-  if (user)
+const char *
+url_error (int error_code)
+{
+  assert (error_code >= 0 && error_code < ARRAY_SIZE (parse_errors));
+  return parse_errors[error_code];
+}
+
+static void
+parse_path (const char *quoted_path, char **dir, char **file)
+{
+  char *path, *last_slash;
+
+  STRDUP_ALLOCA (path, quoted_path);
+  decode_string (path);
+
+  last_slash = strrchr (path, '/');
+  if (!last_slash)
     {
-      memcpy (res + l, user, lu);
-      l += lu;
-      if (passwd)
-       {
-         res[l++] = ':';
-         memcpy (res + l, passwd, lp);
-         l += lp;
-       }
-      res[l++] = '@';
+      *dir = xstrdup ("");
+      *file = xstrdup (path);
     }
-  memcpy (res + l, host, lh);
-  l += lh;
-  if (u->port != proto_default_port)
+  else
     {
-      res[l++] = ':';
-      long_to_string (res + l, (long)u->port);
-      l += numdigit (u->port);
+      *dir = strdupdelim (path, last_slash);
+      *file = xstrdup (last_slash + 1);
     }
-  res[l++] = '/';
-  memcpy (res + l, dir, ld);
-  l += ld;
-  if (*dir)
-    res[l++] = '/';
-  strcpy (res + l, file);
-  xfree (host);
-  xfree (dir);
-  xfree (file);
-  FREE_MAYBE (user);
-  FREE_MAYBE (passwd);
-  return res;
 }
 
-/* Check whether two URL-s are equivalent, i.e. pointing to the same
-   location.  Uses parseurl to parse them, and compares the canonical
-   forms.
+/* Note: URL's "full path" is the path with the query string and
+   params appended.  The "fragment" (#foo) is intentionally ignored,
+   but that might be changed.  For example, if the original URL was
+   "http://host:port/foo/bar/baz;bullshit?querystring#uselessfragment",
+   the full path will be "/foo/bar/baz;bullshit?querystring".  */
 
-   Returns 1 if the URL1 is equivalent to URL2, 0 otherwise.  Also
-   return 0 on error.  */
-int
-url_equal (const char *url1, const char *url2)
+/* Return the length of the full path, without the terminating
+   zero.  */
+
+static int
+full_path_length (const struct url *url)
 {
-  struct urlinfo *u1, *u2;
-  uerr_t err;
-  int res;
+  int len = 0;
+
+#define FROB(el) if (url->el) len += 1 + strlen (url->el)
+
+  FROB (path);
+  FROB (params);
+  FROB (query);
+
+#undef FROB
+
+  return len;
+}
+
+/* Write out the full path. */
+
+static void
+full_path_write (const struct url *url, char *where)
+{
+#define FROB(el, chr) do {                     \
+  char *f_el = url->el;                                \
+  if (f_el) {                                  \
+    int l = strlen (f_el);                     \
+    *where++ = chr;                            \
+    memcpy (where, f_el, l);                   \
+    where += l;                                        \
+  }                                            \
+} while (0)
+
+  FROB (path, '/');
+  FROB (params, ';');
+  FROB (query, '?');
+
+#undef FROB
+}
+
+/* Public function for getting the "full path". */
+char *
+url_full_path (const struct url *url)
+{
+  int length = full_path_length (url);
+  char *full_path = (char *)xmalloc(length + 1);
+
+  full_path_write (url, full_path);
+  full_path[length] = '\0';
+
+  return full_path;
+}
+
+/* Sync u->path and u->url with u->dir and u->file. */
+static void
+sync_path (struct url *url)
+{
+  char *newpath;
+
+  xfree (url->path);
 
-  u1 = newurl ();
-  err = parseurl (url1, u1, 0);
-  if (err != URLOK)
+  if (!*url->dir)
     {
-      freeurl (u1, 1);
-      return 0;
+      newpath = xstrdup (url->file);
+      REENCODE (newpath);
     }
-  u2 = newurl ();
-  err = parseurl (url2, u2, 0);
-  if (err != URLOK)
+  else
     {
-      freeurl (u2, 1);
-      return 0;
+      int dirlen = strlen (url->dir);
+      int filelen = strlen (url->file);
+
+      newpath = xmalloc (dirlen + 1 + filelen + 1);
+      memcpy (newpath, url->dir, dirlen);
+      newpath[dirlen] = '/';
+      memcpy (newpath + dirlen + 1, url->file, filelen);
+      newpath[dirlen + 1 + filelen] = '\0';
+      REENCODE (newpath);
     }
-  res = !strcmp (u1->url, u2->url);
-  freeurl (u1, 1);
-  freeurl (u2, 1);
-  return res;
+
+  url->path = newpath;
+
+  /* Synchronize u->url. */
+  xfree (url->url);
+  url->url = url_string (url, 0);
+}
+
+/* Mutators.  Code in ftp.c insists on changing u->dir and u->file.
+   This way we can sync u->path and u->url when they get changed.  */
+
+void
+url_set_dir (struct url *url, const char *newdir)
+{
+  xfree (url->dir);
+  url->dir = xstrdup (newdir);
+  sync_path (url);
+}
+
+void
+url_set_file (struct url *url, const char *newfile)
+{
+  xfree (url->file);
+  url->file = xstrdup (newfile);
+  sync_path (url);
+}
+
+void
+url_free (struct url *url)
+{
+  xfree (url->host);
+  xfree (url->path);
+  xfree (url->url);
+
+  FREE_MAYBE (url->params);
+  FREE_MAYBE (url->query);
+  FREE_MAYBE (url->fragment);
+  FREE_MAYBE (url->user);
+  FREE_MAYBE (url->passwd);
+
+  xfree (url->dir);
+  xfree (url->file);
+
+  xfree (url);
 }
 \f
-urlpos *
+struct urlpos *
 get_urls_file (const char *file)
 {
   struct file_memory *fm;
-  urlpos *head, *tail;
+  struct urlpos *head, *tail;
   const char *text, *text_end;
 
   /* Load the file.  */
@@ -836,10 +1005,28 @@ get_urls_file (const char *file)
        --line_end;
       if (line_end > line_beg)
        {
-         urlpos *entry = (urlpos *)xmalloc (sizeof (urlpos));
+         int up_error_code;
+         char *url_text;
+         struct urlpos *entry;
+         struct url *url;
+
+         /* We must copy the URL to a zero-terminated string.  *sigh*.  */
+         url_text = strdupdelim (line_beg, line_end);
+         url = url_parse (url_text, &up_error_code);
+         if (!url)
+           {
+             logprintf (LOG_NOTQUIET, "%s: Invalid URL %s: %s\n",
+                        file, url_text, url_error (up_error_code));
+             xfree (url_text);
+             continue;
+           }
+         xfree (url_text);
+
+         entry = (struct urlpos *)xmalloc (sizeof (struct urlpos));
          memset (entry, 0, sizeof (*entry));
          entry->next = NULL;
-         entry->url = strdupdelim (line_beg, line_end);
+         entry->url = url;
+
          if (!head)
            head = entry;
          else
@@ -853,12 +1040,13 @@ get_urls_file (const char *file)
 \f
 /* Free the linked list of urlpos.  */
 void
-free_urlpos (urlpos *l)
+free_urlpos (struct urlpos *l)
 {
   while (l)
     {
-      urlpos *next = l->next;
-      xfree (l->url);
+      struct urlpos *next = l->next;
+      if (l->url)
+       url_free (l->url);
       FREE_MAYBE (l->local_name);
       xfree (l);
       l = next;
@@ -954,14 +1142,13 @@ count_slashes (const char *s)
 /* Return the path name of the URL-equivalent file name, with a
    remote-like structure of directories.  */
 static char *
-mkstruct (const struct urlinfo *u)
+mkstruct (const struct url *u)
 {
-  char *host, *dir, *file, *res, *dirpref;
+  char *dir, *dir_preencoding;
+  char *file, *res, *dirpref;
+  char *query = u->query && *u->query ? u->query : NULL;
   int l;
 
-  assert (u->dir != NULL);
-  assert (u->host != NULL);
-
   if (opt.cut_dirs)
     {
       char *ptr = u->dir + (*u->dir == '/');
@@ -975,36 +1162,35 @@ mkstruct (const struct urlinfo *u)
   else
     dir = u->dir + (*u->dir == '/');
 
-  host = xstrdup (u->host);
   /* Check for the true name (or at least a consistent name for saving
      to directory) of HOST, reusing the hlist if possible.  */
-  if (opt.add_hostdir && !opt.simple_check)
-    {
-      char *nhost = realhost (host);
-      xfree (host);
-      host = nhost;
-    }
-  /* Add dir_prefix and hostname (if required) to the beginning of
-     dir.  */
   if (opt.add_hostdir)
     {
+      /* Add dir_prefix and hostname (if required) to the beginning of
+        dir.  */
+      dirpref = (char *)alloca (strlen (opt.dir_prefix) + 1
+                               + strlen (u->host)
+                               + 1 + numdigit (u->port)
+                               + 1);
       if (!DOTP (opt.dir_prefix))
+       sprintf (dirpref, "%s/%s", opt.dir_prefix, u->host);
+      else
+       strcpy (dirpref, u->host);
+
+      if (u->port != scheme_default_port (u->scheme))
        {
-         dirpref = (char *)alloca (strlen (opt.dir_prefix) + 1
-                                   + strlen (host) + 1);
-         sprintf (dirpref, "%s/%s", opt.dir_prefix, host);
+         int len = strlen (dirpref);
+         dirpref[len] = ':';
+         long_to_string (dirpref + len + 1, u->port);
        }
-      else
-       STRDUP_ALLOCA (dirpref, host);
     }
-  else                         /* not add_hostdir */
+  else                         /* not add_hostdir */
     {
       if (!DOTP (opt.dir_prefix))
        dirpref = opt.dir_prefix;
       else
        dirpref = "";
     }
-  xfree (host);
 
   /* If there is a prefix, prepend it.  */
   if (*dirpref)
@@ -1013,7 +1199,10 @@ mkstruct (const struct urlinfo *u)
       sprintf (newdir, "%s%s%s", dirpref, *dir == '/' ? "" : "/", dir);
       dir = newdir;
     }
-  dir = encode_string (dir);
+
+  dir_preencoding = dir;
+  dir = reencode_string (dir_preencoding);
+
   l = strlen (dir);
   if (l && dir[l - 1] == '/')
     dir[l - 1] = '\0';
@@ -1024,48 +1213,81 @@ mkstruct (const struct urlinfo *u)
     file = u->file;
 
   /* Finally, construct the full name.  */
-  res = (char *)xmalloc (strlen (dir) + 1 + strlen (file) + 1);
+  res = (char *)xmalloc (strlen (dir) + 1 + strlen (file)
+                        + (query ? (1 + strlen (query)) : 0)
+                        + 1);
   sprintf (res, "%s%s%s", dir, *dir ? "/" : "", file);
-  xfree (dir);
+  if (query)
+    {
+      strcat (res, "?");
+      strcat (res, query);
+    }
+  if (dir != dir_preencoding)
+    xfree (dir);
   return res;
 }
 
-/* Return a malloced copy of S, but protect any '/' characters. */
+/* Compose a file name out of BASE, an unescaped file name, and QUERY,
+   an escaped query string.  The trick is to make sure that unsafe
+   characters in BASE are escaped, and that slashes in QUERY are also
+   escaped.  */
 
 static char *
-file_name_protect_query_string (const char *s)
+compose_file_name (char *base, char *query)
 {
-  const char *from;
-  char *to, *dest;
-  int destlen = 0;
-  for (from = s; *from; from++)
+  char result[256];
+  char *from;
+  char *to = result;
+
+  /* Copy BASE to RESULT and encode all unsafe characters.  */
+  from = base;
+  while (*from && to - result < sizeof (result))
     {
-      ++destlen;
-      if (*from == '/')
-       destlen += 2;           /* each / gets replaced with %2F, so
-                                  it adds two more chars.  */
+      if (UNSAFE_CHAR (*from))
+       {
+         unsigned char c = *from++;
+         *to++ = '%';
+         *to++ = XDIGIT_TO_XCHAR (c >> 4);
+         *to++ = XDIGIT_TO_XCHAR (c & 0xf);
+       }
+      else
+       *to++ = *from++;
     }
-  dest = (char *)xmalloc (destlen + 1);
-  for (from = s, to = dest; *from; from++)
+
+  if (query && to - result < sizeof (result))
     {
-      if (*from != '/')
-       *to++ = *from;
-      else
+      *to++ = '?';
+
+      /* Copy QUERY to RESULT and encode all '/' characters. */
+      from = query;
+      while (*from && to - result < sizeof (result))
        {
-         *to++ = '%';
-         *to++ = '2';
-         *to++ = 'F';
+         if (*from == '/')
+           {
+             *to++ = '%';
+             *to++ = '2';
+             *to++ = 'F';
+             ++from;
+           }
+         else
+           *to++ = *from++;
        }
     }
-  assert (to - dest == destlen);
-  *to = '\0';
-  return dest;
+
+  if (to - result < sizeof (result))
+    *to = '\0';
+  else
+    /* Truncate input which is too long, presumably due to a huge
+       query string.  */
+    result[sizeof (result) - 1] = '\0';
+
+  return xstrdup (result);
 }
 
 /* Create a unique filename, corresponding to a given URL.  Calls
    mkstruct if necessary.  Does *not* actually create any directories.  */
 char *
-url_filename (const struct urlinfo *u)
+url_filename (const struct url *u)
 {
   char *file, *name;
   int have_prefix = 0;         /* whether we must prepend opt.dir_prefix */
@@ -1077,23 +1299,9 @@ url_filename (const struct urlinfo *u)
     }
   else
     {
-      if (!*u->file)
-       file = xstrdup ("index.html");
-      else
-       {
-         /* If the URL came with a query string, u->file will contain
-            a question mark followed by query string contents.  These
-            contents can contain '/' which would make us create
-            unwanted directories.  These slashes must be protected
-            explicitly.  */
-         if (!strchr (u->file, '/'))
-           file = xstrdup (u->file);
-         else
-           {
-             /*assert (strchr (u->file, '?') != NULL);*/
-             file = file_name_protect_query_string (u->file);
-           }
-       }
+      char *base = *u->file ? u->file : "index.html";
+      char *query = u->query && *u->query ? u->query : NULL;
+      file = compose_file_name (base, query);
     }
 
   if (!have_prefix)
@@ -1142,10 +1350,8 @@ url_filename (const struct urlinfo *u)
 static int
 urlpath_length (const char *url)
 {
-  const char *q = strchr (url, '?');
-  if (q)
-    return q - url;
-  return strlen (url);
+  const char *q = strpbrk_or_eos (url, "?;#");
+  return q - url;
 }
 
 /* Find the last occurrence of character C in the range [b, e), or
@@ -1167,7 +1373,7 @@ find_last_char (const char *b, const char *e, char c)
    Either of the URIs may be absolute or relative, complete with the
    host name, or path only.  This tries to behave "reasonably" in all
    foreseeable cases.  It employs little specific knowledge about
-   protocols or URL-specific stuff -- it just works on strings.
+   schemes or URL-specific stuff -- it just works on strings.
 
    The parameters LINKLENGTH is useful if LINK is not zero-terminated.
    See uri_merge for a gentler interface to this functionality.
@@ -1175,71 +1381,50 @@ find_last_char (const char *b, const char *e, char c)
    #### This function should handle `./' and `../' so that the evil
    path_simplify can go.  */
 static char *
-uri_merge_1 (const char *base, const char *link, int linklength, int no_proto)
+uri_merge_1 (const char *base, const char *link, int linklength, int no_scheme)
 {
   char *constr;
 
-  if (no_proto)
+  if (no_scheme)
     {
       const char *end = base + urlpath_length (base);
 
-      if (*link != '/')
+      if (!*link)
        {
-         /* LINK is a relative URL: we need to replace everything
-            after last slash (possibly empty) with LINK.
-
-            So, if BASE is "whatever/foo/bar", and LINK is "qux/xyzzy",
-            our result should be "whatever/foo/qux/xyzzy".  */
-         int need_explicit_slash = 0;
-         int span;
-         const char *start_insert;
-         const char *last_slash = find_last_char (base, end, '/');
-         if (!last_slash)
-           {
-             /* No slash found at all.  Append LINK to what we have,
-                but we'll need a slash as a separator.
-
-                Example: if base == "foo" and link == "qux/xyzzy", then
-                we cannot just append link to base, because we'd get
-                "fooqux/xyzzy", whereas what we want is
-                "foo/qux/xyzzy".
-
-                To make sure the / gets inserted, we set
-                need_explicit_slash to 1.  We also set start_insert
-                to end + 1, so that the length calculations work out
-                correctly for one more (slash) character.  Accessing
-                that character is fine, since it will be the
-                delimiter, '\0' or '?'.  */
-             /* example: "foo?..." */
-             /*               ^    ('?' gets changed to '/') */
-             start_insert = end + 1;
-             need_explicit_slash = 1;
-           }
-         else if (last_slash && last_slash != base && *(last_slash - 1) == '/')
-           {
-             /* example: http://host"  */
-             /*                      ^ */
-             start_insert = end + 1;
-             need_explicit_slash = 1;
-           }
-         else
-           {
-             /* example: "whatever/foo/bar" */
-             /*                        ^    */
-             start_insert = last_slash + 1;
-           }
-
-         span = start_insert - base;
-         constr = (char *)xmalloc (span + linklength + 1);
-         if (span)
-           memcpy (constr, base, span);
-         if (need_explicit_slash)
-           constr[span - 1] = '/';
-         if (linklength)
-           memcpy (constr + span, link, linklength);
-         constr[span + linklength] = '\0';
+         /* Empty LINK points back to BASE, query string and all. */
+         constr = xstrdup (base);
+       }
+      else if (*link == '?')
+       {
+         /* LINK points to the same location, but changes the query
+            string.  Examples: */
+         /* uri_merge("path",         "?new") -> "path?new"     */
+         /* uri_merge("path?foo",     "?new") -> "path?new"     */
+         /* uri_merge("path?foo#bar", "?new") -> "path?new"     */
+         /* uri_merge("path#foo",     "?new") -> "path?new"     */
+         int baselength = end - base;
+         constr = xmalloc (baselength + linklength + 1);
+         memcpy (constr, base, baselength);
+         memcpy (constr + baselength, link, linklength);
+         constr[baselength + linklength] = '\0';
+       }
+      else if (*link == '#')
+       {
+         /* uri_merge("path",         "#new") -> "path#new"     */
+         /* uri_merge("path#foo",     "#new") -> "path#new"     */
+         /* uri_merge("path?foo",     "#new") -> "path?foo#new" */
+         /* uri_merge("path?foo#bar", "#new") -> "path?foo#new" */
+         int baselength;
+         const char *end1 = strchr (base, '#');
+         if (!end1)
+           end1 = base + strlen (base);
+         baselength = end1 - base;
+         constr = xmalloc (baselength + linklength + 1);
+         memcpy (constr, base, baselength);
+         memcpy (constr + baselength, link, linklength);
+         constr[baselength + linklength] = '\0';
        }
-      else /* *link == `/' */
+      else if (*link == '/')
        {
          /* LINK is an absolute path: we need to replace everything
              after (and including) the FIRST slash with LINK.
@@ -1295,8 +1480,64 @@ uri_merge_1 (const char *base, const char *link, int linklength, int no_proto)
            memcpy (constr + span, link, linklength);
          constr[span + linklength] = '\0';
        }
+      else
+       {
+         /* LINK is a relative URL: we need to replace everything
+            after last slash (possibly empty) with LINK.
+
+            So, if BASE is "whatever/foo/bar", and LINK is "qux/xyzzy",
+            our result should be "whatever/foo/qux/xyzzy".  */
+         int need_explicit_slash = 0;
+         int span;
+         const char *start_insert;
+         const char *last_slash = find_last_char (base, end, '/');
+         if (!last_slash)
+           {
+             /* No slash found at all.  Append LINK to what we have,
+                but we'll need a slash as a separator.
+
+                Example: if base == "foo" and link == "qux/xyzzy", then
+                we cannot just append link to base, because we'd get
+                "fooqux/xyzzy", whereas what we want is
+                "foo/qux/xyzzy".
+
+                To make sure the / gets inserted, we set
+                need_explicit_slash to 1.  We also set start_insert
+                to end + 1, so that the length calculations work out
+                correctly for one more (slash) character.  Accessing
+                that character is fine, since it will be the
+                delimiter, '\0' or '?'.  */
+             /* example: "foo?..." */
+             /*               ^    ('?' gets changed to '/') */
+             start_insert = end + 1;
+             need_explicit_slash = 1;
+           }
+         else if (last_slash && last_slash != base && *(last_slash - 1) == '/')
+           {
+             /* example: http://host"  */
+             /*                      ^ */
+             start_insert = end + 1;
+             need_explicit_slash = 1;
+           }
+         else
+           {
+             /* example: "whatever/foo/bar" */
+             /*                        ^    */
+             start_insert = last_slash + 1;
+           }
+
+         span = start_insert - base;
+         constr = (char *)xmalloc (span + linklength + 1);
+         if (span)
+           memcpy (constr, base, span);
+         if (need_explicit_slash)
+           constr[span - 1] = '/';
+         if (linklength)
+           memcpy (constr + span, link, linklength);
+         constr[span + linklength] = '\0';
+       }
     }
-  else /* !no_proto */
+  else /* !no_scheme */
     {
       constr = strdupdelim (link, link + linklength);
     }
@@ -1309,42 +1550,140 @@ uri_merge_1 (const char *base, const char *link, int linklength, int no_proto)
 char *
 uri_merge (const char *base, const char *link)
 {
-  return uri_merge_1 (base, link, strlen (link), !has_proto (link));
+  return uri_merge_1 (base, link, strlen (link), !url_has_scheme (link));
 }
 \f
-/* Optimize URL by host, destructively replacing u->host with realhost
-   (u->host).  Do this regardless of opt.simple_check.  */
-void
-opt_url (struct urlinfo *u)
+#define APPEND(p, s) do {                      \
+  int len = strlen (s);                                \
+  memcpy (p, s, len);                          \
+  p += len;                                    \
+} while (0)
+
+/* Use this instead of password when the actual password is supposed
+   to be hidden.  We intentionally use a generic string without giving
+   away the number of characters in the password, like previous
+   versions did.  */
+#define HIDDEN_PASSWORD "*password*"
+
+/* Recreate the URL string from the data in URL.
+
+   If HIDE is non-zero (as it is when we're calling this on a URL we
+   plan to print, but not when calling it to canonicalize a URL for
+   use within the program), password will be hidden.  Unsafe
+   characters in the URL will be quoted.  */
+
+char *
+url_string (const struct url *url, int hide_password)
 {
-  /* Find the "true" host.  */
-  char *host = realhost (u->host);
-  xfree (u->host);
-  u->host = host;
-  assert (u->dir != NULL);      /* the URL must have been parsed */
-  /* Refresh the printed representation.  */
-  xfree (u->url);
-  u->url = str_url (u, 0);
+  int size;
+  char *result, *p;
+  char *quoted_user = NULL, *quoted_passwd = NULL;
+
+  int scheme_port  = supported_schemes[url->scheme].default_port;
+  char *scheme_str = supported_schemes[url->scheme].leading_string;
+  int fplen = full_path_length (url);
+
+  assert (scheme_str != NULL);
+
+  /* Make sure the user name and password are quoted. */
+  if (url->user)
+    {
+      quoted_user = encode_string_maybe (url->user);
+      if (url->passwd)
+       {
+         if (hide_password)
+           quoted_passwd = HIDDEN_PASSWORD;
+         else
+           quoted_passwd = encode_string_maybe (url->passwd);
+       }
+    }
+
+  size = (strlen (scheme_str)
+         + strlen (url->host)
+         + fplen
+         + 1);
+  if (url->port != scheme_port)
+    size += 1 + numdigit (url->port);
+  if (quoted_user)
+    {
+      size += 1 + strlen (quoted_user);
+      if (quoted_passwd)
+       size += 1 + strlen (quoted_passwd);
+    }
+
+  p = result = xmalloc (size);
+
+  APPEND (p, scheme_str);
+  if (quoted_user)
+    {
+      APPEND (p, quoted_user);
+      if (quoted_passwd)
+       {
+         *p++ = ':';
+         APPEND (p, quoted_passwd);
+       }
+      *p++ = '@';
+    }
+
+  APPEND (p, url->host);
+  if (url->port != scheme_port)
+    {
+      *p++ = ':';
+      long_to_string (p, url->port);
+      p += strlen (p);
+    }
+
+  full_path_write (url, p);
+  p += fplen;
+  *p++ = '\0';
+
+  assert (p - result == size);
+
+  if (quoted_user && quoted_user != url->user)
+    xfree (quoted_user);
+  if (quoted_passwd && !hide_password
+      && quoted_passwd != url->passwd)
+    xfree (quoted_passwd);
+
+  return result;
 }
 \f
-/* Returns proxy host address, in accordance with PROTO.  */
+/* Returns proxy host address, in accordance with SCHEME.  */
 char *
-getproxy (uerr_t proto)
+getproxy (enum url_scheme scheme)
 {
-  char *proxy;
+  char *proxy = NULL;
+  char *rewritten_url;
+  static char rewritten_storage[1024];
 
-  if (proto == URLHTTP)
-    proxy = opt.http_proxy ? opt.http_proxy : getenv ("http_proxy");
-  else if (proto == URLFTP)
-    proxy = opt.ftp_proxy ? opt.ftp_proxy : getenv ("ftp_proxy");
+  switch (scheme)
+    {
+    case SCHEME_HTTP:
+      proxy = opt.http_proxy ? opt.http_proxy : getenv ("http_proxy");
+      break;
 #ifdef HAVE_SSL
-  else if (proto == URLHTTPS)
-    proxy = opt.https_proxy ? opt.https_proxy : getenv ("https_proxy");
-#endif /* HAVE_SSL */
-  else
-    proxy = NULL;
+    case SCHEME_HTTPS:
+      proxy = opt.https_proxy ? opt.https_proxy : getenv ("https_proxy");
+      break;
+#endif
+    case SCHEME_FTP:
+      proxy = opt.ftp_proxy ? opt.ftp_proxy : getenv ("ftp_proxy");
+      break;
+    case SCHEME_INVALID:
+      break;
+    }
   if (!proxy || !*proxy)
     return NULL;
+
+  /* Handle shorthands. */
+  rewritten_url = rewrite_shorthand_url (proxy);
+  if (rewritten_url)
+    {
+      strncpy (rewritten_storage, rewritten_url, sizeof(rewritten_storage));
+      rewritten_storage[sizeof (rewritten_storage) - 1] = '\0';
+      proxy = rewritten_storage;
+    }
+
   return proxy;
 }
 
@@ -1360,16 +1699,21 @@ no_proxy_match (const char *host, const char **no_proxy)
 \f
 static void write_backup_file PARAMS ((const char *, downloaded_file_t));
 static void replace_attr PARAMS ((const char **, int, FILE *, const char *));
+static char *local_quote_string PARAMS ((const char *));
 
-/* Change the links in an HTML document.  Accepts a structure that
-   defines the positions of all the links.  */
+/* Change the links in one HTML file.  LINKS is a list of links in the
+   document, along with their positions and the desired direction of
+   the conversion.  */
 void
-convert_links (const char *file, urlpos *l)
+convert_links (const char *file, struct urlpos *links)
 {
   struct file_memory *fm;
-  FILE               *fp;
-  const char         *p;
-  downloaded_file_t  downloaded_file_return;
+  FILE *fp;
+  const char *p;
+  downloaded_file_t downloaded_file_return;
+
+  struct urlpos *link;
+  int to_url_count = 0, to_file_count = 0;
 
   logprintf (LOG_VERBOSE, _("Converting %s... "), file);
 
@@ -1377,12 +1721,12 @@ convert_links (const char *file, urlpos *l)
     /* First we do a "dry run": go through the list L and see whether
        any URL needs to be converted in the first place.  If not, just
        leave the file alone.  */
-    int count = 0;
-    urlpos *dry = l;
-    for (dry = l; dry; dry = dry->next)
+    int dry_count = 0;
+    struct urlpos *dry = links;
+    for (dry = links; dry; dry = dry->next)
       if (dry->convert != CO_NOCONVERT)
-       ++count;
-    if (!count)
+       ++dry_count;
+    if (!dry_count)
       {
        logputs (LOG_VERBOSE, _("nothing to do.\n"));
        return;
@@ -1424,19 +1768,19 @@ convert_links (const char *file, urlpos *l)
   /* Here we loop through all the URLs in file, replacing those of
      them that are downloaded with relative references.  */
   p = fm->content;
-  for (; l; l = l->next)
+  for (link = links; link; link = link->next)
     {
-      char *url_start = fm->content + l->pos;
+      char *url_start = fm->content + link->pos;
 
-      if (l->pos >= fm->length)
+      if (link->pos >= fm->length)
        {
          DEBUGP (("Something strange is going on.  Please investigate."));
          break;
        }
       /* If the URL is not to be converted, skip it.  */
-      if (l->convert == CO_NOCONVERT)
+      if (link->convert == CO_NOCONVERT)
        {
-         DEBUGP (("Skipping %s at position %d.\n", l->url, l->pos));
+         DEBUGP (("Skipping %s at position %d.\n", link->url->url, link->pos));
          continue;
        }
 
@@ -1444,26 +1788,28 @@ convert_links (const char *file, urlpos *l)
          quote, to the outfile.  */
       fwrite (p, 1, url_start - p, fp);
       p = url_start;
-      if (l->convert == CO_CONVERT_TO_RELATIVE)
+      if (link->convert == CO_CONVERT_TO_RELATIVE)
        {
          /* Convert absolute URL to relative. */
-         char *newname = construct_relative (file, l->local_name);
-         char *quoted_newname = html_quote_string (newname);
-         replace_attr (&p, l->size, fp, quoted_newname);
+         char *newname = construct_relative (file, link->local_name);
+         char *quoted_newname = local_quote_string (newname);
+         replace_attr (&p, link->size, fp, quoted_newname);
          DEBUGP (("TO_RELATIVE: %s to %s at position %d in %s.\n",
-                  l->url, newname, l->pos, file));
+                  link->url->url, newname, link->pos, file));
          xfree (newname);
          xfree (quoted_newname);
+         ++to_file_count;
        }
-      else if (l->convert == CO_CONVERT_TO_COMPLETE)
+      else if (link->convert == CO_CONVERT_TO_COMPLETE)
        {
          /* Convert the link to absolute URL. */
-         char *newlink = l->url;
+         char *newlink = link->url->url;
          char *quoted_newlink = html_quote_string (newlink);
-         replace_attr (&p, l->size, fp, quoted_newlink);
+         replace_attr (&p, link->size, fp, quoted_newlink);
          DEBUGP (("TO_COMPLETE: <something> to %s at position %d in %s.\n",
-                  newlink, l->pos, file));
+                  newlink, link->pos, file));
          xfree (quoted_newlink);
+         ++to_url_count;
        }
     }
   /* Output the rest of the file. */
@@ -1471,7 +1817,8 @@ convert_links (const char *file, urlpos *l)
     fwrite (p, 1, fm->length - (p - fm->content), fp);
   fclose (fp);
   read_file_free (fm);
-  logputs (LOG_VERBOSE, _("done.\n"));
+  logprintf (LOG_VERBOSE,
+            _("%d-%d\n"), to_file_count, to_url_count);
 }
 
 /* Construct and return a malloced copy of the relative link from two
@@ -1528,20 +1875,6 @@ construct_relative (const char *s1, const char *s2)
   return res;
 }
 \f
-/* Add URL to the head of the list L.  */
-urlpos *
-add_url (urlpos *l, const char *url, const char *file)
-{
-  urlpos *t;
-
-  t = (urlpos *)xmalloc (sizeof (urlpos));
-  memset (t, 0, sizeof (*t));
-  t->url = xstrdup (url);
-  t->local_name = xstrdup (file);
-  t->next = l;
-  return t;
-}
-
 static void
 write_backup_file (const char *file, downloaded_file_t downloaded_file_return)
 {
@@ -1612,15 +1945,9 @@ write_backup_file (const char *file, downloaded_file_t downloaded_file_return)
         -- Dan Harkless <wget@harkless.org>
 
          This [adding a field to the urlpos structure] didn't work
-         because convert_file() is called twice: once after all its
-         sublinks have been retrieved in recursive_retrieve(), and
-         once at the end of the day in convert_all_links().  The
-         original linked list collected in recursive_retrieve() is
-         lost after the first invocation of convert_links(), and
-         convert_all_links() makes a new one (it calls get_urls_html()
-         for each file it covers.)  That's why your first approach didn't
-         work.  The way to make it work is perhaps to make this flag a
-         field in the `urls_html' list.
+         because convert_file() is called from convert_all_links at
+         the end of the retrieval with a freshly built new urlpos
+         list.
         -- Hrvoje Niksic <hniksic@arsdigita.com>
       */
       converted_file_ptr = xmalloc(sizeof(*converted_file_ptr));
@@ -1644,10 +1971,10 @@ replace_attr (const char **pp, int raw_size, FILE *fp, const char *new_str)
 
   /* Structure of our string is:
        "...old-contents..."
-       <---  l->size   --->  (with quotes)
+       <---    size    --->  (with quotes)
      OR:
        ...old-contents...
-       <---  l->size  -->    (no quotes)   */
+       <---    size   -->    (no quotes)   */
 
   if (*p == '\"' || *p == '\'')
     {
@@ -1703,13 +2030,100 @@ find_fragment (const char *beg, int size, const char **bp, const char **ep)
   return 0;
 }
 
-typedef struct _downloaded_file_list {
-  char*                          file;
-  downloaded_file_t              download_type;
-  struct _downloaded_file_list*  next;
-} downloaded_file_list;
+/* The idea here was to quote ? as %3F to avoid passing part of the
+   file name as the parameter when browsing the converted file through
+   HTTP.  However, actually doing that breaks local browsing because
+   "index.html%3Ffoo=bar" isn't even recognized as an HTML file!
+   Perhaps this should be controlled by an option, but for now I'm
+   leaving the question marks.
+
+   This is the original docstring of this function:
+
+   FILE should be a relative link to a local file.  It should be
+   quoted as HTML because it will be used in HTML context.  However,
+   we need to quote ? as %3F to avoid passing part of the file name as
+   the parameter.  (This is not a problem when viewing locally, but is
+   if the downloaded and converted tree is served by an HTTP
+   server.)  */
+
+/* Quote string as HTML. */
+
+static char *
+local_quote_string (const char *file)
+{
+  return html_quote_string (file);
+
+#if 0
+  const char *file_sans_qmark;
+  int qm = count_char (file, '?');
+
+  if (qm)
+    {
+      const char *from = file;
+      char *to, *newname;
+
+      /* qm * 2 because we replace each question mark with "%3F",
+        i.e. replace one char with three, hence two more.  */
+      int fsqlen = strlen (file) + qm * 2;
+
+      to = newname = (char *)alloca (fsqlen + 1);
+      for (; *from; from++)
+       {
+         if (*from != '?')
+           *to++ = *from;
+         else
+           {
+             *to++ = '%';
+             *to++ = '3';
+             *to++ = 'F';
+           }
+       }
+      assert (to - newname == fsqlen);
+      *to = '\0';
+
+      file_sans_qmark = newname;
+    }
+  else
+    file_sans_qmark = file;
+
+  return html_quote_string (file_sans_qmark);
+#endif
+}
+
+/* We're storing "modes" of type downloaded_file_t in the hash table.
+   However, our hash tables only accept pointers for keys and values.
+   So when we need a pointer, we use the address of a
+   downloaded_file_t variable of static storage.  */
+   
+static downloaded_file_t *
+downloaded_mode_to_ptr (downloaded_file_t mode)
+{
+  static downloaded_file_t
+    v1 = FILE_NOT_ALREADY_DOWNLOADED,
+    v2 = FILE_DOWNLOADED_NORMALLY,
+    v3 = FILE_DOWNLOADED_AND_HTML_EXTENSION_ADDED,
+    v4 = CHECK_FOR_FILE;
+
+  switch (mode)
+    {
+    case FILE_NOT_ALREADY_DOWNLOADED:
+      return &v1;
+    case FILE_DOWNLOADED_NORMALLY:
+      return &v2;
+    case FILE_DOWNLOADED_AND_HTML_EXTENSION_ADDED:
+      return &v3;
+    case CHECK_FOR_FILE:
+      return &v4;
+    }
+  return NULL;
+}
+
+/* This should really be merged with dl_file_url_map and
+   downloaded_html_files in recur.c.  This was originally a list, but
+   I changed it to a hash table beause it was actually taking a lot of
+   time to find things in it.  */
 
-static downloaded_file_list *downloaded_files;
+static struct hash_table *downloaded_files_hash;
 
 /* Remembers which files have been downloaded.  In the standard case, should be
    called with mode == FILE_DOWNLOADED_NORMALLY for each file we actually
@@ -1724,46 +2138,47 @@ static downloaded_file_list *downloaded_files;
    it, call with mode == CHECK_FOR_FILE.  Please be sure to call this function
    with local filenames, not remote URLs. */
 downloaded_file_t
-downloaded_file (downloaded_file_t  mode, const char*  file)
+downloaded_file (downloaded_file_t mode, const char *file)
 {
-  boolean                       found_file = FALSE;
-  downloaded_file_list*         rover = downloaded_files;
+  downloaded_file_t *ptr;
 
-  while (rover != NULL)
-    if (strcmp(rover->file, file) == 0)
-      {
-       found_file = TRUE;
-       break;
-      }
-    else
-      rover = rover->next;
-
-  if (found_file)
-    return rover->download_type;  /* file had already been downloaded */
-  else
+  if (mode == CHECK_FOR_FILE)
     {
-      if (mode != CHECK_FOR_FILE)
-       {
-         rover = xmalloc(sizeof(*rover));
-         rover->file = xstrdup(file); /* use xstrdup() so die on out-of-mem. */
-         rover->download_type = mode;
-         rover->next = downloaded_files;
-         downloaded_files = rover;
-       }
-
-      return FILE_NOT_ALREADY_DOWNLOADED;
+      if (!downloaded_files_hash)
+       return FILE_NOT_ALREADY_DOWNLOADED;
+      ptr = hash_table_get (downloaded_files_hash, file);
+      if (!ptr)
+       return FILE_NOT_ALREADY_DOWNLOADED;
+      return *ptr;
     }
+
+  if (!downloaded_files_hash)
+    downloaded_files_hash = make_string_hash_table (0);
+
+  ptr = hash_table_get (downloaded_files_hash, file);
+  if (ptr)
+    return *ptr;
+
+  ptr = downloaded_mode_to_ptr (mode);
+  hash_table_put (downloaded_files_hash, xstrdup (file), &ptr);
+
+  return FILE_NOT_ALREADY_DOWNLOADED;
+}
+
+static int
+df_free_mapper (void *key, void *value, void *ignored)
+{
+  xfree (key);
+  return 0;
 }
 
 void
 downloaded_files_free (void)
 {
-  downloaded_file_list*         rover = downloaded_files;
-  while (rover)
+  if (downloaded_files_hash)
     {
-      downloaded_file_list *next = rover->next;
-      xfree (rover->file);
-      xfree (rover);
-      rover = next;
+      hash_table_map (downloaded_files_hash, df_free_mapper, NULL);
+      hash_table_destroy (downloaded_files_hash);
+      downloaded_files_hash = NULL;
     }
 }