]> sjero.net Git - wget/blobdiff - src/url.c
[svn] Improved --restrict-file-names to accept ",nocontrol".
[wget] / src / url.c
index 38469418abb6954081c79528b4ae546b7815688c..307da8d572c42a0e053d2a87c285a6406afd1a3a 100644 (file)
--- a/src/url.c
+++ b/src/url.c
@@ -1,5 +1,6 @@
 /* URL handling.
-   Copyright (C) 1995, 1996, 1997, 2000, 2001 Free Software Foundation, Inc.
+   Copyright (C) 1995, 1996, 1997, 2000, 2001, 2003, 2003
+   Free Software Foundation, Inc.
 
 This file is part of GNU Wget.
 
@@ -15,7 +16,17 @@ GNU General Public License for more details.
 
 You should have received a copy of the GNU General Public License
 along with Wget; if not, write to the Free Software
-Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.  */
+Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+In addition, as a special exception, the Free Software Foundation
+gives permission to link the code of its release of Wget with the
+OpenSSL project's "OpenSSL" library (or with modified versions of it
+that use the same license as the "OpenSSL" library), and distribute
+the linked executables.  You must obey the GNU General Public License
+in all respects for all of the code used other than "OpenSSL".  If you
+modify this file, you may extend this exception to your version of the
+file, but you are not obligated to do so.  If you do not wish to do
+so, delete this exception statement from your version.  */
 
 #include <config.h>
 
@@ -48,28 +59,36 @@ extern int errno;
 /* Is X ".."?  */
 #define DDOTP(x) ((*(x) == '.') && (*(x + 1) == '.') && (!*(x + 2)))
 
-static int urlpath_length PARAMS ((const char *));
+static const int NS_INADDRSZ  = 4;
+static const int NS_IN6ADDRSZ = 16;
+static const int NS_INT16SZ = 2;
+
 
 struct scheme_data
 {
   char *leading_string;
   int default_port;
+  int enabled;
 };
 
 /* Supported schemes: */
 static struct scheme_data supported_schemes[] =
 {
-  { "http://",  DEFAULT_HTTP_PORT },
+  { "http://",  DEFAULT_HTTP_PORT,  1 },
 #ifdef HAVE_SSL
-  { "https://", DEFAULT_HTTPS_PORT },
+  { "https://", DEFAULT_HTTPS_PORT, 1 },
 #endif
-  { "ftp://",   DEFAULT_FTP_PORT },
+  { "ftp://",   DEFAULT_FTP_PORT,   1 },
 
   /* SCHEME_INVALID */
-  { NULL,       -1 }
+  { NULL,       -1,                 0 }
 };
 
+/* Forward declarations: */
+
 static char *construct_relative PARAMS ((const char *, const char *));
+static int path_simplify PARAMS ((char *));
+
 
 \f
 /* Support for encoding and decoding of URL strings.  We determine
@@ -77,24 +96,22 @@ static char *construct_relative PARAMS ((const char *, const char *));
    code assumes ASCII character set and 8-bit chars.  */
 
 enum {
+  /* rfc1738 reserved chars, preserved from encoding.  */
   urlchr_reserved = 1,
+
+  /* rfc1738 unsafe chars, plus some more.  */
   urlchr_unsafe   = 2
 };
 
+#define urlchr_test(c, mask) (urlchr_table[(unsigned char)(c)] & (mask))
+#define URL_RESERVED_CHAR(c) urlchr_test(c, urlchr_reserved)
+#define URL_UNSAFE_CHAR(c) urlchr_test(c, urlchr_unsafe)
+
+/* Shorthands for the table: */
 #define R  urlchr_reserved
 #define U  urlchr_unsafe
 #define RU R|U
 
-#define urlchr_test(c, mask) (urlchr_table[(unsigned char)(c)] & (mask))
-
-/* rfc1738 reserved chars, preserved from encoding.  */
-
-#define RESERVED_CHAR(c) urlchr_test(c, urlchr_reserved)
-
-/* rfc1738 unsafe chars, plus some more.  */
-
-#define UNSAFE_CHAR(c) urlchr_test(c, urlchr_unsafe)
-
 const static unsigned char urlchr_table[256] =
 {
   U,  U,  U,  U,   U,  U,  U,  U,   /* NUL SOH STX ETX  EOT ENQ ACK BEL */
@@ -108,7 +125,7 @@ const static unsigned char urlchr_table[256] =
  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   */
-  0,  0,  0,  U,   U,  U,  U,  0,   /* X   Y   Z   [    \   ]   ^   _   */
+  0,  0,  0, RU,   U, RU,  U,  0,   /* X   Y   Z   [    \   ]   ^   _   */
   U,  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   */
@@ -124,15 +141,21 @@ const static unsigned char urlchr_table[256] =
   U, U, U, U,  U, U, U, U,  U, U, U, U,  U, U, U, U,
   U, U, U, U,  U, U, U, U,  U, U, U, U,  U, U, U, U,
 };
+#undef R
+#undef U
+#undef RU
 
-/* Decodes the forms %xy in a URL to the character the hexadecimal
-   code of which is xy.  xy are hexadecimal digits from
-   [0123456789ABCDEF] (case-insensitive).  If x or y are not
-   hex-digits or `%' precedes `\0', the sequence is inserted
-   literally.  */
+/* URL-unescape the string S.
+
+   This is done by transforming the sequences "%HH" to the character
+   represented by the hexadecimal digits HH.  If % is not followed by
+   two hexadecimal digits, it is inserted literally.
+
+   The transformation is done in place.  If you need the original
+   string intact, make a copy before calling this function.  */
 
 static void
-decode_string (char *s)
+url_unescape (char *s)
 {
   char *t = s;                 /* t - tortoise */
   char *h = s;                 /* h - hare     */
@@ -157,10 +180,15 @@ decode_string (char *s)
   *t = '\0';
 }
 
-/* Like encode_string, but return S if there are no unsafe chars.  */
+/* The core of url_escape_* functions.  Escapes the characters that
+   match the provided mask in urlchr_table.
+
+   If ALLOW_PASSTHROUGH is non-zero, a string with no unsafe chars
+   will be returned unchanged.  If ALLOW_PASSTHROUGH is zero, a
+   freshly allocated string will be returned in all cases.  */
 
 static char *
