diff options
Diffstat (limited to 'indra/llvfs')
| -rw-r--r-- | indra/llvfs/lldir.cpp | 458 | ||||
| -rw-r--r-- | indra/llvfs/lldir.h | 91 | ||||
| -rw-r--r-- | indra/llvfs/tests/lldir_test.cpp | 339 | 
3 files changed, 771 insertions, 117 deletions
| diff --git a/indra/llvfs/lldir.cpp b/indra/llvfs/lldir.cpp index 32d081d552..a7d12476a4 100644 --- a/indra/llvfs/lldir.cpp +++ b/indra/llvfs/lldir.cpp @@ -41,6 +41,12 @@  #include "lluuid.h"  #include "lldiriterator.h" +#include "stringize.h" +#include <boost/foreach.hpp> +#include <boost/range/begin.hpp> +#include <boost/range/end.hpp> +#include <algorithm> +#include <iomanip>  #if LL_WINDOWS  #include "lldir_win32.h" @@ -58,6 +64,14 @@ LLDir_Linux gDirUtil;  LLDir *gDirUtilp = (LLDir *)&gDirUtil; +/// Values for findSkinnedFilenames(subdir) parameter +const char +	*LLDir::XUI      = "xui", +	*LLDir::TEXTURES = "textures", +	*LLDir::SKINBASE = ""; + +static const char* const empty = ""; +  LLDir::LLDir()  :	mAppName(""),  	mExecutablePathAndName(""), @@ -70,7 +84,8 @@ LLDir::LLDir()  	mOSCacheDir(""),  	mCAFile(""),  	mTempDir(""), -	mDirDelimiter("/") // fallback to forward slash if not overridden +	mDirDelimiter("/"), // fallback to forward slash if not overridden +	mLanguage("en")  {  } @@ -96,9 +111,7 @@ S32 LLDir::deleteFilesInDir(const std::string &dirname, const std::string &mask)  	LLDirIterator iter(dirname, mask);  	while (iter.next(filename))  	{ -		fullpath = dirname; -		fullpath += getDirDelimiter(); -		fullpath += filename; +		fullpath = add(dirname, filename);  		if(LLFile::isdir(fullpath))  		{ @@ -270,12 +283,12 @@ std::string LLDir::buildSLOSCacheDir() const  		}  		else  		{ -			res = getOSUserAppDir() + mDirDelimiter + "cache"; +			res = add(getOSUserAppDir(), "cache");  		}  	}  	else  	{ -		res = getOSCacheDir() + mDirDelimiter + "SecondLife"; +		res = add(getOSCacheDir(), "SecondLife");  	}  	return res;  } @@ -298,19 +311,24 @@ const std::string &LLDir::getDirDelimiter() const  	return mDirDelimiter;  } +const std::string& LLDir::getDefaultSkinDir() const +{ +	return mDefaultSkinDir; +} +  const std::string &LLDir::getSkinDir() const  {  	return mSkinDir;  } -const std::string &LLDir::getUserSkinDir() const +const std::string &LLDir::getUserDefaultSkinDir() const  { -	return mUserSkinDir; +    return mUserDefaultSkinDir;  } -const std::string& LLDir::getDefaultSkinDir() const +const std::string &LLDir::getUserSkinDir() const  { -	return mDefaultSkinDir; +	return mUserSkinDir;  }  const std::string LLDir::getSkinBaseDir() const @@ -323,6 +341,41 @@ const std::string &LLDir::getLLPluginDir() const  	return mLLPluginDir;  } +static std::string ELLPathToString(ELLPath location) +{ +    typedef std::map<ELLPath, const char*> ELLPathMap; +#define ENT(symbol) ELLPathMap::value_type(symbol, #symbol) +    static ELLPathMap::value_type init[] = +    { +        ENT(LL_PATH_NONE), +        ENT(LL_PATH_USER_SETTINGS), +        ENT(LL_PATH_APP_SETTINGS), +        ENT(LL_PATH_PER_SL_ACCOUNT), // returns/expands to blank string if we don't know the account name yet +        ENT(LL_PATH_CACHE), +        ENT(LL_PATH_CHARACTER), +        ENT(LL_PATH_HELP), +        ENT(LL_PATH_LOGS), +        ENT(LL_PATH_TEMP), +        ENT(LL_PATH_SKINS), +        ENT(LL_PATH_TOP_SKIN), +        ENT(LL_PATH_CHAT_LOGS), +        ENT(LL_PATH_PER_ACCOUNT_CHAT_LOGS), +        ENT(LL_PATH_USER_SKIN), +        ENT(LL_PATH_LOCAL_ASSETS), +        ENT(LL_PATH_EXECUTABLE), +        ENT(LL_PATH_DEFAULT_SKIN), +        ENT(LL_PATH_FONTS), +        ENT(LL_PATH_LAST) +    }; +#undef ENT +    static const ELLPathMap sMap(boost::begin(init), boost::end(init)); + +    ELLPathMap::const_iterator found = sMap.find(location); +    if (found != sMap.end()) +        return found->second; +    return STRINGIZE("Invalid ELLPath value " << location); +} +  std::string LLDir::getExpandedFilename(ELLPath location, const std::string& filename) const  {  	return getExpandedFilename(location, "", filename); @@ -343,15 +396,11 @@ std::string LLDir::getExpandedFilename(ELLPath location, const std::string& subd  		break;  	case LL_PATH_APP_SETTINGS: -		prefix = getAppRODataDir(); -		prefix += mDirDelimiter; -		prefix += "app_settings"; +		prefix = add(getAppRODataDir(), "app_settings");  		break;  	case LL_PATH_CHARACTER: -		prefix = getAppRODataDir(); -		prefix += mDirDelimiter; -		prefix += "character"; +		prefix = add(getAppRODataDir(), "character");  		break;  	case LL_PATH_HELP: @@ -363,16 +412,22 @@ std::string LLDir::getExpandedFilename(ELLPath location, const std::string& subd  		break;  	case LL_PATH_USER_SETTINGS: -		prefix = getOSUserAppDir(); -		prefix += mDirDelimiter; -		prefix += "user_settings"; +		prefix = add(getOSUserAppDir(), "user_settings");  		break;  	case LL_PATH_PER_SL_ACCOUNT:  		prefix = getLindenUserDir();  		if (prefix.empty())  		{ -			// if we're asking for the per-SL-account directory but we haven't logged in yet (or otherwise don't know the account name from which to build this string), then intentionally return a blank string to the caller and skip the below warning about a blank prefix. +			// if we're asking for the per-SL-account directory but we haven't +			// logged in yet (or otherwise don't know the account name from +			// which to build this string), then intentionally return a blank +			// string to the caller and skip the below warning about a blank +			// prefix. +			LL_DEBUGS("LLDir") << "getLindenUserDir() not yet set: " +							   << ELLPathToString(location) +							   << ", '" << subdir1 << "', '" << subdir2 << "', '" << in_filename +							   << "' => ''" << LL_ENDL;  			return std::string();  		}  		break; @@ -386,9 +441,7 @@ std::string LLDir::getExpandedFilename(ELLPath location, const std::string& subd  		break;  	case LL_PATH_LOGS: -		prefix = getOSUserAppDir(); -		prefix += mDirDelimiter; -		prefix += "logs"; +		prefix = add(getOSUserAppDir(), "logs");  		break;  	case LL_PATH_TEMP: @@ -412,9 +465,7 @@ std::string LLDir::getExpandedFilename(ELLPath location, const std::string& subd  		break;  	case LL_PATH_LOCAL_ASSETS: -		prefix = getAppRODataDir(); -		prefix += mDirDelimiter; -		prefix += "local_assets"; +		prefix = add(getAppRODataDir(), "local_assets");  		break;  	case LL_PATH_EXECUTABLE: @@ -422,56 +473,36 @@ std::string LLDir::getExpandedFilename(ELLPath location, const std::string& subd  		break;  	case LL_PATH_FONTS: -		prefix = getAppRODataDir(); -		prefix += mDirDelimiter; -		prefix += "fonts"; +		prefix = add(getAppRODataDir(), "fonts");  		break;  	default:  		llassert(0);  	} -	std::string filename = in_filename; -	if (!subdir2.empty()) -	{ -		filename = subdir2 + mDirDelimiter + filename; -	} - -	if (!subdir1.empty()) -	{ -		filename = subdir1 + mDirDelimiter + filename; -	} -  	if (prefix.empty())  	{ -		llwarns << "prefix is empty, possible bad filename" << llendl; -	} -	 -	std::string expanded_filename; -	if (!filename.empty()) -	{ -		if (!prefix.empty()) -		{ -			expanded_filename += prefix; -			expanded_filename += mDirDelimiter; -			expanded_filename += filename; -		} -		else -		{ -			expanded_filename = filename; -		} -	} -	else if (!prefix.empty()) -	{ -		// Directory only, no file name. -		expanded_filename = prefix; +		llwarns << ELLPathToString(location) +				<< ", '" << subdir1 << "', '" << subdir2 << "', '" << in_filename +				<< "': prefix is empty, possible bad filename" << llendl;  	} -	else + +	std::string expanded_filename = add(add(prefix, subdir1), subdir2); +	if (expanded_filename.empty() && in_filename.empty())  	{ -		expanded_filename.assign(""); +		return "";  	} - -	//llinfos << "*** EXPANDED FILENAME: <" << expanded_filename << ">" << llendl; +	// Use explicit concatenation here instead of another add() call. Callers +	// passing in_filename as "" expect to obtain a pathname ending with +	// mDirSeparator so they can later directly concatenate with a specific +	// filename. A caller using add() doesn't care, but there's still code +	// loose in the system that uses std::string::operator+(). +	expanded_filename += mDirDelimiter; +	expanded_filename += in_filename; + +	LL_DEBUGS("LLDir") << ELLPathToString(location) +					   << ", '" << subdir1 << "', '" << subdir2 << "', '" << in_filename +					   << "' => '" << expanded_filename << "'" << LL_ENDL;  	return expanded_filename;  } @@ -511,31 +542,168 @@ std::string LLDir::getExtension(const std::string& filepath) const  	return exten;  } -std::string LLDir::findSkinnedFilename(const std::string &filename) const +std::string LLDir::findSkinnedFilenameBaseLang(const std::string &subdir, +											   const std::string &filename, +											   bool merge) const  { -	return findSkinnedFilename("", "", filename); +	// This implementation is basically just as described in the declaration comments. +	std::vector<std::string> found(findSkinnedFilenames(subdir, filename, merge)); +	if (found.empty()) +	{ +		return ""; +	} +	return found.front();  } -std::string LLDir::findSkinnedFilename(const std::string &subdir, const std::string &filename) const +std::string LLDir::findSkinnedFilename(const std::string &subdir, +									   const std::string &filename, +									   bool merge) const  { -	return findSkinnedFilename("", subdir, filename); +	// This implementation is basically just as described in the declaration comments. +	std::vector<std::string> found(findSkinnedFilenames(subdir, filename, merge)); +	if (found.empty()) +	{ +		return ""; +	} +	return found.back();  } -std::string LLDir::findSkinnedFilename(const std::string &subdir1, const std::string &subdir2, const std::string &filename) const +std::vector<std::string> LLDir::findSkinnedFilenames(const std::string& subdir, +													 const std::string& filename, +													 bool merge) const  { -	// generate subdirectory path fragment, e.g. "/foo/bar", "/foo", "" -	std::string subdirs = ((subdir1.empty() ? "" : mDirDelimiter) + subdir1) -						 + ((subdir2.empty() ? "" : mDirDelimiter) + subdir2); +	// Recognize subdirs that have no localization. +	static const char* sUnlocalizedData[] = +	{ +		"",							// top-level directory not localized +		"textures"					// textures not localized +	}; +	static const std::set<std::string> sUnlocalized(boost::begin(sUnlocalizedData), +													boost::end(sUnlocalizedData)); + +	LL_DEBUGS("LLDir") << "subdir '" << subdir << "', filename '" << filename +					   << "', merge " << std::boolalpha << merge << LL_ENDL; + +	// Cache the default language directory for each subdir we've encountered. +	// A cache entry whose value is the empty string means "not localized, +	// don't bother checking again." +	typedef std::map<std::string, std::string> LocalizedMap; +	static LocalizedMap sLocalized; + +	// Check whether we've already discovered if this subdir is localized. +	LocalizedMap::const_iterator found = sLocalized.find(subdir); +	if (found == sLocalized.end()) +	{ +		// We have not yet determined that. Is it one of the subdirs "known" +		// to be unlocalized? +		if (sUnlocalized.find(subdir) != sUnlocalized.end()) +		{ +			// This subdir is known to be unlocalized. Remember that. +			found = sLocalized.insert(LocalizedMap::value_type(subdir, "")).first; +		} +		else +		{ +			// We do not recognize this subdir. Investigate. +			std::string subdir_path(add(getDefaultSkinDir(), subdir)); +			if (fileExists(add(subdir_path, "en"))) +			{ +				// defaultSkinDir/subdir contains subdir "en". That's our +				// default language; this subdir is localized.  +				found = sLocalized.insert(LocalizedMap::value_type(subdir, "en")).first; +			} +			else if (fileExists(add(subdir_path, "en-us"))) +			{ +				// defaultSkinDir/subdir contains subdir "en-us" but not "en". +				// Set as default language; this subdir is localized. +				found = sLocalized.insert(LocalizedMap::value_type(subdir, "en-us")).first; +			} +			else +			{ +				// defaultSkinDir/subdir contains neither "en" nor "en-us". +				// Assume it's not localized. Remember that assumption. +				found = sLocalized.insert(LocalizedMap::value_type(subdir, "")).first; +			} +		} +	} +	// Every code path above should have resulted in 'found' becoming a valid +	// iterator to an entry in sLocalized. +	llassert(found != sLocalized.end()); + +	// Now -- is this subdir localized, or not? The answer determines what +	// subdirectories we check (under subdir) for the requested filename. +	std::vector<std::string> subsubdirs; +	if (found->second.empty()) +	{ +		// subdir is not localized. filename should be located directly within it. +		subsubdirs.push_back(""); +	} +	else +	{ +		// subdir is localized, and found->second is the default language +		// directory within it. Check both the default language and the +		// current language -- if it differs from the default, of course. +		subsubdirs.push_back(found->second); +		if (mLanguage != found->second) +		{ +			subsubdirs.push_back(mLanguage); +		} +	} +	// Code below relies on subsubdirs not being empty: more specifically, on +	// front() being valid. There may or may not be additional entries, but we +	// have at least one. For an unlocalized subdir, it's the only one; for a +	// localized subdir, it's the default one. +	llassert(! subsubdirs.empty()); + +	// Build results vector. +	std::vector<std::string> results; +	BOOST_FOREACH(std::string skindir, mSearchSkinDirs) +	{ +		std::string subdir_path(add(skindir, subdir)); +		// Does subdir_path/subsubdirs[0]/filename exist? If there's more than +		// one entry in subsubdirs, the first is the default language ("en"), +		// the second is the current language. A skin that contains +		// subdir/language/filename without also containing subdir/en/filename +		// is ill-formed: skip any such skin. So to decide whether to keep +		// this skin dir or skip it, we need only check for the existence of +		// the first subsubdir entry ("en" or only). +		std::string subsubdir_path(add(add(subdir_path, subsubdirs.front()), filename)); +		if (! fileExists(subsubdir_path)) +			continue; -	std::vector<std::string> search_paths; -	 -	search_paths.push_back(getUserSkinDir() + subdirs);		// first look in user skin override -	search_paths.push_back(getSkinDir() + subdirs);			// then in current skin -	search_paths.push_back(getDefaultSkinDir() + subdirs);  // then default skin -	search_paths.push_back(getCacheDir() + subdirs);		// and last in preload directory +		// Here the desired filename exists in the first subsubdir. That means +		// this is a skindir we want to record in results. But if the caller +		// passed merge=false, we must discard all previous skindirs. +		if (! merge) +		{ +			results.clear(); +		} + +		// Now add every subsubdir in which filename exists. We already know +		// it exists in the first one. +		results.push_back(subsubdir_path); + +		// Append all remaining subsubdirs in which filename exists. +		for (std::vector<std::string>::const_iterator ssdi(subsubdirs.begin() + 1), ssdend(subsubdirs.end()); +			 ssdi != ssdend; ++ssdi) +		{ +			subsubdir_path = add(add(subdir_path, *ssdi), filename); +			if (fileExists(subsubdir_path)) +			{ +				results.push_back(subsubdir_path); +			} +		} +	} -	std::string found_file = findFile(filename, search_paths); -	return found_file; +	LL_DEBUGS("LLDir") << empty; +	const char* comma = ""; +	BOOST_FOREACH(std::string path, results) +	{ +		LL_CONT << comma << "'" << path << "'"; +		comma = ", "; +	} +	LL_CONT << LL_ENDL; + +	return results;  }  std::string LLDir::getTempFilename() const @@ -546,12 +714,7 @@ std::string LLDir::getTempFilename() const  	random_uuid.generate();  	random_uuid.toString(uuid_str); -	std::string temp_filename = getTempDir(); -	temp_filename += mDirDelimiter; -	temp_filename += uuid_str; -	temp_filename += ".tmp"; - -	return temp_filename; +	return add(getTempDir(), uuid_str + ".tmp");  }  // static @@ -587,9 +750,7 @@ void LLDir::setLindenUserDir(const std::string &username)  		std::string userlower(username);  		LLStringUtil::toLower(userlower);  		LLStringUtil::replaceChar(userlower, ' ', '_'); -		mLindenUserDir = getOSUserAppDir(); -		mLindenUserDir += mDirDelimiter; -		mLindenUserDir += userlower; +		mLindenUserDir = add(getOSUserAppDir(), userlower);  	}  	else  	{ @@ -621,9 +782,7 @@ void LLDir::setPerAccountChatLogsDir(const std::string &username)  		std::string userlower(username);  		LLStringUtil::toLower(userlower);  		LLStringUtil::replaceChar(userlower, ' ', '_'); -		mPerAccountChatLogsDir = getChatLogsDir(); -		mPerAccountChatLogsDir += mDirDelimiter; -		mPerAccountChatLogsDir += userlower; +		mPerAccountChatLogsDir = add(getChatLogsDir(), userlower);  	}  	else  	{ @@ -632,25 +791,59 @@ void LLDir::setPerAccountChatLogsDir(const std::string &username)  } -void LLDir::setSkinFolder(const std::string &skin_folder) +void LLDir::setSkinFolder(const std::string &skin_folder, const std::string& language)  { -	mSkinDir = getSkinBaseDir(); -	mSkinDir += mDirDelimiter; -	mSkinDir += skin_folder; +	LL_DEBUGS("LLDir") << "Setting skin '" << skin_folder << "', language '" << language << "'" +					   << LL_ENDL; +	mSkinName = skin_folder; +	mLanguage = language; -	// user modifications to current skin -	// e.g. c:\documents and settings\users\username\application data\second life\skins\dazzle -	mUserSkinDir = getOSUserAppDir(); -	mUserSkinDir += mDirDelimiter; -	mUserSkinDir += "skins"; -	mUserSkinDir += mDirDelimiter;	 -	mUserSkinDir += skin_folder; +	// This method is called multiple times during viewer initialization. Each +	// time it's called, reset mSearchSkinDirs. +	mSearchSkinDirs.clear();  	// base skin which is used as fallback for all skinned files  	// e.g. c:\program files\secondlife\skins\default  	mDefaultSkinDir = getSkinBaseDir(); -	mDefaultSkinDir += mDirDelimiter;	 -	mDefaultSkinDir += "default"; +	append(mDefaultSkinDir, "default"); +	// This is always the most general of the search skin directories. +	addSearchSkinDir(mDefaultSkinDir); + +	mSkinDir = getSkinBaseDir(); +	append(mSkinDir, skin_folder); +	// Next level of generality is a skin installed with the viewer. +	addSearchSkinDir(mSkinDir); + +	// user modifications to skins, current and default +	// e.g. c:\documents and settings\users\username\application data\second life\skins\dazzle +	mUserSkinDir = getOSUserAppDir(); +	append(mUserSkinDir, "skins"); +	mUserDefaultSkinDir = mUserSkinDir; +	append(mUserDefaultSkinDir, "default"); +	append(mUserSkinDir, skin_folder); +	// Next level of generality is user modifications to default skin... +	addSearchSkinDir(mUserDefaultSkinDir); +	// then user-defined skins. +	addSearchSkinDir(mUserSkinDir); +} + +void LLDir::addSearchSkinDir(const std::string& skindir) +{ +	if (std::find(mSearchSkinDirs.begin(), mSearchSkinDirs.end(), skindir) == mSearchSkinDirs.end()) +	{ +		LL_DEBUGS("LLDir") << "search skin: '" << skindir << "'" << LL_ENDL; +		mSearchSkinDirs.push_back(skindir); +	} +} + +std::string LLDir::getSkinFolder() const +{ +	return mSkinName; +} + +std::string LLDir::getLanguage() const +{ +	return mLanguage;  }  bool LLDir::setCacheDir(const std::string &path) @@ -664,7 +857,7 @@ bool LLDir::setCacheDir(const std::string &path)  	else  	{  		LLFile::mkdir(path); -		std::string tempname = path + mDirDelimiter + "temp"; +		std::string tempname = add(path, "temp");  		LLFILE* file = LLFile::fopen(tempname,"wt");  		if (file)  		{ @@ -697,6 +890,57 @@ void LLDir::dumpCurrentDirectories()  	LL_DEBUGS2("AppInit","Directories") << "  SkinDir:               " << getSkinDir() << LL_ENDL;  } +std::string LLDir::add(const std::string& path, const std::string& name) const +{ +	std::string destpath(path); +	append(destpath, name); +	return destpath; +} + +void LLDir::append(std::string& destpath, const std::string& name) const +{ +	// Delegate question of whether we need a separator to helper method. +	SepOff sepoff(needSep(destpath, name)); +	if (sepoff.first)               // do we need a separator? +	{ +		destpath += mDirDelimiter; +	} +	// If destpath ends with a separator, AND name starts with one, skip +	// name's leading separator. +	destpath += name.substr(sepoff.second); +} + +LLDir::SepOff LLDir::needSep(const std::string& path, const std::string& name) const +{ +	if (path.empty() || name.empty()) +	{ +		// If either path or name are empty, we do not need a separator +		// between them. +		return SepOff(false, 0); +	} +	// Here we know path and name are both non-empty. But if path already ends +	// with a separator, or if name already starts with a separator, we need +	// not add one. +	std::string::size_type seplen(mDirDelimiter.length()); +	bool path_ends_sep(path.substr(path.length() - seplen) == mDirDelimiter); +	bool name_starts_sep(name.substr(0, seplen) == mDirDelimiter); +	if ((! path_ends_sep) && (! name_starts_sep)) +	{ +		// If neither path nor name brings a separator to the junction, then +		// we need one. +		return SepOff(true, 0); +	} +	if (path_ends_sep && name_starts_sep) +	{ +		// But if BOTH path and name bring a separator, we need not add one. +		// Moreover, we should actually skip the leading separator of 'name'. +		return SepOff(false, seplen); +	} +	// Here we know that either path_ends_sep or name_starts_sep is true -- +	// but not both. So don't add a separator, and don't skip any characters: +	// simple concatenation will do the trick. +	return SepOff(false, 0); +}  void dir_exists_or_crash(const std::string &dir_name)  { diff --git a/indra/llvfs/lldir.h b/indra/llvfs/lldir.h index 5ee8bdb542..a242802979 100644 --- a/indra/llvfs/lldir.h +++ b/indra/llvfs/lldir.h @@ -56,7 +56,7 @@ typedef enum ELLPath  	LL_PATH_LAST  } ELLPath; - +/// Directory operations  class LLDir  {   public: @@ -100,9 +100,10 @@ class LLDir  	const std::string &getOSCacheDir() const;		// location of OS-specific cache folder (may be empty string)  	const std::string &getCAFile() const;			// File containing TLS certificate authorities  	const std::string &getDirDelimiter() const;	// directory separator for platform (ie. '\' or '/' or ':') +	const std::string &getDefaultSkinDir() const;	// folder for default skin. e.g. c:\program files\second life\skins\default  	const std::string &getSkinDir() const;		// User-specified skin folder. +	const std::string &getUserDefaultSkinDir() const; // dir with user modifications to default skin  	const std::string &getUserSkinDir() const;		// User-specified skin folder with user modifications. e.g. c:\documents and settings\username\application data\second life\skins\curskin -	const std::string &getDefaultSkinDir() const;	// folder for default skin. e.g. c:\program files\second life\skins\default  	const std::string getSkinBaseDir() const;		// folder that contains all installed skins (not user modifications). e.g. c:\program files\second life\skins  	const std::string &getLLPluginDir() const;		// Directory containing plugins and plugin shell @@ -117,10 +118,59 @@ class LLDir  	std::string getExtension(const std::string& filepath) const; // Excludes '.', e.g getExtension("foo.wav") == "wav"  	// these methods search the various skin paths for the specified file in the following order: -	// getUserSkinDir(), getSkinDir(), getDefaultSkinDir() -	std::string findSkinnedFilename(const std::string &filename) const; -	std::string findSkinnedFilename(const std::string &subdir, const std::string &filename) const; -	std::string findSkinnedFilename(const std::string &subdir1, const std::string &subdir2, const std::string &filename) const; +	// getUserSkinDir(), getUserDefaultSkinDir(), getSkinDir(), getDefaultSkinDir() +	/** +	 * Given a filename within skin, return an ordered sequence of paths to +	 * search. Nonexistent files will be filtered out -- which means that the +	 * vector might be empty. +	 * +	 * @param subdir Identify top-level skin subdirectory by passing one of +	 * LLDir::XUI (file lives under "xui" subtree), LLDir::TEXTURES (file +	 * lives under "textures" subtree), LLDir::SKINBASE (file lives at top +	 * level of skin subdirectory). +	 * @param filename Desired filename within subdir within skin, e.g. +	 * "panel_login.xml". DO NOT prepend (e.g.) "xui" or the desired language. +	 * @param merge Callers perform two different kinds of processing. When +	 * fetching a XUI file, for instance, the existence of @a filename in the +	 * specified skin completely supercedes any @a filename in the default +	 * skin. For that case, leave the default @a merge=false. The returned +	 * vector will contain only +	 * ".../<i>current_skin</i>/xui/en/<i>filename</i>", +	 * ".../<i>current_skin</i>/xui/<i>current_language</i>/<i>filename</i>". +	 * But for (e.g.) "strings.xml", we want a given skin to be able to +	 * override only specific entries from the default skin. Any string not +	 * defined in the specified skin will be sought in the default skin. For +	 * that case, pass @a merge=true. The returned vector will contain at +	 * least ".../default/xui/en/strings.xml", +	 * ".../default/xui/<i>current_language</i>/strings.xml", +	 * ".../<i>current_skin</i>/xui/en/strings.xml", +	 * ".../<i>current_skin</i>/xui/<i>current_language</i>/strings.xml". +	 */ +	std::vector<std::string> findSkinnedFilenames(const std::string& subdir, +												  const std::string& filename, +												  bool merge=false) const; +	/// Values for findSkinnedFilenames(subdir) parameter +	static const char *XUI, *TEXTURES, *SKINBASE; +	/** +	 * Return the base-language pathname from findSkinnedFilenames(), or +	 * the empty string if no such file exists. Parameters are identical to +	 * findSkinnedFilenames(). This is shorthand for capturing the vector +	 * returned by findSkinnedFilenames(), checking for empty() and then +	 * returning front(). +	 */ +	std::string findSkinnedFilenameBaseLang(const std::string &subdir, +											const std::string &filename, +											bool merge=false) const; +	/** +	 * Return the "most localized" pathname from findSkinnedFilenames(), or +	 * the empty string if no such file exists. Parameters are identical to +	 * findSkinnedFilenames(). This is shorthand for capturing the vector +	 * returned by findSkinnedFilenames(), checking for empty() and then +	 * returning back(). +	 */ +	std::string findSkinnedFilename(const std::string &subdir, +									const std::string &filename, +									bool merge=false) const;  	// random filename in common temporary directory  	std::string getTempFilename() const; @@ -132,15 +182,30 @@ class LLDir  	virtual void setChatLogsDir(const std::string &path);		// Set the chat logs dir to this user's dir  	virtual void setPerAccountChatLogsDir(const std::string &username);		// Set the per user chat log directory.  	virtual void setLindenUserDir(const std::string &username);		// Set the linden user dir to this user's dir -	virtual void setSkinFolder(const std::string &skin_folder); +	virtual void setSkinFolder(const std::string &skin_folder, const std::string& language); +	virtual std::string getSkinFolder() const; +	virtual std::string getLanguage() const;  	virtual bool setCacheDir(const std::string &path);  	virtual void dumpCurrentDirectories(); -	 +  	// Utility routine  	std::string buildSLOSCacheDir() const; +	/// Append specified @a name to @a destpath, separated by getDirDelimiter() +	/// if both are non-empty. +	void append(std::string& destpath, const std::string& name) const; +	/// Append specified @a name to @a path, separated by getDirDelimiter() +	/// if both are non-empty. Return result, leaving @a path unmodified. +	std::string add(const std::string& path, const std::string& name) const; +  protected: +	// Does an add() or append() call need a directory delimiter? +	typedef std::pair<bool, unsigned short> SepOff; +	SepOff needSep(const std::string& path, const std::string& name) const; +	// build mSearchSkinDirs without adding duplicates +	void addSearchSkinDir(const std::string& skindir); +  	std::string mAppName;               // install directory under progams/ ie "SecondLife"     	std::string mExecutablePathAndName; // full path + Filename of .exe  	std::string mExecutableFilename;    // Filename of .exe @@ -158,10 +223,18 @@ protected:  	std::string mDefaultCacheDir;	// default cache diretory  	std::string mOSCacheDir;		// operating system cache dir  	std::string mDirDelimiter; +	std::string mSkinName;           // caller-specified skin name  	std::string mSkinBaseDir;			// Base for skins paths. -	std::string mSkinDir;			// Location for current skin info.  	std::string mDefaultSkinDir;			// Location for default skin info. +	std::string mSkinDir;			// Location for current skin info. +	std::string mUserDefaultSkinDir;		// Location for default skin info.  	std::string mUserSkinDir;			// Location for user-modified skin info. +	// Skin directories to search, most general to most specific. This order +	// works well for composing fine-grained files, in which an individual item +	// in a specific file overrides the corresponding item in more general +	// files. Of course, for a file-level search, iterate backwards. +	std::vector<std::string> mSearchSkinDirs; +	std::string mLanguage;              // Current viewer language  	std::string mLLPluginDir;			// Location for plugins and plugin shell  }; diff --git a/indra/llvfs/tests/lldir_test.cpp b/indra/llvfs/tests/lldir_test.cpp index ea321c5ae9..a00fc8684c 100644 --- a/indra/llvfs/tests/lldir_test.cpp +++ b/indra/llvfs/tests/lldir_test.cpp @@ -27,11 +27,161 @@  #include "linden_common.h" +#include "llstring.h" +#include "tests/StringVec.h"  #include "../lldir.h"  #include "../lldiriterator.h"  #include "../test/lltut.h" +#include "stringize.h" +#include <boost/foreach.hpp> +#include <boost/assign/list_of.hpp> + +using boost::assign::list_of; + +// We use ensure_equals(..., vec(list_of(...))) not because it's functionally +// required, but because ensure_equals() knows how to format a StringVec. +// Turns out that when ensure_equals() displays a test failure with just +// list_of("string")("another"), you see 'stringanother' vs. '("string", +// "another")'. +StringVec vec(const StringVec& v) +{ +    return v; +} +// For some tests, use a dummy LLDir that uses memory data instead of touching +// the filesystem +struct LLDir_Dummy: public LLDir +{ +    /*----------------------------- LLDir API ------------------------------*/ +    LLDir_Dummy() +    { +        // Initialize important LLDir data members based on the filesystem +        // data below. +        mDirDelimiter = "/"; +        mExecutableDir = "install"; +        mExecutableFilename = "test"; +        mExecutablePathAndName = add(mExecutableDir, mExecutableFilename); +        mWorkingDir = mExecutableDir; +        mAppRODataDir = "install"; +        mSkinBaseDir = add(mAppRODataDir, "skins"); +        mOSUserDir = "user"; +        mOSUserAppDir = mOSUserDir; +        mLindenUserDir = ""; + +        // Make the dummy filesystem look more or less like what we expect in +        // the real one. +        static const char* preload[] = +        { +            "install/skins/default/colors.xml", +            "install/skins/default/xui/en/strings.xml", +            "install/skins/default/xui/fr/strings.xml", +            "install/skins/default/xui/en/floater.xml", +            "install/skins/default/xui/fr/floater.xml", +            "install/skins/default/xui/en/newfile.xml", +            "install/skins/default/xui/fr/newfile.xml", +            "install/skins/default/html/en-us/welcome.html", +            "install/skins/default/html/fr/welcome.html", +            "install/skins/default/textures/only_default.jpeg", +            "install/skins/default/future/somefile.txt", +            "install/skins/steam/colors.xml", +            "install/skins/steam/xui/en/strings.xml", +            "install/skins/steam/xui/fr/strings.xml", +            "install/skins/steam/textures/only_steam.jpeg", +            "user/skins/default/colors.xml", +            "user/skins/default/xui/en/strings.xml", +            "user/skins/default/xui/fr/strings.xml", +            // This is an attempted override that doesn't work: for a +            // localized subdir, a skin must have subdir/en/filename as well +            // as subdir/language/filename. +            "user/skins/default/xui/fr/floater.xml", +            // This is an override that only specifies the "en" version +            "user/skins/default/xui/en/newfile.xml", +            "user/skins/default/textures/only_user_default.jpeg", +            "user/skins/steam/colors.xml", +            "user/skins/steam/xui/en/strings.xml", +            "user/skins/steam/xui/fr/strings.xml", +            "user/skins/steam/textures/only_user_steam.jpeg" +        }; +        BOOST_FOREACH(const char* path, preload) +        { +            buildFilesystem(path); +        } +    } + +    virtual ~LLDir_Dummy() {} + +    virtual void initAppDirs(const std::string& app_name, const std::string& app_read_only_data_dir) +    { +        // Implement this when we write a test that needs it +    } + +    virtual std::string getCurPath() +    { +        // Implement this when we write a test that needs it +        return ""; +    } + +    virtual U32 countFilesInDir(const std::string& dirname, const std::string& mask) +    { +        // Implement this when we write a test that needs it +        return 0; +    } + +    virtual BOOL fileExists(const std::string& pathname) const +    { +        // Record fileExists() calls so we can check whether caching is +        // working right. Certain LLDir calls should be able to make decisions +        // without calling fileExists() again, having already checked existence. +        mChecked.insert(pathname); +        // For our simple flat set of strings, see whether the identical +        // pathname exists in our set. +        return (mFilesystem.find(pathname) != mFilesystem.end()); +    } + +    virtual std::string getLLPluginLauncher() +    { +        // Implement this when we write a test that needs it +        return ""; +    } + +    virtual std::string getLLPluginFilename(std::string base_name) +    { +        // Implement this when we write a test that needs it +        return ""; +    } + +    /*----------------------------- Dummy data -----------------------------*/ +    void clearFilesystem() { mFilesystem.clear(); } +    void buildFilesystem(const std::string& path) +    { +        // Split the pathname on slashes, ignoring leading, trailing, doubles +        StringVec components; +        LLStringUtil::getTokens(path, components, "/"); +        // Ensure we have an entry representing every level of this path +        std::string partial; +        BOOST_FOREACH(std::string component, components) +        { +            append(partial, component); +            mFilesystem.insert(partial); +        } +    } + +    void clear_checked() { mChecked.clear(); } +    void ensure_checked(const std::string& pathname) const +    { +        tut::ensure(STRINGIZE(pathname << " was not checked but should have been"), +                    mChecked.find(pathname) != mChecked.end()); +    } +    void ensure_not_checked(const std::string& pathname) const +    { +        tut::ensure(STRINGIZE(pathname << " was checked but should not have been"), +                    mChecked.find(pathname) == mChecked.end()); +    } + +    std::set<std::string> mFilesystem; +    mutable std::set<std::string> mChecked; +};  namespace tut  { @@ -419,5 +569,192 @@ namespace tut        LLFile::rmdir(dir1);        LLFile::rmdir(dir2);     } -} +    template<> template<> +    void LLDirTest_object_t::test<6>() +    { +        set_test_name("findSkinnedFilenames()"); +        LLDir_Dummy lldir; +        /*------------------------ "default", "en" -------------------------*/ +        // Setting "default" means we shouldn't consider any "*/skins/steam" +        // directories; setting "en" means we shouldn't consider any "xui/fr" +        // directories. +        lldir.setSkinFolder("default", "en"); +        ensure_equals(lldir.getSkinFolder(), "default"); +        ensure_equals(lldir.getLanguage(), "en"); + +        // top-level directory of a skin isn't localized +        ensure_equals(lldir.findSkinnedFilenames(LLDir::SKINBASE, "colors.xml", true), +                      vec(list_of("install/skins/default/colors.xml") +                                 ("user/skins/default/colors.xml"))); +        // We should not have needed to check for skins/default/en. We should +        // just "know" that SKINBASE is not localized. +        lldir.ensure_not_checked("install/skins/default/en"); + +        ensure_equals(lldir.findSkinnedFilenames(LLDir::TEXTURES, "only_default.jpeg"), +                      vec(list_of("install/skins/default/textures/only_default.jpeg"))); +        // Nor should we have needed to check skins/default/textures/en +        // because textures is known to be unlocalized. +        lldir.ensure_not_checked("install/skins/default/textures/en"); + +        StringVec expected(vec(list_of("install/skins/default/xui/en/strings.xml") +                               ("user/skins/default/xui/en/strings.xml"))); +        ensure_equals(lldir.findSkinnedFilenames(LLDir::XUI, "strings.xml", true), +                      expected); +        // The first time, we had to probe to find out whether xui was localized. +        lldir.ensure_checked("install/skins/default/xui/en"); +        lldir.clear_checked(); +        // Now make the same call again -- should return same result -- +        ensure_equals(lldir.findSkinnedFilenames(LLDir::XUI, "strings.xml", true), +                      expected); +        // but this time it should remember that xui is localized. +        lldir.ensure_not_checked("install/skins/default/xui/en"); + +        // localized subdir with "en-us" instead of "en" +        ensure_equals(lldir.findSkinnedFilenames("html", "welcome.html"), +                      vec(list_of("install/skins/default/html/en-us/welcome.html"))); +        lldir.ensure_checked("install/skins/default/html/en"); +        lldir.ensure_checked("install/skins/default/html/en-us"); +        lldir.clear_checked(); +        ensure_equals(lldir.findSkinnedFilenames("html", "welcome.html"), +                      vec(list_of("install/skins/default/html/en-us/welcome.html"))); +        lldir.ensure_not_checked("install/skins/default/html/en"); +        lldir.ensure_not_checked("install/skins/default/html/en-us"); + +        ensure_equals(lldir.findSkinnedFilenames("future", "somefile.txt"), +                      vec(list_of("install/skins/default/future/somefile.txt"))); +        // Test probing for an unrecognized unlocalized future subdir. +        lldir.ensure_checked("install/skins/default/future/en"); +        lldir.clear_checked(); +        ensure_equals(lldir.findSkinnedFilenames("future", "somefile.txt"), +                      vec(list_of("install/skins/default/future/somefile.txt"))); +        // Second time it should remember that future is unlocalized. +        lldir.ensure_not_checked("install/skins/default/future/en"); + +        // When language is set to "en", requesting an html file pulls up the +        // "en-us" version -- not because it magically matches those strings, +        // but because there's no "en" localization and it falls back on the +        // default "en-us"! Note that it would probably still be better to +        // make the default localization be "en" and allow "en-gb" (or +        // whatever) localizations, which would work much more the way you'd +        // expect. +        ensure_equals(lldir.findSkinnedFilenames("html", "welcome.html"), +                      vec(list_of("install/skins/default/html/en-us/welcome.html"))); + +        /*------------------------ "default", "fr" -------------------------*/ +        // We start being able to distinguish localized subdirs from +        // unlocalized when we ask for a non-English language. +        lldir.setSkinFolder("default", "fr"); +        ensure_equals(lldir.getLanguage(), "fr"); + +        // pass merge=true to request this filename in all relevant skins +        ensure_equals(lldir.findSkinnedFilenames(LLDir::XUI, "strings.xml", true), +                      vec(list_of +                          ("install/skins/default/xui/en/strings.xml") +                          ("install/skins/default/xui/fr/strings.xml") +                          ("user/skins/default/xui/en/strings.xml") +                          ("user/skins/default/xui/fr/strings.xml"))); + +        // pass (or default) merge=false to request only most specific skin +        ensure_equals(lldir.findSkinnedFilenames(LLDir::XUI, "strings.xml"), +                      vec(list_of +                          ("user/skins/default/xui/en/strings.xml") +                          ("user/skins/default/xui/fr/strings.xml"))); + +        // The most specific skin for our dummy floater.xml is the installed +        // default. Although we have a user xui/fr/floater.xml, we would also +        // need a xui/en/floater.xml file to consider the user skin for this. +        ensure_equals(lldir.findSkinnedFilenames(LLDir::XUI, "floater.xml"), +                      vec(list_of +                          ("install/skins/default/xui/en/floater.xml") +                          ("install/skins/default/xui/fr/floater.xml"))); + +        // The user override for the default skin does define newfile.xml, but +        // only an "en" version, not a "fr" version as well. Nonetheless +        // that's the most specific skin we have, regardless of the existence +        // of a "fr" version in the installed default skin. +        ensure_equals(lldir.findSkinnedFilenames(LLDir::XUI, "newfile.xml"), +                      vec(list_of("user/skins/default/xui/en/newfile.xml"))); + +        ensure_equals(lldir.findSkinnedFilenames("html", "welcome.html"), +                      vec(list_of +                          ("install/skins/default/html/en-us/welcome.html") +                          ("install/skins/default/html/fr/welcome.html"))); + +        /*------------------------ "default", "zh" -------------------------*/ +        lldir.setSkinFolder("default", "zh"); +        // Because the user default skins strings.xml has only a "fr" override +        // but not a "zh" override, the most localized version we can find is "en". +        ensure_equals(lldir.findSkinnedFilenames(LLDir::XUI, "strings.xml"), +                      vec(list_of("user/skins/default/xui/en/strings.xml"))); + +        /*------------------------- "steam", "en" --------------------------*/ +        lldir.setSkinFolder("steam", "en"); + +        ensure_equals(lldir.findSkinnedFilenames(LLDir::SKINBASE, "colors.xml", true), +                      vec(list_of +                          ("install/skins/default/colors.xml") +                          ("install/skins/steam/colors.xml") +                          ("user/skins/default/colors.xml") +                          ("user/skins/steam/colors.xml"))); + +        ensure_equals(lldir.findSkinnedFilenames(LLDir::TEXTURES, "only_default.jpeg"), +                      vec(list_of("install/skins/default/textures/only_default.jpeg"))); + +        ensure_equals(lldir.findSkinnedFilenames(LLDir::TEXTURES, "only_steam.jpeg"), +                      vec(list_of("install/skins/steam/textures/only_steam.jpeg"))); + +        ensure_equals(lldir.findSkinnedFilenames(LLDir::TEXTURES, "only_user_default.jpeg"), +                      vec(list_of("user/skins/default/textures/only_user_default.jpeg"))); + +        ensure_equals(lldir.findSkinnedFilenames(LLDir::TEXTURES, "only_user_steam.jpeg"), +                      vec(list_of("user/skins/steam/textures/only_user_steam.jpeg"))); + +        // merge=false +        ensure_equals(lldir.findSkinnedFilenames(LLDir::XUI, "strings.xml"), +                      vec(list_of("user/skins/steam/xui/en/strings.xml"))); + +        // pass merge=true to request this filename in all relevant skins +        ensure_equals(lldir.findSkinnedFilenames(LLDir::XUI, "strings.xml", true), +                      vec(list_of +                          ("install/skins/default/xui/en/strings.xml") +                          ("install/skins/steam/xui/en/strings.xml") +                          ("user/skins/default/xui/en/strings.xml") +                          ("user/skins/steam/xui/en/strings.xml"))); + +        /*------------------------- "steam", "fr" --------------------------*/ +        lldir.setSkinFolder("steam", "fr"); + +        // pass merge=true to request this filename in all relevant skins +        ensure_equals(lldir.findSkinnedFilenames(LLDir::XUI, "strings.xml"), +                      vec(list_of +                          ("user/skins/steam/xui/en/strings.xml") +                          ("user/skins/steam/xui/fr/strings.xml"))); + +        // pass merge=true to request this filename in all relevant skins +        ensure_equals(lldir.findSkinnedFilenames(LLDir::XUI, "strings.xml", true), +                      vec(list_of +                          ("install/skins/default/xui/en/strings.xml") +                          ("install/skins/default/xui/fr/strings.xml") +                          ("install/skins/steam/xui/en/strings.xml") +                          ("install/skins/steam/xui/fr/strings.xml") +                          ("user/skins/default/xui/en/strings.xml") +                          ("user/skins/default/xui/fr/strings.xml") +                          ("user/skins/steam/xui/en/strings.xml") +                          ("user/skins/steam/xui/fr/strings.xml"))); +    } + +    template<> template<> +    void LLDirTest_object_t::test<7>() +    { +        set_test_name("add()"); +        LLDir_Dummy lldir; +        ensure_equals("both empty", lldir.add("", ""), ""); +        ensure_equals("path empty", lldir.add("", "b"), "b"); +        ensure_equals("name empty", lldir.add("a", ""), "a"); +        ensure_equals("both simple", lldir.add("a", "b"), "a/b"); +        ensure_equals("name leading slash", lldir.add("a", "/b"), "a/b"); +        ensure_equals("path trailing slash", lldir.add("a/", "b"), "a/b"); +        ensure_equals("both bring slashes", lldir.add("a/", "/b"), "a/b"); +    } +} | 
