]> sjero.net Git - wget/blobdiff - src/cookies.c
[svn] Avoid code repetition between time_str and datetime_str.
[wget] / src / cookies.c
index fad20e7e9fc32287c2e2afb081a137a0e1b873bd..e173c5bc381d6fb41193aa8b7b9d9c1059a5ef00 100644 (file)
@@ -1,5 +1,5 @@
 /* Support for cookies.
-   Copyright (C) 2001, 2002 Free Software Foundation, Inc.
+   Copyright (C) 2001-2006 Free Software Foundation, Inc.
 
 This file is part of GNU Wget.
 
@@ -14,8 +14,8 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 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.
+along with Wget; if not, write to the Free Software Foundation, Inc.,
+51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 
 In addition, as a special exception, the Free Software Foundation
 gives permission to link the code of its release of Wget with the
@@ -45,22 +45,17 @@ so, delete this exception statement from your version.  */
 #include <config.h>
 
 #include <stdio.h>
-#ifdef HAVE_STRING_H
-# include <string.h>
-#else
-# include <strings.h>
-#endif
+#include <string.h>
 #include <stdlib.h>
 #include <assert.h>
 #include <errno.h>
+#include <time.h>
 
 #include "wget.h"
 #include "utils.h"
 #include "hash.h"
 #include "cookies.h"
-
-/* This should *really* be in a .h file!  */
-time_t http_atotm PARAMS ((const char *));
+#include "http.h"              /* for http_atotm */
 \f
 /* Declarations of `struct cookie' and the most basic functions. */
 
