diff options
Diffstat (limited to 'intl/strres')
-rw-r--r-- | intl/strres/moz.build | 29 | ||||
-rw-r--r-- | intl/strres/nsIStringBundle.idl | 111 | ||||
-rw-r--r-- | intl/strres/nsStringBundle.cpp | 1026 | ||||
-rw-r--r-- | intl/strres/nsStringBundle.h | 88 | ||||
-rw-r--r-- | intl/strres/nsStringBundleService.h | 69 | ||||
-rw-r--r-- | intl/strres/tests/unit/397093.properties | 4 | ||||
-rw-r--r-- | intl/strres/tests/unit/strres.properties | 14 | ||||
-rw-r--r-- | intl/strres/tests/unit/test_bug378839.js | 58 | ||||
-rw-r--r-- | intl/strres/tests/unit/test_bug397093.js | 39 | ||||
-rw-r--r-- | intl/strres/tests/unit/xpcshell.ini | 8 |
10 files changed, 1446 insertions, 0 deletions
diff --git a/intl/strres/moz.build b/intl/strres/moz.build new file mode 100644 index 0000000000..0146366cde --- /dev/null +++ b/intl/strres/moz.build @@ -0,0 +1,29 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +XPCSHELL_TESTS_MANIFESTS += ["tests/unit/xpcshell.ini"] + +XPIDL_SOURCES += [ + "nsIStringBundle.idl", +] + +XPIDL_MODULE = "intl" + +UNIFIED_SOURCES += [ + "nsStringBundle.cpp", +] + +LOCAL_INCLUDES += [ + "/xpcom/ds", +] + +EXPORTS += [ + "nsStringBundle.h", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/intl/strres/nsIStringBundle.idl b/intl/strres/nsIStringBundle.idl new file mode 100644 index 0000000000..a548517703 --- /dev/null +++ b/intl/strres/nsIStringBundle.idl @@ -0,0 +1,111 @@ +/* -*- Mode: IDL; 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 "nsISupports.idl" +#include "nsISimpleEnumerator.idl" + +%{C++ +#include "mozilla/MemoryReporting.h" + +namespace mozilla { +namespace dom { +class ContentParent; +} +namespace ipc { +class FileDescriptor; +} +} + +// Define Contractid and CID +// {D85A17C1-AA7C-11d2-9B8C-00805F8A16D9} +#define NS_STRINGBUNDLESERVICE_CID \ +{ 0xd85a17c1, 0xaa7c, 0x11d2, \ + { 0x9b, 0x8c, 0x0, 0x80, 0x5f, 0x8a, 0x16, 0xd9 } } + +#define NS_STRINGBUNDLE_CONTRACTID "@mozilla.org/intl/stringbundle;1" + +%} + +[ptr] native ContentParent(mozilla::dom::ContentParent); +[ref] native FileDescriptor(mozilla::ipc::FileDescriptor); +native MallocSizeOf(mozilla::MallocSizeOf); + +[scriptable, builtinclass, uuid(D85A17C2-AA7C-11d2-9B8C-00805F8A16D9)] +interface nsIStringBundle : nsISupports +{ + AString GetStringFromID(in long aID); + + // This method is mostly used from JS, where AUTF8String is appropriate. + [binaryname(GetStringFromAUTF8Name)] + AString GetStringFromName(in AUTF8String aName); + + // This method is mostly used from C++, where |string| is appropriate because + // the names are most often 8-bit string literals (normally ASCII, though + // u8"foo" literals will also work). + [noscript, binaryname(GetStringFromName)] + AString GetStringFromNameCpp(in string aName); + + // this is kind of like ssprintf - except that you can + // only pass it unicode strings, using the %S formatting character. + // the id or name should refer to a string in the bundle that + // uses %S.. do NOT try to use any other types. + // this uses nsTextFormatter::ssprintf to do the dirty work. + AString formatStringFromID(in long aID, in Array<AString> params); + + // This method is mostly used from JS, where AUTF8String is appropriate. + [binaryname(FormatStringFromAUTF8Name)] + AString formatStringFromName(in AUTF8String aName, in Array<AString> params); + + // This method is mostly used from C++, where |string| is appropriate because + // the names are most often 8-bit string literals (normally ASCII, though + // u8"foo" literals will also work). + [noscript, binaryname(FormatStringFromName)] + AString formatStringFromNameCpp(in string aName, in Array<AString> params); + + /* + Implements nsISimpleEnumerator, replaces nsIEnumerator + */ + nsISimpleEnumerator getSimpleEnumeration(); + // Preloads string bundle data asynchronously + void asyncPreload(); + + [notxpcom, nostdcall] size_t SizeOfIncludingThis(in MallocSizeOf aMallocSizeOf); + [notxpcom, nostdcall] size_t SizeOfIncludingThisIfUnshared(in MallocSizeOf aMallocSizeOf); +}; + +[scriptable, builtinclass, uuid(D85A17C0-AA7C-11d2-9B8C-00805F8A16D9)] +interface nsIStringBundleService : nsISupports +{ + nsIStringBundle createBundle(in string aURLSpec); + + /** + * Formats a message string from a status code and status arguments. + * @param aStatus - The status code. This is mapped into a string ID and + * used in the string lookup process. + * @param aStatusArg - The status message argument(s). Multiple arguments + * can be separated by newline ('\n') characters. + * @return the formatted message + */ + AString formatStatusMessage(in nsresult aStatus, in wstring aStatusArg); + + /** + * flushes the string bundle cache - useful when the locale changes or + * when we need to get some extra memory back + * + * at some point, we might want to make this flush all the bundles, + * because any bundles that are floating around when the locale changes + * will suddenly contain bad data + * + */ + void flushBundles(); + + [notxpcom, nostdcall] size_t sizeOfIncludingThis(in MallocSizeOf aMallocSizeOf); + + [notxpcom, nostdcall] void sendContentBundles(in ContentParent aContentParent); + + [notxpcom, nostdcall] void registerContentBundle(in ACString aBundleURL, + [const] in FileDescriptor aMapFile, + in size_t aMapSize); +}; diff --git a/intl/strres/nsStringBundle.cpp b/intl/strres/nsStringBundle.cpp new file mode 100644 index 0000000000..320d122c72 --- /dev/null +++ b/intl/strres/nsStringBundle.cpp @@ -0,0 +1,1026 @@ +/* -*- 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 "netCore.h" +#include "nsID.h" +#include "nsString.h" +#include "nsIStringBundle.h" +#include "nsStringBundleService.h" +#include "nsArrayEnumerator.h" +#include "nscore.h" +#include "nsNetUtil.h" +#include "nsComponentManagerUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsIChannel.h" +#include "nsIInputStream.h" +#include "nsIURI.h" +#include "nsIObserverService.h" +#include "nsCOMArray.h" +#include "nsTextFormatter.h" +#include "nsContentUtils.h" +#include "nsPersistentProperties.h" +#include "nsQueryObject.h" +#include "nsSimpleEnumerator.h" +#include "nsStringStream.h" +#include "mozilla/dom/txXSLTMsgsURL.h" +#include "mozilla/BinarySearch.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/URLPreloader.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/ipc/SharedStringMap.h" + +// for async loading +#ifdef ASYNC_LOADING +# include "nsIBinaryInputStream.h" +# include "nsIStringStream.h" +#endif + +using namespace mozilla; + +using mozilla::dom::ContentParent; +using mozilla::dom::StringBundleDescriptor; +using mozilla::dom::ipc::SharedStringMap; +using mozilla::dom::ipc::SharedStringMapBuilder; +using mozilla::ipc::FileDescriptor; + +/** + * A set of string bundle URLs which are loaded by content processes, and + * should be allocated in a shared memory region, and then sent to content + * processes. + * + * Note: This layout is chosen to avoid having to create a separate char* + * array pointing to the string constant values, which would require + * per-process relocations. The second array size is the length of the longest + * URL plus its null terminator. Shorter strings are null padded to this + * length. + * + * This should be kept in sync with the similar array in nsContentUtils.cpp, + * and updated with any other property files which need to be loaded in all + * content processes. + */ +static const char kContentBundles[][52] = { + "chrome://branding/locale/brand.properties", + "chrome://global/locale/commonDialogs.properties", + "chrome://global/locale/css.properties", + "chrome://global/locale/dom/dom.properties", + "chrome://global/locale/layout/HtmlForm.properties", + "chrome://global/locale/layout/htmlparser.properties", + "chrome://global/locale/layout_errors.properties", + "chrome://global/locale/mathml/mathml.properties", + "chrome://global/locale/printing.properties", + "chrome://global/locale/security/csp.properties", + "chrome://global/locale/security/security.properties", + "chrome://global/locale/svg/svg.properties", + "chrome://global/locale/xul.properties", + "chrome://necko/locale/necko.properties", +}; + +static bool IsContentBundle(const nsCString& aUrl) { + size_t index; + return BinarySearchIf( + kContentBundles, 0, MOZ_ARRAY_LENGTH(kContentBundles), + [&](const char* aElem) { + return Compare(aUrl, nsDependentCString(aElem)); + }, + &index); +} + +namespace { + +#define STRINGBUNDLEPROXY_IID \ + { \ + 0x537cf21b, 0x99fc, 0x4002, { \ + 0x9e, 0xec, 0x97, 0xbe, 0x4d, 0xe0, 0xb3, 0xdc \ + } \ + } + +/** + * A simple proxy class for a string bundle instance which will be replaced by + * a different implementation later in the session. + * + * This is used when creating string bundles which should use shared memory, + * but the content process has not yet received their shared memory buffer. + * When the shared memory variant becomes available, this proxy is retarged to + * that instance, and the original non-shared instance is destroyed. + * + * At that point, the cache entry for the proxy is replaced with the shared + * memory instance, and callers which already have an instance of the proxy + * are redirected to the new instance. + */ +class StringBundleProxy : public nsIStringBundle { + NS_DECL_THREADSAFE_ISUPPORTS + + NS_DECLARE_STATIC_IID_ACCESSOR(STRINGBUNDLEPROXY_IID) + + explicit StringBundleProxy(already_AddRefed<nsIStringBundle> aTarget) + : mMutex("StringBundleProxy::mMutex"), mTarget(aTarget) {} + + void Retarget(nsIStringBundle* aTarget) { + MutexAutoLock automon(mMutex); + mTarget = aTarget; + } + + // Forward nsIStringBundle methods (other than the `SizeOf*` methods) to + // `Target()`. + NS_IMETHOD GetStringFromID(int32_t aID, nsAString& _retval) override { + return Target()->GetStringFromID(aID, _retval); + } + NS_IMETHOD GetStringFromAUTF8Name(const nsACString& aName, + nsAString& _retval) override { + return Target()->GetStringFromAUTF8Name(aName, _retval); + } + NS_IMETHOD GetStringFromName(const char* aName, nsAString& _retval) override { + return Target()->GetStringFromName(aName, _retval); + } + NS_IMETHOD FormatStringFromID(int32_t aID, const nsTArray<nsString>& params, + nsAString& _retval) override { + return Target()->FormatStringFromID(aID, params, _retval); + } + NS_IMETHOD FormatStringFromAUTF8Name(const nsACString& aName, + const nsTArray<nsString>& params, + nsAString& _retval) override { + return Target()->FormatStringFromAUTF8Name(aName, params, _retval); + } + NS_IMETHOD FormatStringFromName(const char* aName, + const nsTArray<nsString>& params, + nsAString& _retval) override { + return Target()->FormatStringFromName(aName, params, _retval); + } + NS_IMETHOD GetSimpleEnumeration(nsISimpleEnumerator** _retval) override { + return Target()->GetSimpleEnumeration(_retval); + } + NS_IMETHOD AsyncPreload() override { return Target()->AsyncPreload(); } + + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) override { + return aMallocSizeOf(this); + } + + size_t SizeOfIncludingThisIfUnshared( + mozilla::MallocSizeOf aMallocSizeOf) override { + return mRefCnt == 1 ? SizeOfIncludingThis(aMallocSizeOf) : 0; + } + + protected: + virtual ~StringBundleProxy() = default; + + private: + Mutex mMutex MOZ_UNANNOTATED; + nsCOMPtr<nsIStringBundle> mTarget; + + // Atomically reads mTarget and returns a strong reference to it. This + // allows for safe multi-threaded use when the proxy may be retargetted by + // the main thread during access. + nsCOMPtr<nsIStringBundle> Target() { + MutexAutoLock automon(mMutex); + return mTarget; + } +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(StringBundleProxy, STRINGBUNDLEPROXY_IID) + +NS_IMPL_ISUPPORTS(StringBundleProxy, nsIStringBundle, StringBundleProxy) + +#define SHAREDSTRINGBUNDLE_IID \ + { \ + 0x7a8df5f7, 0x9e50, 0x44f6, { \ + 0xbf, 0x89, 0xc7, 0xad, 0x6c, 0x17, 0xf8, 0x5f \ + } \ + } + +/** + * A string bundle backed by a read-only, shared memory buffer. This should + * only be used for string bundles which are used in child processes. + * + * Important: The memory allocated by these string bundles will never be freed + * before process shutdown, per the restrictions in SharedStringMap.h, so they + * should never be used for short-lived bundles. + */ +class SharedStringBundle final : public nsStringBundleBase { + public: + /** + * Initialize the string bundle with a file descriptor pointing to a + * pre-populated key-value store for this string bundle. This should only be + * called in child processes, for bundles initially created in the parent + * process. + */ + void SetMapFile(const FileDescriptor& aFile, size_t aSize); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECLARE_STATIC_IID_ACCESSOR(SHAREDSTRINGBUNDLE_IID) + + nsresult LoadProperties() override; + + /** + * Returns a copy of the file descriptor pointing to the shared memory + * key-values tore for this string bundle. This should only be called in the + * parent process, and may be used to send shared string bundles to child + * processes. + */ + FileDescriptor CloneFileDescriptor() const { + MOZ_ASSERT(XRE_IsParentProcess()); + if (mMapFile.isSome()) { + return mMapFile.ref(); + } + return mStringMap->CloneFileDescriptor(); + } + + size_t MapSize() const { + if (mMapFile.isSome()) { + return mMapSize; + } + if (mStringMap) { + return mStringMap->MapSize(); + } + return 0; + } + + bool Initialized() const { return mStringMap || mMapFile.isSome(); } + + StringBundleDescriptor GetDescriptor() const { + MOZ_ASSERT(Initialized()); + + StringBundleDescriptor descriptor; + descriptor.bundleURL() = BundleURL(); + descriptor.mapFile() = CloneFileDescriptor(); + descriptor.mapSize() = MapSize(); + return descriptor; + } + + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) override; + + static SharedStringBundle* Cast(nsIStringBundle* aStringBundle) { + return static_cast<SharedStringBundle*>(aStringBundle); + } + + protected: + friend class nsStringBundleBase; + + explicit SharedStringBundle(const char* aURLSpec) + : nsStringBundleBase(aURLSpec) {} + + ~SharedStringBundle() override = default; + + nsresult GetStringImpl(const nsACString& aName, nsAString& aResult) override; + + nsresult GetSimpleEnumerationImpl(nsISimpleEnumerator** elements) override; + + private: + RefPtr<SharedStringMap> mStringMap; + + Maybe<FileDescriptor> mMapFile; + size_t mMapSize; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(SharedStringBundle, SHAREDSTRINGBUNDLE_IID) + +class StringMapEnumerator final : public nsSimpleEnumerator { + public: + NS_DECL_NSISIMPLEENUMERATOR + + explicit StringMapEnumerator(SharedStringMap* aStringMap) + : mStringMap(aStringMap) {} + + const nsID& DefaultInterface() override { + return NS_GET_IID(nsIPropertyElement); + } + + protected: + virtual ~StringMapEnumerator() = default; + + private: + RefPtr<SharedStringMap> mStringMap; + + uint32_t mIndex = 0; +}; + +template <typename T, typename... Args> +already_AddRefed<T> MakeBundle(Args... args) { + return nsStringBundleBase::Create<T>(args...); +} + +template <typename T, typename... Args> +RefPtr<T> MakeBundleRefPtr(Args... args) { + return nsStringBundleBase::Create<T>(args...); +} + +} // anonymous namespace + +NS_IMPL_ISUPPORTS(nsStringBundleBase, nsIStringBundle, nsIMemoryReporter) + +NS_IMPL_ISUPPORTS_INHERITED0(nsStringBundle, nsStringBundleBase) +NS_IMPL_ISUPPORTS_INHERITED(SharedStringBundle, nsStringBundleBase, + SharedStringBundle) + +nsStringBundleBase::nsStringBundleBase(const char* aURLSpec) + : mPropertiesURL(aURLSpec), + mMutex("nsStringBundle.mMutex"), + mAttemptedLoad(false), + mLoaded(false) {} + +nsStringBundleBase::~nsStringBundleBase() { + UnregisterWeakMemoryReporter(this); +} + +void nsStringBundleBase::RegisterMemoryReporter() { + RegisterWeakMemoryReporter(this); +} + +template <typename T, typename... Args> +/* static */ +already_AddRefed<T> nsStringBundleBase::Create(Args... args) { + RefPtr<T> bundle = new T(args...); + bundle->RegisterMemoryReporter(); + return bundle.forget(); +} + +nsStringBundle::nsStringBundle(const char* aURLSpec) + : nsStringBundleBase(aURLSpec) {} + +nsStringBundle::~nsStringBundle() = default; + +NS_IMETHODIMP +nsStringBundleBase::AsyncPreload() { + return NS_DispatchToCurrentThreadQueue( + NewIdleRunnableMethod("nsStringBundleBase::LoadProperties", this, + &nsStringBundleBase::LoadProperties), + EventQueuePriority::Idle); +} + +size_t nsStringBundle::SizeOfIncludingThis( + mozilla::MallocSizeOf aMallocSizeOf) { + size_t n = 0; + if (mProps) { + n += mProps->SizeOfIncludingThis(aMallocSizeOf); + } + return aMallocSizeOf(this) + n; +} + +size_t nsStringBundleBase::SizeOfIncludingThis( + mozilla::MallocSizeOf aMallocSizeOf) { + return 0; +} + +size_t nsStringBundleBase::SizeOfIncludingThisIfUnshared( + mozilla::MallocSizeOf aMallocSizeOf) { + if (mRefCnt == 1) { + return SizeOfIncludingThis(aMallocSizeOf); + } else { + return 0; + } +} + +size_t SharedStringBundle::SizeOfIncludingThis( + mozilla::MallocSizeOf aMallocSizeOf) { + size_t n = 0; + if (mStringMap) { + n += aMallocSizeOf(mStringMap); + } + return aMallocSizeOf(this) + n; +} + +NS_IMETHODIMP +nsStringBundleBase::CollectReports(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, bool aAnonymize) { + // String bundle URLs are always local, and part of the distribution. + // There's no need to anonymize. + nsAutoCStringN<64> escapedURL(mPropertiesURL); + escapedURL.ReplaceChar('/', '\\'); + + size_t sharedSize = 0; + size_t heapSize = SizeOfIncludingThis(MallocSizeOf); + + nsAutoCStringN<256> path("explicit/string-bundles/"); + if (RefPtr<SharedStringBundle> shared = do_QueryObject(this)) { + path.AppendLiteral("SharedStringBundle"); + if (XRE_IsParentProcess()) { + sharedSize = shared->MapSize(); + } + } else { + path.AppendLiteral("nsStringBundle"); + } + + path.AppendLiteral("(url=\""); + path.Append(escapedURL); + + // Note: The memory reporter service holds a strong reference to reporters + // while collecting reports, so we want to ignore the extra ref in reports. + path.AppendLiteral("\", shared="); + path.AppendASCII(mRefCnt > 2 ? "true" : "false"); + path.AppendLiteral(", refCount="); + path.AppendInt(uint32_t(mRefCnt - 1)); + + if (sharedSize) { + path.AppendLiteral(", sharedMemorySize="); + path.AppendInt(uint32_t(sharedSize)); + } + + path.AppendLiteral(")"); + + constexpr auto desc = + "A StringBundle instance representing the data in a (probably " + "localized) .properties file. Data may be shared between " + "processes."_ns; + + aHandleReport->Callback(""_ns, path, KIND_HEAP, UNITS_BYTES, heapSize, desc, + aData); + + if (sharedSize) { + path.ReplaceLiteral(0, sizeof("explicit/") - 1, "shared-"); + + aHandleReport->Callback(""_ns, path, KIND_OTHER, UNITS_BYTES, sharedSize, + desc, aData); + } + + return NS_OK; +} + +nsresult nsStringBundleBase::ParseProperties(nsIPersistentProperties** aProps) { + // 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; + } + + MOZ_ASSERT(NS_IsMainThread(), + "String bundles must be initialized on the main thread " + "before they may be used off-main-thread"); + + mAttemptedLoad = true; + + nsresult rv; + + // do it synchronously + nsCOMPtr<nsIURI> 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<nsIInputStream> in; + + auto result = URLPreloader::ReadURI(uri); + if (result.isOk()) { + MOZ_TRY(NS_NewCStringInputStream(getter_AddRefs(in), result.unwrap())); + } else { + nsCOMPtr<nsIChannel> channel; + rv = NS_NewChannel(getter_AddRefs(channel), uri, + nsContentUtils::GetSystemPrincipal(), + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_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("text/plain"_ns); + + rv = channel->Open(getter_AddRefs(in)); + if (NS_FAILED(rv)) return rv; + } + + auto props = MakeRefPtr<nsPersistentProperties>(); + + mAttemptedLoad = true; + + MOZ_TRY(props->Load(in)); + props.forget(aProps); + + mLoaded = true; + return NS_OK; +} + +nsresult nsStringBundle::LoadProperties() { + // Something such as Necko might use string bundle after ClearOnShutdown is + // called. LocaleService etc is already down, so we cannot get bundle data. + if (PastShutdownPhase(ShutdownPhase::XPCOMShutdown)) { + return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + } + + if (mProps) { + return NS_OK; + } + return ParseProperties(getter_AddRefs(mProps)); +} + +nsresult SharedStringBundle::LoadProperties() { + if (mStringMap) return NS_OK; + + if (mMapFile.isSome()) { + mStringMap = new SharedStringMap(mMapFile.ref(), mMapSize); + mMapFile.reset(); + return NS_OK; + } + + MOZ_ASSERT(NS_IsMainThread(), + "String bundles must be initialized on the main thread " + "before they may be used off-main-thread"); + + // We can't access the locale service after shutdown has started, which + // means we can't attempt to load chrome: locale resources (which most of + // our string bundles come from). Since shared string bundles won't be + // useful after shutdown has started anyway (and we almost certainly got + // here from a pre-load attempt in an idle task), just bail out. + if (PastShutdownPhase(ShutdownPhase::XPCOMShutdown)) { + return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + } + + // We should only populate shared memory string bundles in the parent + // process. Instances in the child process should always be instantiated + // with a shared memory file descriptor sent from the parent. + MOZ_ASSERT(XRE_IsParentProcess()); + + nsCOMPtr<nsIPersistentProperties> props; + MOZ_TRY(ParseProperties(getter_AddRefs(props))); + + SharedStringMapBuilder builder; + + nsCOMPtr<nsISimpleEnumerator> iter; + MOZ_TRY(props->Enumerate(getter_AddRefs(iter))); + bool hasMore; + while (NS_SUCCEEDED(iter->HasMoreElements(&hasMore)) && hasMore) { + nsCOMPtr<nsISupports> next; + MOZ_TRY(iter->GetNext(getter_AddRefs(next))); + + nsresult rv; + nsCOMPtr<nsIPropertyElement> elem = do_QueryInterface(next, &rv); + MOZ_TRY(rv); + + nsCString key; + nsString value; + MOZ_TRY(elem->GetKey(key)); + MOZ_TRY(elem->GetValue(value)); + + builder.Add(key, value); + } + + mStringMap = new SharedStringMap(std::move(builder)); + + ContentParent::BroadcastStringBundle(GetDescriptor()); + + return NS_OK; +} + +void SharedStringBundle::SetMapFile(const FileDescriptor& aFile, size_t aSize) { + MOZ_ASSERT(XRE_IsContentProcess()); + mStringMap = nullptr; + mMapFile.emplace(aFile); + mMapSize = aSize; +} + +NS_IMETHODIMP +nsStringBundleBase::GetStringFromID(int32_t aID, nsAString& aResult) { + nsAutoCString idStr; + idStr.AppendInt(aID, 10); + return GetStringFromName(idStr.get(), aResult); +} + +NS_IMETHODIMP +nsStringBundleBase::GetStringFromAUTF8Name(const nsACString& aName, + nsAString& aResult) { + return GetStringFromName(PromiseFlatCString(aName).get(), aResult); +} + +NS_IMETHODIMP +nsStringBundleBase::GetStringFromName(const char* aName, nsAString& aResult) { + NS_ENSURE_ARG_POINTER(aName); + + MutexAutoLock autolock(mMutex); + + return GetStringImpl(nsDependentCString(aName), aResult); +} + +nsresult nsStringBundle::GetStringImpl(const nsACString& aName, + nsAString& aResult) { + MOZ_TRY(LoadProperties()); + + return mProps->GetStringProperty(aName, aResult); +} + +nsresult SharedStringBundle::GetStringImpl(const nsACString& aName, + nsAString& aResult) { + MOZ_TRY(LoadProperties()); + + if (mStringMap->Get(PromiseFlatCString(aName), aResult)) { + return NS_OK; + } + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsStringBundleBase::FormatStringFromID(int32_t aID, + const nsTArray<nsString>& aParams, + nsAString& aResult) { + nsAutoCString idStr; + idStr.AppendInt(aID, 10); + return FormatStringFromName(idStr.get(), aParams, aResult); +} + +// this function supports at most 10 parameters.. see below for why +NS_IMETHODIMP +nsStringBundleBase::FormatStringFromAUTF8Name(const nsACString& aName, + const nsTArray<nsString>& aParams, + nsAString& aResult) { + return FormatStringFromName(PromiseFlatCString(aName).get(), aParams, + aResult); +} + +// this function supports at most 10 parameters.. see below for why +NS_IMETHODIMP +nsStringBundleBase::FormatStringFromName(const char* aName, + const nsTArray<nsString>& aParams, + nsAString& aResult) { + NS_ASSERTION(!aParams.IsEmpty(), + "FormatStringFromName() without format parameters: use " + "GetStringFromName() instead"); + + nsAutoString formatStr; + nsresult rv = GetStringFromName(aName, formatStr); + if (NS_FAILED(rv)) return rv; + + return FormatString(formatStr.get(), aParams, aResult); +} + +NS_IMETHODIMP +nsStringBundleBase::GetSimpleEnumeration(nsISimpleEnumerator** aElements) { + NS_ENSURE_ARG_POINTER(aElements); + + return GetSimpleEnumerationImpl(aElements); +} + +nsresult nsStringBundle::GetSimpleEnumerationImpl( + nsISimpleEnumerator** elements) { + MOZ_TRY(LoadProperties()); + + return mProps->Enumerate(elements); +} + +nsresult SharedStringBundle::GetSimpleEnumerationImpl( + nsISimpleEnumerator** aEnumerator) { + MOZ_TRY(LoadProperties()); + + auto iter = MakeRefPtr<StringMapEnumerator>(mStringMap); + iter.forget(aEnumerator); + return NS_OK; +} + +NS_IMETHODIMP +StringMapEnumerator::HasMoreElements(bool* aHasMore) { + *aHasMore = mIndex < mStringMap->Count(); + return NS_OK; +} + +NS_IMETHODIMP +StringMapEnumerator::GetNext(nsISupports** aNext) { + if (mIndex >= mStringMap->Count()) { + return NS_ERROR_FAILURE; + } + + auto elem = MakeRefPtr<nsPropertyElement>(mStringMap->GetKeyAt(mIndex), + mStringMap->GetValueAt(mIndex)); + + elem.forget(aNext); + + mIndex++; + return NS_OK; +} + +nsresult nsStringBundleBase::FormatString(const char16_t* aFormatStr, + const nsTArray<nsString>& aParams, + nsAString& aResult) { + auto length = aParams.Length(); + NS_ENSURE_ARG(length <= 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 + nsTextFormatter::ssprintf(aResult, aFormatStr, + length >= 1 ? aParams[0].get() : nullptr, + length >= 2 ? aParams[1].get() : nullptr, + length >= 3 ? aParams[2].get() : nullptr, + length >= 4 ? aParams[3].get() : nullptr, + length >= 5 ? aParams[4].get() : nullptr, + length >= 6 ? aParams[5].get() : nullptr, + length >= 7 ? aParams[6].get() : nullptr, + length >= 8 ? aParams[7].get() : nullptr, + length >= 9 ? aParams[8].get() : nullptr, + length >= 10 ? aParams[9].get() : nullptr); + + return NS_OK; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +#define MAX_CACHED_BUNDLES 16 + +struct bundleCacheEntry_t final : public LinkedListElement<bundleCacheEntry_t> { + nsCString mHashKey; + nsCOMPtr<nsIStringBundle> mBundle; + + MOZ_COUNTED_DEFAULT_CTOR(bundleCacheEntry_t) + + MOZ_COUNTED_DTOR(bundleCacheEntry_t) +}; + +nsStringBundleService::nsStringBundleService() + : mBundleMap(MAX_CACHED_BUNDLES) {} + +NS_IMPL_ISUPPORTS(nsStringBundleService, nsIStringBundleService, nsIObserver, + nsISupportsWeakReference, nsIMemoryReporter) + +nsStringBundleService::~nsStringBundleService() { + UnregisterWeakMemoryReporter(this); + flushBundleCache(/* ignoreShared = */ false); +} + +nsresult nsStringBundleService::Init() { + nsCOMPtr<nsIObserverService> 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, "intl:app-locales-changed", true); + } + + RegisterWeakMemoryReporter(this); + + return NS_OK; +} + +size_t nsStringBundleService::SizeOfIncludingThis( + mozilla::MallocSizeOf aMallocSizeOf) { + size_t n = mBundleMap.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (const auto& data : mBundleMap.Values()) { + n += aMallocSizeOf(data); + n += data->mHashKey.SizeOfExcludingThisIfUnshared(aMallocSizeOf); + } + return aMallocSizeOf(this) + n; +} + +NS_IMETHODIMP +nsStringBundleService::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aSomeData) { + if (strcmp("profile-do-change", aTopic) == 0 || + strcmp("chrome-flush-caches", aTopic) == 0 || + strcmp("intl:app-locales-changed", aTopic) == 0) { + flushBundleCache(/* ignoreShared = */ false); + } else if (strcmp("memory-pressure", aTopic) == 0) { + flushBundleCache(/* ignoreShared = */ true); + } + + return NS_OK; +} + +void nsStringBundleService::flushBundleCache(bool ignoreShared) { + LinkedList<bundleCacheEntry_t> newList; + + while (!mBundleCache.isEmpty()) { + UniquePtr<bundleCacheEntry_t> entry(mBundleCache.popFirst()); + auto* bundle = nsStringBundleBase::Cast(entry->mBundle); + + if (ignoreShared && bundle->IsShared()) { + newList.insertBack(entry.release()); + } else { + mBundleMap.Remove(entry->mHashKey); + } + } + + mBundleCache = std::move(newList); +} + +NS_IMETHODIMP +nsStringBundleService::FlushBundles() { + flushBundleCache(/* ignoreShared = */ false); + return NS_OK; +} + +void nsStringBundleService::SendContentBundles(ContentParent* aContentParent) { + nsTArray<StringBundleDescriptor> bundles; + + for (auto* entry : mSharedBundles) { + auto bundle = SharedStringBundle::Cast(entry->mBundle); + + if (bundle->Initialized()) { + bundles.AppendElement(bundle->GetDescriptor()); + } + } + + Unused << aContentParent->SendRegisterStringBundles(std::move(bundles)); +} + +void nsStringBundleService::RegisterContentBundle( + const nsACString& aBundleURL, const FileDescriptor& aMapFile, + size_t aMapSize) { + RefPtr<StringBundleProxy> proxy; + + bundleCacheEntry_t* cacheEntry = mBundleMap.Get(aBundleURL); + if (cacheEntry) { + if (RefPtr<SharedStringBundle> shared = + do_QueryObject(cacheEntry->mBundle)) { + return; + } + + proxy = do_QueryObject(cacheEntry->mBundle); + MOZ_ASSERT(proxy); + cacheEntry->remove(); + delete cacheEntry; + } + + auto bundle = MakeBundleRefPtr<SharedStringBundle>( + PromiseFlatCString(aBundleURL).get()); + bundle->SetMapFile(aMapFile, aMapSize); + + if (proxy) { + proxy->Retarget(bundle); + } + + cacheEntry = insertIntoCache(bundle.forget(), aBundleURL); + mSharedBundles.insertBack(cacheEntry); +} + +void nsStringBundleService::getStringBundle(const char* aURLSpec, + nsIStringBundle** aResult) { + nsDependentCString key(aURLSpec); + bundleCacheEntry_t* cacheEntry = mBundleMap.Get(key); + + RefPtr<SharedStringBundle> shared; + + if (cacheEntry) { + // Remove the entry from the list so it can be re-inserted at the back. + cacheEntry->remove(); + + shared = do_QueryObject(cacheEntry->mBundle); + } else { + nsCOMPtr<nsIStringBundle> bundle; + bool isContent = IsContentBundle(key); + if (!isContent || !XRE_IsParentProcess()) { + bundle = MakeBundle<nsStringBundle>(aURLSpec); + } + + // If this is a bundle which is used by the content processes, we want to + // load it into a shared memory region. + // + // If we're in the parent process, just create a new SharedStringBundle, + // and populate it from the properties file. + // + // If we're in a child process, the fact that the bundle is not already in + // the cache means that we haven't received its shared memory descriptor + // from the parent yet. There's not much we can do about that besides + // wait, but we need to return a bundle now. So instead of a shared memory + // bundle, we create a temporary proxy, which points to a non-shared + // bundle initially, and is retarged to a shared memory bundle when it + // becomes available. + if (isContent) { + if (XRE_IsParentProcess()) { + shared = MakeBundle<SharedStringBundle>(aURLSpec); + bundle = shared; + } else { + bundle = new StringBundleProxy(bundle.forget()); + } + } + + cacheEntry = insertIntoCache(bundle.forget(), key); + } + + if (shared) { + mSharedBundles.insertBack(cacheEntry); + } else { + mBundleCache.insertBack(cacheEntry); + } + + // finally, return the value + *aResult = cacheEntry->mBundle; + NS_ADDREF(*aResult); +} + +UniquePtr<bundleCacheEntry_t> nsStringBundleService::evictOneEntry() { + for (auto* entry : mBundleCache) { + auto* bundle = nsStringBundleBase::Cast(entry->mBundle); + if (!bundle->IsShared()) { + entry->remove(); + mBundleMap.Remove(entry->mHashKey); + return UniquePtr<bundleCacheEntry_t>(entry); + } + } + return nullptr; +} + +bundleCacheEntry_t* nsStringBundleService::insertIntoCache( + already_AddRefed<nsIStringBundle> aBundle, const nsACString& aHashKey) { + UniquePtr<bundleCacheEntry_t> cacheEntry; + + if (mBundleMap.Count() >= MAX_CACHED_BUNDLES) { + cacheEntry = evictOneEntry(); + } + + if (!cacheEntry) { + cacheEntry.reset(new bundleCacheEntry_t()); + } + + cacheEntry->mHashKey = aHashKey; + cacheEntry->mBundle = aBundle; + + mBundleMap.InsertOrUpdate(cacheEntry->mHashKey, cacheEntry.get()); + + return cacheEntry.release(); +} + +NS_IMETHODIMP +nsStringBundleService::CreateBundle(const char* aURLSpec, + nsIStringBundle** aResult) { + getStringBundle(aURLSpec, aResult); + return NS_OK; +} + +#define GLOBAL_PROPERTIES "chrome://global/locale/global-strres.properties" + +nsresult nsStringBundleService::FormatWithBundle( + nsIStringBundle* bundle, nsresult aStatus, + const nsTArray<nsString>& argArray, nsAString& result) { + nsresult rv; + + // try looking up the error message with the int key: + uint16_t code = NS_ERROR_GET_CODE(aStatus); + rv = bundle->FormatStringFromID(code, argArray, 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)) { + AutoTArray<nsString, 1> otherArgArray; + otherArgArray.AppendElement()->AppendInt(static_cast<uint32_t>(aStatus), + 16); + uint16_t code = NS_ERROR_GET_CODE(NS_ERROR_FAILURE); + rv = bundle->FormatStringFromID(code, otherArgArray, result); + } + + return rv; +} + +NS_IMETHODIMP +nsStringBundleService::FormatStatusMessage(nsresult aStatus, + const char16_t* aStatusArg, + nsAString& result) { + uint32_t i, argCount = 0; + nsCOMPtr<nsIStringBundle> bundle; + + // XXX hack for mailnews who has already formatted their messages: + if (aStatus == NS_OK && aStatusArg) { + result.Assign(aStatusArg); + 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 + AutoTArray<nsString, 10> argArray; + + // convert the aStatusArg into an nsString array + if (argCount == 1) { + argArray.AppendElement(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.AppendElement(Substring(args, offset, pos - offset)); + offset = pos + 1; + } + } + + switch (NS_ERROR_GET_MODULE(aStatus)) { + case NS_ERROR_MODULE_XSLT: + getStringBundle(XSLT_MSGS_URL, getter_AddRefs(bundle)); + break; + case NS_ERROR_MODULE_NETWORK: + getStringBundle(NECKO_MSGS_URL, getter_AddRefs(bundle)); + break; + default: + getStringBundle(GLOBAL_PROPERTIES, getter_AddRefs(bundle)); + break; + } + + return FormatWithBundle(bundle, aStatus, argArray, result); +} diff --git a/intl/strres/nsStringBundle.h b/intl/strres/nsStringBundle.h new file mode 100644 index 0000000000..96781f81a1 --- /dev/null +++ b/intl/strres/nsStringBundle.h @@ -0,0 +1,88 @@ +/* -*- Mode: C++; tab-width: 4; 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/. */ + +#ifndef nsStringBundle_h__ +#define nsStringBundle_h__ + +#include "mozilla/Mutex.h" +#include "nsIStringBundle.h" +#include "nsIMemoryReporter.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsCOMArray.h" + +class nsIPersistentProperties; + +class nsStringBundleBase : public nsIStringBundle, public nsIMemoryReporter { + public: + MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf) + + nsresult ParseProperties(nsIPersistentProperties**); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSISTRINGBUNDLE + NS_DECL_NSIMEMORYREPORTER + + virtual nsresult LoadProperties() = 0; + + const nsCString& BundleURL() const { return mPropertiesURL; } + + // Returns true if this bundle has more than one reference. If it has only + // a single reference, it is assumed to be held alive by the bundle cache. + bool IsShared() const { return mRefCnt > 1; } + + static nsStringBundleBase* Cast(nsIStringBundle* aBundle) { + return static_cast<nsStringBundleBase*>(aBundle); + } + + template <typename T, typename... Args> + static already_AddRefed<T> Create(Args... args); + + protected: + nsStringBundleBase(const char* aURLSpec); + + virtual ~nsStringBundleBase(); + + virtual nsresult GetStringImpl(const nsACString& aName, + nsAString& aResult) = 0; + + virtual nsresult GetSimpleEnumerationImpl(nsISimpleEnumerator** elements) = 0; + + void RegisterMemoryReporter(); + + nsCString mPropertiesURL; + mozilla::Mutex mMutex MOZ_UNANNOTATED; + bool mAttemptedLoad; + bool mLoaded; + + public: + static nsresult FormatString(const char16_t* formatStr, + const nsTArray<nsString>& aParams, + nsAString& aResult); +}; + +class nsStringBundle : public nsStringBundleBase { + public: + NS_DECL_ISUPPORTS_INHERITED + + nsCOMPtr<nsIPersistentProperties> mProps; + + nsresult LoadProperties() override; + + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) override; + + protected: + friend class nsStringBundleBase; + + explicit nsStringBundle(const char* aURLSpec); + + virtual ~nsStringBundle(); + + nsresult GetStringImpl(const nsACString& aName, nsAString& aResult) override; + + nsresult GetSimpleEnumerationImpl(nsISimpleEnumerator** elements) override; +}; + +#endif diff --git a/intl/strres/nsStringBundleService.h b/intl/strres/nsStringBundleService.h new file mode 100644 index 0000000000..a9f3402892 --- /dev/null +++ b/intl/strres/nsStringBundleService.h @@ -0,0 +1,69 @@ +/* -*- 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/. */ + +#ifndef nsStringBundleService_h__ +#define nsStringBundleService_h__ + +#include "nsCOMPtr.h" +#include "nsTHashMap.h" +#include "nsHashKeys.h" +#include "nsIStringBundle.h" +#include "nsIObserver.h" +#include "nsWeakReference.h" +#include "nsIMemoryReporter.h" + +#include "mozilla/LinkedList.h" +#include "mozilla/UniquePtr.h" + +struct bundleCacheEntry_t; + +class nsStringBundleService : public nsIStringBundleService, + public nsIObserver, + public nsSupportsWeakReference, + public nsIMemoryReporter { + MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf); + + public: + nsStringBundleService(); + + nsresult Init(); + + NS_DECL_ISUPPORTS + NS_DECL_NSISTRINGBUNDLESERVICE + NS_DECL_NSIOBSERVER + + NS_IMETHOD CollectReports(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, bool anonymize) override { + size_t amt = SizeOfIncludingThis(MallocSizeOf); + + MOZ_COLLECT_REPORT("explicit/string-bundles/service", KIND_HEAP, + UNITS_BYTES, amt, + "Memory used for StringBundleService overhead"); + return NS_OK; + }; + + private: + virtual ~nsStringBundleService(); + + void getStringBundle(const char* aUrl, nsIStringBundle** aResult); + nsresult FormatWithBundle(nsIStringBundle* bundle, nsresult aStatus, + const nsTArray<nsString>& argArray, + nsAString& result); + + void flushBundleCache(bool ignoreShared = true); + + mozilla::UniquePtr<bundleCacheEntry_t> evictOneEntry(); + + bundleCacheEntry_t* insertIntoCache(already_AddRefed<nsIStringBundle> aBundle, + const nsACString& aHashKey); + + nsTHashMap<nsCStringHashKey, bundleCacheEntry_t*> mBundleMap; + // LRU list of cached entries, with the least-recently-used entry first. + mozilla::LinkedList<bundleCacheEntry_t> mBundleCache; + // List of cached shared-memory string bundles, in arbitrary order. + mozilla::AutoCleanLinkedList<bundleCacheEntry_t> mSharedBundles; +}; + +#endif diff --git a/intl/strres/tests/unit/397093.properties b/intl/strres/tests/unit/397093.properties new file mode 100644 index 0000000000..88303d447a --- /dev/null +++ b/intl/strres/tests/unit/397093.properties @@ -0,0 +1,4 @@ +# Property file for test_bug397093.js +asciiProperty=Foo +utf8Property=Fòò +latin1Property=Fòò diff --git a/intl/strres/tests/unit/strres.properties b/intl/strres/tests/unit/strres.properties new file mode 100644 index 0000000000..388f5acff7 --- /dev/null +++ b/intl/strres/tests/unit/strres.properties @@ -0,0 +1,14 @@ +# 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/. +file=File +loyal=\u5fe0\u5fc3 +trout=é³Ÿéš +edit= Edit +view=View +go=\u0020Go +message=Message\u0020 +communicator=Communicator +help=Help +123=onetwothree +hello=Hello %S diff --git a/intl/strres/tests/unit/test_bug378839.js b/intl/strres/tests/unit/test_bug378839.js new file mode 100644 index 0000000000..b3bc76f3d6 --- /dev/null +++ b/intl/strres/tests/unit/test_bug378839.js @@ -0,0 +1,58 @@ +/* Tests getting properties from string bundles + */ + +const name_file = "file"; +const value_file = "File"; + +const name_loyal = "loyal"; +const value_loyal = "\u5fe0\u5fc3"; // tests escaped Unicode + +const name_trout = "trout"; +const value_trout = "\u9cdf\u9b5a"; // tests UTF-8 + +const name_edit = "edit"; +const value_edit = "Edit"; // tests literal leading spaces are stripped + +const name_view = "view"; +const value_view = "View"; // tests literal trailing spaces are stripped + +const name_go = "go"; +const value_go = " Go"; // tests escaped leading spaces are not stripped + +const name_message = "message"; +const value_message = "Message "; // tests escaped trailing spaces are not stripped + +const name_hello = "hello"; +const var_hello = "World"; +const value_hello = "Hello World"; // tests formatStringFromName with parameter + +function run_test() { + var StringBundle = Services.strings; + var bundleURI = Services.io.newFileURI(do_get_file("strres.properties")); + + var bundle = StringBundle.createBundle(bundleURI.spec); + + var bundle_file = bundle.GetStringFromName(name_file); + Assert.equal(bundle_file, value_file); + + var bundle_loyal = bundle.GetStringFromName(name_loyal); + Assert.equal(bundle_loyal, value_loyal); + + var bundle_trout = bundle.GetStringFromName(name_trout); + Assert.equal(bundle_trout, value_trout); + + var bundle_edit = bundle.GetStringFromName(name_edit); + Assert.equal(bundle_edit, value_edit); + + var bundle_view = bundle.GetStringFromName(name_view); + Assert.equal(bundle_view, value_view); + + var bundle_go = bundle.GetStringFromName(name_go); + Assert.equal(bundle_go, value_go); + + var bundle_message = bundle.GetStringFromName(name_message); + Assert.equal(bundle_message, value_message); + + var bundle_hello = bundle.formatStringFromName(name_hello, [var_hello]); + Assert.equal(bundle_hello, value_hello); +} diff --git a/intl/strres/tests/unit/test_bug397093.js b/intl/strres/tests/unit/test_bug397093.js new file mode 100644 index 0000000000..a2bb1eb236 --- /dev/null +++ b/intl/strres/tests/unit/test_bug397093.js @@ -0,0 +1,39 @@ +/* Tests getting properties from string bundles with incorrect encoding. + * The string bundle contains one ascii property, one UTF-8 and one Latin-1. + * Expected behaviour is that the whole string bundle should be rejected and + * all GetStringFromName calls should fail. + */ + +const name_ascii = "asciiProperty"; +const value_ascii = ""; + +const name_utf8 = "utf8Property"; +const value_utf8 = ""; + +const name_latin1 = "latin1"; +const value_latin1 = ""; + +function run_test() { + var StringBundle = Services.strings; + var bundleURI = Services.io.newFileURI(do_get_file("397093.properties")); + + var bundle = StringBundle.createBundle(bundleURI.spec); + + var bundle_ascii = "", + bundle_utf8 = "", + bundle_latin1 = ""; + try { + bundle_ascii = bundle.GetStringFromName(name_ascii); + } catch (e) {} + Assert.equal(bundle_ascii, value_ascii); + + try { + bundle_utf8 = bundle.GetStringFromName(name_utf8); + } catch (e) {} + Assert.equal(bundle_utf8, value_utf8); + + try { + bundle_latin1 = bundle.GetStringFromName(name_latin1); + } catch (e) {} + Assert.equal(bundle_latin1, value_latin1); +} diff --git a/intl/strres/tests/unit/xpcshell.ini b/intl/strres/tests/unit/xpcshell.ini new file mode 100644 index 0000000000..49840c144a --- /dev/null +++ b/intl/strres/tests/unit/xpcshell.ini @@ -0,0 +1,8 @@ +[DEFAULT] +head = +support-files = + 397093.properties + strres.properties + +[test_bug378839.js] +[test_bug397093.js] |