-encode_string_maybe (const char *s)
+url_escape_1 (const char *s, unsigned char mask, int allow_passthrough)
 {
   const char *p1;
   char *p2, *newstr;
@@ -168,11 +196,11 @@ encode_string_maybe (const char *s)
   int addition = 0;
 
   for (p1 = s; *p1; p1++)
-    if (UNSAFE_CHAR (*p1))
+    if (urlchr_test (*p1, mask))
       addition += 2;           /* Two more characters (hex digits) */
 
   if (!addition)
-    return (char *)s;
+    return allow_passthrough ? (char *)s : xstrdup (s);
 
   newlen = (p1 - s) + addition;
   newstr = (char *)xmalloc (newlen + 1);
@@ -181,7 +209,8 @@ encode_string_maybe (const char *s)
   p2 = newstr;
   while (*p1)
     {
-      if (UNSAFE_CHAR (*p1))
+      /* Quote the characters that match the test mask. */
+      if (urlchr_test (*p1, mask))
        {
          unsigned char c = *p1++;
          *p2++ = '%';
@@ -191,37 +220,29 @@ encode_string_maybe (const char *s)
       else
        *p2++ = *p1++;
     }
-  *p2 = '\0';
   assert (p2 - newstr == newlen);
+  *p2 = '\0';
 
   return newstr;
 }
 
-/* Encode the unsafe characters (as determined by UNSAFE_CHAR) in a
-   given string, returning a malloc-ed %XX encoded string.  */
-  
+/* URL-escape the unsafe characters (see urlchr_table) in a given
+   string, returning a freshly allocated string.  */
+
 char *
-encode_string (const char *s)
+url_escape (const char *s)
 {
-  char *encoded = encode_string_maybe (s);
-  if (encoded != s)
-    return encoded;
-  else
-    return xstrdup (s);
+  return url_escape_1 (s, urlchr_unsafe, 0);
 }
 
-/* Encode unsafe characters in PTR to %xx.  If such encoding is done,
-   the old value of PTR is freed and PTR is made to point to the newly
-   allocated storage.  */
-
-#define ENCODE(ptr) do {                       \
-  char *e_new = encode_string_maybe (ptr);     \
-  if (e_new != ptr)                            \
-    {                                          \
-      xfree (ptr);                             \
-      ptr = e_new;                             \
-    }                                          \
-} while (0)
+/* URL-escape the unsafe characters (see urlchr_table) in a given
+   string.  If no characters are unsafe, S is returned.  */
+
+static char *
+url_escape_allow_passthrough (const char *s)
+{
+  return url_escape_1 (s, urlchr_unsafe, 1);
+}
 \f
 enum copy_method { CM_DECODE, CM_ENCODE, CM_PASSTHROUGH };
 
@@ -240,7 +261,7 @@ decide_copy_method (const char *p)
          char preempt = (XCHAR_TO_XDIGIT (*(p + 1)) << 4) +
            XCHAR_TO_XDIGIT (*(p + 2));
 
-         if (UNSAFE_CHAR (preempt) || RESERVED_CHAR (preempt))
+         if (URL_UNSAFE_CHAR (preempt) || URL_RESERVED_CHAR (preempt))
            return CM_PASSTHROUGH;
          else
            return CM_DECODE;
@@ -249,20 +270,20 @@ decide_copy_method (const char *p)
        /* Garbled %.. sequence: encode `%'. */
        return CM_ENCODE;
     }
-  else if (UNSAFE_CHAR (*p) && !RESERVED_CHAR (*p))
+  else if (URL_UNSAFE_CHAR (*p) && !URL_RESERVED_CHAR (*p))
     return CM_ENCODE;
   else
     return CM_PASSTHROUGH;
 }
 
-/* Translate a %-quoting (but possibly non-conformant) input string S
-   into a %-quoting (and conformant) output string.  If no characters
+/* Translate a %-escaped (but possibly non-conformant) input string S
+   into a %-escaped (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
+   while those that don't call url_unescape() 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.
@@ -275,20 +296,21 @@ decide_copy_method (const char *p)
 
        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
+   It appears that the unsafe chars need to be quoted, for example
+   with url_escape.  But what if we're requested to download
+   `abc%20def'?  url_escape transforms "%" to "%25", which would leave
+   us with `abc%2520def'.  This is incorrect -- since %-escapes are
+   part of URL syntax, "%20" is the correct way to denote a literal
+   space on the Wget command line.  This leaves us in the conclusion
+   that in that case Wget should not call url_escape, but leave the
+   `%20' as is.
+
+   And what if the requested URI is `abc%20 def'?  If we call
+   url_escape, we end up with `/abc%2520%20def', which is almost
+   certainly not intended.  If we don't call url_escape, we are left
+   with the embedded space and cannot complete the request.  What the
    user meant was for Wget to request `/abc%20%20def', and this is
-   where reencode_string kicks in.
+   where reencode_escapes kicks in.
 
    Wget used to solve this by first decoding %-quotes, and then
    encoding all the "unsafe" characters found in the resulting string.
@@ -299,7 +321,7 @@ decide_copy_method (const char *p)
    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
+   literal plus.  reencode_escapes correctly translates the above to
    "a%2B+b", i.e. returns the original string.
 
    This function uses an algorithm proposed by Anon Sricharoenchai:
@@ -333,8 +355,8 @@ decide_copy_method (const char *p)
    "foo+bar"         -> "foo+bar"            (plus is reserved!)
    "foo%2b+bar"      -> "foo%2b+bar"  */
 
-char *
-reencode_string (const char *s)
+static char *
+reencode_escapes (const char *s)
 {
   const char *p1;
   char *newstr, *p2;
@@ -398,19 +420,6 @@ reencode_string (const char *s)
   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.  */
@@ -420,9 +429,15 @@ 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;
+    if (0 == strncasecmp (url, supported_schemes[i].leading_string,
+                         strlen (supported_schemes[i].leading_string)))
+      {
+       if (supported_schemes[i].enabled)
+         return (enum url_scheme) i;
+       else
+         return SCHEME_INVALID;
+      }
+
   return SCHEME_INVALID;
 }
 
@@ -466,6 +481,12 @@ scheme_default_port (enum url_scheme scheme)
   return supported_schemes[scheme].default_port;
 }
 
+void
+scheme_disable (enum url_scheme scheme)
+{
+  supported_schemes[scheme].enabled = 0;
+}
+
 /* 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 scheme.
@@ -513,6 +534,11 @@ parse_uname (const char *str, int len, char **user, char **passwd)
   memcpy (*user, str, len);
   (*user)[len] = '\0';
 
+  if (*user)
+    url_unescape (*user);
+  if (*passwd)
+    url_unescape (*passwd);
+
   return 1;
 }
 
@@ -546,19 +572,17 @@ rewrite_shorthand_url (const char *url)
 
   if (*p == ':')
     {
-      const char *pp, *path;
+      const char *pp;
       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'))
+      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 '/'. */
@@ -578,6 +602,29 @@ rewrite_shorthand_url (const char *url)
 \f
 static void parse_path PARAMS ((const char *, char **, char **));
 
+/* Like strpbrk, with the exception that it returns the pointer to the
+   terminating zero (end-of-string aka "eos") if no matching character
+   is found.
+
+   Although I normally balk at Gcc-specific optimizations, it probably
+   makes sense here: glibc has optimizations that detect strpbrk being
+   called with literal string as ACCEPT and inline the search.  That
+   optimization is defeated if strpbrk is hidden within the call to
+   another function.  (And no, making strpbrk_or_eos inline doesn't
+   help because the check for literal accept is in the
+   preprocessor.)  */
+
+#ifdef __GNUC__
+
+#define strpbrk_or_eos(s, accept) ({           \
+  char *SOE_p = strpbrk (s, accept);           \
+  if (!SOE_p)                                  \
+    SOE_p = (char *)s + strlen (s);            \
+  SOE_p;                                       \
+})
+
+#else  /* not __GNUC__ */
+
 static char *
 strpbrk_or_eos (const char *s, const char *accept)
 {
@@ -586,6 +633,7 @@ strpbrk_or_eos (const char *s, const char *accept)
     p = (char *)s + strlen (s);
   return p;
 }
+#endif
 
 /* Turn STR into lowercase; return non-zero if a character was
    actually changed. */
