summaryrefslogtreecommitdiffstats
path: root/intl/strres
diff options
context:
space:
mode:
Diffstat (limited to 'intl/strres')
-rw-r--r--intl/strres/moz.build29
-rw-r--r--intl/strres/nsIStringBundle.idl111
-rw-r--r--intl/strres/nsStringBundle.cpp1026
-rw-r--r--intl/strres/nsStringBundle.h88
-rw-r--r--intl/strres/nsStringBundleService.h69
-rw-r--r--intl/strres/tests/unit/397093.properties4
-rw-r--r--intl/strres/tests/unit/strres.properties14
-rw-r--r--intl/strres/tests/unit/test_bug378839.js58
-rw-r--r--intl/strres/tests/unit/test_bug397093.js39
-rw-r--r--intl/strres/tests/unit/xpcshell.ini8
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]