From 4043b6ccae25087c4a707b6ff687026e3447b8d3 Mon Sep 17 00:00:00 2001
From: Tatsuhiro Tsujikawa <tatsuhiro.t@gmail.com>
Date: Thu, 28 Jan 2010 14:01:50 +0000
Subject: [PATCH] 2010-01-28  Tatsuhiro Tsujikawa 
 <t-tujikawa@users.sourceforge.net>

	Rewritten Cookie storage.
	* src/Cookie.cc
	* src/Cookie.h
	* src/CookieParser.cc
	* src/CookieStorage.cc
	* src/CookieStorage.h
	* src/a2functional.h
	* test/CookieParserTest.cc
	* test/CookieStorageTest.cc
	* test/CookieTest.cc
	* test/HttpResponseTest.cc
	* test/TestUtil.h
	* test/a2functionalTest.cc
---
 ChangeLog                 |  16 +++
 src/Cookie.cc             |  86 ++++++++------
 src/Cookie.h              |  20 ++++
 src/CookieParser.cc       |  22 +++-
 src/CookieStorage.cc      | 228 +++++++++++++++++++++++++++++++-------
 src/CookieStorage.h       | 113 +++++++++++++++++--
 src/a2functional.h        |   9 ++
 test/CookieParserTest.cc  |   2 +-
 test/CookieStorageTest.cc | 157 ++++++++++++++++++++------
 test/CookieTest.cc        |  12 ++
 test/HttpResponseTest.cc  |   7 +-
 test/TestUtil.h           |  15 +++
 test/a2functionalTest.cc  |  30 ++++-
 13 files changed, 587 insertions(+), 130 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index f125642f..fea2cb25 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,19 @@
+2010-01-28  Tatsuhiro Tsujikawa  <t-tujikawa@users.sourceforge.net>
+
+	Rewritten Cookie storage.
+	* src/Cookie.cc
+	* src/Cookie.h
+	* src/CookieParser.cc
+	* src/CookieStorage.cc
+	* src/CookieStorage.h
+	* src/a2functional.h
+	* test/CookieParserTest.cc
+	* test/CookieStorageTest.cc
+	* test/CookieTest.cc
+	* test/HttpResponseTest.cc
+	* test/TestUtil.h
+	* test/a2functionalTest.cc
+
 2010-01-26  Tatsuhiro Tsujikawa  <t-tujikawa@users.sourceforge.net>
 
 	Handle redirected URI which is not properly percent encoded.
diff --git a/src/Cookie.cc b/src/Cookie.cc
index ffd15edd..b8ff5eeb 100644
--- a/src/Cookie.cc
+++ b/src/Cookie.cc
@@ -54,7 +54,7 @@ static std::string prependDotIfNotExists(const std::string& domain)
   return (!domain.empty() && domain[0] != '.') ? "."+domain : domain;
 }
 