@@ -595,7 +643,7 @@ lowercase_str (char *str)
 {
   int change = 0;
   for (; *str; str++)
-    if (!ISLOWER (*str))
+    if (ISUPPER (*str))
       {
        change = 1;
        *str = TOLOWER (*str);
@@ -604,16 +652,22 @@ lowercase_str (char *str)
 }
 
 static char *parse_errors[] = {
-#define PE_NO_ERROR            0
+#define PE_NO_ERROR                    0
   "No error",
-#define PE_UNRECOGNIZED_SCHEME 1
-  "Unrecognized scheme",
-#define PE_EMPTY_HOST          2
+#define PE_UNSUPPORTED_SCHEME          1
+  "Unsupported scheme",
+#define PE_EMPTY_HOST                  2
   "Empty host",
-#define PE_BAD_PORT_NUMBER     3
+#define PE_BAD_PORT_NUMBER             3
   "Bad port number",
-#define PE_INVALID_USER_NAME   4
-  "Invalid user name"
+#define PE_INVALID_USER_NAME           4
+  "Invalid user name",
+#define PE_UNTERMINATED_IPV6_ADDRESS   5
+  "Unterminated IPv6 numeric address",
+#define PE_IPV6_NOT_SUPPORTED          6
+  "IPv6 addresses not supported",
+#define PE_INVALID_IPV6_ADDRESS                7
+  "Invalid IPv6 numeric address"
 };
 
 #define SETERR(p, v) do {                      \
@@ -621,6 +675,138 @@ static char *parse_errors[] = {
     *(p) = (v);                                        \
 } while (0)
 
+#ifdef ENABLE_IPV6
+/* The following two functions were adapted from glibc. */
+
+static int
+is_valid_ipv4_address (const char *str, const char *end)
+{
+  int saw_digit, octets;
+  int val;
+
+  saw_digit = 0;
+  octets = 0;
+  val = 0;
+
+  while (str < end) {
+    int ch = *str++;
+
+    if (ch >= '0' && ch <= '9') {
+      val = val * 10 + (ch - '0');
+
+      if (val > 255)
+        return 0;
+      if (saw_digit == 0) {
+        if (++octets > 4)
+          return 0;
+        saw_digit = 1;
+      }
+    } else if (ch == '.' && saw_digit == 1) {
+      if (octets == 4)
+        return 0;
+      val = 0;
+      saw_digit = 0;
+    } else
+      return 0;
+  }
+  if (octets < 4)
+    return 0;
+  
+  return 1;
+}
+
+static int
+is_valid_ipv6_address (const char *str, const char *end)
+{
+  static const char xdigits[] = "0123456789abcdef";
+  const char *curtok;
+  int tp;
+  const char *colonp;
+  int saw_xdigit;
+  unsigned int val;
+
+  tp = 0;
+  colonp = NULL;
+
+  if (str == end)
+    return 0;
+  
+  /* Leading :: requires some special handling. */
+  if (*str == ':')
+    {
+      ++str;
+      if (str == end || *str != ':')
+       return 0;
+    }
+
+  curtok = str;
+  saw_xdigit = 0;
+  val = 0;
+
+  while (str < end) {
+    int ch = *str++;
+    const char *pch;
+
+    /* if ch is a number, add it to val. */
+    pch = strchr(xdigits, ch);
+    if (pch != NULL) {
+      val <<= 4;
+      val |= (pch - xdigits);
+      if (val > 0xffff)
+       return 0;
+      saw_xdigit = 1;
+      continue;
+    }
+
+    /* if ch is a colon ... */
+    if (ch == ':') {
+      curtok = str;
+      if (saw_xdigit == 0) {
+       if (colonp != NULL)
+         return 0;
+       colonp = str + tp;
+       continue;
+      } else if (str == end) {
+       return 0;
+      }
+      if (tp > NS_IN6ADDRSZ - NS_INT16SZ)
+       return 0;
+      tp += NS_INT16SZ;
+      saw_xdigit = 0;
+      val = 0;
+      continue;
+    }
+
+    /* if ch is a dot ... */
+    if (ch == '.' && (tp <= NS_IN6ADDRSZ - NS_INADDRSZ) &&
+       is_valid_ipv4_address(curtok, end) == 1) {
+      tp += NS_INADDRSZ;
+      saw_xdigit = 0;
+      break;
+    }
+    
+    return 0;
+  }
+
+  if (saw_xdigit == 1) {
+    if (tp > NS_IN6ADDRSZ - NS_INT16SZ) 
+      return 0;
+    tp += NS_INT16SZ;
+  }
+
+  if (colonp != NULL) {
+    if (tp == NS_IN6ADDRSZ) 
+      return 0;
+    tp = NS_IN6ADDRSZ;
+  }
+
+  if (tp != NS_IN6ADDRSZ)
+    return 0;
+
+  return 1;
+}
+#endif
+
 /* Parse a URL.
 
    Return a new struct url if successful, NULL on error.  In case of
@@ -650,11 +836,11 @@ url_parse (const char *url, int *error)
   scheme = url_scheme (url);
   if (scheme == SCHEME_INVALID)
     {
-      SETERR (error, PE_UNRECOGNIZED_SCHEME);
+      SETERR (error, PE_UNSUPPORTED_SCHEME);
       return NULL;
     }
 
-  url_encoded = reencode_string (url);
+  url_encoded = reencode_escapes (url);
   p = url_encoded;
 
   p += strlen (supported_schemes[scheme].leading_string);
@@ -675,8 +861,43 @@ url_parse (const char *url, int *error)
   fragment_b = fragment_e = NULL;
 
   host_b = p;
-  p = strpbrk_or_eos (p, ":/;?#");
-  host_e = p;
+
+  if (*p == '[')
+    {
+      /* Handle IPv6 address inside square brackets.  Ideally we'd
+        just look for the terminating ']', but rfc2732 mandates
+        rejecting invalid IPv6 addresses.  */
+
+      /* The address begins after '['. */
+      host_b = p + 1;
+      host_e = strchr (host_b, ']');
+
+      if (!host_e)
+       {
+         SETERR (error, PE_UNTERMINATED_IPV6_ADDRESS);
+         return NULL;
+       }
+
+#ifdef ENABLE_IPV6
+      /* Check if the IPv6 address is valid. */
+      if (!is_valid_ipv6_address(host_b, host_e))
+       {
+         SETERR (error, PE_INVALID_IPV6_ADDRESS);
+         return NULL;
+       }
+
+      /* Continue parsing after the closing ']'. */
+      p = host_e + 1;
+#else
+      SETERR (error, PE_IPV6_NOT_SUPPORTED);
+      return NULL;
+#endif
+    }
+  else
+    {
+      p = strpbrk_or_eos (p, ":/;?#");
+      host_e = p;
+    }
 
   if (host_b == host_e)
     {
@@ -713,6 +934,7 @@ url_parse (const char *url, int *error)
              SETERR (error, PE_BAD_PORT_NUMBER);
              return NULL;
            }
+         
          port = 10 * port + (*pp - '0');
        }
     }
@@ -743,6 +965,15 @@ url_parse (const char *url, int *error)
       query_b = p;
       p = strpbrk_or_eos (p, "#");
       query_e = p;
