summaryrefslogtreecommitdiff
path: root/indra/llmessage
diff options
context:
space:
mode:
Diffstat (limited to 'indra/llmessage')
-rw-r--r--indra/llmessage/CMakeLists.txt3
-rw-r--r--indra/llmessage/llavatarnamecache.cpp794
-rw-r--r--indra/llmessage/llavatarnamecache.h103
-rw-r--r--indra/llmessage/llcachename.cpp191
-rw-r--r--indra/llmessage/llcachename.h39
-rw-r--r--indra/llmessage/mean_collision_data.h5
-rw-r--r--indra/llmessage/tests/llavatarnamecache_test.cpp109
7 files changed, 1118 insertions, 126 deletions
diff --git a/indra/llmessage/CMakeLists.txt b/indra/llmessage/CMakeLists.txt
index 1f8ee26716..1cad0f6d22 100644
--- a/indra/llmessage/CMakeLists.txt
+++ b/indra/llmessage/CMakeLists.txt
@@ -25,6 +25,7 @@ set(llmessage_SOURCE_FILES
llares.cpp
llareslistener.cpp
llassetstorage.cpp
+ llavatarnamecache.cpp
llblowfishcipher.cpp
llbuffer.cpp
llbufferstream.cpp
@@ -110,6 +111,7 @@ set(llmessage_HEADER_FILES
llares.h
llareslistener.h
llassetstorage.h
+ llavatarnamecache.h
llblowfishcipher.h
llbuffer.h
llbufferstream.h
@@ -248,6 +250,7 @@ if (LL_TESTS)
"${CMAKE_CURRENT_SOURCE_DIR}/tests/test_llsdmessage_peer.py"
)
+ LL_ADD_INTEGRATION_TEST(llavatarnamecache "" "${test_libs}")
LL_ADD_INTEGRATION_TEST(llhost "" "${test_libs}")
LL_ADD_INTEGRATION_TEST(llpartdata "" "${test_libs}")
LL_ADD_INTEGRATION_TEST(llxfer_file "" "${test_libs}")
diff --git a/indra/llmessage/llavatarnamecache.cpp b/indra/llmessage/llavatarnamecache.cpp
new file mode 100644
index 0000000000..4b41c7e5b1
--- /dev/null
+++ b/indra/llmessage/llavatarnamecache.cpp
@@ -0,0 +1,794 @@
+/**
+ * @file llavatarnamecache.cpp
+ * @brief Provides lookup of avatar SLIDs ("bobsmith123") and display names
+ * ("James Cook") from avatar UUIDs.
+ *
+ * $LicenseInfo:firstyear=2010&license=viewergpl$
+ *
+ * Copyright (c) 2010, Linden Research, Inc.
+ *
+ * Second Life Viewer Source Code
+ * The source code in this file ("Source Code") is provided by Linden Lab
+ * to you under the terms of the GNU General Public License, version 2.0
+ * ("GPL"), unless you have obtained a separate licensing agreement
+ * ("Other License"), formally executed by you and Linden Lab. Terms of
+ * the GPL can be found in doc/GPL-license.txt in this distribution, or
+ * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2
+ *
+ * There are special exceptions to the terms and conditions of the GPL as
+ * it is applied to this Source Code. View the full text of the exception
+ * in the file doc/FLOSS-exception.txt in this software distribution, or
+ * online at
+ * http://secondlifegrid.net/programs/open_source/licensing/flossexception
+ *
+ * By copying, modifying or distributing this software, you acknowledge
+ * that you have read and understood your obligations described above,
+ * and agree to abide by those obligations.
+ *
+ * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO
+ * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY,
+ * COMPLETENESS OR PERFORMANCE.
+ * $/LicenseInfo$
+ */
+#include "linden_common.h"
+
+#include "llavatarnamecache.h"
+
+#include "llcachename.h" // we wrap this system
+#include "llframetimer.h"
+#include "llhttpclient.h"
+#include "llsd.h"
+#include "llsdserialize.h"
+
+#include <boost/tokenizer.hpp>
+
+#include <map>
+#include <set>
+
+namespace LLAvatarNameCache
+{
+ // Manual override for display names - can disable even if the region
+ // supports it.
+ bool sUseDisplayNames = true;
+
+ // Cache starts in a paused state until we can determine if the
+ // current region supports display names.
+ bool sRunning = false;
+
+ // Base lookup URL for name service.
+ // On simulator, loaded from indra.xml
+ // On viewer, usually a simulator capability (at People API team's request)
+ // Includes the trailing slash, like "http://pdp60.lindenlab.com:8000/agents/"
+ std::string sNameLookupURL;
+
+ // accumulated agent IDs for next query against service
+ typedef std::set<LLUUID> ask_queue_t;
+ ask_queue_t sAskQueue;
+
+ // agent IDs that have been requested, but with no reply
+ // maps agent ID to frame time request was made
+ typedef std::map<LLUUID, F64> pending_queue_t;
+ pending_queue_t sPendingQueue;
+
+ // Callbacks to fire when we received a name.
+ // May have multiple callbacks for a single ID, which are
+ // represented as multiple slots bound to the signal.
+ // Avoid copying signals via pointers.
+ typedef std::map<LLUUID, callback_signal_t*> signal_map_t;
+ signal_map_t sSignalMap;
+
+ // names we know about
+ typedef std::map<LLUUID, LLAvatarName> cache_t;
+ cache_t sCache;
+
+ // Send bulk lookup requests a few times a second at most
+ // only need per-frame timing resolution
+ LLFrameTimer sRequestTimer;
+
+ // Periodically clean out expired entries from the cache
+ LLFrameTimer sEraseExpiredTimer;
+
+ //-----------------------------------------------------------------------
+ // Internal methods
+ //-----------------------------------------------------------------------
+
+ // Handle name response off network.
+ // Optionally skip adding to cache, used when this is a fallback to the
+ // legacy name system.
+ void processName(const LLUUID& agent_id,
+ const LLAvatarName& av_name,
+ bool add_to_cache);
+
+ void requestNamesViaCapability();
+
+ // Legacy name system callback
+ void legacyNameCallback(const LLUUID& agent_id,
+ const std::string& full_name,
+ bool is_group);
+
+ void requestNamesViaLegacy();
+
+ // Fill in an LLAvatarName with the legacy name data
+ void buildLegacyName(const std::string& full_name,
+ LLAvatarName* av_name);
+
+ // Do a single callback to a given slot
+ void fireSignal(const LLUUID& agent_id,
+ const callback_slot_t& slot,
+ const LLAvatarName& av_name);
+
+ // Is a request in-flight over the network?
+ bool isRequestPending(const LLUUID& agent_id);
+
+ // Erase expired names from cache
+ void eraseExpired();
+
+ bool expirationFromCacheControl(LLSD headers, F64 *expires);
+}
+
+/* Sample response:
+<?xml version="1.0"?>
+<llsd>
+ <map>
+ <key>agents</key>
+ <array>
+ <map>
+ <key>display_name_next_update</key>
+ <date>2010-04-16T21:34:02+00:00Z</date>
+ <key>display_name_expires</key>
+ <date>2010-04-16T21:32:26.142178+00:00Z</date>
+ <key>display_name</key>
+ <string>MickBot390 LLQABot</string>
+ <key>sl_id</key>
+ <string>mickbot390.llqabot</string>
+ <key>id</key>
+ <string>0012809d-7d2d-4c24-9609-af1230a37715</string>
+ <key>is_display_name_default</key>
+ <boolean>false</boolean>
+ </map>
+ <map>
+ <key>display_name_next_update</key>
+ <date>2010-04-16T21:34:02+00:00Z</date>
+ <key>display_name_expires</key>
+ <date>2010-04-16T21:32:26.142178+00:00Z</date>
+ <key>display_name</key>
+ <string>Bjork Gudmundsdottir</string>
+ <key>sl_id</key>
+ <string>sardonyx.linden</string>
+ <key>id</key>
+ <string>3941037e-78ab-45f0-b421-bd6e77c1804d</string>
+ <key>is_display_name_default</key>
+ <boolean>true</boolean>
+ </map>
+ </array>
+ </map>
+</llsd>
+*/
+
+class LLAvatarNameResponder : public LLHTTPClient::Responder
+{
+private:
+ // need to store agent ids that are part of this request in case of
+ // an error, so we can flag them as unavailable
+ std::vector<LLUUID> mAgentIDs;
+
+ // Need the headers to look up Expires: and Retry-After:
+ LLSD mHeaders;
+
+public:
+ LLAvatarNameResponder(const std::vector<LLUUID>& agent_ids)
+ : mAgentIDs(agent_ids),
+ mHeaders()
+ { }
+
+ /*virtual*/ void completedHeader(U32 status, const std::string& reason,
+ const LLSD& headers)
+ {
+ mHeaders = headers;
+ }
+
+ /*virtual*/ void result(const LLSD& content)
+ {
+ // Pull expiration out of headers if available
+ F64 expires = LLAvatarNameCache::nameExpirationFromHeaders(mHeaders);
+
+ LLSD agents = content["agents"];
+ LLSD::array_const_iterator it = agents.beginArray();
+ for ( ; it != agents.endArray(); ++it)
+ {
+ const LLSD& row = *it;
+ LLUUID agent_id = row["id"].asUUID();
+
+ LLAvatarName av_name;
+ av_name.fromLLSD(row);
+
+ // Use expiration time from header
+ av_name.mExpires = expires;
+
+ // Some avatars don't have explicit display names set
+ if (av_name.mDisplayName.empty())
+ {
+ av_name.mDisplayName = av_name.mUsername;
+ }
+
+ // cache it and fire signals
+ LLAvatarNameCache::processName(agent_id, av_name, true);
+ }
+
+ // Same logic as error response case
+ LLSD unresolved_agents = content["bad_ids"];
+ if (unresolved_agents.size() > 0)
+ {
+ const std::string DUMMY_NAME("\?\?\?");
+ LLAvatarName av_name;
+ av_name.mUsername = DUMMY_NAME;
+ av_name.mDisplayName = DUMMY_NAME;
+ av_name.mIsDisplayNameDefault = false;
+ av_name.mIsDummy = true;
+ av_name.mExpires = expires;
+
+ it = unresolved_agents.beginArray();
+ for ( ; it != unresolved_agents.endArray(); ++it)
+ {
+ const LLUUID& agent_id = *it;
+ // cache it and fire signals
+ LLAvatarNameCache::processName(agent_id, av_name, true);
+ }
+ }
+ }
+
+ /*virtual*/ void error(U32 status, const std::string& reason)
+ {
+ // We're going to construct a dummy record and cache it for a while,
+ // either briefly for a 503 Service Unavailable, or longer for other
+ // errors.
+ F64 retry_timestamp = errorRetryTimestamp(status);
+
+ // *NOTE: "??" starts trigraphs in C/C++, escape the question marks.
+ const std::string DUMMY_NAME("\?\?\?");
+ LLAvatarName av_name;
+ av_name.mUsername = DUMMY_NAME;
+ av_name.mDisplayName = DUMMY_NAME;
+ av_name.mIsDisplayNameDefault = false;
+ av_name.mIsDummy = true;
+ av_name.mExpires = retry_timestamp;
+
+ // Add dummy records for all agent IDs in this request
+ std::vector<LLUUID>::const_iterator it = mAgentIDs.begin();
+ for ( ; it != mAgentIDs.end(); ++it)
+ {
+ const LLUUID& agent_id = *it;
+ // cache it and fire signals
+ LLAvatarNameCache::processName(agent_id, av_name, true);
+ }
+ }
+
+ // Return time to retry a request that generated an error, based on
+ // error type and headers. Return value is seconds-since-epoch.
+ F64 errorRetryTimestamp(S32 status)
+ {
+ F64 now = LLFrameTimer::getTotalSeconds();
+
+ // Retry-After takes priority
+ LLSD retry_after = mHeaders["retry-after"];
+ if (retry_after.isDefined())
+ {
+ // We only support the delta-seconds type
+ S32 delta_seconds = retry_after.asInteger();
+ if (delta_seconds > 0)
+ {
+ // ...valid delta-seconds
+ return now + F64(delta_seconds);
+ }
+ }
+
+ // If no Retry-After, look for Cache-Control max-age
+ F64 expires = 0.0;
+ if (LLAvatarNameCache::expirationFromCacheControl(mHeaders, &expires))
+ {
+ return expires;
+ }
+
+ // No information in header, make a guess
+ if (status == 503)
+ {
+ // ...service unavailable, retry soon
+ const F64 SERVICE_UNAVAILABLE_DELAY = 600.0; // 10 min
+ return now + SERVICE_UNAVAILABLE_DELAY;
+ }
+ else
+ {
+ // ...other unexpected error
+ const F64 DEFAULT_DELAY = 3600.0; // 1 hour
+ return now + DEFAULT_DELAY;
+ }
+ }
+};
+
+void LLAvatarNameCache::processName(const LLUUID& agent_id,
+ const LLAvatarName& av_name,
+ bool add_to_cache)
+{
+ if (add_to_cache)
+ {
+ sCache[agent_id] = av_name;
+ }
+
+ sPendingQueue.erase(agent_id);
+
+ // signal everyone waiting on this name
+ signal_map_t::iterator sig_it = sSignalMap.find(agent_id);
+ if (sig_it != sSignalMap.end())
+ {
+ callback_signal_t* signal = sig_it->second;
+ (*signal)(agent_id, av_name);
+
+ sSignalMap.erase(agent_id);
+
+ delete signal;
+ signal = NULL;
+ }
+}
+
+void LLAvatarNameCache::requestNamesViaCapability()
+{
+ F64 now = LLFrameTimer::getTotalSeconds();
+
+ // URL format is like:
+ // http://pdp60.lindenlab.com:8000/agents/?ids=3941037e-78ab-45f0-b421-bd6e77c1804d&ids=0012809d-7d2d-4c24-9609-af1230a37715&ids=0019aaba-24af-4f0a-aa72-6457953cf7f0
+ //
+ // Apache can handle URLs of 4096 chars, but let's be conservative
+ const U32 NAME_URL_MAX = 4096;
+ const U32 NAME_URL_SEND_THRESHOLD = 3000;
+ std::string url;
+ url.reserve(NAME_URL_MAX);
+
+ std::vector<LLUUID> agent_ids;
+ agent_ids.reserve(128);
+
+ ask_queue_t::const_iterator it = sAskQueue.begin();
+ for ( ; it != sAskQueue.end(); ++it)
+ {
+ const LLUUID& agent_id = *it;
+
+ if (url.empty())
+ {
+ // ...starting new request
+ url += sNameLookupURL;
+ url += "?ids=";
+ }
+ else
+ {
+ // ...continuing existing request
+ url += "&ids=";
+ }
+ url += agent_id.asString();
+ agent_ids.push_back(agent_id);
+
+ // mark request as pending
+ sPendingQueue[agent_id] = now;
+
+ if (url.size() > NAME_URL_SEND_THRESHOLD)
+ {
+ //llinfos << "requestNames " << url << llendl;
+ LLHTTPClient::get(url, new LLAvatarNameResponder(agent_ids));
+ url.clear();
+ agent_ids.clear();
+ }
+ }
+
+ if (!url.empty())
+ {
+ //llinfos << "requestNames " << url << llendl;
+ LLHTTPClient::get(url, new LLAvatarNameResponder(agent_ids));
+ url.clear();
+ agent_ids.clear();
+ }
+
+ // We've moved all asks to the pending request queue
+ sAskQueue.clear();
+}
+
+void LLAvatarNameCache::legacyNameCallback(const LLUUID& agent_id,
+ const std::string& full_name,
+ bool is_group)
+{
+ // Construct a dummy record for this name. By convention, SLID is blank
+ // Never expires, but not written to disk, so lasts until end of session.
+ LLAvatarName av_name;
+ buildLegacyName(full_name, &av_name);
+
+ // Don't add to cache, the data already exists in the legacy name system
+ // cache and we don't want or need duplicate storage, because keeping the
+ // two copies in sync is complex.
+ processName(agent_id, av_name, false);
+}
+
+void LLAvatarNameCache::requestNamesViaLegacy()
+{
+ F64 now = LLFrameTimer::getTotalSeconds();
+ std::string full_name;
+ ask_queue_t::const_iterator it = sAskQueue.begin();
+ for (; it != sAskQueue.end(); ++it)
+ {
+ const LLUUID& agent_id = *it;
+
+ // Mark as pending first, just in case the callback is immediately
+ // invoked below. This should never happen in practice.
+ sPendingQueue[agent_id] = now;
+
+ gCacheName->get(agent_id, false,
+ boost::bind(&LLAvatarNameCache::legacyNameCallback,
+ _1, _2, _3));
+ }
+
+ // We've either answered immediately or moved all asks to the
+ // pending queue
+ sAskQueue.clear();
+}
+
+void LLAvatarNameCache::initClass(bool running)
+{
+ sRunning = running;
+}
+
+void LLAvatarNameCache::cleanupClass()
+{
+}
+
+void LLAvatarNameCache::importFile(std::istream& istr)
+{
+ LLSD data;
+ S32 parse_count = LLSDSerialize::fromXMLDocument(data, istr);
+ if (parse_count < 1) return;
+
+ // by convention LLSD storage is a map
+ // we only store one entry in the map
+ LLSD agents = data["agents"];
+
+ LLUUID agent_id;
+ LLAvatarName av_name;
+ LLSD::map_const_iterator it = agents.beginMap();
+ for ( ; it != agents.endMap(); ++it)
+ {
+ agent_id.set(it->first);
+ av_name.fromLLSD( it->second );
+ sCache[agent_id] = av_name;
+ }
+ // entries may have expired since we last ran the viewer, just
+ // clean them out now
+ eraseExpired();
+ llinfos << "loaded " << sCache.size() << llendl;
+}
+
+void LLAvatarNameCache::exportFile(std::ostream& ostr)
+{
+ LLSD agents;
+ cache_t::const_iterator it = sCache.begin();
+ for ( ; it != sCache.end(); ++it)
+ {
+ const LLUUID& agent_id = it->first;
+ const LLAvatarName& av_name = it->second;
+ if (!av_name.mIsDummy)
+ {
+ // key must be a string
+ agents[agent_id.asString()] = av_name.asLLSD();
+ }
+ }
+ LLSD data;
+ data["agents"] = agents;
+ LLSDSerialize::toPrettyXML(data, ostr);
+}
+
+void LLAvatarNameCache::setNameLookupURL(const std::string& name_lookup_url)
+{
+ sNameLookupURL = name_lookup_url;
+}
+
+bool LLAvatarNameCache::hasNameLookupURL()
+{
+ return !sNameLookupURL.empty();
+}
+
+void LLAvatarNameCache::idle()
+{
+ // By convention, start running at first idle() call
+ sRunning = true;
+
+ // 100 ms is the threshold for "user speed" operations, so we can
+ // stall for about that long to batch up requests.
+ const F32 SECS_BETWEEN_REQUESTS = 0.1f;
+ if (!sRequestTimer.checkExpirationAndReset(SECS_BETWEEN_REQUESTS))
+ {
+ return;
+ }
+
+ // Must be large relative to above
+ const F32 ERASE_EXPIRED_TIMEOUT = 60.f; // seconds
+ if (sEraseExpiredTimer.checkExpirationAndReset(ERASE_EXPIRED_TIMEOUT))
+ {
+ eraseExpired();
+ }
+
+ if (sAskQueue.empty())
+ {
+ return;
+ }
+
+ if (useDisplayNames())
+ {
+ requestNamesViaCapability();
+ }
+ else
+ {
+ // ...fall back to legacy name cache system
+ requestNamesViaLegacy();
+ }
+}
+
+bool LLAvatarNameCache::isRequestPending(const LLUUID& agent_id)
+{
+ const F64 PENDING_TIMEOUT_SECS = 5.0 * 60.0;
+ F64 now = LLFrameTimer::getTotalSeconds();
+ F64 expire_time = now - PENDING_TIMEOUT_SECS;
+
+ pending_queue_t::const_iterator it = sPendingQueue.find(agent_id);
+ if (it != sPendingQueue.end())
+ {
+ bool request_expired = (it->second < expire_time);
+ return !request_expired;
+ }
+ return false;
+}
+
+void LLAvatarNameCache::eraseExpired()
+{
+ F64 now = LLFrameTimer::getTotalSeconds();
+ cache_t::iterator it = sCache.begin();
+ while (it != sCache.end())
+ {
+ cache_t::iterator cur = it;
+ ++it;
+ const LLAvatarName& av_name = cur->second;
+ if (av_name.mExpires < now)
+ {
+ sCache.erase(cur);
+ }
+ }
+}
+
+void LLAvatarNameCache::buildLegacyName(const std::string& full_name,
+ LLAvatarName* av_name)
+{
+ llassert(av_name);
+ av_name->mUsername = "";
+ av_name->mDisplayName = full_name;
+ av_name->mIsDisplayNameDefault = true;
+ av_name->mIsDummy = true;
+ av_name->mExpires = F64_MAX;
+}
+
+bool LLAvatarNameCache::get(const LLUUID& agent_id, LLAvatarName *av_name)
+{
+ if (sRunning)
+ {
+ // ...only do immediate lookups when cache is running
+ if (useDisplayNames())
+ {
+ // ...use display names cache
+ std::map<LLUUID,LLAvatarName>::iterator it = sCache.find(agent_id);
+ if (it != sCache.end())
+ {
+ *av_name = it->second;
+ return true;
+ }
+ }
+ else
+ {
+ // ...use legacy names cache
+ std::string full_name;
+ if (gCacheName->getFullName(agent_id, full_name))
+ {
+ buildLegacyName(full_name, av_name);
+ return true;
+ }
+ }
+ }
+
+ if (!isRequestPending(agent_id))
+ {
+ sAskQueue.insert(agent_id);
+ }
+
+ return false;
+}
+
+void LLAvatarNameCache::fireSignal(const LLUUID& agent_id,
+ const callback_slot_t& slot,
+ const LLAvatarName& av_name)
+{
+ callback_signal_t signal;
+ signal.connect(slot);
+ signal(agent_id, av_name);
+}
+
+void LLAvatarNameCache::get(const LLUUID& agent_id, callback_slot_t slot)
+{
+ if (sRunning)
+ {
+ // ...only do immediate lookups when cache is running
+ if (useDisplayNames())
+ {
+ // ...use new cache
+ std::map<LLUUID,LLAvatarName>::iterator it = sCache.find(agent_id);
+ if (it != sCache.end())
+ {
+ // ...name already exists in cache, fire callback now
+ fireSignal(agent_id, slot, it->second);
+ return;
+ }
+ }
+ else
+ {
+ // ...use old name system
+ std::string full_name;
+ if (gCacheName->getFullName(agent_id, full_name))
+ {
+ LLAvatarName av_name;
+ buildLegacyName(full_name, &av_name);
+ fireSignal(agent_id, slot, av_name);
+ return;
+ }
+ }
+ }
+
+ // schedule a request
+ if (!isRequestPending(agent_id))
+ {
+ sAskQueue.insert(agent_id);
+ }
+
+ // always store additional callback, even if request is pending
+ signal_map_t::iterator sig_it = sSignalMap.find(agent_id);
+ if (sig_it == sSignalMap.end())
+ {
+ // ...new callback for this id
+ callback_signal_t* signal = new callback_signal_t();
+ signal->connect(slot);
+ sSignalMap[agent_id] = signal;
+ }
+ else
+ {
+ // ...existing callback, bind additional slot
+ callback_signal_t* signal = sig_it->second;
+ signal->connect(slot);
+ }
+}
+
+
+void LLAvatarNameCache::setUseDisplayNames(bool use)
+{
+ if (use != sUseDisplayNames)
+ {
+ sUseDisplayNames = use;
+ // flush our cache
+ sCache.clear();
+ }
+}
+
+bool LLAvatarNameCache::useDisplayNames()
+{
+ // Must be both manually set on and able to look up names.
+ return sUseDisplayNames && !sNameLookupURL.empty();
+}
+
+void LLAvatarNameCache::erase(const LLUUID& agent_id)
+{
+ sCache.erase(agent_id);
+}
+
+void LLAvatarNameCache::fetch(const LLUUID& agent_id)
+{
+ // re-request, even if request is already pending
+ sAskQueue.insert(agent_id);
+}
+
+void LLAvatarNameCache::insert(const LLUUID& agent_id, const LLAvatarName& av_name)
+{
+ // *TODO: update timestamp if zero?
+ sCache[agent_id] = av_name;
+}
+
+F64 LLAvatarNameCache::nameExpirationFromHeaders(LLSD headers)
+{
+ F64 expires = 0.0;
+ if (expirationFromCacheControl(headers, &expires))
+ {
+ return expires;
+ }
+ else
+ {
+ // With no expiration info, default to a day
+ const F64 DEFAULT_EXPIRES = 24.0 * 60.0 * 60.0;
+ F64 now = LLFrameTimer::getTotalSeconds();
+ return now + DEFAULT_EXPIRES;
+ }
+}
+
+bool LLAvatarNameCache::expirationFromCacheControl(LLSD headers, F64 *expires)
+{
+ // Allow the header to override the default
+ LLSD cache_control_header = headers["cache-control"];
+ if (cache_control_header.isDefined())
+ {
+ S32 max_age = 0;
+ std::string cache_control = cache_control_header.asString();
+ if (max_age_from_cache_control(cache_control, &max_age))
+ {
+ F64 now = LLFrameTimer::getTotalSeconds();
+ *expires = now + (F64)max_age;
+ return true;
+ }
+ }
+ return false;
+}
+
+static const std::string MAX_AGE("max-age");
+static const boost::char_separator<char> EQUALS_SEPARATOR("=");
+static const boost::char_separator<char> COMMA_SEPARATOR(",");
+
+bool max_age_from_cache_control(const std::string& cache_control, S32 *max_age)
+{
+ // Split the string on "," to get a list of directives
+ typedef boost::tokenizer<boost::char_separator<char> > tokenizer;
+ tokenizer directives(cache_control, COMMA_SEPARATOR);
+
+ tokenizer::iterator token_it = directives.begin();
+ for ( ; token_it != directives.end(); ++token_it)
+ {
+ // Tokens may have leading or trailing whitespace
+ std::string token = *token_it;
+ LLStringUtil::trim(token);
+
+ if (token.compare(0, MAX_AGE.size(), MAX_AGE) == 0)
+ {
+ // ...this token starts with max-age, so let's chop it up by "="
+ tokenizer subtokens(token, EQUALS_SEPARATOR);
+ tokenizer::iterator subtoken_it = subtokens.begin();
+
+ // Must have a token
+ if (subtoken_it == subtokens.end()) return false;
+ std::string subtoken = *subtoken_it;
+
+ // Must exactly equal "max-age"
+ LLStringUtil::trim(subtoken);
+ if (subtoken != MAX_AGE) return false;
+
+ // Must have another token
+ ++subtoken_it;
+ if (subtoken_it == subtokens.end()) return false;
+ subtoken = *subtoken_it;
+
+ // Must be a valid integer
+ // *NOTE: atoi() returns 0 for invalid values, so we have to
+ // check the string first.
+ // *TODO: Do servers ever send "0000" for zero? We don't handle it
+ LLStringUtil::trim(subtoken);
+ if (subtoken == "0")
+ {
+ *max_age = 0;
+ return true;
+ }
+ S32 val = atoi( subtoken.c_str() );
+ if (val > 0 && val < S32_MAX)
+ {
+ *max_age = val;
+ return true;
+ }
+ return false;
+ }
+ }
+ return false;
+}
+
+
diff --git a/indra/llmessage/llavatarnamecache.h b/indra/llmessage/llavatarnamecache.h
new file mode 100644
index 0000000000..0a8b987b05
--- /dev/null
+++ b/indra/llmessage/llavatarnamecache.h
@@ -0,0 +1,103 @@
+/**
+ * @file llavatarnamecache.h
+ * @brief Provides lookup of avatar SLIDs ("bobsmith123") and display names
+ * ("James Cook") from avatar UUIDs.
+ *
+ * $LicenseInfo:firstyear=2010&license=viewergpl$
+ *
+ * Copyright (c) 2010, Linden Research, Inc.
+ *
+ * Second Life Viewer Source Code
+ * The source code in this file ("Source Code") is provided by Linden Lab
+ * to you under the terms of the GNU General Public License, version 2.0
+ * ("GPL"), unless you have obtained a separate licensing agreement
+ * ("Other License"), formally executed by you and Linden Lab. Terms of
+ * the GPL can be found in doc/GPL-license.txt in this distribution, or
+ * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2
+ *
+ * There are special exceptions to the terms and conditions of the GPL as
+ * it is applied to this Source Code. View the full text of the exception
+ * in the file doc/FLOSS-exception.txt in this software distribution, or
+ * online at
+ * http://secondlifegrid.net/programs/open_source/licensing/flossexception
+ *
+ * By copying, modifying or distributing this software, you acknowledge
+ * that you have read and understood your obligations described above,
+ * and agree to abide by those obligations.
+ *
+ * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO
+ * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY,
+ * COMPLETENESS OR PERFORMANCE.
+ * $/LicenseInfo$
+ */
+#ifndef LLAVATARNAMECACHE_H
+#define LLAVATARNAMECACHE_H
+
+#include "llavatarname.h" // for convenience
+
+#include <boost/signals2.hpp>
+
+class LLSD;
+class LLUUID;
+
+namespace LLAvatarNameCache
+{
+ // Until the cache is set running, immediate lookups will fail and
+ // async lookups will be queued. This allows us to block requests
+ // until we know if the first region supports display names.
+ void initClass(bool running);
+ void cleanupClass();
+
+ void importFile(std::istream& istr);
+ void exportFile(std::ostream& ostr);
+
+ // On the viewer, usually a simulator capabilitity
+ // If empty, name cache will fall back to using legacy name
+ // lookup system
+ void setNameLookupURL(const std::string& name_lookup_url);
+
+ // Do we have a valid lookup URL, hence are we trying to use the
+ // new display name lookup system?
+ bool hasNameLookupURL();
+
+ // Periodically makes a batch request for display names not already in
+ // cache. Call once per frame.
+ void idle();
+
+ // If name is in cache, returns true and fills in provided LLAvatarName
+ // otherwise returns false
+ bool get(const LLUUID& agent_id, LLAvatarName *av_name);
+
+ // Callback types for get() below
+ typedef boost::signals2::signal<
+ void (const LLUUID& agent_id, const LLAvatarName& av_name)>
+ callback_signal_t;
+ typedef callback_signal_t::slot_type callback_slot_t;
+
+ // Fetches name information and calls callback.
+ // If name information is in cache, callback will be called immediately.
+ void get(const LLUUID& agent_id, callback_slot_t slot);
+
+ // Allow display names to be explicitly disabled for testing.
+ void setUseDisplayNames(bool use);
+ bool useDisplayNames();
+
+ void erase(const LLUUID& agent_id);
+
+ // Force a re-fetch of the most recent data, but keep the current
+ // data in cache
+ void fetch(const LLUUID& agent_id);
+
+ void insert(const LLUUID& agent_id, const LLAvatarName& av_name);
+
+ // Compute name expiration time from HTTP Cache-Control header,
+ // or return default value, in seconds from epoch.
+ F64 nameExpirationFromHeaders(LLSD headers);
+}
+
+// Parse a cache-control header to get the max-age delta-seconds.
+// Returns true if header has max-age param and it parses correctly.
+// Exported here to ease unit testing.
+bool max_age_from_cache_control(const std::string& cache_control, S32 *max_age);
+
+#endif
diff --git a/indra/llmessage/llcachename.cpp b/indra/llmessage/llcachename.cpp
index 9871c922f1..8e87b6f9b9 100644
--- a/indra/llmessage/llcachename.cpp
+++ b/indra/llmessage/llcachename.cpp
@@ -75,6 +75,8 @@ public:
public:
bool mIsGroup;
U32 mCreateTime; // unix time_t
+ // IDEVO TODO collapse names to one field, which will eliminate
+ // many string compares on "Resident"
std::string mFirstName;
std::string mLastName;
std::string mGroupName;
@@ -220,7 +222,9 @@ public:
Impl(LLMessageSystem* msg);
~Impl();
-
+
+ BOOL getName(const LLUUID& id, std::string& first, std::string& last);
+
boost::signals2::connection addPending(const LLUUID& id, const LLCacheNameCallback& callback);
void addPending(const LLUUID& id, const LLHost& host);
@@ -306,89 +310,10 @@ boost::signals2::connection LLCacheName::addObserver(const LLCacheNameCallback&
return impl.mSignal.connect(callback);
}
-void LLCacheName::importFile(LLFILE* fp)
-{
- S32 count = 0;
-
- const S32 BUFFER_SIZE = 1024;
- char buffer[BUFFER_SIZE]; /*Flawfinder: ignore*/
-
- // *NOTE: These buffer sizes are hardcoded into sscanf() below
- char id_string[MAX_STRING]; /*Flawfinder: ignore*/
- char firstname[MAX_STRING]; /*Flawfinder: ignore*/
- char lastname[MAX_STRING]; /*Flawfinder: ignore*/
- U32 create_time;
-
- // This is OK if the first line is actually a name. We just don't load it.
- char* valid = fgets(buffer, BUFFER_SIZE, fp);
- if (!valid) return;
-
- // *NOTE: This buffer size is hardcoded into sscanf() below
- char version_string[BUFFER_SIZE]; /*Flawfinder: ignore*/
- S32 version = 0;
- S32 match = sscanf( /* Flawfinder: ignore */
- buffer,
- "%1023s %d",
- version_string, &version);
- if ( match != 2
- || strcmp(version_string, "version")
- || version != CN_FILE_VERSION)
- {
- llwarns << "Ignoring old cache name file format" << llendl;
- return;
- }
-
- // We'll expire entries more than a week old
- U32 now = (U32)time(NULL);
- const U32 SECS_PER_DAY = 60 * 60 * 24;
- U32 delete_before_time = now - (7 * SECS_PER_DAY);
-
- while(!feof(fp))
- {
- valid = fgets(buffer, BUFFER_SIZE, fp);
- if (!valid) break;
-
- match = sscanf( /* Flawfinder: ignore */
- buffer,
- "%254s %u %254s %254s",
- id_string,
- &create_time,
- firstname,
- lastname);
- if (4 != match) continue;
-
- LLUUID id(id_string);
- if (id.isNull()) continue;
-
- // undo trivial XOR
- S32 i;
- for (i = 0; i < UUID_BYTES; i++)
- {
- id.mData[i] ^= 0x33;
- }
-
- // Don't load entries that are more than a week old
- if (create_time < delete_before_time) continue;
-
- LLCacheNameEntry* entry = new LLCacheNameEntry();
- entry->mIsGroup = false;
- entry->mCreateTime = create_time;
- entry->mFirstName = firstname;
- entry->mLastName = lastname;
- impl.mCache[id] = entry;
- std::string fullname = entry->mFirstName + " " + entry->mLastName;
- impl.mReverseCache[fullname] = id;
-
- count++;
- }
-
- llinfos << "LLCacheName loaded " << count << " names" << llendl;
-}
-
bool LLCacheName::importFile(std::istream& istr)
{
LLSD data;
- if(LLSDSerialize::fromXML(data, istr) < 1)
+ if(LLSDSerialize::fromXMLDocument(data, istr) < 1)
return false;
// We'll expire entries more than a week old
@@ -414,7 +339,7 @@ bool LLCacheName::importFile(std::istream& istr)
entry->mFirstName = agent[FIRST].asString();
entry->mLastName = agent[LAST].asString();
impl.mCache[id] = entry;
- std::string fullname = entry->mFirstName + " " + entry->mLastName;
+ std::string fullname = buildFullName(entry->mFirstName, entry->mLastName);
impl.mReverseCache[fullname] = id;
++count;
@@ -463,6 +388,7 @@ void LLCacheName::exportFile(std::ostream& ostr)
// store it
LLUUID id = iter->first;
std::string id_str = id.asString();
+ // IDEVO TODO: Should we store SLIDs with last name "Resident" or not?
if(!entry->mFirstName.empty() && !entry->mLastName.empty())
{
data[AGENTS][id_str][FIRST] = entry->mFirstName;
@@ -480,7 +406,7 @@ void LLCacheName::exportFile(std::ostream& ostr)
}
-BOOL LLCacheName::getName(const LLUUID& id, std::string& first, std::string& last)
+BOOL LLCacheName::Impl::getName(const LLUUID& id, std::string& first, std::string& last)
{
if(id.isNull())
{
@@ -489,7 +415,7 @@ BOOL LLCacheName::getName(const LLUUID& id, std::string& first, std::string& las
return TRUE;
}
- LLCacheNameEntry* entry = get_ptr_in_map(impl.mCache, id );
+ LLCacheNameEntry* entry = get_ptr_in_map(mCache, id );
if (entry)
{
first = entry->mFirstName;
@@ -500,16 +426,17 @@ BOOL LLCacheName::getName(const LLUUID& id, std::string& first, std::string& las
{
first = sCacheName["waiting"];
last.clear();
- if (!impl.isRequestPending(id))
+ if (!isRequestPending(id))
{
- impl.mAskNameQueue.insert(id);
+ mAskNameQueue.insert(id);
}
return FALSE;
}
}
+
// static
-void LLCacheName::LocalizeCacheName(std::string key, std::string value)
+void LLCacheName::localizeCacheName(std::string key, std::string value)
{
if (key!="" && value!= "" )
sCacheName[key]=value;
@@ -520,11 +447,13 @@ void LLCacheName::LocalizeCacheName(std::string key, std::string value)
BOOL LLCacheName::getFullName(const LLUUID& id, std::string& fullname)
{
std::string first_name, last_name;
- BOOL res = getName(id, first_name, last_name);
- fullname = first_name + " " + last_name;
+ BOOL res = impl.getName(id, first_name, last_name);
+ fullname = buildFullName(first_name, last_name);
return res;
}
+
+
BOOL LLCacheName::getGroupName(const LLUUID& id, std::string& group)
{
if(id.isNull())
@@ -561,13 +490,13 @@ BOOL LLCacheName::getGroupName(const LLUUID& id, std::string& group)
BOOL LLCacheName::getUUID(const std::string& first, const std::string& last, LLUUID& id)
{
- std::string fullname = first + " " + last;
- return getUUID(fullname, id);
+ std::string full_name = buildFullName(first, last);
+ return getUUID(full_name, id);
}
-BOOL LLCacheName::getUUID(const std::string& fullname, LLUUID& id)
+BOOL LLCacheName::getUUID(const std::string& full_name, LLUUID& id)
{
- ReverseCache::iterator iter = impl.mReverseCache.find(fullname);
+ ReverseCache::iterator iter = impl.mReverseCache.find(full_name);
if (iter != impl.mReverseCache.end())
{
id = iter->second;
@@ -579,6 +508,25 @@ BOOL LLCacheName::getUUID(const std::string& fullname, LLUUID& id)
}
}
+//static
+std::string LLCacheName::buildFullName(const std::string& first, const std::string& last)
+{
+ std::string fullname = first;
+ if (!last.empty()
+ && last != "Resident")
+ {
+ fullname += ' ';
+ fullname += last;
+ }
+ return fullname;
+}
+
+//static
+std::string LLCacheName::cleanFullName(const std::string& full_name)
+{
+ return full_name.substr(0, full_name.find(" Resident"));
+}
+
// This is a little bit kludgy. LLCacheNameCallback is a slot instead of a function pointer.
// The reason it is a slot is so that the legacy get() function below can bind an old callback
// and pass it as a slot. The reason it isn't a boost::function is so that trackable behavior
@@ -586,7 +534,7 @@ BOOL LLCacheName::getUUID(const std::string& fullname, LLUUID& id)
// we call it immediately. -Steve
// NOTE: Even though passing first and last name is a bit of extra overhead, it eliminates the
// potential need for any parsing should any code need to handle first and last name independently.
-boost::signals2::connection LLCacheName::get(const LLUUID& id, BOOL is_group, const LLCacheNameCallback& callback)
+boost::signals2::connection LLCacheName::get(const LLUUID& id, bool is_group, const LLCacheNameCallback& callback)
{
boost::signals2::connection res;
@@ -594,7 +542,7 @@ boost::signals2::connection LLCacheName::get(const LLUUID& id, BOOL is_group, co
{
LLCacheNameSignal signal;
signal.connect(callback);
- signal(id, sCacheName["nobody"], "", is_group);
+ signal(id, sCacheName["nobody"], is_group);
return res;
}
@@ -606,11 +554,13 @@ boost::signals2::connection LLCacheName::get(const LLUUID& id, BOOL is_group, co
// id found in map therefore we can call the callback immediately.
if (entry->mIsGroup)
{
- signal(id, entry->mGroupName, "", entry->mIsGroup);
+ signal(id, entry->mGroupName, entry->mIsGroup);
}
else
{
- signal(id, entry->mFirstName, entry->mLastName, entry->mIsGroup);
+ std::string fullname =
+ buildFullName(entry->mFirstName, entry->mLastName);
+ signal(id, fullname, entry->mIsGroup);
}
}
else
@@ -632,9 +582,9 @@ boost::signals2::connection LLCacheName::get(const LLUUID& id, BOOL is_group, co
return res;
}
-boost::signals2::connection LLCacheName::get(const LLUUID& id, BOOL is_group, old_callback_t callback, void* user_data)
+boost::signals2::connection LLCacheName::get(const LLUUID& id, bool is_group, old_callback_t callback, void* user_data)
{
- return get(id, is_group, boost::bind(callback, _1, _2, _3, _4, user_data));
+ return get(id, is_group, boost::bind(callback, _1, _2, _3, user_data));
}
void LLCacheName::processPending()
@@ -706,7 +656,7 @@ void LLCacheName::dump()
{
llinfos
<< iter->first << " = "
- << entry->mFirstName << " " << entry->mLastName
+ << buildFullName(entry->mFirstName, entry->mLastName)
<< " @ " << entry->mCreateTime
<< llendl;
}
@@ -725,12 +675,24 @@ void LLCacheName::dumpStats()
<< llendl;
}
+void LLCacheName::clear()
+{
+ for_each(impl.mCache.begin(), impl.mCache.end(), DeletePairedPointer());
+ impl.mCache.clear();
+}
+
//static
std::string LLCacheName::getDefaultName()
{
return sCacheName["waiting"];
}
+//static
+std::string LLCacheName::getDefaultLastName()
+{
+ return "Resident";
+}
+
void LLCacheName::Impl::processPendingAsks()
{
LLMemType mt_ppa(LLMemType::MTYPE_CACHE_PROCESS_PENDING_ASKS);
@@ -752,11 +714,13 @@ void LLCacheName::Impl::processPendingReplies()
if (!entry->mIsGroup)
{
- (reply->mSignal)(reply->mID, entry->mFirstName, entry->mLastName, FALSE);
+ std::string fullname =
+ LLCacheName::buildFullName(entry->mFirstName, entry->mLastName);
+ (reply->mSignal)(reply->mID, fullname, false);
}
else
{
- (reply->mSignal)(reply->mID, entry->mGroupName, "", TRUE);
+ (reply->mSignal)(reply->mID, entry->mGroupName, true);
}
}
@@ -927,13 +891,27 @@ void LLCacheName::Impl::processUUIDReply(LLMessageSystem* msg, bool isGroup)
if (!isGroup)
{
- mSignal(id, entry->mFirstName, entry->mLastName, FALSE);
- std::string fullname = entry->mFirstName + " " + entry->mLastName;
- mReverseCache[fullname] = id;
+ // NOTE: Very occasionally the server sends down a full name
+ // in the first name field with an empty last name, for example,
+ // first = "Ladanie1 Resident", last = "".
+ // I cannot reproduce this, nor can I find a bug in the server code.
+ // Ensure "Resident" does not appear via cleanFullName, because
+ // buildFullName only checks last name. JC
+ std::string full_name;
+ if (entry->mLastName.empty())
+ {
+ full_name = cleanFullName(entry->mFirstName);
+ }
+ else
+ {
+ full_name = LLCacheName::buildFullName(entry->mFirstName, entry->mLastName);
+ }
+ mSignal(id, full_name, false);
+ mReverseCache[full_name] = id;
}
else
{
- mSignal(id, entry->mGroupName, "", TRUE);
+ mSignal(id, entry->mGroupName, true);
mReverseCache[entry->mGroupName] = id;
}
}
@@ -962,4 +940,3 @@ void LLCacheName::Impl::handleUUIDGroupNameReply(LLMessageSystem* msg, void** us
{
((LLCacheName::Impl*)userData)->processUUIDReply(msg, true);
}
-
diff --git a/indra/llmessage/llcachename.h b/indra/llmessage/llcachename.h
index 111cc8b650..6b6bbde6ab 100644
--- a/indra/llmessage/llcachename.h
+++ b/indra/llmessage/llcachename.h
@@ -42,13 +42,12 @@ class LLUUID;
typedef boost::signals2::signal<void (const LLUUID& id,
- const std::string& first_name,
- const std::string& last_name,
- BOOL is_group)> LLCacheNameSignal;
+ const std::string& name,
+ bool is_group)> LLCacheNameSignal;
typedef LLCacheNameSignal::slot_type LLCacheNameCallback;
// Old callback with user data for compatability
-typedef void (*old_callback_t)(const LLUUID&, const std::string&, const std::string&, BOOL, void*);
+typedef void (*old_callback_t)(const LLUUID&, const std::string&, bool, void*);
// Here's the theory:
// If you request a name that isn't in the cache, it returns "waiting"
@@ -71,24 +70,26 @@ public:
boost::signals2::connection addObserver(const LLCacheNameCallback& callback);
- // janky old format. Remove after a while. Phoenix. 2008-01-30
- void importFile(LLFILE* fp);
-
// storing cache on disk; for viewer, in name.cache
bool importFile(std::istream& istr);
void exportFile(std::ostream& ostr);
- // If available, copies the first and last name into the strings provided.
- // first must be at least DB_FIRST_NAME_BUF_SIZE characters.
- // last must be at least DB_LAST_NAME_BUF_SIZE characters.
+ // If available, copies name ("bobsmith123" or "James Linden") into string
// If not available, copies the string "waiting".
// Returns TRUE iff available.
- BOOL getName(const LLUUID& id, std::string& first, std::string& last);
- BOOL getFullName(const LLUUID& id, std::string& fullname);
-
+ BOOL getFullName(const LLUUID& id, std::string& full_name);
+
// Reverse lookup of UUID from name
BOOL getUUID(const std::string& first, const std::string& last, LLUUID& id);
BOOL getUUID(const std::string& fullname, LLUUID& id);
+
+ // IDEVO Temporary code
+ // Clean up new-style "bobsmith123 Resident" names to "bobsmith123" for display
+ static std::string buildFullName(const std::string& first, const std::string& last);
+
+ // Clean up legacy "bobsmith123 Resident" to "bobsmith123"
+ // If name does not contain "Resident" returns it unchanged.
+ static std::string cleanFullName(const std::string& full_name);
// If available, this method copies the group name into the string
// provided. The caller must allocate at least
@@ -100,10 +101,10 @@ public:
// If the data is currently available, may call the callback immediatly
// otherwise, will request the data, and will call the callback when
// available. There is no garuntee the callback will ever be called.
- boost::signals2::connection get(const LLUUID& id, BOOL is_group, const LLCacheNameCallback& callback);
+ boost::signals2::connection get(const LLUUID& id, bool is_group, const LLCacheNameCallback& callback);
// LEGACY
- boost::signals2::connection get(const LLUUID& id, BOOL is_group, old_callback_t callback, void* user_data);
+ boost::signals2::connection get(const LLUUID& id, bool is_group, old_callback_t callback, void* user_data);
// This method needs to be called from time to time to send out
// requests.
void processPending();
@@ -114,9 +115,15 @@ public:
// Debugging
void dump(); // Dumps the contents of the cache
void dumpStats(); // Dumps the sizes of the cache and associated queues.
+ void clear(); // Deletes all entries from the cache
static std::string getDefaultName();
- static void LocalizeCacheName(std::string key, std::string value);
+
+ // Returns "Resident", the default last name for SLID-based accounts
+ // that have no last name.
+ static std::string getDefaultLastName();
+
+ static void localizeCacheName(std::string key, std::string value);
static std::map<std::string, std::string> sCacheName;
private:
diff --git a/indra/llmessage/mean_collision_data.h b/indra/llmessage/mean_collision_data.h
index 03b96f9f90..a6c635e81e 100644
--- a/indra/llmessage/mean_collision_data.h
+++ b/indra/llmessage/mean_collision_data.h
@@ -61,7 +61,7 @@ public:
LLMeanCollisionData(LLMeanCollisionData *mcd)
: mVictim(mcd->mVictim), mPerp(mcd->mPerp), mTime(mcd->mTime), mType(mcd->mType), mMag(mcd->mMag),
- mFirstName(mcd->mFirstName), mLastName(mcd->mLastName)
+ mFullName(mcd->mFullName)
{
}
@@ -95,8 +95,7 @@ public:
time_t mTime;
EMeanCollisionType mType;
F32 mMag;
- std::string mFirstName;
- std::string mLastName;
+ std::string mFullName;
};
diff --git a/indra/llmessage/tests/llavatarnamecache_test.cpp b/indra/llmessage/tests/llavatarnamecache_test.cpp
new file mode 100644
index 0000000000..eeadb703d1
--- /dev/null
+++ b/indra/llmessage/tests/llavatarnamecache_test.cpp
@@ -0,0 +1,109 @@
+/**
+ * @file llhost_test.cpp
+ * @author Adroit
+ * @date 2007-02
+ * @brief llhost test cases.
+ *
+ * $LicenseInfo:firstyear=2007&license=viewergpl$
+ *
+ * Copyright (c) 2007-2009, Linden Research, Inc.
+ *
+ * Second Life Viewer Source Code
+ * The source code in this file ("Source Code") is provided by Linden Lab
+ * to you under the terms of the GNU General Public License, version 2.0
+ * ("GPL"), unless you have obtained a separate licensing agreement
+ * ("Other License"), formally executed by you and Linden Lab. Terms of
+ * the GPL can be found in doc/GPL-license.txt in this distribution, or
+ * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2
+ *
+ * There are special exceptions to the terms and conditions of the GPL as
+ * it is applied to this Source Code. View the full text of the exception
+ * in the file doc/FLOSS-exception.txt in this software distribution, or
+ * online at
+ * http://secondlifegrid.net/programs/open_source/licensing/flossexception
+ *
+ * By copying, modifying or distributing this software, you acknowledge
+ * that you have read and understood your obligations described above,
+ * and agree to abide by those obligations.
+ *
+ * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO
+ * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY,
+ * COMPLETENESS OR PERFORMANCE.
+ * $/LicenseInfo$
+ */
+
+#include "linden_common.h"
+
+#include "../llavatarnamecache.h"
+
+#include "../test/lltut.h"
+
+namespace tut
+{
+ struct avatarnamecache_data
+ {
+ };
+ typedef test_group<avatarnamecache_data> avatarnamecache_test;
+ typedef avatarnamecache_test::object avatarnamecache_object;
+ tut::avatarnamecache_test avatarnamecache_testcase("llavatarnamecache");
+
+ template<> template<>
+ void avatarnamecache_object::test<1>()
+ {
+ bool valid = false;
+ S32 max_age = 0;
+
+ valid = max_age_from_cache_control("max-age=3600", &max_age);
+ ensure("typical input valid", valid);
+ ensure_equals("typical input parsed", max_age, 3600);
+
+ valid = max_age_from_cache_control(
+ " max-age=600 , no-cache,private=\"stuff\" ", &max_age);
+ ensure("complex input valid", valid);
+ ensure_equals("complex input parsed", max_age, 600);
+
+ valid = max_age_from_cache_control(
+ "no-cache, max-age = 123 ", &max_age);
+ ensure("complex input 2 valid", valid);
+ ensure_equals("complex input 2 parsed", max_age, 123);
+ }
+
+ template<> template<>
+ void avatarnamecache_object::test<2>()
+ {
+ bool valid = false;
+ S32 max_age = -1;
+
+ valid = max_age_from_cache_control("", &max_age);
+ ensure("empty input returns invalid", !valid);
+ ensure_equals("empty input doesn't change val", max_age, -1);
+
+ valid = max_age_from_cache_control("no-cache", &max_age);
+ ensure("no max-age field returns invalid", !valid);
+
+ valid = max_age_from_cache_control("max", &max_age);
+ ensure("just 'max' returns invalid", !valid);
+
+ valid = max_age_from_cache_control("max-age", &max_age);
+ ensure("partial max-age is invalid", !valid);
+
+ valid = max_age_from_cache_control("max-age=", &max_age);
+ ensure("longer partial max-age is invalid", !valid);
+
+ valid = max_age_from_cache_control("max-age=FOO", &max_age);
+ ensure("invalid integer max-age is invalid", !valid);
+
+ valid = max_age_from_cache_control("max-age 234", &max_age);
+ ensure("space separated max-age is invalid", !valid);
+
+ valid = max_age_from_cache_control("max-age=0", &max_age);
+ ensure("zero max-age is valid", valid);
+
+ // *TODO: Handle "0000" as zero
+ //valid = max_age_from_cache_control("max-age=0000", &max_age);
+ //ensure("multi-zero max-age is valid", valid);
+
+ valid = max_age_from_cache_control("max-age=-123", &max_age);
+ ensure("less than zero max-age is invalid", !valid);
+ }
+}