-static std::string normalizeDomain(const std::string& domain)
+std::string Cookie::normalizeDomain(const std::string& domain)
 {
   if(domain.empty() || util::isNumbersAndDotsNotation(domain)) {
     return domain;
@@ -79,7 +79,9 @@ Cookie::Cookie(const std::string& name,
   _expiry(expiry),
   _path(path),
   _domain(normalizeDomain(domain)),
-  _secure(secure) {}
+  _secure(secure),
+  _creationTime(time(0)),
+  _lastAccess(_creationTime) {}
 
 Cookie::Cookie(const std::string& name,
                const std::string& value,
@@ -91,9 +93,11 @@ Cookie::Cookie(const std::string& name,
   _expiry(0),
   _path(path),
   _domain(normalizeDomain(domain)),
-  _secure(secure) {}
+  _secure(secure),
+  _creationTime(time(0)),
+  _lastAccess(_creationTime) {}
 
-Cookie::Cookie():_expiry(0), _secure(false) {}
+Cookie::Cookie():_expiry(0), _secure(false), _lastAccess(time(0)) {}
 
 Cookie::~Cookie() {}
 
@@ -142,9 +146,9 @@ bool Cookie::match(const std::string& requestHost,
                    const std::string& requestPath,
                    time_t date, bool secure) const
 {
-  std::string normReqHost = normalizeDomain(requestHost);
   if((secure || (!_secure && !secure)) &&
-     domainMatch(normReqHost, _domain) &&
+     (requestHost == _domain || // For default domain or IP address
+      domainMatch(normalizeDomain(requestHost), _domain)) &&
      pathInclude(requestPath, _path) &&
      (isSessionCookie() || (date < _expiry))) {
     return true;
@@ -156,35 +160,37 @@ bool Cookie::match(const std::string& requestHost,
 bool Cookie::validate(const std::string& requestHost,
                       const std::string& requestPath) const
 {
-  std::string normReqHost = normalizeDomain(requestHost);
-  // If _domain is IP address, then it should be matched to requestHost.
-  // In other words, _domain == normReqHost
-  if(normReqHost != _domain) {
-    // domain must start with '.'
-    if(*_domain.begin() != '.') {
-      return false;
+  // If _domain doesn't start with "." or it is IP address, then it
+  // must equal to requestHost. Otherwise, do domain tail match.
+  if(requestHost != _domain) {
+    std::string normReqHost = normalizeDomain(requestHost);
+    if(normReqHost != _domain) {
+      // domain must start with '.'
+      if(*_domain.begin() != '.') {
+        return false;
+      }
+      // domain must not end with '.'
+      if(*_domain.rbegin() == '.') {
+        return false;
+      }
+      // domain must include at least one embeded '.'
+      if(_domain.size() < 4 || _domain.find(".", 1) == std::string::npos) {
+        return false;
+      }
+      if(!util::endsWith(normReqHost, _domain)) {
+        return false;
+      }
+      // From RFC2965 3.3.2 Rejecting Cookies
+      // * The request-host is a HDN (not IP address) and has the form HD,
+      //   where D is the value of the Domain attribute, and H is a string
+      //   that contains one or more dots.
+      size_t dotCount = std::count(normReqHost.begin(),
+                                   normReqHost.begin()+
+                                   (normReqHost.size()-_domain.size()), '.');
+      if(dotCount > 1 || (dotCount == 1 && normReqHost[0] != '.')) {
+        return false;
+      } 
     }
-    // domain must not end with '.'
-    if(*_domain.rbegin() == '.') {
-      return false;
-    }
-    // domain must include at least one embeded '.'
-    if(_domain.size() < 4 || _domain.find(".", 1) == std::string::npos) {
-      return false;
-    }
-    if(!util::endsWith(normReqHost, _domain)) {
-      return false;
-    }
-    // From RFC2965 3.3.2 Rejecting Cookies
-    // * The request-host is a HDN (not IP address) and has the form HD,
-    //   where D is the value of the Domain attribute, and H is a string
-    //   that contains one or more dots.
-    size_t dotCount = std::count(normReqHost.begin(),
-                                 normReqHost.begin()+
-                                 (normReqHost.size()-_domain.size()), '.');
-    if(dotCount > 1 || (dotCount == 1 && normReqHost[0] != '.')) {
-      return false;
-    } 
   }
   if(requestPath != _path) {
     // From RFC2965 3.3.2 Rejecting Cookies
@@ -230,4 +236,16 @@ std::string Cookie::toNsCookieFormat() const
   return ss.str();
 }
 
+void Cookie::markOriginServerOnly()
+{
+  if(util::startsWith(_domain, A2STR::DOT_C)) {
+    _domain.erase(_domain.begin(), _domain.begin()+1);
+  }
+}
+
+void Cookie::updateLastAccess()
+{
+  _lastAccess = time(0);
+}
+
 } // namespace aria2
diff --git a/src/Cookie.h b/src/Cookie.h
index e88ebd25..9b223aaa 100644
--- a/src/Cookie.h
+++ b/src/Cookie.h
@@ -52,6 +52,8 @@ private:
   std::string _path;
   std::string _domain;
   bool _secure;
+  time_t _creationTime;
+  time_t _lastAccess;
 public:
   /*
    * If expires = 0 is given, then the cookie becomes session cookie.
@@ -127,6 +129,24 @@ public:
   }
 
   std::string toNsCookieFormat() const;
+
+  // Makes this Cookie only sent to the origin server.  This function
+  // removes first "." from _domain if _domain starts with ".".
+  void markOriginServerOnly();
+
+  time_t getCreationTime() const
+  {
+    return _creationTime;
+  }
+
+  void updateLastAccess();
+
+  time_t getLastAccess() const
+  {
+    return _lastAccess;
+  }
+
+  static std::string normalizeDomain(const std::string& domain);
 };
 
 typedef std::deque<Cookie> Cookies;
diff --git a/src/CookieParser.cc b/src/CookieParser.cc
index bf14af7b..a0cbe2dd 100644
--- a/src/CookieParser.cc
+++ b/src/CookieParser.cc
@@ -71,26 +71,38 @@ Cookie CookieParser::parse(const std::string& cookieStr, const std::string& defa
   util::split(nameValue, terms.front(), '=');
 
   std::map<std::string, std::string> values;
-  values[C_DOMAIN] = defaultDomain;
-  values[C_PATH] = defaultPath;
-  
   for(std::vector<std::string>::iterator itr = terms.begin()+1;
       itr != terms.end(); ++itr) {
     std::pair<std::string, std::string> nv;
     util::split(nv, *itr, '=');
     values[nv.first] = nv.second;
   }
+  bool useDefaultDomain = false;
+  std::map<std::string, std::string>::iterator mitr;
+  mitr = values.find(C_DOMAIN);
+  if(mitr == values.end() || (*mitr).second.empty()) {
+    useDefaultDomain = true;
+    values[C_DOMAIN] = defaultDomain;
+  }
+  mitr = values.find(C_PATH);
+  if(mitr == values.end() || (*mitr).second.empty()) {
+    values[C_PATH] = defaultPath;
+  }
   time_t expiry = 0;
-  if(values.find(C_EXPIRES) != values.end()) {
+  if(values.count(C_EXPIRES)) {
     Time expiryTime = Time::parseHTTPDate(values[C_EXPIRES]);
     if(expiryTime.good()) {
       expiry = expiryTime.getTime();
     }
   }
-  return Cookie(nameValue.first, nameValue.second,
+  Cookie cookie(nameValue.first, nameValue.second,
                 expiry,
                 values[C_PATH], values[C_DOMAIN],
                 values.find(C_SECURE) != values.end());
+  if(useDefaultDomain) {
+    cookie.markOriginServerOnly();
+  }
+  return cookie;
 }
 
 
diff --git a/src/CookieStorage.cc b/src/CookieStorage.cc
index 1dee23de..4347e98f 100644
--- a/src/CookieStorage.cc
+++ b/src/CookieStorage.cc
@@ -45,28 +45,40 @@
 #include "StringFormat.h"
 #include "NsCookieParser.h"
 #include "File.h"
+#include "a2functional.h"
 #ifdef HAVE_SQLITE3
 # include "Sqlite3MozCookieParser.h"
 #endif // HAVE_SQLITE3
 
 namespace aria2 {
 
-CookieStorage::CookieStorage():_logger(LogFactory::getInstance()) {}
-
-CookieStorage::~CookieStorage() {}
-
-bool CookieStorage::store(const Cookie& cookie)
+CookieStorage::DomainEntry::DomainEntry
+(const std::string& domain):_key(domain)
 {
-  if(!cookie.good()) {
-    return false;
-  }
+  std::reverse(_key.begin(), _key.end());
+}
+
+void CookieStorage::DomainEntry::updateLastAccess()
+{
+  _lastAccess = time(0);
+}
+
+bool CookieStorage::DomainEntry::addCookie(const Cookie& cookie)
+{
+  updateLastAccess();
   std::deque<Cookie>::iterator i = std::find(_cookies.begin(), _cookies.end(),
                                              cookie);
   if(i == _cookies.end()) {
     if(cookie.isExpired()) {
       return false;
     } else {
-      _cookies.push_back(cookie);
+      if(_cookies.size() >= CookieStorage::MAX_COOKIE_PER_DOMAIN) {
+        std::deque<Cookie>::iterator m = std::min_element
+          (_cookies.begin(), _cookies.end(), LeastRecentAccess<Cookie>());
+        *m = cookie;
+      } else {
+        _cookies.push_back(cookie);
+      }
       return true;
     }
   } else if(cookie.isExpired()) {
@@ -78,6 +90,55 @@ bool CookieStorage::store(const Cookie& cookie)
   }
 }
 
+bool CookieStorage::DomainEntry::contains(const Cookie& cookie) const
+{
+  return std::find(_cookies.begin(), _cookies.end(), cookie) != _cookies.end();
+}
+
+void CookieStorage::DomainEntry::writeCookie(std::ostream& o) const
+{
+  for(std::deque<Cookie>::const_iterator i = _cookies.begin();
+      i != _cookies.end(); ++i) {
+    o << (*i).toNsCookieFormat() << "\n";
+  }
+}
+
+CookieStorage::CookieStorage():_logger(LogFactory::getInstance()) {}
+
+CookieStorage::~CookieStorage() {}
+
+// See CookieStorageTest::testDomainIsFull() in CookieStorageTest.cc
+static const size_t DOMAIN_EVICTION_TRIGGER = 600;
+
+static const double DOMAIN_EVICTION_RATE = 0.1;
+
+bool CookieStorage::store(const Cookie& cookie)
+{
+  if(!cookie.good()) {
+    return false;
+  }
+  if(_domains.size() >= DOMAIN_EVICTION_TRIGGER) {
+    std::sort(_domains.begin(), _domains.end(),
+              LeastRecentAccess<DomainEntry>());
+    size_t delnum = _domains.size()*DOMAIN_EVICTION_RATE;
+    _domains.erase(_domains.begin(), _domains.begin()+delnum);
+    std::sort(_domains.begin(), _domains.end());
+  }
+  DomainEntry v(cookie.getDomain());
+  std::deque<DomainEntry>::iterator i =
+    std::lower_bound(_domains.begin(), _domains.end(), v);
+  bool added = false;
+  if(i != _domains.end() && (*i).getKey() == v.getKey()) {
+    added = (*i).addCookie(cookie);
+  } else {
+    added = v.addCookie(cookie);
+    if(added) {
+      _domains.insert(i, v);
+    }
+  }
+  return added;
+}
+
 void CookieStorage::storeCookies(const std::deque<Cookie>& cookies)
 {
   for(std::deque<Cookie>::const_iterator i = cookies.begin();
@@ -98,48 +159,138 @@ bool CookieStorage::parseAndStore(const std::string& setCookieString,
   }
 }
 
-class CriteriaMatch:public std::unary_function<Cookie, bool> {
-private:
-  std::string _requestHost;
-  std::string _requestPath;
-  time_t _date;
-  bool _secure;
-public:
-  CriteriaMatch(const std::string& requestHost, const std::string& requestPath,
-                time_t date, bool secure):
-    _requestHost(requestHost),
-    _requestPath(requestPath),
-    _date(date),
-    _secure(secure) {}
-
-  bool operator()(const Cookie& cookie) const
+struct CookiePathDivider {
+  Cookie _cookie;
+  int _pathDepth;
+  CookiePathDivider(const Cookie& cookie):_cookie(cookie)
   {
-    return cookie.match(_requestHost, _requestPath, _date, _secure);
+    std::vector<std::string> paths;
+    util::split(_cookie.getPath(), std::back_inserter(paths), A2STR::SLASH_C);
+    _pathDepth = paths.size();
   }
 };
 
-class OrderByPathDesc:public std::binary_function<Cookie, Cookie, bool> {
+class CookiePathDividerConverter {
 public:
-  bool operator()(const Cookie& lhs, const Cookie& rhs) const
+  CookiePathDivider operator()(const Cookie& cookie) const
   {
-    return lhs.getPath() > rhs.getPath();
+    return CookiePathDivider(cookie);
+  }
+
+  Cookie operator()(const CookiePathDivider& cookiePathDivider) const
+  {
+    return cookiePathDivider._cookie;
   }
 };
 
+class OrderByPathDepthDesc:public std::binary_function<Cookie, Cookie, bool> {
+public:
+  bool operator()
+  (const CookiePathDivider& lhs, const CookiePathDivider& rhs) const
+  {
+    // Sort by path-length.
+    //
+    // RFC2965 says: Note that the NAME=VALUE pair for the cookie with
+    // the more specific Path attribute, /acme/ammo, comes before the
+    // one with the less specific Path attribute, /acme.  Further note
+    // that the same cookie name appears more than once.
+    //
+    // Netscape spec says: When sending cookies to a server, all
+    // cookies with a more specific path mapping should be sent before
+    // cookies with less specific path mappings. For example, a cookie
+    // "name1=foo" with a path mapping of "/" should be sent after a
+    // cookie "name1=foo2" with a path mapping of "/bar" if they are
+    // both to be sent.
+    int comp = lhs._pathDepth-rhs._pathDepth;
+    if(comp == 0) {
+      return lhs._cookie.getCreationTime() < rhs._cookie.getCreationTime();
+    } else {
+      return comp > 0;
+    }
+  }
+};
+
+template<typename DomainInputIterator, typename CookieOutputIterator>
+static void searchCookieByDomainSuffix
+(const std::string& domain,
+ DomainInputIterator first, DomainInputIterator last, CookieOutputIterator out,
+ const std::string& requestHost,
+ const std::string& requestPath,
+ time_t date, bool secure)
+{
+  CookieStorage::DomainEntry v(domain);
+  std::deque<CookieStorage::DomainEntry>::iterator i =
+    std::lower_bound(first, last, v);
+  if(i != last && (*i).getKey() == v.getKey()) {
+    (*i).updateLastAccess();
+    (*i).findCookie(out, requestHost, requestPath, date, secure);
+  }
+}
+
+bool CookieStorage::contains(const Cookie& cookie) const
+{
+  CookieStorage::DomainEntry v(cookie.getDomain());
+  std::deque<CookieStorage::DomainEntry>::const_iterator i =
+    std::lower_bound(_domains.begin(), _domains.end(), v);
+  if(i != _domains.end() && (*i).getKey() == v.getKey()) {
+    return (*i).contains(cookie);
+  } else {
+    return false;
+  }
+}
+
 std::deque<Cookie> CookieStorage::criteriaFind(const std::string& requestHost,
                                                const std::string& requestPath,
-                                               time_t date, bool secure) const
+                                               time_t date, bool secure)
 {
   std::deque<Cookie> res;
-  std::remove_copy_if(_cookies.begin(), _cookies.end(), std::back_inserter(res),
-                      std::not1(CriteriaMatch(requestHost, requestPath, date, secure)));
-  std::sort(res.begin(), res.end(), OrderByPathDesc());
+  bool numericHost = util::isNumbersAndDotsNotation(requestHost);
+  searchCookieByDomainSuffix
+    ((!numericHost && requestHost.find(A2STR::DOT_C) == std::string::npos)?
+     requestHost+".local":requestHost,
+     _domains.begin(), _domains.end(),
+     std::back_inserter(res),
+     requestHost, requestPath, date, secure);
+  if(!numericHost) {
+    std::string normRequestHost = Cookie::normalizeDomain(requestHost);
+    std::vector<std::string> domainComponents;
+    util::split(normRequestHost, std::back_inserter(domainComponents),
+                A2STR::DOT_C);
+    if(domainComponents.size() <= 1) {
+      return res;
+    }
+    std::reverse(domainComponents.begin(), domainComponents.end());
+    std::string domain = A2STR::DOT_C;
+    domain += domainComponents[0];
+    for(std::vector<std::string>::const_iterator di =
+          domainComponents.begin()+1; di != domainComponents.end(); ++di) {
+      domain = strconcat(A2STR::DOT_C, *di, domain);
+      const size_t prenum = res.size();
+      searchCookieByDomainSuffix(domain, _domains.begin(), _domains.end(),
+                                 std::back_inserter(res),
+                                 normRequestHost, requestPath, date, secure);
+      if(prenum == res.size()) {
+        break;
+      } 
+    }
+  }
+  std::vector<CookiePathDivider> divs;
+  std::transform(res.begin(), res.end(), std::back_inserter(divs),
+                 CookiePathDividerConverter());
+  std::sort(divs.begin(), divs.end(), OrderByPathDepthDesc());
+  std::transform(divs.begin(), divs.end(), res.begin(),
+                 CookiePathDividerConverter());
   return res;
 }
 
 size_t CookieStorage::size() const
 {
-  return _cookies.size();
+  size_t numCookie = 0;
+  for(std::deque<DomainEntry>::const_iterator i = _domains.begin();
+      i != _domains.end(); ++i) {
+    numCookie += (*i).countCookie();
+  }
+  return numCookie;
 }
 
 bool CookieStorage::load(const std::string& filename)
@@ -185,14 +336,9 @@ bool CookieStorage::saveNsFormat(const std::string& filename)
                      filename.c_str(), strerror(errno));
       return false;
     }
-    for(std::deque<Cookie>::const_iterator i = _cookies.begin();
-        i != _cookies.end(); ++i) {
-      o << (*i).toNsCookieFormat() << "\n";
-      if(!o) {
-        _logger->error("Failed to save cookies to %s, cause %s",
-                       filename.c_str(), strerror(errno));
-        return false;
-      }
+    for(std::deque<DomainEntry>::const_iterator i = _domains.begin();
+        i != _domains.end(); ++i) {
+      (*i).writeCookie(o);
     }
     o.flush();
     if(!o) {
diff --git a/src/CookieStorage.h b/src/CookieStorage.h
index 193d8d1e..84121a6e 100644
--- a/src/CookieStorage.h
+++ b/src/CookieStorage.h
@@ -39,6 +39,7 @@
 
 #include <string>
 #include <deque>
+#include <algorithm>
 
 #include "a2time.h"
 #include "Cookie.h"
@@ -49,8 +50,96 @@ namespace aria2 {
 class Logger;
 
 class CookieStorage {
+public:
+
+  static const size_t MAX_COOKIE_PER_DOMAIN = 50;
+
+  class FindCookie:public std::unary_function<Cookie&, bool> {
+  private:
+    std::string _requestHost;
+    std::string _requestPath;
+    time_t _date;
+    bool _secure;
+  public:
+    FindCookie(const std::string& requestHost,
+               const std::string& requestPath,
+               time_t date, bool secure):
+      _requestHost(requestHost),
+      _requestPath(requestPath),
+      _date(date),
+      _secure(secure) {}
+
+    bool operator()(Cookie& cookie) const
+    {
+      if(cookie.match(_requestHost, _requestPath, _date, _secure)) {
+        cookie.updateLastAccess();
+        return true;
+      } else {
+        return false;
+      }
+    }
+  };
+
+  class DomainEntry {
+  private:
+    std::string _key;
+
+    time_t _lastAccess;
+
+    std::deque<Cookie> _cookies;
+  public:
+    DomainEntry(const std::string& domain);
+
+    const std::string& getKey() const
+    {
+      return _key;
+    }
+
+    template<typename OutputIterator>
+    OutputIterator findCookie
+    (OutputIterator out,
+     const std::string& requestHost,
+     const std::string& requestPath,
+     time_t date, bool secure)
+    {
+      OutputIterator last =
+        std::remove_copy_if
+        (_cookies.begin(), _cookies.end(), out,
+         std::not1(FindCookie(requestHost, requestPath, date, secure)));
+      return last;
+    }
+
+    size_t countCookie() const
+    {
+      return _cookies.size();
+    }
+
+    bool addCookie(const Cookie& cookie);
+
+    void updateLastAccess();
+
+    time_t getLastAccess() const
+    {
+      return _lastAccess;
+    }
+
+    void writeCookie(std::ostream& o) const;
+
+    bool contains(const Cookie& cookie) const;
+
+    template<typename OutputIterator>
+    OutputIterator dumpCookie(OutputIterator out) const
+    {
+      return std::copy(_cookies.begin(), _cookies.end(), out);
+    }
+
+    bool operator<(const DomainEntry& de) const
+    {
+      return _key < de._key;
+    }
+  };
 private:
-  std::deque<Cookie> _cookies;
+  std::deque<DomainEntry> _domains;
 
   CookieParser _parser;
 
@@ -72,9 +161,11 @@ public:
                      const std::string& requestHost,
                      const std::string& requestPath);
 
+  // Finds cookies matched with given criteria and returns them.
+  // Matched cookies' _lastAccess property is updated.
   std::deque<Cookie> criteriaFind(const std::string& requestHost,
                                   const std::string& requestPath,
-                                  time_t date, bool secure) const;
+                                  time_t date, bool secure);
 
   // Loads Cookies from file denoted by filename.  If compiled with
   // libsqlite3, this method automatically detects the specified file
@@ -88,18 +179,22 @@ public:
   // this method returns true, otherwise returns false.
   bool saveNsFormat(const std::string& filename);
 
+  // Returns the number of cookies this object stores.
   size_t size() const;
   
-  std::deque<Cookie>::const_iterator begin() const
-  {
-    return _cookies.begin();
-  }
+  // Returns true if this object contains a cookie x where x == cookie
+  // satisfies.
+  bool contains(const Cookie& cookie) const;
 
-  std::deque<Cookie>::const_iterator end() const
+  template<typename OutputIterator>
+  OutputIterator dumpCookie(OutputIterator out) const
   {
-    return _cookies.end();
+    for(std::deque<DomainEntry>::const_iterator i = _domains.begin();
+        i != _domains.end(); ++i) {
+      out = (*i).dumpCookie(out);
+    }
+    return out;
   }
-
 };
 
 } // namespace aria2
diff --git a/src/a2functional.h b/src/a2functional.h
index eee08b82..34e0ccf4 100644
--- a/src/a2functional.h
+++ b/src/a2functional.h
@@ -381,6 +381,15 @@ inline void strappend(std::string& base,
   base += a7; base += a8;
 }
 
+template<typename T>
+class LeastRecentAccess:public std::binary_function<T, T, bool> {
+public:
+  bool operator()(const T& lhs, const T& rhs) const
+  {
+    return lhs.getLastAccess() < rhs.getLastAccess();
+  }
+};
+
 } // namespace aria2
 
 #endif // _D_A2_FUNCTIONAL_H_
diff --git a/test/CookieParserTest.cc b/test/CookieParserTest.cc
index 8cb8aaaa..245243a9 100644
--- a/test/CookieParserTest.cc
+++ b/test/CookieParserTest.cc
@@ -43,7 +43,7 @@ void CookieParserTest::testParse()
   CPPUNIT_ASSERT_EQUAL(std::string("JSESSIONID"), c.getName());
   CPPUNIT_ASSERT_EQUAL(std::string("123456789"), c.getValue());
   CPPUNIT_ASSERT_EQUAL((time_t)0, c.getExpiry());
-  CPPUNIT_ASSERT_EQUAL(std::string(".default.domain"), c.getDomain());
+  CPPUNIT_ASSERT_EQUAL(std::string("default.domain"), c.getDomain());
   CPPUNIT_ASSERT_EQUAL(std::string("/default/path"), c.getPath());
   CPPUNIT_ASSERT_EQUAL(false, c.isSecureCookie());
   CPPUNIT_ASSERT_EQUAL(true, c.isSessionCookie());
diff --git a/test/CookieStorageTest.cc b/test/CookieStorageTest.cc
index fa15a3d9..e2ff5278 100644
--- a/test/CookieStorageTest.cc
+++ b/test/CookieStorageTest.cc
@@ -11,6 +11,7 @@
 #include "CookieParser.h"
 #include "RecoverableException.h"
 #include "File.h"
+#include "TestUtil.h"
 
 namespace aria2 {
 
@@ -25,12 +26,16 @@ class CookieStorageTest:public CppUnit::TestFixture {
   CPPUNIT_TEST(testLoad_fileNotfound);
   CPPUNIT_TEST(testSaveNsFormat);
   CPPUNIT_TEST(testSaveNsFormat_fail);
+  CPPUNIT_TEST(testCookieIsFull);
+  CPPUNIT_TEST(testDomainIsFull);
   CPPUNIT_TEST_SUITE_END();
 public:
   void setUp() {}
 
   void tearDown() {}
 
+  void dumpCookie(std::vector<Cookie>& cookies, const CookieStorage& cs);
+
   void testStore();
   void testParseAndStore();
   void testCriteriaFind();
@@ -39,57 +44,64 @@ public:
   void testLoad_fileNotfound();
   void testSaveNsFormat();
   void testSaveNsFormat_fail();
+  void testCookieIsFull();
+  void testDomainIsFull();
 };
 
 
 CPPUNIT_TEST_SUITE_REGISTRATION(CookieStorageTest);
 
+void CookieStorageTest::dumpCookie
+(std::vector<Cookie>& cookies, const CookieStorage& st)
+{
+  st.dumpCookie(std::back_inserter(cookies));
+  std::sort(cookies.begin(), cookies.end(), CookieSorter());
+}
+
 void CookieStorageTest::testStore()
 {
   CookieStorage st;
   Cookie goodCookie("k", "v", "/", "localhost", false);
   CPPUNIT_ASSERT(st.store(goodCookie));
   CPPUNIT_ASSERT_EQUAL((size_t)1, st.size());
-  CPPUNIT_ASSERT(std::find(st.begin(), st.end(), goodCookie) != st.end());
+  CPPUNIT_ASSERT(st.contains(goodCookie));
 
   Cookie anotherCookie("k", "v", "/", "mirror", true);
   CPPUNIT_ASSERT(st.store(anotherCookie));
   CPPUNIT_ASSERT_EQUAL((size_t)2, st.size());
-  CPPUNIT_ASSERT(std::find(st.begin(), st.end(), anotherCookie) != st.end());
-  CPPUNIT_ASSERT(std::find(st.begin(), st.end(), goodCookie) != st.end());
+  CPPUNIT_ASSERT(st.contains(anotherCookie));
+  CPPUNIT_ASSERT(st.contains(goodCookie));
 
   Cookie updateGoodCookie("k", "v2", "/", "localhost", false);
   CPPUNIT_ASSERT(st.store(goodCookie));
   CPPUNIT_ASSERT_EQUAL((size_t)2, st.size());
-  CPPUNIT_ASSERT(std::find(st.begin(), st.end(), updateGoodCookie) != st.end());
-  CPPUNIT_ASSERT(std::find(st.begin(), st.end(), anotherCookie) != st.end());  
+  CPPUNIT_ASSERT(st.contains(updateGoodCookie));
+  CPPUNIT_ASSERT(st.contains(anotherCookie));
 
   Cookie expireGoodCookie("k", "v3", 1, "/", "localhost", false);
   CPPUNIT_ASSERT(!st.store(expireGoodCookie));
   CPPUNIT_ASSERT_EQUAL((size_t)1, st.size());
-  CPPUNIT_ASSERT(std::find(st.begin(), st.end(), anotherCookie) != st.end());
+  CPPUNIT_ASSERT(st.contains(anotherCookie));
 
   Cookie badCookie("", "", "/", "localhost", false);
   CPPUNIT_ASSERT(!st.store(badCookie));
   CPPUNIT_ASSERT_EQUAL((size_t)1, st.size());
-  CPPUNIT_ASSERT(std::find(st.begin(), st.end(), anotherCookie) != st.end());
+  CPPUNIT_ASSERT(st.contains(anotherCookie));
 
   Cookie fromNumericHost("k", "v", "/", "192.168.1.1", false);
   CPPUNIT_ASSERT(st.store(fromNumericHost));
   CPPUNIT_ASSERT_EQUAL((size_t)2, st.size());
-  CPPUNIT_ASSERT(std::find(st.begin(), st.end(), fromNumericHost) != st.end());
+  CPPUNIT_ASSERT(st.contains(fromNumericHost));
 
   Cookie sessionScopedGoodCookie("k", "v3", 0, "/", "localhost", false);
   CPPUNIT_ASSERT(st.store(sessionScopedGoodCookie));
   CPPUNIT_ASSERT_EQUAL((size_t)3, st.size());
-  CPPUNIT_ASSERT(std::find(st.begin(), st.end(),
-                           sessionScopedGoodCookie) != st.end());
+  CPPUNIT_ASSERT(st.contains(sessionScopedGoodCookie));
 
   Cookie sessionScopedGoodCookie2("k2", "v3", "/", "localhost", false);
   CPPUNIT_ASSERT(st.store(sessionScopedGoodCookie2));
   CPPUNIT_ASSERT_EQUAL((size_t)4, st.size());
-  CPPUNIT_ASSERT(std::find(st.begin(), st.end(),
-                           sessionScopedGoodCookie2) != st.end());
+  CPPUNIT_ASSERT(st.contains(sessionScopedGoodCookie2));
 }
 
 void CookieStorageTest::testParseAndStore()
@@ -111,6 +123,10 @@ void CookieStorageTest::testParseAndStore()
     " expires=Fri, 2038-01-01 00:00:00 GMT; path=/; domain=192.168.1.1;";
   CPPUNIT_ASSERT(st.parseAndStore(numericHostCookieStr, "192.168.1.1", "/"));
 
+  // No domain and no path are specified.
+  std::string noDomainPathCookieStr = "k=v";
+  CPPUNIT_ASSERT
+    (st.parseAndStore(noDomainPathCookieStr, "aria2.sf.net", "/downloads"));
 }
 
 void CookieStorageTest::testCriteriaFind()
@@ -125,6 +141,16 @@ void CookieStorageTest::testCriteriaFind()
   Cookie echo("echo", "ECHO", "/", "www.aria2.org", false);
   Cookie foxtrot("foxtrot", "FOXTROT", "/", ".sf.net", false);
   Cookie golf("golf", "GOLF", "/", "192.168.1.1", false);
+  Cookie hotel1("hotel", "HOTEL1", "/", "samename.x", false);
+  Cookie hotel2("hotel", "HOTEL2", "/hotel", "samename.x", false);
+  Cookie hotel3("hotel", "HOTEL3", "/bar/wine", "samename.x", false);
+  Cookie hotel4("hotel", "HOTEL4", "/bar/", "samename.x", false);
+  Cookie india1("india", "INDIA1", "/foo", "default.domain", false);
+  india1.markOriginServerOnly();
+  Cookie india2("india", "INDIA2", "/", "default.domain", false);
+  Cookie juliet1("juliet", "JULIET1", "/foo", "localhost", false);
+  juliet1.markOriginServerOnly();
+  Cookie juliet2("juliet", "JULIET2", "/", "localhost", false);
 
   CPPUNIT_ASSERT(st.store(alpha));
   CPPUNIT_ASSERT(st.store(bravo));
@@ -133,7 +159,15 @@ void CookieStorageTest::testCriteriaFind()
   CPPUNIT_ASSERT(st.store(echo));
   CPPUNIT_ASSERT(st.store(foxtrot));
   CPPUNIT_ASSERT(st.store(golf));
-  
+  CPPUNIT_ASSERT(st.store(hotel1));
+  CPPUNIT_ASSERT(st.store(hotel2));
+  CPPUNIT_ASSERT(st.store(hotel3));
+  CPPUNIT_ASSERT(st.store(hotel4));
+  CPPUNIT_ASSERT(st.store(india1));
+  CPPUNIT_ASSERT(st.store(india2));
+  CPPUNIT_ASSERT(st.store(juliet1));
+  CPPUNIT_ASSERT(st.store(juliet2));
+
   std::deque<Cookie> aria2Slash = st.criteriaFind("www.aria2.org", "/",
                                                   0, false);
   CPPUNIT_ASSERT_EQUAL((size_t)2, aria2Slash.size());
@@ -168,6 +202,30 @@ void CookieStorageTest::testCriteriaFind()
                                                           false);
   CPPUNIT_ASSERT_EQUAL((size_t)1, numericHostCookies.size());
   CPPUNIT_ASSERT_EQUAL(std::string("golf"), numericHostCookies[0].getName());
+
+  std::deque<Cookie> sameNameCookies =
+    st.criteriaFind("samename.x", "/bar/wine", 0, false);
+  CPPUNIT_ASSERT_EQUAL((size_t)3, sameNameCookies.size());
+  CPPUNIT_ASSERT_EQUAL(std::string("HOTEL3"), sameNameCookies[0].getValue());
+  CPPUNIT_ASSERT_EQUAL(std::string("HOTEL4"), sameNameCookies[1].getValue());
+  CPPUNIT_ASSERT_EQUAL(std::string("HOTEL1"), sameNameCookies[2].getValue());
+
+  std::deque<Cookie> defaultDomainCookies =
+    st.criteriaFind("default.domain", "/foo", 0, false);
+  CPPUNIT_ASSERT_EQUAL((size_t)2, defaultDomainCookies.size());
+  CPPUNIT_ASSERT_EQUAL(std::string("INDIA1"),
+                       defaultDomainCookies[0].getValue());
+  CPPUNIT_ASSERT_EQUAL(std::string("INDIA2"),
+                       defaultDomainCookies[1].getValue());
+
+  // localhost.local case
+  std::deque<Cookie> localDomainCookies =
+    st.criteriaFind("localhost", "/foo", 0, false);
+  CPPUNIT_ASSERT_EQUAL((size_t)2, localDomainCookies.size());
+  CPPUNIT_ASSERT_EQUAL(std::string("JULIET1"),
+                       localDomainCookies[0].getValue());
+  CPPUNIT_ASSERT_EQUAL(std::string("JULIET2"),
+                       localDomainCookies[1].getValue());
 }
 
 void CookieStorageTest::testLoad()
@@ -178,7 +236,10 @@ void CookieStorageTest::testLoad()
 
   CPPUNIT_ASSERT_EQUAL((size_t)4, st.size());
 
-  Cookie c = *st.begin();
+  std::vector<Cookie> cookies;
+  dumpCookie(cookies, st);
+
+  Cookie c = cookies[0];
   CPPUNIT_ASSERT_EQUAL(std::string("JSESSIONID"), c.getName());
   CPPUNIT_ASSERT_EQUAL(std::string("123456789"), c.getValue());
   CPPUNIT_ASSERT_EQUAL((time_t)2147483647, c.getExpiry());
@@ -186,7 +247,15 @@ void CookieStorageTest::testLoad()
   CPPUNIT_ASSERT_EQUAL(std::string(".localhost.local"), c.getDomain());
   CPPUNIT_ASSERT(c.isSecureCookie());
 
-  c = *(st.begin()+1);
+  c = cookies[1];
+  CPPUNIT_ASSERT_EQUAL(std::string("novalue"), c.getName());
+  CPPUNIT_ASSERT_EQUAL(std::string(""), c.getValue());
+  CPPUNIT_ASSERT_EQUAL((time_t)2147483647, c.getExpiry());
+  CPPUNIT_ASSERT_EQUAL(std::string("/"), c.getPath());
+  CPPUNIT_ASSERT_EQUAL(std::string(".localhost.local"), c.getDomain());
+  CPPUNIT_ASSERT(!c.isSecureCookie());
+
+  c = cookies[2];
   CPPUNIT_ASSERT_EQUAL(std::string("passwd"), c.getName());
   CPPUNIT_ASSERT_EQUAL(std::string("secret"), c.getValue());
   CPPUNIT_ASSERT_EQUAL((time_t)2147483647, c.getExpiry());
@@ -194,21 +263,13 @@ void CookieStorageTest::testLoad()
   CPPUNIT_ASSERT_EQUAL(std::string(".localhost.local"), c.getDomain());
   CPPUNIT_ASSERT(!c.isSecureCookie());
 
-  c = *(st.begin()+2);
+  c = cookies[3];
   CPPUNIT_ASSERT_EQUAL(std::string("TAX"), c.getName());
   CPPUNIT_ASSERT_EQUAL(std::string("1000"), c.getValue());
   CPPUNIT_ASSERT_EQUAL((time_t)2147483647, c.getExpiry());
   CPPUNIT_ASSERT_EQUAL(std::string("/"), c.getPath());
   CPPUNIT_ASSERT_EQUAL(std::string(".overflow.local"), c.getDomain());
   CPPUNIT_ASSERT(!c.isSecureCookie());
-
-  c = *(st.begin()+3);
-  CPPUNIT_ASSERT_EQUAL(std::string("novalue"), c.getName());
-  CPPUNIT_ASSERT_EQUAL(std::string(""), c.getValue());
-  CPPUNIT_ASSERT_EQUAL((time_t)2147483647, c.getExpiry());
-  CPPUNIT_ASSERT_EQUAL(std::string("/"), c.getPath());
-  CPPUNIT_ASSERT_EQUAL(std::string(".localhost.local"), c.getDomain());
-  CPPUNIT_ASSERT(!c.isSecureCookie());
 }
 
 void CookieStorageTest::testLoad_sqlite3()
@@ -217,8 +278,9 @@ void CookieStorageTest::testLoad_sqlite3()
 #ifdef HAVE_SQLITE3
   st.load("cookies.sqlite");
   CPPUNIT_ASSERT_EQUAL((size_t)3, st.size());
-  std::deque<Cookie>::const_iterator i = st.begin();
-  Cookie c = *i++;
+  std::vector<Cookie> cookies;
+  dumpCookie(cookies, st);
+  Cookie c = cookies[0];
   CPPUNIT_ASSERT_EQUAL(std::string("JSESSIONID"), c.getName());
   CPPUNIT_ASSERT_EQUAL(std::string("123456789"), c.getValue());
   CPPUNIT_ASSERT_EQUAL((time_t)2147483647, c.getExpiry());
@@ -227,7 +289,7 @@ void CookieStorageTest::testLoad_sqlite3()
   CPPUNIT_ASSERT(c.isSecureCookie());
   CPPUNIT_ASSERT(!c.isSessionCookie());
 
-  c = *i++;
+  c = cookies[1];
   CPPUNIT_ASSERT_EQUAL(std::string("uid"), c.getName());
   CPPUNIT_ASSERT_EQUAL(std::string(""), c.getValue());
   CPPUNIT_ASSERT_EQUAL((time_t)0, c.getExpiry());
@@ -236,7 +298,7 @@ void CookieStorageTest::testLoad_sqlite3()
   CPPUNIT_ASSERT(!c.isSecureCookie());
   CPPUNIT_ASSERT(c.isSessionCookie());
 
-  c = *i++;
+  c = cookies[2];
   CPPUNIT_ASSERT_EQUAL(std::string("foo"), c.getName());
   CPPUNIT_ASSERT_EQUAL(std::string("bar"), c.getValue());
   CPPUNIT_ASSERT_EQUAL((time_t)2147483647, c.getExpiry());
@@ -258,19 +320,24 @@ void CookieStorageTest::testLoad_fileNotfound()
 
 void CookieStorageTest::testSaveNsFormat()
 {
+  // TODO add cookie with default domain
   std::string filename = "/tmp/aria2_CookieStorageTest_testSaveNsFormat";
   File(filename).remove();
   CookieStorage st;
-  st.store(Cookie("uid","tujikawa","/",".domain.org",true));
-  st.store(Cookie("favorite","classic","/config",".domain.org",false));
+  st.store(Cookie("favorite","classic","/config",".domain.org",true));
+  st.store(Cookie("uid","tujikawa","/",".domain.org",false));
   st.saveNsFormat(filename);
   CookieStorage loadst;
   loadst.load(filename);
   CPPUNIT_ASSERT_EQUAL((size_t)2, loadst.size());
-  CPPUNIT_ASSERT_EQUAL(std::string("uid"), (*loadst.begin()).getName());
-  CPPUNIT_ASSERT_EQUAL((time_t)0, (*loadst.begin()).getExpiry());
-  CPPUNIT_ASSERT((*loadst.begin()).isSessionCookie());
-  CPPUNIT_ASSERT_EQUAL(std::string("favorite"), (*(loadst.begin()+1)).getName());
+
+  std::vector<Cookie> cookies;
+  dumpCookie(cookies, loadst);
+
+  CPPUNIT_ASSERT_EQUAL(std::string("favorite"), cookies[0].getName());
+  CPPUNIT_ASSERT_EQUAL((time_t)0, cookies[0].getExpiry());
+  CPPUNIT_ASSERT(cookies[0].isSessionCookie());
+  CPPUNIT_ASSERT_EQUAL(std::string("uid"), cookies[1].getName());
 }
 
 void CookieStorageTest::testSaveNsFormat_fail()
@@ -284,4 +351,26 @@ void CookieStorageTest::testSaveNsFormat_fail()
   CPPUNIT_ASSERT(!st.saveNsFormat(filename));
 }
 
+void CookieStorageTest::testCookieIsFull()
+{
+  CookieStorage st;
+  for(size_t i = 0; i < CookieStorage::MAX_COOKIE_PER_DOMAIN+1; ++i) {
+    Cookie c("k"+util::itos(i), "v", "/", ".aria2.org", false);
+    st.store(c);
+  }
+  CPPUNIT_ASSERT_EQUAL((size_t)CookieStorage::MAX_COOKIE_PER_DOMAIN, st.size());
+}
+
+void CookieStorageTest::testDomainIsFull()
+{
+  // See DOMAIN_EVICTION_TRIGGER and DOMAIN_EVICTION_RATE in
+  // CookieStorage.cc
+  CookieStorage st;
+  for(size_t i = 0; i < 601; ++i) {
+    Cookie c("k", "v", "/", "domain"+util::itos(i), false);
+    st.store(c);
+  }
+  CPPUNIT_ASSERT_EQUAL((size_t)541, st.size());
+}
+
 } // namespace aria2
diff --git a/test/CookieTest.cc b/test/CookieTest.cc
index e18bf96d..4290c7fe 100644
--- a/test/CookieTest.cc
+++ b/test/CookieTest.cc
@@ -19,6 +19,7 @@ class CookieTest:public CppUnit::TestFixture {
   CPPUNIT_TEST(testIsExpired);
   CPPUNIT_TEST(testNormalizeDomain);
   CPPUNIT_TEST(testToNsCookieFormat);
+  CPPUNIT_TEST(testMarkOriginServerOnly);
   CPPUNIT_TEST_SUITE_END();
 public:
   void setUp() {}
@@ -31,6 +32,7 @@ public:
   void testIsExpired();
   void testNormalizeDomain();
   void testToNsCookieFormat();
+  void testMarkOriginServerOnly();
 };
 
 
@@ -189,4 +191,14 @@ void CookieTest::testToNsCookieFormat()
      Cookie("hello","world","/","domain.org",true).toNsCookieFormat());
 }
 
+void CookieTest::testMarkOriginServerOnly()
+{
+  Cookie c("k", "v", "/", "aria2.sf.net", false);
+  c.markOriginServerOnly();
+  CPPUNIT_ASSERT_EQUAL(std::string("aria2.sf.net"), c.getDomain());
+  Cookie ip("k", "v", "/", "192.168.0.1", false);
+  ip.markOriginServerOnly();
+  CPPUNIT_ASSERT_EQUAL(std::string("192.168.0.1"), ip.getDomain());
+}
+
 } // namespace aria2
diff --git a/test/HttpResponseTest.cc b/test/HttpResponseTest.cc
index 4b3d3b4b..42c342cb 100644
--- a/test/HttpResponseTest.cc
+++ b/test/HttpResponseTest.cc
@@ -495,9 +495,10 @@ void HttpResponseTest::testRetrieveCookie()
 
   CPPUNIT_ASSERT_EQUAL((size_t)2, st->size());
 
-  std::deque<Cookie>::const_iterator citer = st->begin();
-  CPPUNIT_ASSERT_EQUAL(std::string("k2=v2"), (*(st->begin())).toString());
-  CPPUNIT_ASSERT_EQUAL(std::string("k3=v3"), (*(st->begin()+1)).toString());
+  std::vector<Cookie> cookies;
+  st->dumpCookie(std::back_inserter(cookies));
+  CPPUNIT_ASSERT_EQUAL(std::string("k2=v2"), cookies[0].toString());
+  CPPUNIT_ASSERT_EQUAL(std::string("k3=v3"), cookies[1].toString());
 }
 
 void HttpResponseTest::testSupportsPersistentConnection()
diff --git a/test/TestUtil.h b/test/TestUtil.h
index e63c52b1..51d67569 100644
--- a/test/TestUtil.h
+++ b/test/TestUtil.h
@@ -1,10 +1,25 @@
 #include "common.h"
+
 #include <string>
 
+#include "Cookie.h"
+
 namespace aria2 {
 
 void createFile(const std::string& filename, size_t length);
 
 std::string readFile(const std::string& path);
 
+class CookieSorter {
+public:
+  bool operator()(const Cookie& lhs, const Cookie& rhs) const
+  {
+    if(lhs.getDomain() == rhs.getDomain()) {
+      return lhs.getName() < rhs.getName();
+    } else {
+      return lhs.getDomain() < rhs.getDomain();
+    }
+  }
+};
+
 } // namespace aria2
diff --git a/test/a2functionalTest.cc b/test/a2functionalTest.cc
index fd811d0b..776930c7 100644
--- a/test/a2functionalTest.cc
+++ b/test/a2functionalTest.cc
@@ -1,9 +1,10 @@
 #include "a2functional.h"
 
-#include <cppunit/extensions/HelperMacros.h>
-
 #include <string>
 #include <numeric>
+#include <algorithm>
+
+#include <cppunit/extensions/HelperMacros.h>
 
 namespace aria2 {
 
@@ -15,6 +16,7 @@ class a2functionalTest:public CppUnit::TestFixture {
   CPPUNIT_TEST(testStrjoin);
   CPPUNIT_TEST(testStrconcat);
   CPPUNIT_TEST(testStrappend);
+  CPPUNIT_TEST(testLeastRecentAccess);
   CPPUNIT_TEST_SUITE_END();
 public:
   void testMemFunSh();
@@ -22,7 +24,8 @@ public:
   void testStrjoin();
   void testStrconcat();
   void testStrappend();
-  
+  void testLeastRecentAccess();
+
   class Greeting {
   public:
     virtual ~Greeting() {}
@@ -48,6 +51,15 @@ public:
 
   };
 
+  struct LastAccess {
+    time_t _lastAccess;
+    LastAccess(time_t lastAccess):_lastAccess(lastAccess) {}
+
+    time_t getLastAccess() const
+    {
+      return _lastAccess;
+    }
+  };
 };
 
 
@@ -100,4 +112,16 @@ void a2functionalTest::testStrappend()
   CPPUNIT_ASSERT_EQUAL(std::string("X=3,Y=5"), str);
 }
 
+void a2functionalTest::testLeastRecentAccess()
+{
+  std::vector<LastAccess> v;
+  for(int i = 99; i >= 0; --i) {
+    v.push_back(LastAccess(i));
+  }
+  std::sort(v.begin(), v.end(), LeastRecentAccess<LastAccess>());
+  for(int i = 0; i < 100; ++i) {
+    CPPUNIT_ASSERT_EQUAL((time_t)i, v[i]._lastAccess);
+  }
+}
+
 } // namespace aria2