From 5f8de423f190bbb79a62f804151bc24824fa32d8 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 04:16:08 -0500 Subject: Add m-esr52 at 52.6.0 --- intl/strres/nsStringBundle.cpp | 769 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 769 insertions(+) create mode 100644 intl/strres/nsStringBundle.cpp (limited to 'intl/strres/nsStringBundle.cpp') diff --git a/intl/strres/nsStringBundle.cpp b/intl/strres/nsStringBundle.cpp new file mode 100644 index 000000000..ab840a469 --- /dev/null +++ b/intl/strres/nsStringBundle.cpp @@ -0,0 +1,769 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsStringBundle.h" +#include "nsID.h" +#include "nsString.h" +#include "nsIStringBundle.h" +#include "nsStringBundleService.h" +#include "nsStringBundleTextOverride.h" +#include "nsISupportsPrimitives.h" +#include "nsIMutableArray.h" +#include "nsArrayEnumerator.h" +#include "nscore.h" +#include "nsMemory.h" +#include "nsNetUtil.h" +#include "nsComponentManagerUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsIInputStream.h" +#include "nsIURI.h" +#include "nsIObserverService.h" +#include "nsCOMArray.h" +#include "nsTextFormatter.h" +#include "nsIErrorService.h" +#include "nsICategoryManager.h" +#include "nsContentUtils.h" + +// for async loading +#ifdef ASYNC_LOADING +#include "nsIBinaryInputStream.h" +#include "nsIStringStream.h" +#endif + +using namespace mozilla; + +static NS_DEFINE_CID(kErrorServiceCID, NS_ERRORSERVICE_CID); + +nsStringBundle::~nsStringBundle() +{ +} + +nsStringBundle::nsStringBundle(const char* aURLSpec, + nsIStringBundleOverride* aOverrideStrings) : + mPropertiesURL(aURLSpec), + mOverrideStrings(aOverrideStrings), + mReentrantMonitor("nsStringBundle.mReentrantMonitor"), + mAttemptedLoad(false), + mLoaded(false) +{ +} + +nsresult +nsStringBundle::LoadProperties() +{ + // this is different than mLoaded, because we only want to attempt + // to load once + // we only want to load once, but if we've tried once and failed, + // continue to throw an error! + if (mAttemptedLoad) { + if (mLoaded) + return NS_OK; + + return NS_ERROR_UNEXPECTED; + } + + mAttemptedLoad = true; + + nsresult rv; + + // do it synchronously + nsCOMPtr uri; + rv = NS_NewURI(getter_AddRefs(uri), mPropertiesURL); + if (NS_FAILED(rv)) return rv; + + // whitelist check for local schemes + nsCString scheme; + uri->GetScheme(scheme); + if (!scheme.EqualsLiteral("chrome") && !scheme.EqualsLiteral("jar") && + !scheme.EqualsLiteral("resource") && !scheme.EqualsLiteral("file") && + !scheme.EqualsLiteral("data")) { + return NS_ERROR_ABORT; + } + + nsCOMPtr channel; + rv = NS_NewChannel(getter_AddRefs(channel), + uri, + nsContentUtils::GetSystemPrincipal(), + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, + nsIContentPolicy::TYPE_OTHER); + + if (NS_FAILED(rv)) return rv; + + // It's a string bundle. We expect a text/plain type, so set that as hint + channel->SetContentType(NS_LITERAL_CSTRING("text/plain")); + + nsCOMPtr in; + rv = channel->Open2(getter_AddRefs(in)); + if (NS_FAILED(rv)) return rv; + + NS_ASSERTION(NS_SUCCEEDED(rv) && in, "Error in OpenBlockingStream"); + NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && in, NS_ERROR_FAILURE); + + static NS_DEFINE_CID(kPersistentPropertiesCID, NS_IPERSISTENTPROPERTIES_CID); + mProps = do_CreateInstance(kPersistentPropertiesCID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + mAttemptedLoad = mLoaded = true; + rv = mProps->Load(in); + + mLoaded = NS_SUCCEEDED(rv); + + return rv; +} + + +nsresult +nsStringBundle::GetStringFromID(int32_t aID, nsAString& aResult) +{ + ReentrantMonitorAutoEnter automon(mReentrantMonitor); + nsAutoCString name; + name.AppendInt(aID, 10); + + nsresult rv; + + // try override first + if (mOverrideStrings) { + rv = mOverrideStrings->GetStringFromName(mPropertiesURL, + name, + aResult); + if (NS_SUCCEEDED(rv)) return rv; + } + + rv = mProps->GetStringProperty(name, aResult); + + return rv; +} + +nsresult +nsStringBundle::GetStringFromName(const nsAString& aName, + nsAString& aResult) +{ + nsresult rv; + + // try override first + if (mOverrideStrings) { + rv = mOverrideStrings->GetStringFromName(mPropertiesURL, + NS_ConvertUTF16toUTF8(aName), + aResult); + if (NS_SUCCEEDED(rv)) return rv; + } + + rv = mProps->GetStringProperty(NS_ConvertUTF16toUTF8(aName), aResult); + return rv; +} + +NS_IMETHODIMP +nsStringBundle::FormatStringFromID(int32_t aID, + const char16_t **aParams, + uint32_t aLength, + char16_t ** aResult) +{ + nsAutoString idStr; + idStr.AppendInt(aID, 10); + + return FormatStringFromName(idStr.get(), aParams, aLength, aResult); +} + +// this function supports at most 10 parameters.. see below for why +NS_IMETHODIMP +nsStringBundle::FormatStringFromName(const char16_t *aName, + const char16_t **aParams, + uint32_t aLength, + char16_t **aResult) +{ + NS_ENSURE_ARG_POINTER(aName); + NS_ASSERTION(aParams && aLength, "FormatStringFromName() without format parameters: use GetStringFromName() instead"); + NS_ENSURE_ARG_POINTER(aResult); + + nsresult rv; + rv = LoadProperties(); + if (NS_FAILED(rv)) return rv; + + nsAutoString formatStr; + rv = GetStringFromName(nsDependentString(aName), formatStr); + if (NS_FAILED(rv)) return rv; + + return FormatString(formatStr.get(), aParams, aLength, aResult); +} + + +NS_IMPL_ISUPPORTS(nsStringBundle, nsIStringBundle) + +NS_IMETHODIMP +nsStringBundle::GetStringFromID(int32_t aID, char16_t **aResult) +{ + nsresult rv; + rv = LoadProperties(); + if (NS_FAILED(rv)) return rv; + + *aResult = nullptr; + nsAutoString tmpstr; + + rv = GetStringFromID(aID, tmpstr); + NS_ENSURE_SUCCESS(rv, rv); + + *aResult = ToNewUnicode(tmpstr); + NS_ENSURE_TRUE(*aResult, NS_ERROR_OUT_OF_MEMORY); + + return NS_OK; +} + +NS_IMETHODIMP +nsStringBundle::GetStringFromName(const char16_t *aName, char16_t **aResult) +{ + NS_ENSURE_ARG_POINTER(aName); + NS_ENSURE_ARG_POINTER(aResult); + + nsresult rv; + rv = LoadProperties(); + if (NS_FAILED(rv)) return rv; + + ReentrantMonitorAutoEnter automon(mReentrantMonitor); + *aResult = nullptr; + nsAutoString tmpstr; + rv = GetStringFromName(nsDependentString(aName), tmpstr); + if (NS_FAILED(rv)) + { +#if 0 + // it is not uncommon for apps to request a string name which may not exist + // so be quiet about it. + NS_WARNING("String missing from string bundle"); + printf(" '%s' missing from bundle %s\n", NS_ConvertUTF16toUTF8(aName).get(), mPropertiesURL.get()); +#endif + return rv; + } + + *aResult = ToNewUnicode(tmpstr); + NS_ENSURE_TRUE(*aResult, NS_ERROR_OUT_OF_MEMORY); + + return NS_OK; +} + +nsresult +nsStringBundle::GetCombinedEnumeration(nsIStringBundleOverride* aOverrideStrings, + nsISimpleEnumerator** aResult) +{ + nsCOMPtr supports; + nsCOMPtr propElement; + + nsresult rv; + + nsCOMPtr resultArray = + do_CreateInstance(NS_ARRAY_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // first, append the override elements + nsCOMPtr overrideEnumerator; + rv = aOverrideStrings->EnumerateKeysInBundle(mPropertiesURL, + getter_AddRefs(overrideEnumerator)); + + bool hasMore; + rv = overrideEnumerator->HasMoreElements(&hasMore); + NS_ENSURE_SUCCESS(rv, rv); + while (hasMore) { + + rv = overrideEnumerator->GetNext(getter_AddRefs(supports)); + if (NS_SUCCEEDED(rv)) + resultArray->AppendElement(supports, false); + + rv = overrideEnumerator->HasMoreElements(&hasMore); + NS_ENSURE_SUCCESS(rv, rv); + } + + // ok, now we have the override elements in resultArray + nsCOMPtr propEnumerator; + rv = mProps->Enumerate(getter_AddRefs(propEnumerator)); + if (NS_FAILED(rv)) { + // no elements in mProps anyway, just return what we have + return NS_NewArrayEnumerator(aResult, resultArray); + } + + // second, append all the elements that are in mProps + do { + rv = propEnumerator->GetNext(getter_AddRefs(supports)); + if (NS_SUCCEEDED(rv) && + (propElement = do_QueryInterface(supports, &rv))) { + + // now check if its in the override bundle + nsAutoCString key; + propElement->GetKey(key); + + nsAutoString value; + rv = aOverrideStrings->GetStringFromName(mPropertiesURL, key, value); + + // if it isn't there, then it is safe to append + if (NS_FAILED(rv)) + resultArray->AppendElement(propElement, false); + } + + rv = propEnumerator->HasMoreElements(&hasMore); + NS_ENSURE_SUCCESS(rv, rv); + } while (hasMore); + + return resultArray->Enumerate(aResult); +} + + +NS_IMETHODIMP +nsStringBundle::GetSimpleEnumeration(nsISimpleEnumerator** elements) +{ + if (!elements) + return NS_ERROR_INVALID_POINTER; + + nsresult rv; + rv = LoadProperties(); + if (NS_FAILED(rv)) return rv; + + if (mOverrideStrings) + return GetCombinedEnumeration(mOverrideStrings, elements); + + return mProps->Enumerate(elements); +} + +nsresult +nsStringBundle::FormatString(const char16_t *aFormatStr, + const char16_t **aParams, uint32_t aLength, + char16_t **aResult) +{ + NS_ENSURE_ARG_POINTER(aResult); + NS_ENSURE_ARG(aLength <= 10); // enforce 10-parameter limit + + // implementation note: you would think you could use vsmprintf + // to build up an arbitrary length array.. except that there + // is no way to build up a va_list at runtime! + // Don't believe me? See: + // http://www.eskimo.com/~scs/C-faq/q15.13.html + // -alecf + char16_t *text = + nsTextFormatter::smprintf(aFormatStr, + aLength >= 1 ? aParams[0] : nullptr, + aLength >= 2 ? aParams[1] : nullptr, + aLength >= 3 ? aParams[2] : nullptr, + aLength >= 4 ? aParams[3] : nullptr, + aLength >= 5 ? aParams[4] : nullptr, + aLength >= 6 ? aParams[5] : nullptr, + aLength >= 7 ? aParams[6] : nullptr, + aLength >= 8 ? aParams[7] : nullptr, + aLength >= 9 ? aParams[8] : nullptr, + aLength >= 10 ? aParams[9] : nullptr); + + if (!text) { + *aResult = nullptr; + return NS_ERROR_OUT_OF_MEMORY; + } + + // nsTextFormatter does not use the shared nsMemory allocator. + // Instead it is required to free the memory it allocates using + // nsTextFormatter::smprintf_free. Let's instead use nsMemory based + // allocation for the result that we give out and free the string + // returned by smprintf ourselves! + *aResult = NS_strdup(text); + nsTextFormatter::smprintf_free(text); + + return *aResult ? NS_OK : NS_ERROR_OUT_OF_MEMORY; +} + +NS_IMPL_ISUPPORTS(nsExtensibleStringBundle, nsIStringBundle) + +nsExtensibleStringBundle::nsExtensibleStringBundle() +{ + mLoaded = false; +} + +nsresult +nsExtensibleStringBundle::Init(const char * aCategory, + nsIStringBundleService* aBundleService) +{ + + nsresult rv; + nsCOMPtr catman = + do_GetService(NS_CATEGORYMANAGER_CONTRACTID, &rv); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr enumerator; + rv = catman->EnumerateCategory(aCategory, getter_AddRefs(enumerator)); + if (NS_FAILED(rv)) return rv; + + bool hasMore; + while (NS_SUCCEEDED(enumerator->HasMoreElements(&hasMore)) && hasMore) { + nsCOMPtr supports; + rv = enumerator->GetNext(getter_AddRefs(supports)); + if (NS_FAILED(rv)) + continue; + + nsCOMPtr supStr = do_QueryInterface(supports, &rv); + if (NS_FAILED(rv)) + continue; + + nsAutoCString name; + rv = supStr->GetData(name); + if (NS_FAILED(rv)) + continue; + + nsCOMPtr bundle; + rv = aBundleService->CreateBundle(name.get(), getter_AddRefs(bundle)); + if (NS_FAILED(rv)) + continue; + + mBundles.AppendObject(bundle); + } + + return rv; +} + +nsExtensibleStringBundle::~nsExtensibleStringBundle() +{ +} + +nsresult nsExtensibleStringBundle::GetStringFromID(int32_t aID, char16_t ** aResult) +{ + nsresult rv; + const uint32_t size = mBundles.Count(); + for (uint32_t i = 0; i < size; ++i) { + nsIStringBundle *bundle = mBundles[i]; + if (bundle) { + rv = bundle->GetStringFromID(aID, aResult); + if (NS_SUCCEEDED(rv)) + return NS_OK; + } + } + + return NS_ERROR_FAILURE; +} + +nsresult nsExtensibleStringBundle::GetStringFromName(const char16_t *aName, + char16_t ** aResult) +{ + nsresult rv; + const uint32_t size = mBundles.Count(); + for (uint32_t i = 0; i < size; ++i) { + nsIStringBundle* bundle = mBundles[i]; + if (bundle) { + rv = bundle->GetStringFromName(aName, aResult); + if (NS_SUCCEEDED(rv)) + return NS_OK; + } + } + + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsExtensibleStringBundle::FormatStringFromID(int32_t aID, + const char16_t ** aParams, + uint32_t aLength, + char16_t ** aResult) +{ + nsAutoString idStr; + idStr.AppendInt(aID, 10); + return FormatStringFromName(idStr.get(), aParams, aLength, aResult); +} + +NS_IMETHODIMP +nsExtensibleStringBundle::FormatStringFromName(const char16_t *aName, + const char16_t ** aParams, + uint32_t aLength, + char16_t ** aResult) +{ + nsXPIDLString formatStr; + nsresult rv; + rv = GetStringFromName(aName, getter_Copies(formatStr)); + if (NS_FAILED(rv)) + return rv; + + return nsStringBundle::FormatString(formatStr, aParams, aLength, aResult); +} + +nsresult nsExtensibleStringBundle::GetSimpleEnumeration(nsISimpleEnumerator ** aResult) +{ + // XXX write me + *aResult = nullptr; + return NS_ERROR_NOT_IMPLEMENTED; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +#define MAX_CACHED_BUNDLES 16 + +struct bundleCacheEntry_t final : public LinkedListElement { + nsCString mHashKey; + nsCOMPtr mBundle; + + bundleCacheEntry_t() + { + MOZ_COUNT_CTOR(bundleCacheEntry_t); + } + + ~bundleCacheEntry_t() + { + MOZ_COUNT_DTOR(bundleCacheEntry_t); + } +}; + + +nsStringBundleService::nsStringBundleService() : + mBundleMap(MAX_CACHED_BUNDLES) +{ + mErrorService = do_GetService(kErrorServiceCID); + NS_ASSERTION(mErrorService, "Couldn't get error service"); +} + +NS_IMPL_ISUPPORTS(nsStringBundleService, + nsIStringBundleService, + nsIObserver, + nsISupportsWeakReference) + +nsStringBundleService::~nsStringBundleService() +{ + flushBundleCache(); +} + +nsresult +nsStringBundleService::Init() +{ + nsCOMPtr os = mozilla::services::GetObserverService(); + if (os) { + os->AddObserver(this, "memory-pressure", true); + os->AddObserver(this, "profile-do-change", true); + os->AddObserver(this, "chrome-flush-caches", true); + os->AddObserver(this, "xpcom-category-entry-added", true); + } + + // instantiate the override service, if there is any. + // at some point we probably want to make this a category, and + // support multiple overrides + mOverrideStrings = do_GetService(NS_STRINGBUNDLETEXTOVERRIDE_CONTRACTID); + + return NS_OK; +} + +NS_IMETHODIMP +nsStringBundleService::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aSomeData) +{ + if (strcmp("memory-pressure", aTopic) == 0 || + strcmp("profile-do-change", aTopic) == 0 || + strcmp("chrome-flush-caches", aTopic) == 0) + { + flushBundleCache(); + } + else if (strcmp("xpcom-category-entry-added", aTopic) == 0 && + NS_LITERAL_STRING("xpcom-autoregistration").Equals(aSomeData)) + { + mOverrideStrings = do_GetService(NS_STRINGBUNDLETEXTOVERRIDE_CONTRACTID); + } + + return NS_OK; +} + +void +nsStringBundleService::flushBundleCache() +{ + // release all bundles in the cache + mBundleMap.Clear(); + + while (!mBundleCache.isEmpty()) { + delete mBundleCache.popFirst(); + } +} + +NS_IMETHODIMP +nsStringBundleService::FlushBundles() +{ + flushBundleCache(); + return NS_OK; +} + +nsresult +nsStringBundleService::getStringBundle(const char *aURLSpec, + nsIStringBundle **aResult) +{ + nsDependentCString key(aURLSpec); + bundleCacheEntry_t* cacheEntry = mBundleMap.Get(key); + + if (cacheEntry) { + // cache hit! + // remove it from the list, it will later be reinserted + // at the head of the list + cacheEntry->remove(); + + } else { + + // hasn't been cached, so insert it into the hash table + RefPtr bundle = new nsStringBundle(aURLSpec, mOverrideStrings); + cacheEntry = insertIntoCache(bundle.forget(), key); + } + + // at this point the cacheEntry should exist in the hashtable, + // but is not in the LRU cache. + // put the cache entry at the front of the list + mBundleCache.insertFront(cacheEntry); + + // finally, return the value + *aResult = cacheEntry->mBundle; + NS_ADDREF(*aResult); + + return NS_OK; +} + +bundleCacheEntry_t * +nsStringBundleService::insertIntoCache(already_AddRefed aBundle, + nsCString &aHashKey) +{ + bundleCacheEntry_t *cacheEntry; + + if (mBundleMap.Count() < MAX_CACHED_BUNDLES) { + // cache not full - create a new entry + cacheEntry = new bundleCacheEntry_t(); + } else { + // cache is full + // take the last entry in the list, and recycle it. + cacheEntry = mBundleCache.getLast(); + + // remove it from the hash table and linked list + NS_ASSERTION(mBundleMap.Contains(cacheEntry->mHashKey), + "Element will not be removed!"); + mBundleMap.Remove(cacheEntry->mHashKey); + cacheEntry->remove(); + } + + // at this point we have a new cacheEntry that doesn't exist + // in the hashtable, so set up the cacheEntry + cacheEntry->mHashKey = aHashKey; + cacheEntry->mBundle = aBundle; + + // insert the entry into the cache and map, make it the MRU + mBundleMap.Put(cacheEntry->mHashKey, cacheEntry); + + return cacheEntry; +} + +NS_IMETHODIMP +nsStringBundleService::CreateBundle(const char* aURLSpec, + nsIStringBundle** aResult) +{ + return getStringBundle(aURLSpec,aResult); +} + +NS_IMETHODIMP +nsStringBundleService::CreateExtensibleBundle(const char* aCategory, + nsIStringBundle** aResult) +{ + NS_ENSURE_ARG_POINTER(aResult); + *aResult = nullptr; + + RefPtr bundle = new nsExtensibleStringBundle(); + + nsresult res = bundle->Init(aCategory, this); + if (NS_FAILED(res)) { + return res; + } + + bundle.forget(aResult); + return NS_OK; +} + +#define GLOBAL_PROPERTIES "chrome://global/locale/global-strres.properties" + +nsresult +nsStringBundleService::FormatWithBundle(nsIStringBundle* bundle, nsresult aStatus, + uint32_t argCount, char16_t** argArray, + char16_t* *result) +{ + nsresult rv; + nsXPIDLCString key; + + // try looking up the error message with the int key: + uint16_t code = NS_ERROR_GET_CODE(aStatus); + rv = bundle->FormatStringFromID(code, (const char16_t**)argArray, argCount, result); + + // If the int key fails, try looking up the default error message. E.g. print: + // An unknown error has occurred (0x804B0003). + if (NS_FAILED(rv)) { + nsAutoString statusStr; + statusStr.AppendInt(static_cast(aStatus), 16); + const char16_t* otherArgArray[1]; + otherArgArray[0] = statusStr.get(); + uint16_t code = NS_ERROR_GET_CODE(NS_ERROR_FAILURE); + rv = bundle->FormatStringFromID(code, otherArgArray, 1, result); + } + + return rv; +} + +NS_IMETHODIMP +nsStringBundleService::FormatStatusMessage(nsresult aStatus, + const char16_t* aStatusArg, + char16_t* *result) +{ + nsresult rv; + uint32_t i, argCount = 0; + nsCOMPtr bundle; + nsXPIDLCString stringBundleURL; + + // XXX hack for mailnews who has already formatted their messages: + if (aStatus == NS_OK && aStatusArg) { + *result = NS_strdup(aStatusArg); + NS_ENSURE_TRUE(*result, NS_ERROR_OUT_OF_MEMORY); + return NS_OK; + } + + if (aStatus == NS_OK) { + return NS_ERROR_FAILURE; // no message to format + } + + // format the arguments: + const nsDependentString args(aStatusArg); + argCount = args.CountChar(char16_t('\n')) + 1; + NS_ENSURE_ARG(argCount <= 10); // enforce 10-parameter limit + char16_t* argArray[10]; + + // convert the aStatusArg into a char16_t array + if (argCount == 1) { + // avoid construction for the simple case: + argArray[0] = (char16_t*)aStatusArg; + } + else if (argCount > 1) { + int32_t offset = 0; + for (i = 0; i < argCount; i++) { + int32_t pos = args.FindChar('\n', offset); + if (pos == -1) + pos = args.Length(); + argArray[i] = ToNewUnicode(Substring(args, offset, pos - offset)); + if (argArray[i] == nullptr) { + rv = NS_ERROR_OUT_OF_MEMORY; + argCount = i - 1; // don't try to free uninitialized memory + goto done; + } + offset = pos + 1; + } + } + + // find the string bundle for the error's module: + rv = mErrorService->GetErrorStringBundle(NS_ERROR_GET_MODULE(aStatus), + getter_Copies(stringBundleURL)); + if (NS_SUCCEEDED(rv)) { + rv = getStringBundle(stringBundleURL, getter_AddRefs(bundle)); + if (NS_SUCCEEDED(rv)) { + rv = FormatWithBundle(bundle, aStatus, argCount, argArray, result); + } + } + if (NS_FAILED(rv)) { + rv = getStringBundle(GLOBAL_PROPERTIES, getter_AddRefs(bundle)); + if (NS_SUCCEEDED(rv)) { + rv = FormatWithBundle(bundle, aStatus, argCount, argArray, result); + } + } + +done: + if (argCount > 1) { + for (i = 0; i < argCount; i++) { + if (argArray[i]) + free(argArray[i]); + } + } + return rv; +} -- cgit v1.2.3