@@ -85,7 +80,7 @@ struct cookie_jar {
 
 /* Value set by entry point functions, so that the low-level
    routines don't need to call time() all the time.  */
-time_t cookies_now;
+static time_t cookies_now;
 
 struct cookie_jar *
 cookie_jar_new (void)
@@ -101,21 +96,21 @@ struct cookie {
   int port;                    /* port number */
   char *path;                  /* path prefix of the cookie */
 
-  int secure;                  /* whether cookie should be
+  unsigned discard_requested :1; /* whether cookie was created to
+                                  request discarding another
+                                  cookie. */
+
+  unsigned secure :1;          /* whether cookie should be
                                   transmitted over non-https
                                   connections. */
-  int domain_exact;            /* whether DOMAIN must match as a
+  unsigned domain_exact :1;    /* whether DOMAIN must match as a
                                   whole. */
 
-  int permanent;               /* whether the cookie should outlive
+  unsigned permanent :1;       /* whether the cookie should outlive
                                   the session. */
   time_t expiry_time;          /* time when the cookie expires, 0
                                   means undetermined. */
 
-  int discard_requested;       /* whether cookie was created to
-                                  request discarding another
-                                  cookie. */
-
   char *attr;                  /* cookie attribute name */
   char *value;                 /* cookie attribute value */
 
@@ -143,7 +138,7 @@ cookie_new (void)
 /* Non-zero if the cookie has expired.  Assumes cookies_now has been
    set by one of the entry point functions.  */
 
-static int
+static bool
 cookie_expired_p (const struct cookie *c)
 {
   return c->expiry_time != 0 && c->expiry_time < cookies_now;
@@ -261,8 +256,7 @@ store_cookie (struct cookie_jar *jar, struct cookie *cookie)
   hash_table_put (jar->chains, chain_key, cookie);
   ++jar->cookie_count;
 
-#ifdef ENABLE_DEBUG
-  if (opt.debug)
+  IF_DEBUG
     {
       time_t exptime = cookie->expiry_time;
       DEBUGP (("\nStored cookie %s %d%s %s <%s> <%s> [expiry %s] %s %s\n",
@@ -271,10 +265,9 @@ store_cookie (struct cookie_jar *jar, struct cookie *cookie)
               cookie->path,
               cookie->permanent ? "permanent" : "session",
               cookie->secure ? "secure" : "insecure",
-              cookie->expiry_time ? datetime_str (&exptime) : "none",
+              cookie->expiry_time ? datetime_str (exptime) : "none",
               cookie->attr, cookie->value));
     }
-#endif
 }
 
 /* Discard a cookie matching COOKIE's domain, port, path, and
@@ -326,139 +319,10 @@ discard_matching_cookie (struct cookie_jar *jar, struct cookie *cookie)
 /* Functions for parsing the `Set-Cookie' header, and creating new
    cookies from the wire.  */
 
-#define NAME_IS(string_literal)                                        \
-  BOUNDED_EQUAL_NO_CASE (name_b, name_e, string_literal)
-
-#define VALUE_EXISTS (value_b && value_e)
-
-#define VALUE_NON_EMPTY (VALUE_EXISTS && (value_b != value_e))
-
-/* Update the appropriate cookie field.  [name_b, name_e) are expected
-   to delimit the attribute name, while [value_b, value_e) (optional)
-   should delimit the attribute value.
-
-   When called the first time, it will set the cookie's attribute name
-   and value.  After that, it will check the attribute name for
-   special fields such as `domain', `path', etc.  Where appropriate,
-   it will parse the values of the fields it recognizes and fill the
-   corresponding fields in COOKIE.
-
-   Returns 1 on success.  Returns zero in case a syntax error is
-   found; such a cookie should be discarded.  */
-
-static int
-update_cookie_field (struct cookie *cookie,
-                    const char *name_b, const char *name_e,
-                    const char *value_b, const char *value_e)
-{
-  assert (name_b != NULL && name_e != NULL);
-
-  if (!cookie->attr)
-    {
-      if (!VALUE_EXISTS)
-       return 0;
-      cookie->attr = strdupdelim (name_b, name_e);
-      cookie->value = strdupdelim (value_b, value_e);
-      return 1;
-    }
-
-  if (NAME_IS ("domain"))
-    {
-      if (!VALUE_NON_EMPTY)
-       return 0;
-      xfree_null (cookie->domain);
-      /* Strictly speaking, we should set cookie->domain_exact if the
-        domain doesn't begin with a dot.  But many sites set the
-        domain to "foo.com" and expect "subhost.foo.com" to get the
-        cookie, and it apparently works.  */
-      if (*value_b == '.')
-       ++value_b;
-      cookie->domain = strdupdelim (value_b, value_e);
-      return 1;
-    }
-  else if (NAME_IS ("path"))
-    {
-      if (!VALUE_NON_EMPTY)
-       return 0;
-      xfree_null (cookie->path);
-      cookie->path = strdupdelim (value_b, value_e);
-      return 1;
-    }
-  else if (NAME_IS ("expires"))
-    {
-      char *value_copy;
-      time_t expires;
-
-      if (!VALUE_NON_EMPTY)
-       return 0;
-      BOUNDED_TO_ALLOCA (value_b, value_e, value_copy);
-
-      expires = http_atotm (value_copy);
-      if (expires != (time_t) -1)
-       {
-         cookie->permanent = 1;
-         cookie->expiry_time = expires;
-       }
-      else
-       /* Error in expiration spec.  Assume default (cookie doesn't
-          expire, but valid only for this session.)  */
-       ;
-
-      /* According to netscape's specification, expiry time in the
-        past means that discarding of a matching cookie is
-        requested.  */
-      if (cookie->expiry_time < cookies_now)
-       cookie->discard_requested = 1;
-
-      return 1;
-    }
-  else if (NAME_IS ("max-age"))
-    {
-      double maxage = -1;
-      char *value_copy;
-
-      if (!VALUE_NON_EMPTY)
-       return 0;
-      BOUNDED_TO_ALLOCA (value_b, value_e, value_copy);
-
-      sscanf (value_copy, "%lf", &maxage);
-      if (maxage == -1)
-       /* something went wrong. */
-       return 0;
-      cookie->permanent = 1;
-      cookie->expiry_time = cookies_now + maxage;
-
-      /* According to rfc2109, a cookie with max-age of 0 means that
-        discarding of a matching cookie is requested.  */
-      if (maxage == 0)
-       cookie->discard_requested = 1;
-
-      return 1;
-    }
-  else if (NAME_IS ("secure"))
-    {
-      /* ignore value completely */
-      cookie->secure = 1;
-      return 1;
-    }
-  else
-    /* Unrecognized attribute; ignore it. */
-    return 1;
-}
-
-#undef NAME_IS
-
-/* Returns non-zero for characters that are legal in the name of an
-   attribute.  This used to allow only alphanumerics, '-', and '_',
-   but we need to be more lenient because a number of sites wants to
-   use weirder attribute names.  rfc2965 "informally specifies"
-   attribute name (token) as "a sequence of non-special, non-white
-   space characters".  So we allow everything except the stuff we know
-   could harm us.  */
+#define TOKEN_IS(token, string_literal)                                \
+  BOUNDED_EQUAL_NO_CASE (token.b, token.e, string_literal)
 
-#define ATTR_NAME_CHAR(c) ((c) > 32 && (c) < 127       \
-                          && (c) != '"' && (c) != '='  \
-                          && (c) != ';' && (c) != ',')
+#define TOKEN_NON_EMPTY(token) (token.b != NULL && token.b != token.e)
 
 /* Parse the contents of the `Set-Cookie' header.  The header looks
    like this:
@@ -468,182 +332,124 @@ update_cookie_field (struct cookie *cookie,
    Trailing semicolon is optional; spaces are allowed between all
    tokens.  Additionally, values may be quoted.
 
-   A new cookie is returned upon success, NULL otherwise.  The
-   specified CALLBACK function (normally `update_cookie_field' is used
-   to update the fields of the newly created cookie structure.  */
+   A new cookie is returned upon success, NULL otherwise.
+
+   The first name-value pair will be used to set the cookie's
+   attribute name and value.  Subsequent parameters will be checked
+   against field names such as `domain', `path', etc.  Recognized
+   fields will be parsed and the corresponding members of COOKIE
+   filled.  */
 
 static struct cookie *
-parse_set_cookies (const char *sc,
-                  int (*callback) (struct cookie *,
-                                   const char *, const char *,
-                                   const char *, const char *),
-                  int silent)
+parse_set_cookie (const char *set_cookie, bool silent)
 {
+  const char *ptr = set_cookie;
   struct cookie *cookie = cookie_new ();
+  param_token name, value;
 
-  /* #### Hand-written DFAs are no fun to debug.  We'de be better off
-     to rewrite this as an inline parser.  */
-
-  enum { S_START, S_NAME, S_NAME_POST,
-        S_VALUE_PRE, S_VALUE, S_QUOTED_VALUE, S_VALUE_TRAILSPACE,
-        S_ATTR_ACTION, S_DONE, S_ERROR
-  } state = S_START;
-
-  const char *p = sc;
-  char c;
+  if (!extract_param (&ptr, &name, &value, ';'))
+    goto error;
+  if (!value.b)
+    goto error;
+  cookie->attr = strdupdelim (name.b, name.e);
+  cookie->value = strdupdelim (value.b, value.e);
 
-  const char *name_b  = NULL, *name_e  = NULL;
-  const char *value_b = NULL, *value_e = NULL;
-
-  c = *p;
-
-  while (state != S_DONE && state != S_ERROR)
+  while (extract_param (&ptr, &name, &value, ';'))
     {
-      switch (state)
+      if (TOKEN_IS (name, "domain"))
        {
-       case S_START:
-         if (!c)
-           state = S_DONE;
-         else if (ISSPACE (c))
-           /* Strip all whitespace preceding the name. */
-           c = *++p;
-         else if (ATTR_NAME_CHAR (c))
-           {
-             name_b = p;
-             state = S_NAME;
-           }
-         else
-           /* empty attr name not allowed */
-           state = S_ERROR;
-         break;
-       case S_NAME:
-         if (!c || c == ';' || c == '=' || ISSPACE (c))
-           {
-             name_e = p;
-             state = S_NAME_POST;
-           }
-         else if (ATTR_NAME_CHAR (c))
-           c = *++p;
-         else
-           state = S_ERROR;
-         break;
-       case S_NAME_POST:
-         if (!c || c == ';')
-           {
-             value_b = value_e = NULL;
-             if (c == ';')
-               c = *++p;
-             state = S_ATTR_ACTION;
-           }
-         else if (c == '=')
-           {
-             c = *++p;
-             state = S_VALUE_PRE;
-           }
-         else if (ISSPACE (c))
-           /* Ignore space and keep the state. */
-           c = *++p;
-         else
-           state = S_ERROR;
-         break;
-       case S_VALUE_PRE:
-         if (!c || c == ';')
-           {
-             value_b = value_e = p;
-             if (c == ';')
-               c = *++p;
-             state = S_ATTR_ACTION;
-           }
-         else if (c == '"')
-           {
-             c = *++p;
-             value_b = p;
-             state = S_QUOTED_VALUE;
-           }
-         else if (ISSPACE (c))
-           c = *++p;
-         else
-           {
-             value_b = p;
-             value_e = NULL;
-             state = S_VALUE;
-           }
-         break;
-       case S_VALUE:
-         if (!c || c == ';' || ISSPACE (c))
-           {
-             value_e = p;
-             state = S_VALUE_TRAILSPACE;
-           }
-         else
-           {
-             value_e = NULL;   /* no trailing space */
-             c = *++p;
-           }
-         break;
-       case S_QUOTED_VALUE:
-         if (c == '"')
-           {
-             value_e = p;
-             c = *++p;
-             state = S_VALUE_TRAILSPACE;
-           }
-         else if (!c)
-           state = S_ERROR;
-         else
-           c = *++p;
-         break;
-       case S_VALUE_TRAILSPACE:
-         if (c == ';')
+         if (!TOKEN_NON_EMPTY (value))
+           goto error;
+         xfree_null (cookie->domain);
+         /* Strictly speaking, we should set cookie->domain_exact if the
+            domain doesn't begin with a dot.  But many sites set the
+            domain to "foo.com" and expect "subhost.foo.com" to get the
+            cookie, and it apparently works in browsers.  */
+         if (*value.b == '.')
+           ++value.b;
+         cookie->domain = strdupdelim (value.b, value.e);
+       }
+      else if (TOKEN_IS (name, "path"))
+       {
+         if (!TOKEN_NON_EMPTY (value))
+           goto error;
+         xfree_null (cookie->path);
+         cookie->path = strdupdelim (value.b, value.e);
+       }
+      else if (TOKEN_IS (name, "expires"))
+       {
+         char *value_copy;
+         time_t expires;
+
+         if (!TOKEN_NON_EMPTY (value))
+           goto error;
+         BOUNDED_TO_ALLOCA (value.b, value.e, value_copy);
+
+         expires = http_atotm (value_copy);
+         if (expires != (time_t) -1)
            {
-             c = *++p;
-             state = S_ATTR_ACTION;
+             cookie->permanent = 1;
+             cookie->expiry_time = expires;
            }
-         else if (!c)
-           state = S_ATTR_ACTION;
-         else if (ISSPACE (c))
-           c = *++p;
          else
-           state = S_VALUE;
-         break;
-       case S_ATTR_ACTION:
-         {
-           int legal = callback (cookie, name_b, name_e, value_b, value_e);
-           if (!legal)
-             {
-               if (!silent)
-                 {
-                   char *name;
-                   BOUNDED_TO_ALLOCA (name_b, name_e, name);
-                   logprintf (LOG_NOTQUIET,
-                              _("Error in Set-Cookie, field `%s'"),
-                              escnonprint (name));
-                 }
-               state = S_ERROR;
-               break;
-             }
-           state = S_START;
-         }
-         break;
-       case S_DONE:
-       case S_ERROR:
-         /* handled by loop condition */
-         break;
+           /* Error in expiration spec.  Assume default (cookie doesn't
+              expire, but valid only for this session.)  */
+           ;
+
+         /* According to netscape's specification, expiry time in the
+            past means that discarding of a matching cookie is
+            requested.  */
+         if (cookie->expiry_time < cookies_now)
+           cookie->discard_requested = 1;
        }
+      else if (TOKEN_IS (name, "max-age"))
+       {
+         double maxage = -1;
+         char *value_copy;
+
+         if (!TOKEN_NON_EMPTY (value))
+           goto error;
+         BOUNDED_TO_ALLOCA (value.b, value.e, value_copy);
+
+         sscanf (value_copy, "%lf", &maxage);
+         if (maxage == -1)
+           /* something went wrong. */
+           goto error;
+         cookie->permanent = 1;
+         cookie->expiry_time = cookies_now + maxage;
+
+         /* According to rfc2109, a cookie with max-age of 0 means that
+            discarding of a matching cookie is requested.  */
+         if (maxage == 0)
+           cookie->discard_requested = 1;
+       }
+      else if (TOKEN_IS (name, "secure"))
+       {
+         /* ignore value completely */
+         cookie->secure = 1;
+       }
+      else
+       /* Ignore unrecognized attribute. */
+       ;
     }
-  if (state == S_DONE)
-    return cookie;
+  if (*ptr)
+    /* extract_param has encountered a syntax error */
+    goto error;
 
-  delete_cookie (cookie);
-  if (state != S_ERROR)
-    abort ();
+  /* The cookie has been successfully constructed; return it. */
+  return cookie;
 
+ error:
   if (!silent)
     logprintf (LOG_NOTQUIET,
               _("Syntax error in Set-Cookie: %s at position %d.\n"),
-              escnonprint (sc), p - sc);
+              escnonprint (set_cookie), (int) (ptr - set_cookie));
+  delete_cookie (cookie);
   return NULL;
 }