+
+      /* Hack that allows users to use '?' (a wildcard character) in
+        FTP URLs without it being interpreted as a query string
+        delimiter.  */
+      if (scheme == SCHEME_FTP)
+       {
+         query_b = query_e = NULL;
+         path_e = p;
+       }
     }
   if (*p == '#')
     {
@@ -787,13 +1018,11 @@ url_parse (const char *url, int *error)
   if (fragment_b)
     u->fragment = strdupdelim (fragment_b, fragment_e);
 
-
-  if (path_modified || u->fragment || host_modified)
+  if (path_modified || u->fragment || host_modified || path_b == path_e)
     {
-      /* 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.  */
+      /* If we suspect that a transformation has rendered what
+        url_string might return different from URL_ENCODED, rebuild
+        u->url using url_string.  */
       u->url = url_string (u, 0);
 
       if (url_encoded != url)
@@ -802,9 +1031,9 @@ url_parse (const char *url, int *error)
   else
     {
       if (url_encoded == url)
-       u->url    = xstrdup (url);
+       u->url = xstrdup (url);
       else
-       u->url    = url_encoded;
+       u->url = url_encoded;
     }
   url_encoded = NULL;
 
@@ -818,13 +1047,13 @@ url_error (int error_code)
   return parse_errors[error_code];
 }
 
+/* Parse PATH into dir and file.  PATH is extracted from the URL and
+   is URL-escaped.  The function returns unescaped DIR and FILE.  */
+
 static void
-parse_path (const char *quoted_path, char **dir, char **file)
+parse_path (const char *path, char **dir, char **file)
 {
-  char *path, *last_slash;
-
-  STRDUP_ALLOCA (path, quoted_path);
-  decode_string (path);
+  char *last_slash;
 
   last_slash = strrchr (path, '/');
   if (!last_slash)
@@ -837,6 +1066,8 @@ parse_path (const char *quoted_path, char **dir, char **file)
       *dir = strdupdelim (path, last_slash);
       *file = xstrdup (last_slash + 1);
     }
+  url_unescape (*dir);
+  url_unescape (*file);
 }
 
 /* Note: URL's "full path" is the path with the query string and
@@ -886,7 +1117,10 @@ full_path_write (const struct url *url, char *where)
 #undef FROB
 }
 
-/* Public function for getting the "full path". */
+/* Public function for getting the "full path".  E.g. if u->path is
+   "foo/bar" and u->query is "param=value", full_path will be
+   "/foo/bar?param=value". */
+
 char *
 url_full_path (const struct url *url)
 {
@@ -899,37 +1133,83 @@ url_full_path (const struct url *url)
   return full_path;
 }
 
-/* Sync u->path and u->url with u->dir and u->file. */
-static void
-sync_path (struct url *url)
+/* Escape unsafe and reserved characters, except for the slash
+   characters.  */
+
+static char *
+url_escape_dir (const char *dir)
 {
-  char *newpath;
+  char *newdir = url_escape_1 (dir, urlchr_unsafe | urlchr_reserved, 1);
+  char *h, *t;
+  if (newdir == dir)
+    return (char *)dir;
 
-  xfree (url->path);
+  /* Unescape slashes in NEWDIR. */
 
-  if (!*url->dir)
+  h = newdir;                  /* hare */
+  t = newdir;                  /* tortoise */
+
+  for (; *h; h++, t++)
     {
-      newpath = xstrdup (url->file);
-      REENCODE (newpath);
+      if (*h == '%' && h[1] == '2' && h[2] == 'F')
+       {
+         *t = '/';
+         h += 2;
+       }
+      else
+       *t = *h;
     }
+  *t = '\0';
+
+  return newdir;
+}
+
+/* Sync u->path and u->url with u->dir and u->file.  Called after
+   u->file or u->dir have been changed, typically by the FTP code.  */
+
+static void
+sync_path (struct url *u)
+{
+  char *newpath, *efile, *edir;
+
+  xfree (u->path);
+
+  /* u->dir and u->file are not escaped.  URL-escape them before
+     reassembling them into u->path.  That way, if they contain
+     separators like '?' or even if u->file contains slashes, the
+     path will be correctly assembled.  (u->file can contain slashes
+     if the URL specifies it with %2f, or if an FTP server returns
+     it.)  */
+  edir = url_escape_dir (u->dir);
+  efile = url_escape_1 (u->file, urlchr_unsafe | urlchr_reserved, 1);
+
+  if (!*edir)
+    newpath = xstrdup (efile);
   else
     {
-      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);
+      int dirlen = strlen (edir);
+      int filelen = strlen (efile);
+
+      /* Copy "DIR/FILE" to newpath. */
+      char *p = newpath = xmalloc (dirlen + 1 + filelen + 1);
+      memcpy (p, edir, dirlen);
+      p += dirlen;
+      *p++ = '/';
+      memcpy (p, efile, filelen);
+      p += filelen;
+      *p++ = '\0';
     }
 
-  url->path = newpath;
+  u->path = newpath;
 
-  /* Synchronize u->url. */
-  xfree (url->url);
-  url->url = url_string (url, 0);
+  if (edir != u->dir)
+    xfree (edir);
+  if (efile != u->file)
+    xfree (efile);
+
+  /* Regenerate u->url as well.  */
+  xfree (u->url);
+  u->url = url_string (u, 0);
 }
 
 /* Mutators.  Code in ftp.c insists on changing u->dir and u->file.
@@ -985,6 +1265,7 @@ get_urls_file (const char *file)
       return NULL;
     }
   DEBUGP (("Loaded %s (size %ld).\n", file, fm->length));
+
   head = tail = NULL;
   text = fm->content;
   text_end = fm->content + fm->length;
@@ -997,21 +1278,34 @@ get_urls_file (const char *file)
       else
        ++line_end;
       text = line_end;
-      while (line_beg < line_end
-            && ISSPACE (*line_beg))
+
+      /* Strip whitespace from the beginning and end of line. */
+      while (line_beg < line_end && ISSPACE (*line_beg))
        ++line_beg;
-      while (line_end > line_beg + 1
-            && ISSPACE (*(line_end - 1)))
+      while (line_end > line_beg && ISSPACE (*(line_end - 1)))
        --line_end;