+
+#undef TOKEN_IS
+#undef TOKEN_NON_EMPTY
 \f
 /* Sanity checks.  These are important, otherwise it is possible for
    mailcious attackers to destroy important cookie information and/or
@@ -652,14 +458,14 @@ parse_set_cookies (const char *sc,
 
 #define REQUIRE_DIGITS(p) do {                 \
   if (!ISDIGIT (*p))                           \
-    return 0;                                  \
+    return false;                              \
   for (++p; ISDIGIT (*p); p++)                 \
     ;                                          \
 } while (0)
 
 #define REQUIRE_DOT(p) do {                    \
   if (*p++ != '.')                             \
-    return 0;                                  \
+    return false;                              \
 } while (0)
 
 /* Check whether ADDR matches <digits>.<digits>.<digits>.<digits>.
@@ -668,7 +474,7 @@ parse_set_cookies (const char *sc,
    all we need is a check, preferrably one that is small, fast, and
    well-defined.  */
 
-static int
+static bool
 numeric_address_p (const char *addr)
 {
   const char *p = addr;
@@ -682,8 +488,8 @@ numeric_address_p (const char *addr)
   REQUIRE_DIGITS (p);          /* D */
 
   if (*p != '\0')
-    return 0;
-  return 1;
+    return false;
+  return true;
 }
 
 /* Check whether COOKIE_DOMAIN is an appropriate domain for HOST.
@@ -691,7 +497,7 @@ numeric_address_p (const char *addr)
    the sites deviated too often, so I had to fall back to "tail
    matching", as defined by the original Netscape's cookie spec.  */
 