+
       if (line_end > line_beg)
        {
+         /* URL is in the [line_beg, line_end) region. */
+
          int up_error_code;
          char *url_text;
          struct urlpos *entry;
          struct url *url;
 
-         /* We must copy the URL to a zero-terminated string.  *sigh*.  */
+         /* We must copy the URL to a zero-terminated string, and we
+            can't use alloca because we're in a loop.  *sigh*.  */
          url_text = strdupdelim (line_beg, line_end);
+
+         if (opt.base_href)
+           {
+             /* Merge opt.base_href with URL. */
+             char *merged = uri_merge (opt.base_href, url_text);
+             xfree (url_text);
+             url_text = merged;
+           }
+
          url = url_parse (url_text, &up_error_code);
          if (!url)
            {
@@ -1071,8 +1365,6 @@ rotate_backups(const char *fname)
     {
       sprintf (from, "%s.%d", fname, i - 1);
       sprintf (to, "%s.%d", fname, i);
-      /* #### This will fail on machines without the rename() system
-         call.  */
       rename (from, to);
     }
 
@@ -1091,11 +1383,14 @@ mkalldirs (const char *path)
   int res;
 
   p = path + strlen (path);
-  for (; *p != '/' && p != path; p--);
+  for (; *p != '/' && p != path; p--)
+    ;
+
   /* Don't create if it's just a file.  */
   if ((p == path) && (*p != '/'))
     return 0;
   t = strdupdelim (path, p);
+
   /* Check whether the directory exists.  */
   if ((stat (t, &st) == 0))
     {
@@ -1128,205 +1423,288 @@ mkalldirs (const char *path)
   xfree (t);
   return res;
 }
+\f
+/* Functions for constructing the file name out of URL components.  */
 
-static int
-count_slashes (const char *s)
+/* A growable string structure, used by url_file_name and friends.
+   This should perhaps be moved to utils.c.
+
+   The idea is to have a convenient and efficient way to construct a
+   string by having various functions append data to it.  Instead of
+   passing the obligatory BASEVAR, SIZEVAR and TAILPOS to all the
+   functions in questions, we pass the pointer to this struct.  */
+
+struct growable {
+  char *base;
+  int size;
+  int tail;
+};
+
+/* Ensure that the string can accept APPEND_COUNT more characters past
+   the current TAIL position.  If necessary, this will grow the string
+   and update its allocated size.  If the string is already large
+   enough to take TAIL+APPEND_COUNT characters, this does nothing.  */
+#define GROW(g, append_size) do {                                      \
+  struct growable *G_ = g;                                             \
+  DO_REALLOC (G_->base, G_->size, G_->tail + append_size, char);       \
+} while (0)
+
+/* Return the tail position of the string. */
+#define TAIL(r) ((r)->base + (r)->tail)
+
+/* Move the tail position by APPEND_COUNT characters. */
+#define TAIL_INCR(r, append_count) ((r)->tail += append_count)
+
+/* Append the string STR to DEST.  NOTICE: the string in DEST is not
+   terminated.  */
+
+static void
+append_string (const char *str, struct growable *dest)
 {
-  int i = 0;
-  while (*s)
-    if (*s++ == '/')
-      ++i;
-  return i;
+  int l = strlen (str);
+  GROW (dest, l);
+  memcpy (TAIL (dest), str, l);
+  TAIL_INCR (dest, l);
 }
 
-/* Return the path name of the URL-equivalent file name, with a
-   remote-like structure of directories.  */
-static char *
-mkstruct (const struct url *u)
+/* Append CH to DEST.  For example, append_char (0, DEST)
+   zero-terminates DEST.  */
+
+static void
+append_char (char ch, struct growable *dest)
 {
-  char *dir, *dir_preencoding;
-  char *file, *res, *dirpref;
-  char *query = u->query && *u->query ? u->query : NULL;
-  int l;
+  GROW (dest, 1);
+  *TAIL (dest) = ch;
+  TAIL_INCR (dest, 1);
+}
 
-  if (opt.cut_dirs)
-    {
-      char *ptr = u->dir + (*u->dir == '/');
-      int slash_count = 1 + count_slashes (ptr);
-      int cut = MINVAL (opt.cut_dirs, slash_count);
-      for (; cut && *ptr; ptr++)
-       if (*ptr == '/')
-         --cut;
-      STRDUP_ALLOCA (dir, ptr);
-    }
-  else
-    dir = u->dir + (*u->dir == '/');
+enum {
+  filechr_not_unix    = 1,     /* unusable on Unix, / and \0 */
+  filechr_not_windows = 2,     /* unusable on Windows, one of \|/<>?:*" */
+  filechr_control     = 4,     /* a control character, e.g. 0-31 */
+};
 
-  /* 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)
-    {
-      /* 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);
+#define FILE_CHAR_TEST(c, mask) (filechr_table[(unsigned char)(c)] & (mask))
 
-      if (u->port != scheme_default_port (u->scheme))
-       {
-         int len = strlen (dirpref);
-         dirpref[len] = ':';
-         long_to_string (dirpref + len + 1, u->port);
-       }
-    }
-  else                         /* not add_hostdir */
-    {
-      if (!DOTP (opt.dir_prefix))
-       dirpref = opt.dir_prefix;
-      else
-       dirpref = "";
-    }
+/* Shorthands for the table: */
+#define U filechr_not_unix
+#define W filechr_not_windows
+#define C filechr_control
 
-  /* If there is a prefix, prepend it.  */
-  if (*dirpref)
-    {
-      char *newdir = (char *)alloca (strlen (dirpref) + 1 + strlen (dir) + 2);
-      sprintf (newdir, "%s%s%s", dirpref, *dir == '/' ? "" : "/", dir);
-      dir = newdir;
-    }
+#define UW U|W
+#define UWC U|W|C
 
-  dir_preencoding = dir;
-  dir = reencode_string (dir_preencoding);
+/* Table of characters unsafe under various conditions (see above).
 
-  l = strlen (dir);
-  if (l && dir[l - 1] == '/')
-    dir[l - 1] = '\0';
+   Arguably we could also claim `%' to be unsafe, since we use it as
+   the escape character.  If we ever want to be able to reliably
+   translate file name back to URL, this would become important
+   crucial.  Right now, it's better to be minimal in escaping.  */
 
-  if (!*u->file)
-    file = "index.html";
-  else
-    file = u->file;
-
-  /* Finally, construct the full name.  */
-  res = (char *)xmalloc (strlen (dir) + 1 + strlen (file)
-                        + (query ? (1 + strlen (query)) : 0)
-                        + 1);
-  sprintf (res, "%s%s%s", dir, *dir ? "/" : "", file);
-  if (query)
-    {
-      strcat (res, "?");
-      strcat (res, query);
-    }
-  if (dir != dir_preencoding)
-    xfree (dir);
-  return res;
-}
+const static unsigned char filechr_table[256] =
+{
+UWC,  C,  C,  C,   C,  C,  C,  C,   /* NUL SOH STX ETX  EOT ENQ ACK BEL */
+  C,  C,  C,  C,   C,  C,  C,  C,   /* BS  HT  LF  VT   FF  CR  SO  SI  */
+  C,  C,  C,  C,   C,  C,  C,  C,   /* DLE DC1 DC2 DC3  DC4 NAK SYN ETB */
+  C,  C,  C,  C,   C,  C,  C,  C,   /* CAN EM  SUB ESC  FS  GS  RS  US  */
+  0,  0,  W,  0,   0,  0,  0,  0,   /* SP  !   "   #    $   %   &   '   */
+  0,  0,  W,  0,   0,  0,  0, UW,   /* (   )   *   +    ,   -   .   /   */
+  0,  0,  0,  0,   0,  0,  0,  0,   /* 0   1   2   3    4   5   6   7   */
+  0,  0,  W,  0,   W,  0,  W,  W,   /* 8   9   :   ;    <   =   >   ?   */
+  0,  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   */
+  0,  0,  0,  0,   W,  0,  0,  0,   /* X   Y   Z   [    \   ]   ^   _   */
+  0,  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   */
+  0,  0,  0,  0,   0,  0,  0,  0,   /* x   y   z   {    |   }   ~   DEL */
 
-/* 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.  */
+  C, C, C, C,  C, C, C, C,  C, C, C, C,  C, C, C, C, /* 128-143 */
+  C, C, C, C,  C, C, C, C,  C, C, C, C,  C, C, C, C, /* 144-159 */
+  0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,
+  0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,
 
-static char *
-compose_file_name (char *base, char *query)
+  0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,
+  0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,
+  0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,
+  0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,
+};
+
+/* FN_PORT_SEP is the separator between host and port in file names
+   for non-standard port numbers.  On Unix this is normally ':', as in
+   "www.xemacs.org:4001/index.html".  Under Windows, we set it to +
+   because Windows can't handle ':' in file names.  */
+#define FN_PORT_SEP  (opt.restrict_files_os != restrict_windows ? ':' : '+')
+
+/* FN_QUERY_SEP is the separator between the file name and the URL
+   query, normally '?'.  Since Windows cannot handle '?' as part of
+   file name, we use '@' instead there.  */
+#define FN_QUERY_SEP (opt.restrict_files_os != restrict_windows ? '?' : '@')
+
+/* Quote path element, characters in [b, e), as file name, and append
+   the quoted string to DEST.  Each character is quoted as per
+   file_unsafe_char and the corresponding table.  */
+
+static void
+append_uri_pathel (const char *b, const char *e, struct growable *dest)
 {
-  char result[256];
-  char *from;
-  char *to = result;
+  char *pathel;
+  int pathlen;
 
-  /* Copy BASE to RESULT and encode all unsafe characters.  */
-  from = base;
-  while (*from && to - result < sizeof (result))
+  const char *p;
+  int quoted, outlen;
+
+  int mask;
+  if (opt.restrict_files_os == restrict_unix)
+    mask = filechr_not_unix;
+  else
+    mask = filechr_not_windows;
+  if (opt.restrict_files_ctrl)
+    mask |= filechr_control;
+
+  /* Copy [b, e) to PATHEL and URL-unescape it. */
+  BOUNDED_TO_ALLOCA (b, e, pathel);
+  url_unescape (pathel);
+  pathlen = strlen (pathel);
+
+  /* Go through PATHEL and check how many characters we'll need to
+     add for file quoting. */
+  quoted = 0;
+  for (p = pathel; *p; p++)
+    if (FILE_CHAR_TEST (*p, mask))
+      ++quoted;
+
+  /* p - pathel is the string length.  Each quoted char means two
+     additional characters in the string, hence 2*quoted.  */
+  outlen = (p - pathel) + (2 * quoted);
+  GROW (dest, outlen);
+
+  if (!quoted)
     {
-      if (UNSAFE_CHAR (*from))
-       {
-         unsigned char c = *from++;
-         *to++ = '%';
-         *to++ = XDIGIT_TO_XCHAR (c >> 4);
-         *to++ = XDIGIT_TO_XCHAR (c & 0xf);
-       }
-      else
-       *to++ = *from++;
+      /* If there's nothing to quote, we don't need to go through the
+        string the second time.  */
+      memcpy (TAIL (dest), pathel, outlen);
     }
-
-  if (query && to - result < sizeof (result))
+  else
     {
-      *to++ = '?';
-
-      /* Copy QUERY to RESULT and encode all '/' characters. */
-      from = query;
-      while (*from && to - result < sizeof (result))
+      char *q = TAIL (dest);
+      for (p = pathel; *p; p++)
        {
-         if (*from == '/')
+         if (!FILE_CHAR_TEST (*p, mask))
+           *q++ = *p;
+         else
            {
-             *to++ = '%';
-             *to++ = '2';
-             *to++ = 'F';
-             ++from;
+             unsigned char ch = *p;
+             *q++ = '%';
+             *q++ = XDIGIT_TO_XCHAR (ch >> 4);
+             *q++ = XDIGIT_TO_XCHAR (ch & 0xf);
            }
-         else
-           *to++ = *from++;
        }
+      assert (q - TAIL (dest) == outlen);
     }
+  TAIL_INCR (dest, outlen);
+}
 