-static int
+static bool
 check_domain_match (const char *cookie_domain, const char *host)
 {
   DEBUGP (("cdm: 1"));
@@ -705,13 +511,13 @@ check_domain_match (const char *cookie_domain, const char *host)
 
   /* For the sake of efficiency, check for exact match first. */
   if (0 == strcasecmp (cookie_domain, host))
-    return 1;
+    return true;
 
   DEBUGP ((" 3"));
 
   /* HOST must match the tail of cookie_domain. */
-  if (!match_tail (host, cookie_domain, 1))
-    return 0;
+  if (!match_tail (host, cookie_domain, true))
+    return false;
 
   /* We know that COOKIE_DOMAIN is a subset of HOST; however, we must
      make sure that somebody is not trying to set the cookie for a
@@ -757,7 +563,7 @@ check_domain_match (const char *cookie_domain, const char *host)
        case '.':
          if (ldcl == 0)
            /* Empty domain component found -- the domain is invalid. */
-           return 0;
+           return false;
          if (*(p + 1) == '\0')
            {
              /* Tolerate trailing '.' by not treating the domain as
@@ -776,25 +582,25 @@ check_domain_match (const char *cookie_domain, const char *host)
     DEBUGP ((" 5"));
 
     if (dccount < 2)
-      return 0;
+      return false;
 
     DEBUGP ((" 6"));
 
     if (dccount == 2)
       {
        int i;
-       int known_toplevel = 0;
+       int known_toplevel = false;
        static const char *known_toplevel_domains[] = {
          ".com", ".edu", ".net", ".org", ".gov", ".mil", ".int"
        };
        for (i = 0; i < countof (known_toplevel_domains); i++)
-         if (match_tail (cookie_domain, known_toplevel_domains[i], 1))
+         if (match_tail (cookie_domain, known_toplevel_domains[i], true))
            {
-             known_toplevel = 1;
+             known_toplevel = true;
              break;
            }
        if (!known_toplevel && nldcl <= 3)
-         return 0;
+         return false;
       }
   }
 
@@ -810,23 +616,34 @@ check_domain_match (const char *cookie_domain, const char *host)
       /* desired domain:             bar.com */
       /* '.' must be here in host-> ^        */
       if (hlen > dlen && host[hlen - dlen - 1] != '.')
-       return 0;
+       return false;
     }
 
   DEBUGP ((" 8"));
 
-  return 1;
+  return true;
 }
 
-static int path_matches PARAMS ((const char *, const char *));
+static int path_matches (const char *, const char *);
 
 /* Check whether PATH begins with COOKIE_PATH. */
 
-static int
+static bool
 check_path_match (const char *cookie_path, const char *path)
 {
-  return path_matches (path, cookie_path);
+  return path_matches (path, cookie_path) != 0;
 }
+
+/* Prepend '/' to string S.  S is copied to fresh stack-allocated
+   space and its value is modified to point to the new location.  */
+
+#define PREPEND_SLASH(s) do {                                  \
+  char *PS_newstr = (char *) alloca (1 + strlen (s) + 1);      \
+  *PS_newstr = '/';                                            \
+  strcpy (PS_newstr + 1, s);                                   \
+  s = PS_newstr;                                               \
+} while (0)
+
 \f
 /* Process the HTTP `Set-Cookie' header.  This results in storing the
    cookie or discarding a matching one, or ignoring it completely, all
@@ -840,7 +657,12 @@ cookie_handle_set_cookie (struct cookie_jar *jar,
   struct cookie *cookie;
   cookies_now = time (NULL);
 
-  cookie = parse_set_cookies (set_cookie, update_cookie_field, 0);
+  /* Wget's paths don't begin with '/' (blame rfc1808), but cookie
+     usage assumes /-prefixed paths.  Until the rest of Wget is fixed,
+     simply prepend slash to PATH.  */
+  PREPEND_SLASH (path);
+
+  cookie = parse_set_cookie (set_cookie, false);
   if (!cookie)
     goto out;
 