-  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';
+/* Append to DEST the directory structure that corresponds the
+   directory part of URL's path.  For example, if the URL is
+   http://server/dir1/dir2/file, this appends "/dir1/dir2".
+
+   Each path element ("dir1" and "dir2" in the above example) is
+   examined, url-unescaped, and re-escaped as file name element.
+
+   Additionally, it cuts as many directories from the path as
+   specified by opt.cut_dirs.  For example, if opt.cut_dirs is 1, it
+   will produce "bar" for the above example.  For 2 or more, it will
+   produce "".
 
-  return xstrdup (result);
+   Each component of the path is quoted for use as file name.  */
+
+static void
+append_dir_structure (const struct url *u, struct growable *dest)
+{
+  char *pathel, *next;
+  int cut = opt.cut_dirs;
+
+  /* Go through the path components, de-URL-quote them, and quote them
+     (if necessary) as file names.  */
+
+  pathel = u->path;
+  for (; (next = strchr (pathel, '/')) != NULL; pathel = next + 1)
+    {
+      if (cut-- > 0)
+       continue;
+      if (pathel == next)
+       /* Ignore empty pathels.  path_simplify should remove
+          occurrences of "//" from the path, but it has special cases
+          for starting / which generates an empty pathel here.  */
+       continue;
+
+      if (dest->tail)
+       append_char ('/', dest);
+      append_uri_pathel (pathel, next, dest);
+    }
 }
 
-/* Create a unique filename, corresponding to a given URL.  Calls
-   mkstruct if necessary.  Does *not* actually create any directories.  */
+/* Return a unique file name that matches the given URL as good as
+   possible.  Does not create directories on the file system.  */
+
 char *