@@ -982,16 +804,7 @@ find_chains_of_host (struct cookie_jar *jar, const char *host,
 static int
 path_matches (const char *full_path, const char *prefix)
 {
-  int len;
-
-  if (*prefix != '/')
-    /* Wget's HTTP paths do not begin with '/' (the URL code treats it
-       as a mere separator, inspired by rfc1808), but the '/' is
-       assumed when matching against the cookie stuff.  */
-    return 0;
-
-  ++prefix;
-  len = strlen (prefix);
+  int len = strlen (prefix);
 
   if (0 != strncmp (full_path, prefix, len))
     /* FULL_PATH doesn't begin with PREFIX. */
@@ -1001,17 +814,17 @@ path_matches (const char *full_path, const char *prefix)
   return len + 1;
 }
 
-/* Return non-zero iff COOKIE matches the provided parameters of the
-   URL being downloaded: HOST, PORT, PATH, and SECFLAG.
+/* Return true iff COOKIE matches the provided parameters of the URL
+   being downloaded: HOST, PORT, PATH, and SECFLAG.
 
    If PATH_GOODNESS is non-NULL, store the "path goodness" value
    there.  That value is a measure of how closely COOKIE matches PATH,
    used for ordering cookies.  */
 
-static int
+static bool
 cookie_matches_url (const struct cookie *cookie,
                    const char *host, int port, const char *path,
-                   int secflag, int *path_goodness)
+                   bool secflag, int *path_goodness)
 {
   int pg;
 
@@ -1021,31 +834,31 @@ cookie_matches_url (const struct cookie *cookie,
        stale cookies will not be saved by `save_cookies'.  On the
        other hand, this function should be as efficient as
        possible.  */
-    return 0;
+    return false;
 
   if (cookie->secure && !secflag)
     /* Don't transmit secure cookies over insecure connections.  */
-    return 0;
+    return false;
   if (cookie->port != PORT_ANY && cookie->port != port)
-    return 0;
+    return false;
 
   /* If exact domain match is required, verify that cookie's domain is
      equal to HOST.  If not, assume success on the grounds of the
      cookie's chain having been found by find_chains_of_host.  */
   if (cookie->domain_exact
       && 0 != strcasecmp (host, cookie->domain))
-    return 0;
+    return false;
 
   pg = path_matches (path, cookie->path);
-  if (!pg)
-    return 0;
+  if (pg == 0)
+    return false;
 
   if (path_goodness)
     /* If the caller requested path_goodness, we return it.  This is
        an optimization, so that the caller doesn't need to call
        path_matches() again.  */
     *path_goodness = pg;
-  return 1;
+  return true;
 }
 
 /* A structure that points to a cookie, along with the additional
@@ -1144,7 +957,7 @@ goodness_comparator (const void *p1, const void *p2)
 
 char *
 cookie_header (struct cookie_jar *jar, const char *host,
-              int port, const char *path, int secflag)
+              int port, const char *path, bool secflag)
 {
   struct cookie **chains;
   int chain_count;
@@ -1154,6 +967,7 @@ cookie_header (struct cookie_jar *jar, const char *host,
   int count, i, ocnt;
   char *result;
   int result_size, pos;
+  PREPEND_SLASH (path);                /* see cookie_handle_set_cookie */
 
   /* First, find the cookie chains whose domains match HOST. */
 
@@ -1420,44 +1234,13 @@ cookie_jar_load (struct cookie_jar *jar, const char *file)
   fclose (fp);
 }
 
-/* Mapper for save_cookies callable by hash_table_map.  VALUE points
-   to the head in a chain of cookies.  The function prints the entire
-   chain.  */
-
-static int
-save_cookies_mapper (void *key, void *value, void *arg)
-{
-  FILE *fp = (FILE *)arg;
-  char *domain = (char *)key;
-  struct cookie *cookie = (struct cookie *)value;
-  for (; cookie; cookie = cookie->next)
-    {
-      if (!cookie->permanent && !opt.keep_session_cookies)
-       continue;
-      if (cookie_expired_p (cookie))
-       continue;
-      if (!cookie->domain_exact)
-       fputc ('.', fp);
-      fputs (domain, fp);
-      if (cookie->port != PORT_ANY)
-       fprintf (fp, ":%d", cookie->port);
-      fprintf (fp, "\t%s\t%s\t%s\t%.0f\t%s\t%s\n",
-              cookie->domain_exact ? "FALSE" : "TRUE",
-              cookie->path, cookie->secure ? "TRUE" : "FALSE",
-              (double)cookie->expiry_time,
-              cookie->attr, cookie->value);
-      if (ferror (fp))
-       return 1;               /* stop mapping */
-    }
-  return 0;
-}
-
 /* Save cookies, in format described above, to FILE. */
 
 void
 cookie_jar_save (struct cookie_jar *jar, const char *file)
 {
   FILE *fp;
+  hash_table_iterator iter;
 
   DEBUGP (("Saving cookies to %s.\n", file));
 
@@ -1472,11 +1255,36 @@ cookie_jar_save (struct cookie_jar *jar, const char *file)
     }
 
   fputs ("# HTTP cookie file.\n", fp);
-  fprintf (fp, "# Generated by Wget on %s.\n", datetime_str (&cookies_now));
+  fprintf (fp, "# Generated by Wget on %s.\n", datetime_str (cookies_now));
   fputs ("# Edit at your own risk.\n\n", fp);
 
-  hash_table_map (jar->chains, save_cookies_mapper, fp);
-
+  for (hash_table_iterate (jar->chains, &iter);
+       hash_table_iter_next (&iter);
+       )
+    {
+      const char *domain = iter.key;
+      struct cookie *cookie = iter.value;
+      for (; cookie; cookie = cookie->next)
+       {
+         if (!cookie->permanent && !opt.keep_session_cookies)
+           continue;
+         if (cookie_expired_p (cookie))
+           continue;
+         if (!cookie->domain_exact)
+           fputc ('.', fp);
+         fputs (domain, fp);
+         if (cookie->port != PORT_ANY)
+           fprintf (fp, ":%d", cookie->port);
+         fprintf (fp, "\t%s\t%s\t%s\t%.0f\t%s\t%s\n",
+                  cookie->domain_exact ? "FALSE" : "TRUE",
+                  cookie->path, cookie->secure ? "TRUE" : "FALSE",
+                  (double)cookie->expiry_time,
+                  cookie->attr, cookie->value);
+         if (ferror (fp))
+           goto out;
+       }
+    }
+ out:
   if (ferror (fp))
     logprintf (LOG_NOTQUIET, _("Error writing to `%s': %s\n"),
               file, strerror (errno));
@@ -1487,40 +1295,25 @@ cookie_jar_save (struct cookie_jar *jar, const char *file)
   DEBUGP (("Done saving cookies.\n"));
 }
 \f