-url_filename (const struct url *u)
+url_file_name (const struct url *u)
 {
-  char *file, *name;
-  int have_prefix = 0;         /* whether we must prepend opt.dir_prefix */
+  struct growable fnres;
+
+  char *u_file, *u_query;
+  char *fname, *unique;
+
+  fnres.base = NULL;
+  fnres.size = 0;
+  fnres.tail = 0;
+
+  /* Start with the directory prefix, if specified. */
+  if (!DOTP (opt.dir_prefix))
+    append_string (opt.dir_prefix, &fnres);
 
+  /* If "dirstruct" is turned on (typically the case with -r), add
+     the host and port (unless those have been turned off) and
+     directory structure.  */
   if (opt.dirstruct)
     {
-      file = mkstruct (u);
-      have_prefix = 1;
-    }
-  else
-    {
-      char *base = *u->file ? u->file : "index.html";
-      char *query = u->query && *u->query ? u->query : NULL;
-      file = compose_file_name (base, query);
+      if (opt.add_hostdir)
+       {
+         if (fnres.tail)
+           append_char ('/', &fnres);
+         append_string (u->host, &fnres);
+         if (u->port != scheme_default_port (u->scheme))
+           {
+             char portstr[24];
+             number_to_string (portstr, u->port);
+             append_char (FN_PORT_SEP, &fnres);
+             append_string (portstr, &fnres);
+           }
+       }
+
+      append_dir_structure (u, &fnres);
     }
 
-  if (!have_prefix)
+  /* Add the file name. */
+  if (fnres.tail)
+    append_char ('/', &fnres);
+  u_file = *u->file ? u->file : "index.html";
+  append_uri_pathel (u_file, u_file + strlen (u_file), &fnres);
+
+  /* Append "?query" to the file name. */
+  u_query = u->query && *u->query ? u->query : NULL;
+  if (u_query)
     {
-      /* Check whether the prefix directory is something other than "."
-        before prepending it.  */
-      if (!DOTP (opt.dir_prefix))
-       {
-         char *nfile = (char *)xmalloc (strlen (opt.dir_prefix)
-                                        + 1 + strlen (file) + 1);
-         sprintf (nfile, "%s/%s", opt.dir_prefix, file);
-         xfree (file);
-         file = nfile;
-       }
+      append_char (FN_QUERY_SEP, &fnres);
+      append_uri_pathel (u_query, u_query + strlen (u_query), &fnres);
     }
-  /* DOS-ish file systems don't like `%' signs in them; we change it
-     to `@'.  */
-#ifdef WINDOWS
-  {
-    char *p = file;
-    for (p = file; *p; p++)
-      if (*p == '%')
-       *p = '@';
-  }
-#endif /* WINDOWS */
+
+  /* Zero-terminate the file name. */
+  append_char ('\0', &fnres);
+
+  fname = fnres.base;
 
   /* Check the cases in which the unique extensions are not used:
      1) Clobbering is turned off (-nc).
@@ -1335,29 +1713,32 @@ url_filename (const struct url *u)
      4) Hierarchy is built.
 
      The exception is the case when file does exist and is a
-     directory (actually support for bad httpd-s).  */
+     directory (see `mkalldirs' for explanation).  */
+
   if ((opt.noclobber || opt.always_rest || opt.timestamping || opt.dirstruct)
-      && !(file_exists_p (file) && !file_non_directory_p (file)))
-    return file;
+      && !(file_exists_p (fname) && !file_non_directory_p (fname)))
+    return fname;
 
-  /* Find a unique name.  */
-  name = unique_name (file);
-  xfree (file);
-  return name;
+  unique = unique_name (fname, 1);
+  if (unique != fname)
+    xfree (fname);
+  return unique;
 }
 
-/* Like strlen(), but allow the URL to be ended with '?'.  */
+/* Return the length of URL's path.  Path is considered to be
+   terminated by one of '?', ';', '#', or by the end of the
+   string.  */
 static int
-urlpath_length (const char *url)
+path_length (const char *url)
 {
   const char *q = strpbrk_or_eos (url, "?;#");
   return q - url;
 }
 
 /* Find the last occurrence of character C in the range [b, e), or
-   NULL, if none are present.  This is almost completely equivalent to
-   { *e = '\0'; return strrchr(b); }, except that it doesn't change
-   the contents of the string.  */
+   NULL, if none are present.  This is equivalent to strrchr(b, c),
+   except that it accepts an END argument instead of requiring the
+   string to be zero-terminated.  Why is there no memrchr()?  */
 static const char *
 find_last_char (const char *b, const char *e, char c)
 {
@@ -1366,7 +1747,129 @@ find_last_char (const char *b, const char *e, char c)
       return e;
   return NULL;
 }
+\f
+/* Resolve "." and ".." elements of PATH by destructively modifying
+   PATH.  "." is resolved by removing that path element, and ".." is
+   resolved by removing the preceding path element.  Leading and
+   trailing slashes are preserved.
+
+   Return non-zero if any changes have been made.
+
+   For example, "a/b/c/./../d/.." will yield "a/b/".  More exhaustive
+   test examples are provided below.  If you change anything in this
+   function, run test_path_simplify to make sure you haven't broken a
+   test case.
+
+   A previous version of this function was based on path_simplify()
+   from GNU Bash, but it has been rewritten for Wget 1.8.1.  */
+
+static int
+path_simplify (char *path)
+{
+  int change = 0;
+  char *p, *end;
+
+  if (path[0] == '/')
+    ++path;                    /* preserve the leading '/'. */
+
+  p = path;
+  end = p + strlen (p) + 1;    /* position past the terminating zero. */
+
+  while (1)
+    {
+    again:
+      /* P should point to the beginning of a path element. */
+
+      if (*p == '.' && (*(p + 1) == '/' || *(p + 1) == '\0'))
+       {
+         /* Handle "./foo" by moving "foo" two characters to the
+            left. */
+         if (*(p + 1) == '/')
+           {
+             change = 1;
+             memmove (p, p + 2, end - p);
+             end -= 2;
+             goto again;
+           }
+         else
+           {
+             change = 1;
+             *p = '\0';
+             break;
+           }
+       }
+      else if (*p == '.' && *(p + 1) == '.'
+              && (*(p + 2) == '/' || *(p + 2) == '\0'))
+       {
+         /* Handle "../foo" by moving "foo" one path element to the
+            left.  */
+         char *b = p;          /* not p-1 because P can equal PATH */
+
+         /* Backtrack by one path element, but not past the beginning
+            of PATH. */
+
+         /* foo/bar/../baz */
+         /*         ^ p    */
+         /*     ^ b        */
+
+         if (b > path)
+           {
+             /* Move backwards until B hits the beginning of the
+                previous path element or the beginning of path. */
+             for (--b; b > path && *(b - 1) != '/'; b--)
+               ;
+           }
+
+         change = 1;
+         if (*(p + 2) == '/')
+           {
+             memmove (b, p + 3, end - (p + 3));
+             end -= (p + 3) - b;
+             p = b;
+           }
+         else
+           {
+             *b = '\0';
+             break;
+           }
+
+         goto again;
+       }
+      else if (*p == '/')
+       {
+         /* Remove empty path elements.  Not mandated by rfc1808 et
+            al, but it seems like a good idea to get rid of them.
+            Supporting them properly is hard (in which directory do
+            you save http://x.com///y.html?) and they don't seem to
+            bring much gain.  */
+         char *q = p;
+         while (*q == '/')
+           ++q;
+         change = 1;
+         if (*q == '\0')
+           {
+             *p = '\0';
+             break;
+           }
+         memmove (p, q, end - q);
+         end -= q - p;
+         goto again;
+       }
+
+      /* Skip to the next path element. */
+      while (*p && *p != '/')
+       ++p;
+      if (*p == '\0')
+       break;
 
+      /* Make sure P points to the beginning of the next path element,
+        which is location after the slash. */
+      ++p;
+    }
+
+  return change;
+}
+\f
 /* Resolve the result of "linking" a base URI (BASE) to a
    link-specified URI (LINK).
 
@@ -1378,8 +1881,8 @@ find_last_char (const char *b, const char *e, char c)
    The parameters LINKLENGTH is useful if LINK is not zero-terminated.
    See uri_merge for a gentler interface to this functionality.
 
-   #### This function should handle `./' and `../' so that the evil
-   path_simplify can go.  */
+   Perhaps this function should call path_simplify so that the callers
+   don't have to call url_parse unconditionally.  */
 static char *
 uri_merge_1 (const char *base, const char *link, int linklength, int no_scheme)
 {
@@ -1387,7 +1890,7 @@ uri_merge_1 (const char *base, const char *link, int linklength, int no_scheme)
 
   if (no_scheme)
     {
-      const char *end = base + urlpath_length (base);
+      const char *end = base + path_length (base);
 
       if (!*link)
        {
@@ -1424,6 +1927,37 @@ uri_merge_1 (const char *base, const char *link, int linklength, int no_scheme)
          memcpy (constr + baselength, link, linklength);
          constr[baselength + linklength] = '\0';
        }
+      else if (linklength > 1 && *link == '/' && *(link + 1) == '/')
+       {
+         /* LINK begins with "//" and so is a net path: we need to
+            replace everything after (and including) the double slash
+            with LINK. */
+
+         /* uri_merge("foo", "//new/bar")            -> "//new/bar"      */
+         /* uri_merge("//old/foo", "//new/bar")      -> "//new/bar"      */
+         /* uri_merge("http://old/foo", "//new/bar") -> "http://new/bar" */
+
+         int span;
+         const char *slash;
+         const char *start_insert;
+
+         /* Look for first slash. */
+         slash = memchr (base, '/', end - base);
+         /* If found slash and it is a double slash, then replace
+            from this point, else default to replacing from the
+            beginning.  */
+         if (slash && *(slash + 1) == '/')
+           start_insert = slash;
+         else
+           start_insert = base;
+
+         span = start_insert - base;
+         constr = (char *)xmalloc (span + linklength + 1);
+         if (span)
+           memcpy (constr, base, span);
+         memcpy (constr + span, link, linklength);
+         constr[span + linklength] = '\0';
+       }
       else if (*link == '/')
        {
          /* LINK is an absolute path: we need to replace everything
@@ -1583,23 +2117,29 @@ url_string (const struct url *url, int hide_password)
   char *scheme_str = supported_schemes[url->scheme].leading_string;
   int fplen = full_path_length (url);
 
+  int brackets_around_host = 0;
+
   assert (scheme_str != NULL);
 
   /* Make sure the user name and password are quoted. */
   if (url->user)
     {
-      quoted_user = encode_string_maybe (url->user);
+      quoted_user = url_escape_allow_passthrough (url->user);
       if (url->passwd)
        {
          if (hide_password)
            quoted_passwd = HIDDEN_PASSWORD;
          else
-           quoted_passwd = encode_string_maybe (url->passwd);
+           quoted_passwd = url_escape_allow_passthrough (url->passwd);
        }
     }
 
+  if (strchr (url->host, ':'))
+    brackets_around_host = 1;
+
   size = (strlen (scheme_str)
          + strlen (url->host)
+         + (brackets_around_host ? 2 : 0)
          + fplen
          + 1);
   if (url->port != scheme_port)
@@ -1625,12 +2165,15 @@ url_string (const struct url *url, int hide_password)
       *p++ = '@';
     }
 
+  if (brackets_around_host)
+    *p++ = '[';
   APPEND (p, url->host);
+  if (brackets_around_host)
+    *p++ = ']';
   if (url->port != scheme_port)
     {
       *p++ = ':';
-      long_to_string (p, url->port);
-      p += strlen (p);
+      p = number_to_string (p, url->port);
     }
 
   full_path_write (url, p);
@@ -1648,15 +2191,20 @@ url_string (const struct url *url, int hide_password)
   return result;
 }
 \f
-/* Returns proxy host address, in accordance with SCHEME.  */
+/* Return the URL of the proxy appropriate for url U.  */
 char *
-getproxy (enum url_scheme scheme)
+getproxy (struct url *u)
 {
   char *proxy = NULL;
   char *rewritten_url;
   static char rewritten_storage[1024];
 
-  switch (scheme)
+  if (!opt.use_proxy)
+    return NULL;
+  if (!no_proxy_match (u->host, (const char **)opt.no_proxy))
+    return NULL;
+
+  switch (u->scheme)
     {
     case SCHEME_HTTP:
       proxy = opt.http_proxy ? opt.http_proxy : getenv ("http_proxy");
@@ -1675,7 +2223,8 @@ getproxy (enum url_scheme scheme)
   if (!proxy || !*proxy)
     return NULL;
 
-  /* Handle shorthands. */
+  /* Handle shorthands.  `rewritten_storage' is a kludge to allow
+     getproxy() to return static storage. */
   rewritten_url = rewrite_shorthand_url (proxy);
   if (rewritten_url)
     {
@@ -1697,6 +2246,10 @@ no_proxy_match (const char *host, const char **no_proxy)
     return !sufmatch (no_proxy, host);
 }
 \f
+/* Support for converting links for local viewing in downloaded HTML
+   files.  This should be moved to another file, because it has
+   nothing to do with processing URLs.  */
+
 static void write_backup_file PARAMS ((const char *, downloaded_file_t));
 static const char *replace_attr PARAMS ((const char *, int, FILE *,
                                         const char *));
@@ -2081,32 +2634,27 @@ find_fragment (const char *beg, int size, const char **bp, const char **ep)
   return 0;
 }
 
-/* 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:
+/* Quote FILE for use as local reference to an HTML file.
 
-   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. */
+   We quote ? as %3F to avoid passing part of the file name as the
+   parameter when browsing the converted file through HTTP.  However,
+   it is safe to do this only when `--html-extension' is turned on.
+   This is because converting "index.html?foo=bar" to
+   "index.html%3Ffoo=bar" would break local browsing, as the latter
+   isn't even recognized as an HTML file!  However, converting
+   "index.html?foo=bar.html" to "index.html%3Ffoo=bar.html" should be
+   safe for both local and HTTP-served browsing.  */
 
 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, '?');
+  int qm;
+
+  if (!opt.html_extension)
+    return html_quote_string (file);
+
+  qm = count_char (file, '?');
 
   if (qm)
     {
@@ -2138,7 +2686,6 @@ local_quote_string (const char *file)
     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.
@@ -2233,3 +2780,114 @@ downloaded_files_free (void)
       downloaded_files_hash = NULL;
     }
 }
+
+/* Return non-zero if scheme a is similar to scheme b.
+   Schemes are similar if they are equal.  If SSL is supported, schemes
+   are also similar if one is http (SCHEME_HTTP) and the other is https
+   (SCHEME_HTTPS).  */
+int
+schemes_are_similar_p (enum url_scheme a, enum url_scheme b)
+{
+  if (a == b)
+    return 1;
+#ifdef HAVE_SSL
+  if ((a == SCHEME_HTTP && b == SCHEME_HTTPS)
+      || (a == SCHEME_HTTPS && b == SCHEME_HTTP))
+    return 1;
+#endif
+  return 0;
+}
+\f
+#if 0
+/* Debugging and testing support for path_simplify. */
+
+/* Debug: run path_simplify on PATH and return the result in a new
+   string.  Useful for calling from the debugger.  */
+static char *
+ps (char *path)
+{
+  char *copy = xstrdup (path);
+  path_simplify (copy);
+  return copy;
+}
+
+static void
+run_test (char *test, char *expected_result, int expected_change)
+{
+  char *test_copy = xstrdup (test);
+  int modified = path_simplify (test_copy);
+
+  if (0 != strcmp (test_copy, expected_result))
+    {
+      printf ("Failed path_simplify(\"%s\"): expected \"%s\", got \"%s\".\n",
+             test, expected_result, test_copy);
+    }
+  if (modified != expected_change)
+    {
+      if (expected_change == 1)
+       printf ("Expected no modification with path_simplify(\"%s\").\n",
+               test);
+      else
+       printf ("Expected modification with path_simplify(\"%s\").\n",
+               test);
+    }
+  xfree (test_copy);
+}
+
+static void
+test_path_simplify (void)
+{
+  static struct {
+    char *test, *result;
+    int should_modify;
+  } tests[] = {
+    { "",              "",             0 },
+    { ".",             "",             1 },
+    { "..",            "",             1 },
+    { "foo",           "foo",          0 },
+    { "foo/bar",       "foo/bar",      0 },
+    { "foo///bar",     "foo/bar",      1 },
+    { "foo/.",         "foo/",         1 },
+    { "foo/./",                "foo/",         1 },
+    { "foo./",         "foo./",        0 },
+    { "foo/../bar",    "bar",          1 },
+    { "foo/../bar/",   "bar/",         1 },
+    { "foo/bar/..",    "foo/",         1 },
+    { "foo/bar/../x",  "foo/x",        1 },
+    { "foo/bar/../x/", "foo/x/",       1 },
+    { "foo/..",                "",             1 },
+    { "foo/../..",     "",             1 },
+    { "a/b/../../c",   "c",            1 },
+    { "./a/../b",      "b",            1 }
+  };
+  int i;
+
+  for (i = 0; i < ARRAY_SIZE (tests); i++)
+    {
+      char *test = tests[i].test;
+      char *expected_result = tests[i].result;
+      int   expected_change = tests[i].should_modify;
+      run_test (test, expected_result, expected_change);
+    }
+
+  /* Now run all the tests with a leading slash before the test case,
+     to prove that the slash is being preserved.  */
+  for (i = 0; i < ARRAY_SIZE (tests); i++)
+    {
+      char *test, *expected_result;
+      int expected_change = tests[i].should_modify;
+
+      test = xmalloc (1 + strlen (tests[i].test) + 1);
+      sprintf (test, "/%s", tests[i].test);
+
+      expected_result = xmalloc (1 + strlen (tests[i].result) + 1);
+      sprintf (expected_result, "/%s", tests[i].result);
+
+      run_test (test, expected_result, expected_change);
+
+      xfree (test);
+      xfree (expected_result);
+    }
+}
+#endif