-/* Destroy all the elements in the chain and unhook it from the cookie
-   jar.  This is written in the form of a callback to hash_table_map
-   and used by cookie_jar_delete to delete all the cookies in a
-   jar.  */
-
-static int
-nuke_cookie_chain (void *value, void *key, void *arg)
-{
-  char *chain_key = (char *)value;
-  struct cookie *chain = (struct cookie *)key;
-  struct cookie_jar *jar = (struct cookie_jar *)arg;
-
-  /* Remove the chain from the table and free the key. */
-  hash_table_remove (jar->chains, chain_key);
-  xfree (chain_key);
-
-  /* Then delete all the cookies in the chain. */
-  while (chain)
-    {
-      struct cookie *next = chain->next;
-      delete_cookie (chain);
-      chain = next;
-    }
-
-  /* Keep mapping. */
-  return 0;
-}
-
 /* Clean up cookie-related data. */
 
 void
 cookie_jar_delete (struct cookie_jar *jar)
 {
-  hash_table_map (jar->chains, nuke_cookie_chain, jar);
+  /* Iterate over chains (indexed by domain) and free them. */
+  hash_table_iterator iter;
+  for (hash_table_iterate (jar->chains, &iter); hash_table_iter_next (&iter); )
+    {
+      struct cookie *chain = iter.value;
+      xfree (iter.key);
+      /* Then all cookies in this chain. */
+      while (chain)
+       {
+         struct cookie *next = chain->next;
+         delete_cookie (chain);
+         chain = next;
+       }
+    }
   hash_table_destroy (jar->chains);
   xfree (jar);
 }
@@ -1530,27 +1323,14 @@ cookie_jar_delete (struct cookie_jar *jar)
    from main.  */
 
 #ifdef TEST_COOKIES
-int test_count;
-char *test_results[10];
-
-static int test_parse_cookies_callback (struct cookie *ignored,
-                                       const char *nb, const char *ne,
-                                       const char *vb, const char *ve)
-{
-  test_results[test_count++] = strdupdelim (nb, ne);
-  test_results[test_count++] = strdupdelim (vb, ve);
-  return 1;
-}
-
 void
 test_cookies (void)
 {
   /* Tests expected to succeed: */
   static struct {
-    char *data;
-    char *results[10];
+    const char *data;
+    const char *results[10];
   } tests_succ[] = {
-    { "", {NULL} },
     { "arg=value", {"arg", "value", NULL} },
     { "arg1=value1;arg2=value2", {"arg1", "value1", "arg2", "value2", NULL} },
     { "arg1=value1; arg2=value2", {"arg1", "value1", "arg2", "value2", NULL} },
@@ -1574,39 +1354,51 @@ test_cookies (void)
   for (i = 0; i < countof (tests_succ); i++)
     {
       int ind;
-      char *data = tests_succ[i].data;
-      char **expected = tests_succ[i].results;
+      const char *data = tests_succ[i].data;
+      const char **expected = tests_succ[i].results;
       struct cookie *c;
 
-      test_count = 0;
-      c = parse_set_cookies (data, test_parse_cookies_callback, 1);
+      c = parse_set_cookie (data, true);
       if (!c)
        {
          printf ("NULL cookie returned for valid data: %s\n", data);
          continue;
        }
 
-      for (ind = 0; ind < test_count; ind += 2)
-       {
-         if (!expected[ind])
-           break;
-         if (0 != strcmp (expected[ind], test_results[ind]))
-           printf ("Invalid name %d for '%s' (expected '%s', got '%s')\n",
-                   ind / 2 + 1, data, expected[ind], test_results[ind]);
-         if (0 != strcmp (expected[ind + 1], test_results[ind + 1]))
-           printf ("Invalid value %d for '%s' (expected '%s', got '%s')\n",
-                   ind / 2 + 1, data, expected[ind + 1], test_results[ind + 1]);
-       }
-      if (ind < test_count || expected[ind])
-       printf ("Unmatched number of results: %s\n", data);
+      /* Test whether extract_param handles these cases correctly. */
+      {
+       param_token name, value;
+       const char *ptr = data;
+       int j = 0;
+       while (extract_param (&ptr, &name, &value, ';'))
+         {
+           char *n = strdupdelim (name.b, name.e);
+           char *v = strdupdelim (value.b, value.e);
+           if (!expected[j])
+             {
+               printf ("Too many parameters for '%s'\n", data);
+               break;
+             }
+           if (0 != strcmp (expected[j], n))
+             printf ("Invalid name %d for '%s' (expected '%s', got '%s')\n",
+                     j / 2 + 1, data, expected[j], n);
+           if (0 != strcmp (expected[j + 1], v))
+             printf ("Invalid value %d for '%s' (expected '%s', got '%s')\n",
+                     j / 2 + 1, data, expected[j + 1], v);
+           j += 2;
+           free (n);
+           free (v);
+         }
+       if (expected[j])
+         printf ("Too few parameters for '%s'\n", data);
+      }
     }
 
   for (i = 0; i < countof (tests_fail); i++)
     {
       struct cookie *c;
       char *data = tests_fail[i];
-      test_count = 0;
-      c = parse_set_cookies (data, test_parse_cookies_callback, 1);
+      c = parse_set_cookie (data, true);
       if (c)
        printf ("Failed to report error on invalid data: %s\n", data);
     }