From 43a97878ce14b72f0981164f87f2e35e14151312 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 11:22:09 +0200 Subject: Adding upstream version 110.0.1. Signed-off-by: Daniel Baumann --- modules/libpref/Preferences.cpp | 6232 ++++++++ modules/libpref/Preferences.h | 551 + modules/libpref/SharedPrefMap.cpp | 236 + modules/libpref/SharedPrefMap.h | 848 ++ modules/libpref/StaticPrefsBase.h | 93 + modules/libpref/components.conf | 31 + modules/libpref/docs/index.md | 463 + modules/libpref/greprefs.js | 5 + modules/libpref/init/StaticPrefList.yaml | 15005 +++++++++++++++++++ modules/libpref/init/StaticPrefListBegin.h | 65 + modules/libpref/init/StaticPrefListEnd.h | 20 + modules/libpref/init/__init__.py | 0 modules/libpref/init/all.js | 4193 ++++++ modules/libpref/init/generate_static_pref_list.py | 445 + modules/libpref/init/static_prefs/Cargo.toml | 11 + modules/libpref/init/static_prefs/src/lib.rs | 11 + modules/libpref/moz.build | 177 + modules/libpref/nsIPrefBranch.idl | 519 + modules/libpref/nsIPrefLocalizedString.idl | 34 + modules/libpref/nsIPrefService.idl | 206 + modules/libpref/nsIRelativeFilePref.idl | 38 + modules/libpref/nsRelativeFilePref.h | 33 + modules/libpref/parser/Cargo.toml | 7 + modules/libpref/parser/src/lib.rs | 991 ++ modules/libpref/test/browser/browser.ini | 9 + .../test/browser/browser_sanitization_events.js | 88 + .../test/browser/file_access_sanitized_pref.html | 10 + modules/libpref/test/gtest/Basics.cpp | 58 + modules/libpref/test/gtest/Parser.cpp | 496 + modules/libpref/test/gtest/moz.build | 25 + modules/libpref/test/python.ini | 4 + .../libpref/test/test_generate_static_pref_list.py | 471 + modules/libpref/test/unit/data/testParser.js | 98 + modules/libpref/test/unit/data/testPref.js | 6 + modules/libpref/test/unit/data/testPrefLocked.js | 2 + .../libpref/test/unit/data/testPrefLockedUser.js | 3 + modules/libpref/test/unit/data/testPrefSticky.js | 2 + .../libpref/test/unit/data/testPrefStickyUser.js | 5 + modules/libpref/test/unit/data/testPrefUTF8.js | 6 + modules/libpref/test/unit/extdata/testExt.js | 2 + modules/libpref/test/unit/head_libPrefs.js | 37 + modules/libpref/test/unit/test_bug1354613.js | 21 + modules/libpref/test/unit/test_bug345529.js | 25 + modules/libpref/test/unit/test_bug506224.js | 25 + modules/libpref/test/unit/test_bug577950.js | 17 + modules/libpref/test/unit/test_bug790374.js | 50 + modules/libpref/test/unit/test_changeType.js | 164 + modules/libpref/test/unit/test_defaultValues.js | 59 + modules/libpref/test/unit/test_dirtyPrefs.js | 69 + modules/libpref/test/unit/test_libPrefs.js | 441 + .../libpref/test/unit/test_locked_file_prefs.js | 42 + modules/libpref/test/unit/test_parsePrefs.js | 136 + modules/libpref/test/unit/test_parser.js | 107 + modules/libpref/test/unit/test_stickyprefs.js | 187 + modules/libpref/test/unit/test_warnings.js | 62 + modules/libpref/test/unit/xpcshell.ini | 28 + .../libpref/test/unit_ipc/test_existing_prefs.js | 20 + .../libpref/test/unit_ipc/test_initial_prefs.js | 14 + modules/libpref/test/unit_ipc/test_large_pref.js | 104 + modules/libpref/test/unit_ipc/test_locked_prefs.js | 39 + .../libpref/test/unit_ipc/test_observed_prefs.js | 12 + modules/libpref/test/unit_ipc/test_sharedMap.js | 362 + .../test/unit_ipc/test_sharedMap_static_prefs.js | 76 + modules/libpref/test/unit_ipc/test_update_prefs.js | 34 + .../test/unit_ipc/test_user_default_prefs.js | 72 + modules/libpref/test/unit_ipc/xpcshell.ini | 14 + 66 files changed, 33716 insertions(+) create mode 100644 modules/libpref/Preferences.cpp create mode 100644 modules/libpref/Preferences.h create mode 100644 modules/libpref/SharedPrefMap.cpp create mode 100644 modules/libpref/SharedPrefMap.h create mode 100644 modules/libpref/StaticPrefsBase.h create mode 100644 modules/libpref/components.conf create mode 100644 modules/libpref/docs/index.md create mode 100644 modules/libpref/greprefs.js create mode 100644 modules/libpref/init/StaticPrefList.yaml create mode 100644 modules/libpref/init/StaticPrefListBegin.h create mode 100644 modules/libpref/init/StaticPrefListEnd.h create mode 100644 modules/libpref/init/__init__.py create mode 100644 modules/libpref/init/all.js create mode 100644 modules/libpref/init/generate_static_pref_list.py create mode 100644 modules/libpref/init/static_prefs/Cargo.toml create mode 100644 modules/libpref/init/static_prefs/src/lib.rs create mode 100644 modules/libpref/moz.build create mode 100644 modules/libpref/nsIPrefBranch.idl create mode 100644 modules/libpref/nsIPrefLocalizedString.idl create mode 100644 modules/libpref/nsIPrefService.idl create mode 100644 modules/libpref/nsIRelativeFilePref.idl create mode 100644 modules/libpref/nsRelativeFilePref.h create mode 100644 modules/libpref/parser/Cargo.toml create mode 100644 modules/libpref/parser/src/lib.rs create mode 100644 modules/libpref/test/browser/browser.ini create mode 100644 modules/libpref/test/browser/browser_sanitization_events.js create mode 100644 modules/libpref/test/browser/file_access_sanitized_pref.html create mode 100644 modules/libpref/test/gtest/Basics.cpp create mode 100644 modules/libpref/test/gtest/Parser.cpp create mode 100644 modules/libpref/test/gtest/moz.build create mode 100644 modules/libpref/test/python.ini create mode 100644 modules/libpref/test/test_generate_static_pref_list.py create mode 100644 modules/libpref/test/unit/data/testParser.js create mode 100644 modules/libpref/test/unit/data/testPref.js create mode 100644 modules/libpref/test/unit/data/testPrefLocked.js create mode 100644 modules/libpref/test/unit/data/testPrefLockedUser.js create mode 100644 modules/libpref/test/unit/data/testPrefSticky.js create mode 100644 modules/libpref/test/unit/data/testPrefStickyUser.js create mode 100644 modules/libpref/test/unit/data/testPrefUTF8.js create mode 100644 modules/libpref/test/unit/extdata/testExt.js create mode 100644 modules/libpref/test/unit/head_libPrefs.js create mode 100644 modules/libpref/test/unit/test_bug1354613.js create mode 100644 modules/libpref/test/unit/test_bug345529.js create mode 100644 modules/libpref/test/unit/test_bug506224.js create mode 100644 modules/libpref/test/unit/test_bug577950.js create mode 100644 modules/libpref/test/unit/test_bug790374.js create mode 100644 modules/libpref/test/unit/test_changeType.js create mode 100644 modules/libpref/test/unit/test_defaultValues.js create mode 100644 modules/libpref/test/unit/test_dirtyPrefs.js create mode 100644 modules/libpref/test/unit/test_libPrefs.js create mode 100644 modules/libpref/test/unit/test_locked_file_prefs.js create mode 100644 modules/libpref/test/unit/test_parsePrefs.js create mode 100644 modules/libpref/test/unit/test_parser.js create mode 100644 modules/libpref/test/unit/test_stickyprefs.js create mode 100644 modules/libpref/test/unit/test_warnings.js create mode 100644 modules/libpref/test/unit/xpcshell.ini create mode 100644 modules/libpref/test/unit_ipc/test_existing_prefs.js create mode 100644 modules/libpref/test/unit_ipc/test_initial_prefs.js create mode 100644 modules/libpref/test/unit_ipc/test_large_pref.js create mode 100644 modules/libpref/test/unit_ipc/test_locked_prefs.js create mode 100644 modules/libpref/test/unit_ipc/test_observed_prefs.js create mode 100644 modules/libpref/test/unit_ipc/test_sharedMap.js create mode 100644 modules/libpref/test/unit_ipc/test_sharedMap_static_prefs.js create mode 100644 modules/libpref/test/unit_ipc/test_update_prefs.js create mode 100644 modules/libpref/test/unit_ipc/test_user_default_prefs.js create mode 100644 modules/libpref/test/unit_ipc/xpcshell.ini (limited to 'modules/libpref') diff --git a/modules/libpref/Preferences.cpp b/modules/libpref/Preferences.cpp new file mode 100644 index 0000000000..a2f674ef19 --- /dev/null +++ b/modules/libpref/Preferences.cpp @@ -0,0 +1,6232 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +// Documentation for libpref is in modules/libpref/docs/index.rst. + +#include +#include +#include + +#include "SharedPrefMap.h" + +#include "base/basictypes.h" +#include "MainThreadUtils.h" +#include "mozilla/AppShutdown.h" +#include "mozilla/ArenaAllocatorExtensions.h" +#include "mozilla/ArenaAllocator.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/Attributes.h" +#include "mozilla/Components.h" +#include "mozilla/dom/PContent.h" +#include "mozilla/dom/RemoteType.h" +#include "mozilla/HashFunctions.h" +#include "mozilla/HashTable.h" +#include "mozilla/Logging.h" +#include "mozilla/Maybe.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/Omnijar.h" +#include "mozilla/Preferences.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/ProfilerMarkers.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/ServoStyleSet.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/StaticPrefsAll.h" +#include "mozilla/SyncRunnable.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TelemetryEventEnums.h" +#include "mozilla/UniquePtrExtensions.h" +#include "mozilla/URLPreloader.h" +#include "mozilla/Variant.h" +#include "mozilla/Vector.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsCategoryManagerUtils.h" +#include "nsClassHashtable.h" +#include "nsCOMArray.h" +#include "nsCOMPtr.h" +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsCRT.h" +#include "nsTHashMap.h" +#include "nsDirectoryServiceDefs.h" +#include "nsIConsoleService.h" +#include "nsIFile.h" +#include "nsIMemoryReporter.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsIOutputStream.h" +#include "nsIPrefBranch.h" +#include "nsIPrefLocalizedString.h" +#include "nsIRelativeFilePref.h" +#include "nsISafeOutputStream.h" +#include "nsISimpleEnumerator.h" +#include "nsIStringBundle.h" +#include "nsISupportsImpl.h" +#include "nsISupportsPrimitives.h" +#include "nsIZipReader.h" +#include "nsNetUtil.h" +#include "nsPrintfCString.h" +#include "nsQuickSort.h" +#include "nsReadableUtils.h" +#include "nsRefPtrHashtable.h" +#include "nsRelativeFilePref.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsThreadUtils.h" +#include "nsUTF8Utils.h" +#include "nsWeakReference.h" +#include "nsXPCOMCID.h" +#include "nsXPCOM.h" +#include "nsXULAppAPI.h" +#include "nsZipArchive.h" +#include "plbase64.h" +#include "PLDHashTable.h" +#include "plstr.h" +#include "prdtoa.h" +#include "prlink.h" +#include "xpcpublic.h" +#include "js/RootingAPI.h" +#ifdef MOZ_BACKGROUNDTASKS +# include "mozilla/BackgroundTasks.h" +#endif + +#ifdef DEBUG +# include +#endif + +#ifdef MOZ_MEMORY +# include "mozmemory.h" +#endif + +#ifdef XP_WIN +# include "windows.h" +#endif + +#if defined(MOZ_WIDGET_GTK) +# include "mozilla/WidgetUtilsGtk.h" +#endif // defined(MOZ_WIDGET_GTK) + +using namespace mozilla; + +using ipc::FileDescriptor; + +#ifdef DEBUG + +# define ENSURE_PARENT_PROCESS(func, pref) \ + do { \ + if (MOZ_UNLIKELY(!XRE_IsParentProcess())) { \ + nsPrintfCString msg( \ + "ENSURE_PARENT_PROCESS: called %s on %s in a non-parent process", \ + func, pref); \ + NS_ERROR(msg.get()); \ + return NS_ERROR_NOT_AVAILABLE; \ + } \ + } while (0) + +#else // DEBUG + +# define ENSURE_PARENT_PROCESS(func, pref) \ + if (MOZ_UNLIKELY(!XRE_IsParentProcess())) { \ + return NS_ERROR_NOT_AVAILABLE; \ + } + +#endif // DEBUG + +// Forward declarations. +namespace mozilla::StaticPrefs { + +static void InitAll(); +static void StartObservingAlwaysPrefs(); +static void InitOncePrefs(); +static void InitStaticPrefsFromShared(); +static void RegisterOncePrefs(SharedPrefMapBuilder& aBuilder); +static void ShutdownAlwaysPrefs(); + +} // namespace mozilla::StaticPrefs + +//=========================================================================== +// Low-level types and operations +//=========================================================================== + +Atomic sPrefTelemetryEventEnabled(false); + +typedef nsTArray PrefSaveData; + +// 1 MB should be enough for everyone. +static const uint32_t MAX_PREF_LENGTH = 1 * 1024 * 1024; +// Actually, 4kb should be enough for everyone. +static const uint32_t MAX_ADVISABLE_PREF_LENGTH = 4 * 1024; + +// This is used for pref names and string pref values. We encode the string +// length, then a '/', then the string chars. This encoding means there are no +// special chars that are forbidden or require escaping. +static void SerializeAndAppendString(const nsCString& aChars, nsCString& aStr) { + aStr.AppendInt(uint64_t(aChars.Length())); + aStr.Append('/'); + aStr.Append(aChars); +} + +static char* DeserializeString(char* aChars, nsCString& aStr) { + char* p = aChars; + uint32_t length = strtol(p, &p, 10); + MOZ_ASSERT(p[0] == '/'); + p++; // move past the '/' + aStr.Assign(p, length); + p += length; // move past the string itself + return p; +} + +// Keep this in sync with PrefValue in parser/src/lib.rs. +union PrefValue { + // PrefValues within Pref objects own their chars. PrefValues passed around + // as arguments don't own their chars. + const char* mStringVal; + int32_t mIntVal; + bool mBoolVal; + + PrefValue() = default; + + explicit PrefValue(bool aVal) : mBoolVal(aVal) {} + + explicit PrefValue(int32_t aVal) : mIntVal(aVal) {} + + explicit PrefValue(const char* aVal) : mStringVal(aVal) {} + + bool Equals(PrefType aType, PrefValue aValue) { + switch (aType) { + case PrefType::String: { + if (mStringVal && aValue.mStringVal) { + return strcmp(mStringVal, aValue.mStringVal) == 0; + } + if (!mStringVal && !aValue.mStringVal) { + return true; + } + return false; + } + + case PrefType::Int: + return mIntVal == aValue.mIntVal; + + case PrefType::Bool: + return mBoolVal == aValue.mBoolVal; + + default: + MOZ_CRASH("Unhandled enum value"); + } + } + + template + T Get() const; + + void Init(PrefType aNewType, PrefValue aNewValue) { + if (aNewType == PrefType::String) { + MOZ_ASSERT(aNewValue.mStringVal); + aNewValue.mStringVal = moz_xstrdup(aNewValue.mStringVal); + } + *this = aNewValue; + } + + void Clear(PrefType aType) { + if (aType == PrefType::String) { + free(const_cast(mStringVal)); + } + + // Zero the entire value (regardless of type) via mStringVal. + mStringVal = nullptr; + } + + void Replace(bool aHasValue, PrefType aOldType, PrefType aNewType, + PrefValue aNewValue) { + if (aHasValue) { + Clear(aOldType); + } + Init(aNewType, aNewValue); + } + + void ToDomPrefValue(PrefType aType, dom::PrefValue* aDomValue) { + switch (aType) { + case PrefType::String: + *aDomValue = nsDependentCString(mStringVal); + return; + + case PrefType::Int: + *aDomValue = mIntVal; + return; + + case PrefType::Bool: + *aDomValue = mBoolVal; + return; + + default: + MOZ_CRASH(); + } + } + + PrefType FromDomPrefValue(const dom::PrefValue& aDomValue) { + switch (aDomValue.type()) { + case dom::PrefValue::TnsCString: + mStringVal = aDomValue.get_nsCString().get(); + return PrefType::String; + + case dom::PrefValue::Tint32_t: + mIntVal = aDomValue.get_int32_t(); + return PrefType::Int; + + case dom::PrefValue::Tbool: + mBoolVal = aDomValue.get_bool(); + return PrefType::Bool; + + default: + MOZ_CRASH(); + } + } + + void SerializeAndAppend(PrefType aType, nsCString& aStr) { + switch (aType) { + case PrefType::Bool: + aStr.Append(mBoolVal ? 'T' : 'F'); + break; + + case PrefType::Int: + aStr.AppendInt(mIntVal); + break; + + case PrefType::String: { + SerializeAndAppendString(nsDependentCString(mStringVal), aStr); + break; + } + + case PrefType::None: + default: + MOZ_CRASH(); + } + } + + static char* Deserialize(PrefType aType, char* aStr, + Maybe* aDomValue) { + char* p = aStr; + + switch (aType) { + case PrefType::Bool: + if (*p == 'T') { + *aDomValue = Some(true); + } else if (*p == 'F') { + *aDomValue = Some(false); + } else { + *aDomValue = Some(false); + NS_ERROR("bad bool pref value"); + } + p++; + return p; + + case PrefType::Int: { + *aDomValue = Some(int32_t(strtol(p, &p, 10))); + return p; + } + + case PrefType::String: { + nsCString str; + p = DeserializeString(p, str); + *aDomValue = Some(str); + return p; + } + + default: + MOZ_CRASH(); + } + } +}; + +template <> +bool PrefValue::Get() const { + return mBoolVal; +} + +template <> +int32_t PrefValue::Get() const { + return mIntVal; +} + +template <> +nsDependentCString PrefValue::Get() const { + return nsDependentCString(mStringVal); +} + +#ifdef DEBUG +const char* PrefTypeToString(PrefType aType) { + switch (aType) { + case PrefType::None: + return "none"; + case PrefType::String: + return "string"; + case PrefType::Int: + return "int"; + case PrefType::Bool: + return "bool"; + default: + MOZ_CRASH("Unhandled enum value"); + } +} +#endif + +// Assign to aResult a quoted, escaped copy of aOriginal. +static void StrEscape(const char* aOriginal, nsCString& aResult) { + if (aOriginal == nullptr) { + aResult.AssignLiteral("\"\""); + return; + } + + // JavaScript does not allow quotes, slashes, or line terminators inside + // strings so we must escape them. ECMAScript defines four line terminators, + // but we're only worrying about \r and \n here. We currently feed our pref + // script to the JS interpreter as Latin-1 so we won't encounter \u2028 + // (line separator) or \u2029 (paragraph separator). + // + // WARNING: There are hints that we may be moving to storing prefs as utf8. + // If we ever feed them to the JS compiler as UTF8 then we'll have to worry + // about the multibyte sequences that would be interpreted as \u2028 and + // \u2029. + const char* p; + + aResult.Assign('"'); + + // Paranoid worst case all slashes will free quickly. + for (p = aOriginal; *p; ++p) { + switch (*p) { + case '\n': + aResult.AppendLiteral("\\n"); + break; + + case '\r': + aResult.AppendLiteral("\\r"); + break; + + case '\\': + aResult.AppendLiteral("\\\\"); + break; + + case '\"': + aResult.AppendLiteral("\\\""); + break; + + default: + aResult.Append(*p); + break; + } + } + + aResult.Append('"'); +} + +// Mimic the behaviour of nsTStringRepr::ToFloat before bug 840706 to preserve +// error case handling for parsing pref strings. Many callers do not check error +// codes, so the returned values may be used even if an error is set. +// +// This method should never return NaN, but may return +-inf if the provided +// number is too large to fit in a float. +static float ParsePrefFloat(const nsCString& aString, nsresult* aError) { + if (aString.IsEmpty()) { + *aError = NS_ERROR_ILLEGAL_VALUE; + return 0.f; + } + + // PR_strtod does a locale-independent conversion. + char* stopped = nullptr; + float result = PR_strtod(aString.get(), &stopped); + + // Defensively avoid potential breakage caused by returning NaN into + // unsuspecting code. AFAIK this should never happen as PR_strtod cannot + // return NaN as currently configured. + if (mozilla::IsNaN(result)) { + MOZ_ASSERT_UNREACHABLE("PR_strtod shouldn't return NaN"); + *aError = NS_ERROR_ILLEGAL_VALUE; + return 0.f; + } + + *aError = (stopped == aString.EndReading()) ? NS_OK : NS_ERROR_ILLEGAL_VALUE; + return result; +} + +namespace mozilla { +struct PrefsSizes { + PrefsSizes() + : mHashTable(0), + mPrefValues(0), + mStringValues(0), + mRootBranches(0), + mPrefNameArena(0), + mCallbacksObjects(0), + mCallbacksDomains(0), + mMisc(0) {} + + size_t mHashTable; + size_t mPrefValues; + size_t mStringValues; + size_t mRootBranches; + size_t mPrefNameArena; + size_t mCallbacksObjects; + size_t mCallbacksDomains; + size_t mMisc; +}; +} // namespace mozilla + +static StaticRefPtr gSharedMap; + +// Arena for Pref names. +// Never access sPrefNameArena directly, always use PrefNameArena() +// because it must only be accessed on the Main Thread +typedef ArenaAllocator<4096, 1> NameArena; +static NameArena* sPrefNameArena; + +static inline NameArena& PrefNameArena() { + MOZ_ASSERT(NS_IsMainThread()); + + if (!sPrefNameArena) { + sPrefNameArena = new NameArena(); + } + return *sPrefNameArena; +} + +class PrefWrapper; + +// Three forward declarations for immediately below +class Pref; +static bool IsPreferenceSanitized(const Pref* const aPref); +static bool ShouldSanitizePreference(const Pref* const aPref); + +// Note that this never changes in the parent process, and is only read in +// content processes. +static bool gContentProcessPrefsAreInited = false; + +class Pref { + public: + explicit Pref(const nsACString& aName) + : mName(ArenaStrdup(aName, PrefNameArena()), aName.Length()), + mType(static_cast(PrefType::None)), + mIsSticky(false), + mIsLocked(false), + mIsSanitized(false), + mHasDefaultValue(false), + mHasUserValue(false), + mIsSkippedByIteration(false), + mDefaultValue(), + mUserValue() {} + + ~Pref() { + // There's no need to free mName because it's allocated in memory owned by + // sPrefNameArena. + + mDefaultValue.Clear(Type()); + mUserValue.Clear(Type()); + } + + const char* Name() const { return mName.get(); } + const nsDependentCString& NameString() const { return mName; } + + // Types. + + PrefType Type() const { return static_cast(mType); } + void SetType(PrefType aType) { mType = static_cast(aType); } + + bool IsType(PrefType aType) const { return Type() == aType; } + bool IsTypeNone() const { return IsType(PrefType::None); } + bool IsTypeString() const { return IsType(PrefType::String); } + bool IsTypeInt() const { return IsType(PrefType::Int); } + bool IsTypeBool() const { return IsType(PrefType::Bool); } + + // Other properties. + + bool IsLocked() const { return mIsLocked; } + void SetIsLocked(bool aValue) { mIsLocked = aValue; } + bool IsSkippedByIteration() const { return mIsSkippedByIteration; } + void SetIsSkippedByIteration(bool aValue) { mIsSkippedByIteration = aValue; } + + bool IsSticky() const { return mIsSticky; } + + bool IsSanitized() const { return mIsSanitized; } + + bool HasDefaultValue() const { return mHasDefaultValue; } + bool HasUserValue() const { return mHasUserValue; } + + template + void AddToMap(SharedPrefMapBuilder& aMap) { + // Sanitized preferences should never be added to the shared pref map + MOZ_ASSERT(!ShouldSanitizePreference(this)); + aMap.Add(NameString(), + {HasDefaultValue(), HasUserValue(), IsSticky(), IsLocked(), + /* isSanitized */ false, IsSkippedByIteration()}, + HasDefaultValue() ? mDefaultValue.Get() : T(), + HasUserValue() ? mUserValue.Get() : T()); + } + + void AddToMap(SharedPrefMapBuilder& aMap) { + if (IsTypeBool()) { + AddToMap(aMap); + } else if (IsTypeInt()) { + AddToMap(aMap); + } else if (IsTypeString()) { + AddToMap(aMap); + } else { + MOZ_ASSERT_UNREACHABLE("Unexpected preference type"); + } + } + + // Other operations. + +#define CHECK_SANITIZATION() \ + if (IsPreferenceSanitized(this)) { \ + if (!sPrefTelemetryEventEnabled.exchange(true)) { \ + sPrefTelemetryEventEnabled = true; \ + Telemetry::SetEventRecordingEnabled("security"_ns, true); \ + } \ + Telemetry::RecordEvent( \ + Telemetry::EventID::Security_Prefusage_Contentprocess, \ + mozilla::Some(Name()), mozilla::Nothing()); \ + if (sCrashOnBlocklistedPref) { \ + MOZ_CRASH_UNSAFE_PRINTF( \ + "Should not access the preference '%s' in the Content Processes", \ + Name()); \ + } \ + } + + bool GetBoolValue(PrefValueKind aKind = PrefValueKind::User) const { + MOZ_ASSERT(IsTypeBool()); + MOZ_ASSERT(aKind == PrefValueKind::Default ? HasDefaultValue() + : HasUserValue()); + + CHECK_SANITIZATION(); + + return aKind == PrefValueKind::Default ? mDefaultValue.mBoolVal + : mUserValue.mBoolVal; + } + + int32_t GetIntValue(PrefValueKind aKind = PrefValueKind::User) const { + MOZ_ASSERT(IsTypeInt()); + MOZ_ASSERT(aKind == PrefValueKind::Default ? HasDefaultValue() + : HasUserValue()); + + CHECK_SANITIZATION(); + + return aKind == PrefValueKind::Default ? mDefaultValue.mIntVal + : mUserValue.mIntVal; + } + + const char* GetBareStringValue( + PrefValueKind aKind = PrefValueKind::User) const { + MOZ_ASSERT(IsTypeString()); + MOZ_ASSERT(aKind == PrefValueKind::Default ? HasDefaultValue() + : HasUserValue()); + + CHECK_SANITIZATION(); + + return aKind == PrefValueKind::Default ? mDefaultValue.mStringVal + : mUserValue.mStringVal; + } + +#undef CHECK_SANITIZATION + + nsDependentCString GetStringValue( + PrefValueKind aKind = PrefValueKind::User) const { + return nsDependentCString(GetBareStringValue(aKind)); + } + + void ToDomPref(dom::Pref* aDomPref, bool aIsDestinationWebContentProcess) { + MOZ_ASSERT(XRE_IsParentProcess()); + + aDomPref->name() = mName; + + aDomPref->isLocked() = mIsLocked; + + aDomPref->isSanitized() = + aIsDestinationWebContentProcess && ShouldSanitizePreference(this); + + if (mHasDefaultValue) { + aDomPref->defaultValue() = Some(dom::PrefValue()); + mDefaultValue.ToDomPrefValue(Type(), &aDomPref->defaultValue().ref()); + } else { + aDomPref->defaultValue() = Nothing(); + } + + if (mHasUserValue && + !(aDomPref->isSanitized() && sOmitBlocklistedPrefValues)) { + aDomPref->userValue() = Some(dom::PrefValue()); + mUserValue.ToDomPrefValue(Type(), &aDomPref->userValue().ref()); + } else { + aDomPref->userValue() = Nothing(); + } + + MOZ_ASSERT(aDomPref->defaultValue().isNothing() || + aDomPref->userValue().isNothing() || + (mIsSanitized && sOmitBlocklistedPrefValues) || + (aDomPref->defaultValue().ref().type() == + aDomPref->userValue().ref().type())); + } + + void FromDomPref(const dom::Pref& aDomPref, bool* aValueChanged) { + MOZ_ASSERT(!XRE_IsParentProcess()); + MOZ_ASSERT(mName == aDomPref.name()); + + mIsLocked = aDomPref.isLocked(); + mIsSanitized = aDomPref.isSanitized(); + + const Maybe& defaultValue = aDomPref.defaultValue(); + bool defaultValueChanged = false; + if (defaultValue.isSome()) { + PrefValue value; + PrefType type = value.FromDomPrefValue(defaultValue.ref()); + if (!ValueMatches(PrefValueKind::Default, type, value)) { + // Type() is PrefType::None if it's a newly added pref. This is ok. + mDefaultValue.Replace(mHasDefaultValue, Type(), type, value); + SetType(type); + mHasDefaultValue = true; + defaultValueChanged = true; + } + } + // Note: we never clear a default value. + + const Maybe& userValue = aDomPref.userValue(); + bool userValueChanged = false; + if (userValue.isSome()) { + PrefValue value; + PrefType type = value.FromDomPrefValue(userValue.ref()); + if (!ValueMatches(PrefValueKind::User, type, value)) { + // Type() is PrefType::None if it's a newly added pref. This is ok. + mUserValue.Replace(mHasUserValue, Type(), type, value); + SetType(type); + mHasUserValue = true; + userValueChanged = true; + } + } else if (mHasUserValue) { + ClearUserValue(); + userValueChanged = true; + } + + if (userValueChanged || (defaultValueChanged && !mHasUserValue)) { + *aValueChanged = true; + } + } + + void FromWrapper(PrefWrapper& aWrapper); + + bool HasAdvisablySizedValues() { + MOZ_ASSERT(XRE_IsParentProcess()); + + if (!IsTypeString()) { + return true; + } + + if (mHasDefaultValue && + strlen(mDefaultValue.mStringVal) > MAX_ADVISABLE_PREF_LENGTH) { + return false; + } + + if (mHasUserValue && + strlen(mUserValue.mStringVal) > MAX_ADVISABLE_PREF_LENGTH) { + return false; + } + + return true; + } + + private: + bool ValueMatches(PrefValueKind aKind, PrefType aType, PrefValue aValue) { + return IsType(aType) && + (aKind == PrefValueKind::Default + ? mHasDefaultValue && mDefaultValue.Equals(aType, aValue) + : mHasUserValue && mUserValue.Equals(aType, aValue)); + } + + public: + void ClearUserValue() { + mUserValue.Clear(Type()); + mHasUserValue = false; + } + + nsresult SetDefaultValue(PrefType aType, PrefValue aValue, bool aIsSticky, + bool aIsLocked, bool* aValueChanged) { + // Types must always match when setting the default value. + if (!IsType(aType)) { + return NS_ERROR_UNEXPECTED; + } + + // Should we set the default value? Only if the pref is not locked, and + // doing so would change the default value. + if (!IsLocked()) { + if (aIsLocked) { + SetIsLocked(true); + } + if (!ValueMatches(PrefValueKind::Default, aType, aValue)) { + mDefaultValue.Replace(mHasDefaultValue, Type(), aType, aValue); + mHasDefaultValue = true; + if (aIsSticky) { + mIsSticky = true; + } + if (!mHasUserValue) { + *aValueChanged = true; + } + // What if we change the default to be the same as the user value? + // Should we clear the user value? Currently we don't. + } + } + return NS_OK; + } + + nsresult SetUserValue(PrefType aType, PrefValue aValue, bool aFromInit, + bool* aValueChanged) { + // If we have a default value, types must match when setting the user + // value. + if (mHasDefaultValue && !IsType(aType)) { + return NS_ERROR_UNEXPECTED; + } + + // Should we clear the user value, if present? Only if the new user value + // matches the default value, and the pref isn't sticky, and we aren't + // force-setting it during initialization. + if (ValueMatches(PrefValueKind::Default, aType, aValue) && !mIsSticky && + !aFromInit) { + if (mHasUserValue) { + ClearUserValue(); + if (!IsLocked()) { + *aValueChanged = true; + } + } + + // Otherwise, should we set the user value? Only if doing so would + // change the user value. + } else if (!ValueMatches(PrefValueKind::User, aType, aValue)) { + mUserValue.Replace(mHasUserValue, Type(), aType, aValue); + SetType(aType); // needed because we may have changed the type + mHasUserValue = true; + if (!IsLocked()) { + *aValueChanged = true; + } + } + return NS_OK; + } + + // Prefs are serialized in a manner that mirrors dom::Pref. The two should be + // kept in sync. E.g. if something is added to one it should also be added to + // the other. (It would be nice to be able to use the code generated from + // IPDL for serializing dom::Pref here instead of writing by hand this + // serialization/deserialization. Unfortunately, that generated code is + // difficult to use directly, outside of the IPDL IPC code.) + // + // The grammar for the serialized prefs has the following form. + // + // = ':' ':' ? ':' + // ? '\n' + // = 'B' | 'I' | 'S' + // = 'L' | '-' + // = 'S' | '-' + // = + // = | | + // = 'T' | 'F' + // = an integer literal accepted by strtol() + // = '/' + // = any char sequence of length dictated by the preceding + // . + // + // No whitespace is tolerated between tokens. must match the types of + // the values. + // + // The serialization is text-based, rather than binary, for the following + // reasons. + // + // - The size difference wouldn't be much different between text-based and + // binary. Most of the space is for strings (pref names and string pref + // values), which would be the same in both styles. And other differences + // would be minimal, e.g. small integers are shorter in text but long + // integers are longer in text. + // + // - Likewise, speed differences should be negligible. + // + // - It's much easier to debug a text-based serialization. E.g. you can + // print it and inspect it easily in a debugger. + // + // Examples of unlocked boolean prefs: + // - "B--:8/my.bool1:F:T\n" + // - "B--:8/my.bool2:F:\n" + // - "B--:8/my.bool3::T\n" + // + // Examples of sanitized, unlocked boolean prefs: + // - "B-S:8/my.bool1:F:T\n" + // - "B-S:8/my.bool2:F:\n" + // - "B-S:8/my.bool3::T\n" + // + // Examples of locked integer prefs: + // - "IL-:7/my.int1:0:1\n" + // - "IL-:7/my.int2:123:\n" + // - "IL-:7/my.int3::-99\n" + // + // Examples of unlocked string prefs: + // - "S--:10/my.string1:3/abc:4/wxyz\n" + // - "S--:10/my.string2:5/1.234:\n" + // - "S--:10/my.string3::7/string!\n" + + void SerializeAndAppend(nsCString& aStr, bool aSanitizeUserValue) { + switch (Type()) { + case PrefType::Bool: + aStr.Append('B'); + break; + + case PrefType::Int: + aStr.Append('I'); + break; + + case PrefType::String: { + aStr.Append('S'); + break; + } + + case PrefType::None: + default: + MOZ_CRASH(); + } + + aStr.Append(mIsLocked ? 'L' : '-'); + aStr.Append(aSanitizeUserValue ? 'S' : '-'); + aStr.Append(':'); + + SerializeAndAppendString(mName, aStr); + aStr.Append(':'); + + if (mHasDefaultValue) { + mDefaultValue.SerializeAndAppend(Type(), aStr); + } + aStr.Append(':'); + + if (mHasUserValue && !(aSanitizeUserValue && sOmitBlocklistedPrefValues)) { + mUserValue.SerializeAndAppend(Type(), aStr); + } + aStr.Append('\n'); + } + + static char* Deserialize(char* aStr, dom::Pref* aDomPref) { + char* p = aStr; + + // The type. + PrefType type; + if (*p == 'B') { + type = PrefType::Bool; + } else if (*p == 'I') { + type = PrefType::Int; + } else if (*p == 'S') { + type = PrefType::String; + } else { + NS_ERROR("bad pref type"); + type = PrefType::None; + } + p++; // move past the type char + + // Locked? + bool isLocked; + if (*p == 'L') { + isLocked = true; + } else if (*p == '-') { + isLocked = false; + } else { + NS_ERROR("bad pref locked status"); + isLocked = false; + } + p++; // move past the isLocked char + + // Sanitize? + bool isSanitized; + if (*p == 'S') { + isSanitized = true; + } else if (*p == '-') { + isSanitized = false; + } else { + NS_ERROR("bad pref sanitized status"); + isSanitized = false; + } + p++; // move past the isSanitized char + + MOZ_ASSERT(*p == ':'); + p++; // move past the ':' + + // The pref name. + nsCString name; + p = DeserializeString(p, name); + + MOZ_ASSERT(*p == ':'); + p++; // move past the ':' preceding the default value + + Maybe maybeDefaultValue; + if (*p != ':') { + dom::PrefValue defaultValue; + p = PrefValue::Deserialize(type, p, &maybeDefaultValue); + } + + MOZ_ASSERT(*p == ':'); + p++; // move past the ':' between the default and user values + + Maybe maybeUserValue; + if (*p != '\n') { + dom::PrefValue userValue; + p = PrefValue::Deserialize(type, p, &maybeUserValue); + } + + MOZ_ASSERT(*p == '\n'); + p++; // move past the '\n' following the user value + + *aDomPref = dom::Pref(name, isLocked, isSanitized, maybeDefaultValue, + maybeUserValue); + + return p; + } + + void AddSizeOfIncludingThis(MallocSizeOf aMallocSizeOf, PrefsSizes& aSizes) { + // Note: mName is allocated in sPrefNameArena, measured elsewhere. + aSizes.mPrefValues += aMallocSizeOf(this); + if (IsTypeString()) { + if (mHasDefaultValue) { + aSizes.mStringValues += aMallocSizeOf(mDefaultValue.mStringVal); + } + if (mHasUserValue) { + aSizes.mStringValues += aMallocSizeOf(mUserValue.mStringVal); + } + } + } + + void RelocateName(NameArena* aArena) { + mName.Rebind(ArenaStrdup(mName.get(), *aArena), mName.Length()); + } + + private: + nsDependentCString mName; // allocated in sPrefNameArena + + uint32_t mType : 2; + uint32_t mIsSticky : 1; + uint32_t mIsLocked : 1; + uint32_t mIsSanitized : 1; + uint32_t mHasDefaultValue : 1; + uint32_t mHasUserValue : 1; + uint32_t mIsSkippedByIteration : 1; + + PrefValue mDefaultValue; + PrefValue mUserValue; +}; + +struct PrefHasher { + using Key = UniquePtr; + using Lookup = const char*; + + static HashNumber hash(const Lookup aLookup) { return HashString(aLookup); } + + static bool match(const Key& aKey, const Lookup aLookup) { + if (!aLookup || !aKey->Name()) { + return false; + } + + return strcmp(aLookup, aKey->Name()) == 0; + } +}; + +using PrefWrapperBase = Variant; +class MOZ_STACK_CLASS PrefWrapper : public PrefWrapperBase { + using SharedPref = const SharedPrefMap::Pref; + + public: + MOZ_IMPLICIT PrefWrapper(Pref* aPref) : PrefWrapperBase(AsVariant(aPref)) {} + + MOZ_IMPLICIT PrefWrapper(const SharedPrefMap::Pref& aPref) + : PrefWrapperBase(AsVariant(aPref)) {} + + // Types. + + bool IsType(PrefType aType) const { return Type() == aType; } + bool IsTypeNone() const { return IsType(PrefType::None); } + bool IsTypeString() const { return IsType(PrefType::String); } + bool IsTypeInt() const { return IsType(PrefType::Int); } + bool IsTypeBool() const { return IsType(PrefType::Bool); } + +#define FORWARD(retType, method) \ + retType method() const { \ + struct Matcher { \ + retType operator()(const Pref* aPref) { return aPref->method(); } \ + retType operator()(SharedPref& aPref) { return aPref.method(); } \ + }; \ + return match(Matcher()); \ + } + + FORWARD(bool, IsLocked) + FORWARD(bool, IsSanitized) + FORWARD(bool, IsSticky) + FORWARD(bool, HasDefaultValue) + FORWARD(bool, HasUserValue) + FORWARD(const char*, Name) + FORWARD(nsCString, NameString) + FORWARD(PrefType, Type) +#undef FORWARD + +#define FORWARD(retType, method) \ + retType method(PrefValueKind aKind = PrefValueKind::User) const { \ + struct Matcher { \ + PrefValueKind mKind; \ + \ + retType operator()(const Pref* aPref) { return aPref->method(mKind); } \ + retType operator()(SharedPref& aPref) { return aPref.method(mKind); } \ + }; \ + return match(Matcher{aKind}); \ + } + + FORWARD(bool, GetBoolValue) + FORWARD(int32_t, GetIntValue) + FORWARD(nsCString, GetStringValue) + FORWARD(const char*, GetBareStringValue) +#undef FORWARD + + PrefValue GetValue(PrefValueKind aKind = PrefValueKind::User) const { + switch (Type()) { + case PrefType::Bool: + return PrefValue{GetBoolValue(aKind)}; + case PrefType::Int: + return PrefValue{GetIntValue(aKind)}; + case PrefType::String: + return PrefValue{GetBareStringValue(aKind)}; + case PrefType::None: + // This check will be performed in the above functions; but for NoneType + // we need to do it explicitly, then fall-through. + if (IsPreferenceSanitized(Name())) { + if (!sPrefTelemetryEventEnabled.exchange(true)) { + sPrefTelemetryEventEnabled = true; + Telemetry::SetEventRecordingEnabled("security"_ns, true); + } + + Telemetry::RecordEvent( + Telemetry::EventID::Security_Prefusage_Contentprocess, + mozilla::Some(Name()), mozilla::Nothing()); + + if (sCrashOnBlocklistedPref) { + MOZ_CRASH_UNSAFE_PRINTF( + "Should not access the preference '%s' in the Content " + "Processes", + Name()); + } + } + [[fallthrough]]; + default: + MOZ_ASSERT_UNREACHABLE("Unexpected pref type"); + return PrefValue{}; + } + } + + Result WantValueKind(PrefType aType, + PrefValueKind aKind) const { + // WantValueKind may short-circuit GetValue functions and cause them to + // return early, before this check occurs in GetFooValue() + if (this->is() && IsPreferenceSanitized(this->as())) { + if (!sPrefTelemetryEventEnabled.exchange(true)) { + sPrefTelemetryEventEnabled = true; + Telemetry::SetEventRecordingEnabled("security"_ns, true); + } + + Telemetry::RecordEvent( + Telemetry::EventID::Security_Prefusage_Contentprocess, + mozilla::Some(Name()), mozilla::Nothing()); + + if (sCrashOnBlocklistedPref) { + MOZ_CRASH_UNSAFE_PRINTF( + "Should not access the preference '%s' in the Content Processes", + Name()); + } + } else if (!this->is()) { + // While we could use Name() above, and avoid the Variant checks, it + // would less efficient than needed and we can instead do a debug-only + // assert here to limit the inefficientcy + MOZ_ASSERT(!IsPreferenceSanitized(Name()), + "We should never have a sanitized SharedPrefMap::Pref."); + } + + if (Type() != aType) { + return Err(NS_ERROR_UNEXPECTED); + } + + if (aKind == PrefValueKind::Default || IsLocked() || !HasUserValue()) { + if (!HasDefaultValue()) { + return Err(NS_ERROR_UNEXPECTED); + } + return PrefValueKind::Default; + } + return PrefValueKind::User; + } + + nsresult GetValue(PrefValueKind aKind, bool* aResult) const { + PrefValueKind kind; + MOZ_TRY_VAR(kind, WantValueKind(PrefType::Bool, aKind)); + + *aResult = GetBoolValue(kind); + return NS_OK; + } + + nsresult GetValue(PrefValueKind aKind, int32_t* aResult) const { + PrefValueKind kind; + MOZ_TRY_VAR(kind, WantValueKind(PrefType::Int, aKind)); + + *aResult = GetIntValue(kind); + return NS_OK; + } + + nsresult GetValue(PrefValueKind aKind, uint32_t* aResult) const { + return GetValue(aKind, reinterpret_cast(aResult)); + } + + nsresult GetValue(PrefValueKind aKind, float* aResult) const { + nsAutoCString result; + nsresult rv = GetValue(aKind, result); + if (NS_SUCCEEDED(rv)) { + // ParsePrefFloat() does a locale-independent conversion. + // FIXME: Other `GetValue` overloads don't clobber `aResult` on error. + *aResult = ParsePrefFloat(result, &rv); + } + return rv; + } + + nsresult GetValue(PrefValueKind aKind, nsACString& aResult) const { + PrefValueKind kind; + MOZ_TRY_VAR(kind, WantValueKind(PrefType::String, aKind)); + + aResult = GetStringValue(kind); + return NS_OK; + } + + nsresult GetValue(PrefValueKind aKind, nsACString* aResult) const { + return GetValue(aKind, *aResult); + } + + // Returns false if this pref doesn't have a user value worth saving. + bool UserValueToStringForSaving(nsCString& aStr) { + // Should we save the user value, if present? Only if it does not match the + // default value, or it is sticky. + if (HasUserValue() && + (!ValueMatches(PrefValueKind::Default, Type(), GetValue()) || + IsSticky())) { + if (IsTypeString()) { + StrEscape(GetStringValue().get(), aStr); + + } else if (IsTypeInt()) { + aStr.AppendInt(GetIntValue()); + + } else if (IsTypeBool()) { + aStr = GetBoolValue() ? "true" : "false"; + } + return true; + } + + // Do not save default prefs that haven't changed. + return false; + } + + bool Matches(PrefType aType, PrefValueKind aKind, PrefValue& aValue, + bool aIsSticky, bool aIsLocked) const { + return (ValueMatches(aKind, aType, aValue) && aIsSticky == IsSticky() && + aIsLocked == IsLocked()); + } + + bool ValueMatches(PrefValueKind aKind, PrefType aType, + const PrefValue& aValue) const { + if (!IsType(aType)) { + return false; + } + if (!(aKind == PrefValueKind::Default ? HasDefaultValue() + : HasUserValue())) { + return false; + } + switch (aType) { + case PrefType::Bool: + return GetBoolValue(aKind) == aValue.mBoolVal; + case PrefType::Int: + return GetIntValue(aKind) == aValue.mIntVal; + case PrefType::String: + return strcmp(GetBareStringValue(aKind), aValue.mStringVal) == 0; + default: + MOZ_ASSERT_UNREACHABLE("Unexpected preference type"); + return false; + } + } +}; + +void Pref::FromWrapper(PrefWrapper& aWrapper) { + MOZ_ASSERT(aWrapper.is()); + auto pref = aWrapper.as(); + + MOZ_ASSERT(IsTypeNone()); + MOZ_ASSERT(mName == pref.NameString()); + + mType = uint32_t(pref.Type()); + + mIsLocked = pref.IsLocked(); + mIsSanitized = pref.IsSanitized(); + mIsSticky = pref.IsSticky(); + + mHasDefaultValue = pref.HasDefaultValue(); + mHasUserValue = pref.HasUserValue(); + + if (mHasDefaultValue) { + mDefaultValue.Init(Type(), aWrapper.GetValue(PrefValueKind::Default)); + } + if (mHasUserValue) { + mUserValue.Init(Type(), aWrapper.GetValue(PrefValueKind::User)); + } +} + +class CallbackNode { + public: + CallbackNode(const nsACString& aDomain, PrefChangedFunc aFunc, void* aData, + Preferences::MatchKind aMatchKind) + : mDomain(AsVariant(nsCString(aDomain))), + mFunc(aFunc), + mData(aData), + mNextAndMatchKind(aMatchKind) {} + + CallbackNode(const char** aDomains, PrefChangedFunc aFunc, void* aData, + Preferences::MatchKind aMatchKind) + : mDomain(AsVariant(aDomains)), + mFunc(aFunc), + mData(aData), + mNextAndMatchKind(aMatchKind) {} + + // mDomain is a UniquePtr<>, so any uses of Domain() should only be temporary + // borrows. + const Variant& Domain() const { return mDomain; } + + PrefChangedFunc Func() const { return mFunc; } + void ClearFunc() { mFunc = nullptr; } + + void* Data() const { return mData; } + + Preferences::MatchKind MatchKind() const { + return static_cast(mNextAndMatchKind & + kMatchKindMask); + } + + bool DomainIs(const nsACString& aDomain) const { + return mDomain.is() && mDomain.as() == aDomain; + } + + bool DomainIs(const char** aPrefs) const { + return mDomain == AsVariant(aPrefs); + } + + bool Matches(const nsACString& aPrefName) const { + auto match = [&](const nsACString& aStr) { + return MatchKind() == Preferences::ExactMatch + ? aPrefName == aStr + : StringBeginsWith(aPrefName, aStr); + }; + + if (mDomain.is()) { + return match(mDomain.as()); + } + for (const char** ptr = mDomain.as(); *ptr; ptr++) { + if (match(nsDependentCString(*ptr))) { + return true; + } + } + return false; + } + + CallbackNode* Next() const { + return reinterpret_cast(mNextAndMatchKind & kNextMask); + } + + void SetNext(CallbackNode* aNext) { + uintptr_t matchKind = mNextAndMatchKind & kMatchKindMask; + mNextAndMatchKind = reinterpret_cast(aNext); + MOZ_ASSERT((mNextAndMatchKind & kMatchKindMask) == 0); + mNextAndMatchKind |= matchKind; + } + + void AddSizeOfIncludingThis(MallocSizeOf aMallocSizeOf, PrefsSizes& aSizes) { + aSizes.mCallbacksObjects += aMallocSizeOf(this); + if (mDomain.is()) { + aSizes.mCallbacksDomains += + mDomain.as().SizeOfExcludingThisIfUnshared(aMallocSizeOf); + } + } + + private: + static const uintptr_t kMatchKindMask = uintptr_t(0x1); + static const uintptr_t kNextMask = ~kMatchKindMask; + + Variant mDomain; + + // If someone attempts to remove the node from the callback list while + // NotifyCallbacks() is running, |func| is set to nullptr. Such nodes will + // be removed at the end of NotifyCallbacks(). + PrefChangedFunc mFunc; + void* mData; + + // Conceptually this is two fields: + // - CallbackNode* mNext; + // - Preferences::MatchKind mMatchKind; + // They are combined into a tagged pointer to save memory. + uintptr_t mNextAndMatchKind; +}; + +using PrefsHashTable = HashSet, PrefHasher>; + +// The main prefs hash table. Inside a function so we can assert it's only +// accessed on the main thread. (That assertion can be avoided but only do so +// with great care!) +static inline PrefsHashTable*& HashTable(bool aOffMainThread = false) { + MOZ_ASSERT(NS_IsMainThread() || ServoStyleSet::IsInServoTraversal()); + static PrefsHashTable* sHashTable = nullptr; + return sHashTable; +} + +#ifdef DEBUG +// This defines the type used to store our `once` mirrors checker. We can't use +// HashMap for now due to alignment restrictions when dealing with +// std::function (see bug 1557617). +typedef std::function AntiFootgunCallback; +struct CompareStr { + bool operator()(char const* a, char const* b) const { + return std::strcmp(a, b) < 0; + } +}; +typedef std::map AntiFootgunMap; +static AntiFootgunMap* gOnceStaticPrefsAntiFootgun; +#endif + +// The callback list contains all the priority callbacks followed by the +// non-priority callbacks. gLastPriorityNode records where the first part ends. +static CallbackNode* gFirstCallback = nullptr; +static CallbackNode* gLastPriorityNode = nullptr; + +#ifdef DEBUG +# define ACCESS_COUNTS +#endif + +#ifdef ACCESS_COUNTS +using AccessCountsHashTable = nsTHashMap; +static AccessCountsHashTable* gAccessCounts = nullptr; + +static void AddAccessCount(const nsACString& aPrefName) { + // FIXME: Servo reads preferences from background threads in unsafe ways (bug + // 1474789), and triggers assertions here if we try to add usage count entries + // from background threads. + if (NS_IsMainThread()) { + JS::AutoSuppressGCAnalysis nogc; // Hash functions will not GC. + uint32_t& count = gAccessCounts->LookupOrInsert(aPrefName); + count++; + } +} + +static void AddAccessCount(const char* aPrefName) { + AddAccessCount(nsDependentCString(aPrefName)); +} +#else +static void MOZ_MAYBE_UNUSED AddAccessCount(const nsACString& aPrefName) {} + +static void AddAccessCount(const char* aPrefName) {} +#endif + +// These are only used during the call to NotifyCallbacks(). +static bool gCallbacksInProgress = false; +static bool gShouldCleanupDeadNodes = false; + +class PrefsHashIter { + using Iterator = decltype(HashTable()->modIter()); + using ElemType = Pref*; + + Iterator mIter; + + public: + explicit PrefsHashIter(PrefsHashTable* aTable) : mIter(aTable->modIter()) {} + + class Elem { + friend class PrefsHashIter; + + PrefsHashIter& mParent; + bool mDone; + + Elem(PrefsHashIter& aIter, bool aDone) : mParent(aIter), mDone(aDone) {} + + Iterator& Iter() { return mParent.mIter; } + + public: + Elem& operator*() { return *this; } + + ElemType get() { + if (mDone) { + return nullptr; + } + return Iter().get().get(); + } + ElemType get() const { return const_cast(this)->get(); } + + ElemType operator->() { return get(); } + ElemType operator->() const { return get(); } + + operator ElemType() { return get(); } + + void Remove() { Iter().remove(); } + + Elem& operator++() { + MOZ_ASSERT(!mDone); + Iter().next(); + mDone = Iter().done(); + return *this; + } + + bool operator!=(Elem& other) { + return mDone != other.mDone || this->get() != other.get(); + } + }; + + Elem begin() { return Elem(*this, mIter.done()); } + + Elem end() { return Elem(*this, true); } +}; + +class PrefsIter { + using Iterator = decltype(HashTable()->iter()); + using ElemType = PrefWrapper; + + using HashElem = PrefsHashIter::Elem; + using SharedElem = SharedPrefMap::Pref; + + using ElemTypeVariant = Variant; + + SharedPrefMap* mSharedMap; + PrefsHashTable* mHashTable; + PrefsHashIter mIter; + + ElemTypeVariant mPos; + ElemTypeVariant mEnd; + + Maybe mEntry; + + public: + PrefsIter(PrefsHashTable* aHashTable, SharedPrefMap* aSharedMap) + : mSharedMap(aSharedMap), + mHashTable(aHashTable), + mIter(aHashTable), + mPos(AsVariant(mIter.begin())), + mEnd(AsVariant(mIter.end())) { + if (Done()) { + NextIterator(); + } + } + + private: +#define MATCH(type, ...) \ + do { \ + struct Matcher { \ + PrefsIter& mIter; \ + type operator()(HashElem& pos) { \ + HashElem& end MOZ_MAYBE_UNUSED = mIter.mEnd.as(); \ + __VA_ARGS__; \ + } \ + type operator()(SharedElem& pos) { \ + SharedElem& end MOZ_MAYBE_UNUSED = mIter.mEnd.as(); \ + __VA_ARGS__; \ + } \ + }; \ + return mPos.match(Matcher{*this}); \ + } while (0); + + bool Done() { MATCH(bool, return pos == end); } + + PrefWrapper MakeEntry() { MATCH(PrefWrapper, return PrefWrapper(pos)); } + + void NextEntry() { + mEntry.reset(); + MATCH(void, ++pos); + } +#undef MATCH + + bool Next() { + NextEntry(); + return !Done() || NextIterator(); + } + + bool NextIterator() { + if (mPos.is() && mSharedMap) { + mPos = AsVariant(mSharedMap->begin()); + mEnd = AsVariant(mSharedMap->end()); + return !Done(); + } + return false; + } + + bool IteratingBase() { return mPos.is(); } + + PrefWrapper& Entry() { + MOZ_ASSERT(!Done()); + + if (!mEntry.isSome()) { + mEntry.emplace(MakeEntry()); + } + return mEntry.ref(); + } + + public: + class Elem { + friend class PrefsIter; + + PrefsIter& mParent; + bool mDone; + + Elem(PrefsIter& aIter, bool aDone) : mParent(aIter), mDone(aDone) { + SkipDuplicates(); + } + + void Next() { mDone = !mParent.Next(); } + + void SkipDuplicates() { + while (!mDone && + (mParent.IteratingBase() ? mParent.mHashTable->has(ref().Name()) + : ref().IsTypeNone())) { + Next(); + } + } + + public: + Elem& operator*() { return *this; } + + ElemType& ref() { return mParent.Entry(); } + const ElemType& ref() const { return const_cast(this)->ref(); } + + ElemType* operator->() { return &ref(); } + const ElemType* operator->() const { return &ref(); } + + operator ElemType() { return ref(); } + + Elem& operator++() { + MOZ_ASSERT(!mDone); + Next(); + SkipDuplicates(); + return *this; + } + + bool operator!=(Elem& other) { + if (mDone != other.mDone) { + return true; + } + if (mDone) { + return false; + } + return &this->ref() != &other.ref(); + } + }; + + Elem begin() { return {*this, Done()}; } + + Elem end() { return {*this, true}; } +}; + +static Pref* pref_HashTableLookup(const char* aPrefName); + +static void NotifyCallbacks(const nsCString& aPrefName, + const PrefWrapper* aPref = nullptr); + +static void NotifyCallbacks(const nsCString& aPrefName, + const PrefWrapper& aPref) { + NotifyCallbacks(aPrefName, &aPref); +} + +// The approximate number of preferences in the dynamic hashtable for the parent +// and content processes, respectively. These numbers are used to determine the +// initial size of the dynamic preference hashtables, and should be chosen to +// avoid rehashing during normal usage. The actual number of preferences will, +// or course, change over time, but these numbers only need to be within a +// binary order of magnitude of the actual values to remain effective. +// +// The number for the parent process should reflect the total number of +// preferences in the database, since the parent process needs to initially +// build a dynamic hashtable of the entire preference database. The number for +// the child process should reflect the number of preferences which are likely +// to change after the startup of the first content process, since content +// processes only store changed preferences on top of a snapshot of the database +// created at startup. +// +// Note: The capacity of a hashtable doubles when its length reaches an exact +// power of two. A table with an initial length of 64 is twice as large as one +// with an initial length of 63. This is important in content processes, where +// lookup speed is less critical and we pay the price of the additional overhead +// for each content process. So the initial content length should generally be +// *under* the next power-of-two larger than its expected length. +constexpr size_t kHashTableInitialLengthParent = 3000; +constexpr size_t kHashTableInitialLengthContent = 64; + +static PrefSaveData pref_savePrefs() { + MOZ_ASSERT(NS_IsMainThread()); + + PrefSaveData savedPrefs(HashTable()->count()); + + for (auto& pref : PrefsIter(HashTable(), gSharedMap)) { + nsAutoCString prefValueStr; + if (!pref->UserValueToStringForSaving(prefValueStr)) { + continue; + } + + nsAutoCString prefNameStr; + StrEscape(pref->Name(), prefNameStr); + + nsPrintfCString str("user_pref(%s, %s);", prefNameStr.get(), + prefValueStr.get()); + + savedPrefs.AppendElement(str); + } + + return savedPrefs; +} + +static Pref* pref_HashTableLookup(const char* aPrefName) { + MOZ_ASSERT(NS_IsMainThread() || ServoStyleSet::IsInServoTraversal()); + + MOZ_ASSERT_IF(!XRE_IsParentProcess(), gContentProcessPrefsAreInited); + + // We use readonlyThreadsafeLookup() because we often have concurrent lookups + // from multiple Stylo threads. This is safe because those threads cannot + // modify sHashTable, and the main thread is blocked while Stylo threads are + // doing these lookups. + auto p = HashTable()->readonlyThreadsafeLookup(aPrefName); + return p ? p->get() : nullptr; +} + +// While notifying preference callbacks, this holds the wrapper for the +// preference being notified, in order to optimize lookups. +// +// Note: Callbacks and lookups only happen on the main thread, so this is safe +// to use without locking. +static const PrefWrapper* gCallbackPref; + +Maybe pref_SharedLookup(const char* aPrefName) { + MOZ_DIAGNOSTIC_ASSERT(gSharedMap, "gSharedMap must be initialized"); + if (Maybe pref = gSharedMap->Get(aPrefName)) { + return Some(*pref); + } + return Nothing(); +} + +Maybe pref_Lookup(const char* aPrefName, + bool aIncludeTypeNone = false) { + MOZ_ASSERT(NS_IsMainThread() || ServoStyleSet::IsInServoTraversal()); + + AddAccessCount(aPrefName); + + if (gCallbackPref && strcmp(aPrefName, gCallbackPref->Name()) == 0) { + return Some(*gCallbackPref); + } + if (Pref* pref = pref_HashTableLookup(aPrefName)) { + if (aIncludeTypeNone || !pref->IsTypeNone() || pref->IsSanitized()) { + return Some(pref); + } + } else if (gSharedMap) { + return pref_SharedLookup(aPrefName); + } + + return Nothing(); +} + +static Result pref_LookupForModify( + const nsCString& aPrefName, + const std::function& aCheckFn) { + Maybe wrapper = + pref_Lookup(aPrefName.get(), /* includeTypeNone */ true); + if (wrapper.isNothing()) { + return Err(NS_ERROR_INVALID_ARG); + } + if (!aCheckFn(*wrapper)) { + return nullptr; + } + if (wrapper->is()) { + return wrapper->as(); + } + + Pref* pref = new Pref(aPrefName); + if (!HashTable()->putNew(aPrefName.get(), pref)) { + delete pref; + return Err(NS_ERROR_OUT_OF_MEMORY); + } + pref->FromWrapper(*wrapper); + return pref; +} + +static nsresult pref_SetPref(const nsCString& aPrefName, PrefType aType, + PrefValueKind aKind, PrefValue aValue, + bool aIsSticky, bool aIsLocked, bool aFromInit) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownThreads)) { + printf( + "pref_SetPref: Attempt to write pref %s after XPCOMShutdownThreads " + "started.\n", + aPrefName.get()); + if (nsContentUtils::IsInitialized()) { + xpc_DumpJSStack(true, true, false); + } + MOZ_ASSERT(false, "Late preference writes should be avoided."); + return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + } + + if (!HashTable()) { + return NS_ERROR_OUT_OF_MEMORY; + } + + Pref* pref = nullptr; + if (gSharedMap) { + auto result = + pref_LookupForModify(aPrefName, [&](const PrefWrapper& aWrapper) { + return !aWrapper.Matches(aType, aKind, aValue, aIsSticky, aIsLocked); + }); + if (result.isOk() && !(pref = result.unwrap())) { + // No changes required. + return NS_OK; + } + } + + if (!pref) { + auto p = HashTable()->lookupForAdd(aPrefName.get()); + if (!p) { + pref = new Pref(aPrefName); + pref->SetType(aType); + if (!HashTable()->add(p, pref)) { + delete pref; + return NS_ERROR_OUT_OF_MEMORY; + } + } else { + pref = p->get(); + } + } + + bool valueChanged = false; + nsresult rv; + if (aKind == PrefValueKind::Default) { + rv = pref->SetDefaultValue(aType, aValue, aIsSticky, aIsLocked, + &valueChanged); + } else { + MOZ_ASSERT(!aIsLocked); // `locked` is disallowed in user pref files + rv = pref->SetUserValue(aType, aValue, aFromInit, &valueChanged); + } + if (NS_FAILED(rv)) { + NS_WARNING( + nsPrintfCString("Rejected attempt to change type of pref %s's %s value " + "from %s to %s", + aPrefName.get(), + (aKind == PrefValueKind::Default) ? "default" : "user", + PrefTypeToString(pref->Type()), PrefTypeToString(aType)) + .get()); + + return rv; + } + + if (valueChanged) { + if (aKind == PrefValueKind::User) { + Preferences::HandleDirty(); + } + NotifyCallbacks(aPrefName, PrefWrapper(pref)); + } + + return NS_OK; +} + +// Removes |node| from callback list. Returns the node after the deleted one. +static CallbackNode* pref_RemoveCallbackNode(CallbackNode* aNode, + CallbackNode* aPrevNode) { + MOZ_ASSERT(!aPrevNode || aPrevNode->Next() == aNode); + MOZ_ASSERT(aPrevNode || gFirstCallback == aNode); + MOZ_ASSERT(!gCallbacksInProgress); + + CallbackNode* next_node = aNode->Next(); + if (aPrevNode) { + aPrevNode->SetNext(next_node); + } else { + gFirstCallback = next_node; + } + if (gLastPriorityNode == aNode) { + gLastPriorityNode = aPrevNode; + } + delete aNode; + return next_node; +} + +static void NotifyCallbacks(const nsCString& aPrefName, + const PrefWrapper* aPref) { + bool reentered = gCallbacksInProgress; + + gCallbackPref = aPref; + auto cleanup = MakeScopeExit([]() { gCallbackPref = nullptr; }); + + // Nodes must not be deleted while gCallbacksInProgress is true. + // Nodes that need to be deleted are marked for deletion by nulling + // out the |func| pointer. We release them at the end of this function + // if we haven't reentered. + gCallbacksInProgress = true; + + for (CallbackNode* node = gFirstCallback; node; node = node->Next()) { + if (node->Func()) { + if (node->Matches(aPrefName)) { + (node->Func())(aPrefName.get(), node->Data()); + } + } + } + + gCallbacksInProgress = reentered; + + if (gShouldCleanupDeadNodes && !gCallbacksInProgress) { + CallbackNode* prev_node = nullptr; + CallbackNode* node = gFirstCallback; + + while (node) { + if (!node->Func()) { + node = pref_RemoveCallbackNode(node, prev_node); + } else { + prev_node = node; + node = node->Next(); + } + } + gShouldCleanupDeadNodes = false; + } + +#ifdef DEBUG + if (XRE_IsParentProcess() && + !StaticPrefs::preferences_force_disable_check_once_policy() && + (StaticPrefs::preferences_check_once_policy() || xpc::IsInAutomation())) { + // Check that we aren't modifying a `once`-mirrored pref using that pref + // name. We have about 100 `once`-mirrored prefs. std::map performs a + // search in O(log n), so this is fast enough. + MOZ_ASSERT(gOnceStaticPrefsAntiFootgun); + auto search = gOnceStaticPrefsAntiFootgun->find(aPrefName.get()); + if (search != gOnceStaticPrefsAntiFootgun->end()) { + // Run the callback. + (search->second)(); + } + } +#endif +} + +//=========================================================================== +// Prefs parsing +//=========================================================================== + +extern "C" { + +// Keep this in sync with PrefFn in parser/src/lib.rs. +typedef void (*PrefsParserPrefFn)(const char* aPrefName, PrefType aType, + PrefValueKind aKind, PrefValue aValue, + bool aIsSticky, bool aIsLocked); + +// Keep this in sync with ErrorFn in parser/src/lib.rs. +// +// `aMsg` is just a borrow of the string, and must be copied if it is used +// outside the lifetime of the prefs_parser_parse() call. +typedef void (*PrefsParserErrorFn)(const char* aMsg); + +// Keep this in sync with prefs_parser_parse() in parser/src/lib.rs. +bool prefs_parser_parse(const char* aPath, PrefValueKind aKind, + const char* aBuf, size_t aLen, + PrefsParserPrefFn aPrefFn, PrefsParserErrorFn aErrorFn); +} + +class Parser { + public: + Parser() = default; + ~Parser() = default; + + bool Parse(PrefValueKind aKind, const char* aPath, const nsCString& aBuf) { + MOZ_ASSERT(XRE_IsParentProcess()); + return prefs_parser_parse(aPath, aKind, aBuf.get(), aBuf.Length(), + HandlePref, HandleError); + } + + private: + static void HandlePref(const char* aPrefName, PrefType aType, + PrefValueKind aKind, PrefValue aValue, bool aIsSticky, + bool aIsLocked) { + MOZ_ASSERT(XRE_IsParentProcess()); + pref_SetPref(nsDependentCString(aPrefName), aType, aKind, aValue, aIsSticky, + aIsLocked, + /* fromInit */ true); + } + + static void HandleError(const char* aMsg) { + nsresult rv; + nsCOMPtr console = + do_GetService("@mozilla.org/consoleservice;1", &rv); + if (NS_SUCCEEDED(rv)) { + console->LogStringMessage(NS_ConvertUTF8toUTF16(aMsg).get()); + } +#ifdef DEBUG + NS_ERROR(aMsg); +#else + printf_stderr("%s\n", aMsg); +#endif + } +}; + +// The following code is test code for the gtest. + +static void TestParseErrorHandlePref(const char* aPrefName, PrefType aType, + PrefValueKind aKind, PrefValue aValue, + bool aIsSticky, bool aIsLocked) {} + +static nsCString gTestParseErrorMsgs; + +static void TestParseErrorHandleError(const char* aMsg) { + gTestParseErrorMsgs.Append(aMsg); + gTestParseErrorMsgs.Append('\n'); +} + +// Keep this in sync with the declaration in test/gtest/Parser.cpp. +void TestParseError(PrefValueKind aKind, const char* aText, + nsCString& aErrorMsg) { + prefs_parser_parse("test", aKind, aText, strlen(aText), + TestParseErrorHandlePref, TestParseErrorHandleError); + + // Copy the error messages into the outparam, then clear them from + // gTestParseErrorMsgs. + aErrorMsg.Assign(gTestParseErrorMsgs); + gTestParseErrorMsgs.Truncate(); +} + +//=========================================================================== +// nsPrefBranch et al. +//=========================================================================== + +namespace mozilla { +class PreferenceServiceReporter; +} // namespace mozilla + +class PrefCallback : public PLDHashEntryHdr { + friend class mozilla::PreferenceServiceReporter; + + public: + typedef PrefCallback* KeyType; + typedef const PrefCallback* KeyTypePointer; + + static const PrefCallback* KeyToPointer(PrefCallback* aKey) { return aKey; } + + static PLDHashNumber HashKey(const PrefCallback* aKey) { + uint32_t hash = HashString(aKey->mDomain); + return AddToHash(hash, aKey->mCanonical); + } + + public: + // Create a PrefCallback with a strong reference to its observer. + PrefCallback(const nsACString& aDomain, nsIObserver* aObserver, + nsPrefBranch* aBranch) + : mDomain(aDomain), + mBranch(aBranch), + mWeakRef(nullptr), + mStrongRef(aObserver) { + MOZ_COUNT_CTOR(PrefCallback); + nsCOMPtr canonical = do_QueryInterface(aObserver); + mCanonical = canonical; + } + + // Create a PrefCallback with a weak reference to its observer. + PrefCallback(const nsACString& aDomain, nsISupportsWeakReference* aObserver, + nsPrefBranch* aBranch) + : mDomain(aDomain), + mBranch(aBranch), + mWeakRef(do_GetWeakReference(aObserver)), + mStrongRef(nullptr) { + MOZ_COUNT_CTOR(PrefCallback); + nsCOMPtr canonical = do_QueryInterface(aObserver); + mCanonical = canonical; + } + + // This is explicitly not a copy constructor. + explicit PrefCallback(const PrefCallback*& aCopy) + : mDomain(aCopy->mDomain), + mBranch(aCopy->mBranch), + mWeakRef(aCopy->mWeakRef), + mStrongRef(aCopy->mStrongRef), + mCanonical(aCopy->mCanonical) { + MOZ_COUNT_CTOR(PrefCallback); + } + + PrefCallback(const PrefCallback&) = delete; + PrefCallback(PrefCallback&&) = default; + + MOZ_COUNTED_DTOR(PrefCallback) + + bool KeyEquals(const PrefCallback* aKey) const { + // We want to be able to look up a weakly-referencing PrefCallback after + // its observer has died so we can remove it from the table. Once the + // callback's observer dies, its canonical pointer is stale -- in + // particular, we may have allocated a new observer in the same spot in + // memory! So we can't just compare canonical pointers to determine whether + // aKey refers to the same observer as this. + // + // Our workaround is based on the way we use this hashtable: When we ask + // the hashtable to remove a PrefCallback whose weak reference has expired, + // we use as the key for removal the same object as was inserted into the + // hashtable. Thus we can say that if one of the keys' weak references has + // expired, the two keys are equal iff they're the same object. + + if (IsExpired() || aKey->IsExpired()) { + return this == aKey; + } + + if (mCanonical != aKey->mCanonical) { + return false; + } + + return mDomain.Equals(aKey->mDomain); + } + + PrefCallback* GetKey() const { return const_cast(this); } + + // Get a reference to the callback's observer, or null if the observer was + // weakly referenced and has been destroyed. + already_AddRefed GetObserver() const { + if (!IsWeak()) { + nsCOMPtr copy = mStrongRef; + return copy.forget(); + } + + nsCOMPtr observer = do_QueryReferent(mWeakRef); + return observer.forget(); + } + + const nsCString& GetDomain() const { return mDomain; } + + nsPrefBranch* GetPrefBranch() const { return mBranch; } + + // Has this callback's weak reference died? + bool IsExpired() const { + if (!IsWeak()) return false; + + nsCOMPtr observer(do_QueryReferent(mWeakRef)); + return !observer; + } + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { + size_t n = aMallocSizeOf(this); + n += mDomain.SizeOfExcludingThisIfUnshared(aMallocSizeOf); + + // All the other fields are non-owning pointers, so we don't measure them. + + return n; + } + + enum { ALLOW_MEMMOVE = true }; + + private: + nsCString mDomain; + nsPrefBranch* mBranch; + + // Exactly one of mWeakRef and mStrongRef should be non-null. + nsWeakPtr mWeakRef; + nsCOMPtr mStrongRef; + + // We need a canonical nsISupports pointer, per bug 578392. + nsISupports* mCanonical; + + bool IsWeak() const { return !!mWeakRef; } +}; + +class nsPrefBranch final : public nsIPrefBranch, + public nsIObserver, + public nsSupportsWeakReference { + friend class mozilla::PreferenceServiceReporter; + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPREFBRANCH + NS_DECL_NSIOBSERVER + + nsPrefBranch(const char* aPrefRoot, PrefValueKind aKind); + nsPrefBranch() = delete; + + static void NotifyObserver(const char* aNewpref, void* aData); + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const; + + private: + using PrefName = nsCString; + + virtual ~nsPrefBranch(); + + int32_t GetRootLength() const { return mPrefRoot.Length(); } + + nsresult GetDefaultFromPropertiesFile(const char* aPrefName, + nsAString& aReturn); + + // As SetCharPref, but without any check on the length of |aValue|. + nsresult SetCharPrefNoLengthCheck(const char* aPrefName, + const nsACString& aValue); + + // Reject strings that are more than 1Mb, warn if strings are more than 16kb. + nsresult CheckSanityOfStringLength(const char* aPrefName, + const nsAString& aValue); + nsresult CheckSanityOfStringLength(const char* aPrefName, + const nsACString& aValue); + nsresult CheckSanityOfStringLength(const char* aPrefName, + const uint32_t aLength); + + void RemoveExpiredCallback(PrefCallback* aCallback); + + PrefName GetPrefName(const char* aPrefName) const { + return GetPrefName(nsDependentCString(aPrefName)); + } + + PrefName GetPrefName(const nsACString& aPrefName) const; + + void FreeObserverList(void); + + const nsCString mPrefRoot; + PrefValueKind mKind; + + bool mFreeingObserverList; + nsClassHashtable mObservers; +}; + +class nsPrefLocalizedString final : public nsIPrefLocalizedString { + public: + nsPrefLocalizedString(); + + NS_DECL_ISUPPORTS + NS_FORWARD_NSISUPPORTSPRIMITIVE(mUnicodeString->) + NS_FORWARD_NSISUPPORTSSTRING(mUnicodeString->) + + nsresult Init(); + + private: + virtual ~nsPrefLocalizedString(); + + nsCOMPtr mUnicodeString; +}; + +//---------------------------------------------------------------------------- +// nsPrefBranch +//---------------------------------------------------------------------------- + +nsPrefBranch::nsPrefBranch(const char* aPrefRoot, PrefValueKind aKind) + : mPrefRoot(aPrefRoot), + mKind(aKind), + mFreeingObserverList(false), + mObservers() { + nsCOMPtr observerService = services::GetObserverService(); + if (observerService) { + ++mRefCnt; // must be > 0 when we call this, or we'll get deleted! + + // Add weakly so we don't have to clean up at shutdown. + observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, true); + --mRefCnt; + } +} + +nsPrefBranch::~nsPrefBranch() { FreeObserverList(); } + +NS_IMPL_ISUPPORTS(nsPrefBranch, nsIPrefBranch, nsIObserver, + nsISupportsWeakReference) + +NS_IMETHODIMP +nsPrefBranch::GetRoot(nsACString& aRoot) { + aRoot = mPrefRoot; + return NS_OK; +} + +NS_IMETHODIMP +nsPrefBranch::GetPrefType(const char* aPrefName, int32_t* aRetVal) { + NS_ENSURE_ARG(aPrefName); + + const PrefName& prefName = GetPrefName(aPrefName); + *aRetVal = Preferences::GetType(prefName.get()); + return NS_OK; +} + +NS_IMETHODIMP +nsPrefBranch::GetBoolPrefWithDefault(const char* aPrefName, bool aDefaultValue, + uint8_t aArgc, bool* aRetVal) { + nsresult rv = GetBoolPref(aPrefName, aRetVal); + if (NS_FAILED(rv) && aArgc == 1) { + *aRetVal = aDefaultValue; + return NS_OK; + } + + return rv; +} + +NS_IMETHODIMP +nsPrefBranch::GetBoolPref(const char* aPrefName, bool* aRetVal) { + NS_ENSURE_ARG(aPrefName); + + const PrefName& pref = GetPrefName(aPrefName); + return Preferences::GetBool(pref.get(), aRetVal, mKind); +} + +NS_IMETHODIMP +nsPrefBranch::SetBoolPref(const char* aPrefName, bool aValue) { + NS_ENSURE_ARG(aPrefName); + + const PrefName& pref = GetPrefName(aPrefName); + return Preferences::SetBool(pref.get(), aValue, mKind); +} + +NS_IMETHODIMP +nsPrefBranch::GetFloatPrefWithDefault(const char* aPrefName, + float aDefaultValue, uint8_t aArgc, + float* aRetVal) { + nsresult rv = GetFloatPref(aPrefName, aRetVal); + + if (NS_FAILED(rv) && aArgc == 1) { + *aRetVal = aDefaultValue; + return NS_OK; + } + + return rv; +} + +NS_IMETHODIMP +nsPrefBranch::GetFloatPref(const char* aPrefName, float* aRetVal) { + NS_ENSURE_ARG(aPrefName); + + nsAutoCString stringVal; + nsresult rv = GetCharPref(aPrefName, stringVal); + if (NS_SUCCEEDED(rv)) { + // ParsePrefFloat() does a locale-independent conversion. + *aRetVal = ParsePrefFloat(stringVal, &rv); + } + + return rv; +} + +NS_IMETHODIMP +nsPrefBranch::GetCharPrefWithDefault(const char* aPrefName, + const nsACString& aDefaultValue, + uint8_t aArgc, nsACString& aRetVal) { + nsresult rv = GetCharPref(aPrefName, aRetVal); + + if (NS_FAILED(rv) && aArgc == 1) { + aRetVal = aDefaultValue; + return NS_OK; + } + + return rv; +} + +NS_IMETHODIMP +nsPrefBranch::GetCharPref(const char* aPrefName, nsACString& aRetVal) { + NS_ENSURE_ARG(aPrefName); + + const PrefName& pref = GetPrefName(aPrefName); + return Preferences::GetCString(pref.get(), aRetVal, mKind); +} + +NS_IMETHODIMP +nsPrefBranch::SetCharPref(const char* aPrefName, const nsACString& aValue) { + nsresult rv = CheckSanityOfStringLength(aPrefName, aValue); + if (NS_FAILED(rv)) { + return rv; + } + return SetCharPrefNoLengthCheck(aPrefName, aValue); +} + +nsresult nsPrefBranch::SetCharPrefNoLengthCheck(const char* aPrefName, + const nsACString& aValue) { + NS_ENSURE_ARG(aPrefName); + + const PrefName& pref = GetPrefName(aPrefName); + return Preferences::SetCString(pref.get(), aValue, mKind); +} + +NS_IMETHODIMP +nsPrefBranch::GetStringPref(const char* aPrefName, + const nsACString& aDefaultValue, uint8_t aArgc, + nsACString& aRetVal) { + nsCString utf8String; + nsresult rv = GetCharPref(aPrefName, utf8String); + if (NS_SUCCEEDED(rv)) { + aRetVal = utf8String; + return rv; + } + + if (aArgc == 1) { + aRetVal = aDefaultValue; + return NS_OK; + } + + return rv; +} + +NS_IMETHODIMP +nsPrefBranch::SetStringPref(const char* aPrefName, const nsACString& aValue) { + nsresult rv = CheckSanityOfStringLength(aPrefName, aValue); + if (NS_FAILED(rv)) { + return rv; + } + + return SetCharPrefNoLengthCheck(aPrefName, aValue); +} + +NS_IMETHODIMP +nsPrefBranch::GetIntPrefWithDefault(const char* aPrefName, + int32_t aDefaultValue, uint8_t aArgc, + int32_t* aRetVal) { + nsresult rv = GetIntPref(aPrefName, aRetVal); + + if (NS_FAILED(rv) && aArgc == 1) { + *aRetVal = aDefaultValue; + return NS_OK; + } + + return rv; +} + +NS_IMETHODIMP +nsPrefBranch::GetIntPref(const char* aPrefName, int32_t* aRetVal) { + NS_ENSURE_ARG(aPrefName); + const PrefName& pref = GetPrefName(aPrefName); + return Preferences::GetInt(pref.get(), aRetVal, mKind); +} + +NS_IMETHODIMP +nsPrefBranch::SetIntPref(const char* aPrefName, int32_t aValue) { + NS_ENSURE_ARG(aPrefName); + + const PrefName& pref = GetPrefName(aPrefName); + return Preferences::SetInt(pref.get(), aValue, mKind); +} + +NS_IMETHODIMP +nsPrefBranch::GetComplexValue(const char* aPrefName, const nsIID& aType, + void** aRetVal) { + NS_ENSURE_ARG(aPrefName); + + nsresult rv; + nsAutoCString utf8String; + + // We have to do this one first because it's different to all the rest. + if (aType.Equals(NS_GET_IID(nsIPrefLocalizedString))) { + nsCOMPtr theString( + do_CreateInstance(NS_PREFLOCALIZEDSTRING_CONTRACTID, &rv)); + if (NS_FAILED(rv)) { + return rv; + } + + const PrefName& pref = GetPrefName(aPrefName); + bool bNeedDefault = false; + + if (mKind == PrefValueKind::Default) { + bNeedDefault = true; + } else { + // if there is no user (or locked) value + if (!Preferences::HasUserValue(pref.get()) && + !Preferences::IsLocked(pref.get())) { + bNeedDefault = true; + } + } + + // if we need to fetch the default value, do that instead, otherwise use the + // value we pulled in at the top of this function + if (bNeedDefault) { + nsAutoString utf16String; + rv = GetDefaultFromPropertiesFile(pref.get(), utf16String); + if (NS_SUCCEEDED(rv)) { + theString->SetData(utf16String); + } + } else { + rv = GetCharPref(aPrefName, utf8String); + if (NS_SUCCEEDED(rv)) { + theString->SetData(NS_ConvertUTF8toUTF16(utf8String)); + } + } + + if (NS_SUCCEEDED(rv)) { + theString.forget(reinterpret_cast(aRetVal)); + } + + return rv; + } + + // if we can't get the pref, there's no point in being here + rv = GetCharPref(aPrefName, utf8String); + if (NS_FAILED(rv)) { + return rv; + } + + if (aType.Equals(NS_GET_IID(nsIFile))) { + ENSURE_PARENT_PROCESS("GetComplexValue(nsIFile)", aPrefName); + + nsCOMPtr file(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv)); + + if (NS_SUCCEEDED(rv)) { + rv = file->SetPersistentDescriptor(utf8String); + if (NS_SUCCEEDED(rv)) { + file.forget(reinterpret_cast(aRetVal)); + return NS_OK; + } + } + return rv; + } + + if (aType.Equals(NS_GET_IID(nsIRelativeFilePref))) { + ENSURE_PARENT_PROCESS("GetComplexValue(nsIRelativeFilePref)", aPrefName); + + nsACString::const_iterator keyBegin, strEnd; + utf8String.BeginReading(keyBegin); + utf8String.EndReading(strEnd); + + // The pref has the format: [fromKey]a/b/c + if (*keyBegin++ != '[') { + return NS_ERROR_FAILURE; + } + + nsACString::const_iterator keyEnd(keyBegin); + if (!FindCharInReadable(']', keyEnd, strEnd)) { + return NS_ERROR_FAILURE; + } + + nsAutoCString key(Substring(keyBegin, keyEnd)); + + nsCOMPtr fromFile; + nsCOMPtr directoryService( + do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID, &rv)); + if (NS_FAILED(rv)) { + return rv; + } + + rv = directoryService->Get(key.get(), NS_GET_IID(nsIFile), + getter_AddRefs(fromFile)); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr theFile; + rv = NS_NewNativeLocalFile(""_ns, true, getter_AddRefs(theFile)); + if (NS_FAILED(rv)) { + return rv; + } + + rv = theFile->SetRelativeDescriptor(fromFile, Substring(++keyEnd, strEnd)); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr relativePref = new nsRelativeFilePref(); + Unused << relativePref->SetFile(theFile); + Unused << relativePref->SetRelativeToKey(key); + + relativePref.forget(reinterpret_cast(aRetVal)); + return NS_OK; + } + + NS_WARNING("nsPrefBranch::GetComplexValue - Unsupported interface type"); + return NS_NOINTERFACE; +} + +nsresult nsPrefBranch::CheckSanityOfStringLength(const char* aPrefName, + const nsAString& aValue) { + return CheckSanityOfStringLength(aPrefName, aValue.Length()); +} + +nsresult nsPrefBranch::CheckSanityOfStringLength(const char* aPrefName, + const nsACString& aValue) { + return CheckSanityOfStringLength(aPrefName, aValue.Length()); +} + +nsresult nsPrefBranch::CheckSanityOfStringLength(const char* aPrefName, + const uint32_t aLength) { + if (aLength > MAX_PREF_LENGTH) { + return NS_ERROR_ILLEGAL_VALUE; + } + if (aLength <= MAX_ADVISABLE_PREF_LENGTH) { + return NS_OK; + } + + nsresult rv; + nsCOMPtr console = + do_GetService("@mozilla.org/consoleservice;1", &rv); + if (NS_FAILED(rv)) { + return rv; + } + + nsAutoCString message(nsPrintfCString( + "Warning: attempting to write %d bytes to preference %s. This is bad " + "for general performance and memory usage. Such an amount of data " + "should rather be written to an external file.", + aLength, GetPrefName(aPrefName).get())); + + rv = console->LogStringMessage(NS_ConvertUTF8toUTF16(message).get()); + if (NS_FAILED(rv)) { + return rv; + } + return NS_OK; +} + +NS_IMETHODIMP +nsPrefBranch::SetComplexValue(const char* aPrefName, const nsIID& aType, + nsISupports* aValue) { + ENSURE_PARENT_PROCESS("SetComplexValue", aPrefName); + NS_ENSURE_ARG(aPrefName); + + nsresult rv = NS_NOINTERFACE; + + if (aType.Equals(NS_GET_IID(nsIFile))) { + nsCOMPtr file = do_QueryInterface(aValue); + if (!file) { + return NS_NOINTERFACE; + } + + nsAutoCString descriptorString; + rv = file->GetPersistentDescriptor(descriptorString); + if (NS_SUCCEEDED(rv)) { + rv = SetCharPrefNoLengthCheck(aPrefName, descriptorString); + } + return rv; + } + + if (aType.Equals(NS_GET_IID(nsIRelativeFilePref))) { + nsCOMPtr relFilePref = do_QueryInterface(aValue); + if (!relFilePref) { + return NS_NOINTERFACE; + } + + nsCOMPtr file; + relFilePref->GetFile(getter_AddRefs(file)); + if (!file) { + return NS_NOINTERFACE; + } + + nsAutoCString relativeToKey; + (void)relFilePref->GetRelativeToKey(relativeToKey); + + nsCOMPtr relativeToFile; + nsCOMPtr directoryService( + do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID, &rv)); + if (NS_FAILED(rv)) { + return rv; + } + + rv = directoryService->Get(relativeToKey.get(), NS_GET_IID(nsIFile), + getter_AddRefs(relativeToFile)); + if (NS_FAILED(rv)) { + return rv; + } + + nsAutoCString relDescriptor; + rv = file->GetRelativeDescriptor(relativeToFile, relDescriptor); + if (NS_FAILED(rv)) { + return rv; + } + + nsAutoCString descriptorString; + descriptorString.Append('['); + descriptorString.Append(relativeToKey); + descriptorString.Append(']'); + descriptorString.Append(relDescriptor); + return SetCharPrefNoLengthCheck(aPrefName, descriptorString); + } + + if (aType.Equals(NS_GET_IID(nsIPrefLocalizedString))) { + nsCOMPtr theString = do_QueryInterface(aValue); + + if (theString) { + nsString wideString; + + rv = theString->GetData(wideString); + if (NS_SUCCEEDED(rv)) { + // Check sanity of string length before any lengthy conversion + rv = CheckSanityOfStringLength(aPrefName, wideString); + if (NS_FAILED(rv)) { + return rv; + } + rv = SetCharPrefNoLengthCheck(aPrefName, + NS_ConvertUTF16toUTF8(wideString)); + } + } + return rv; + } + + NS_WARNING("nsPrefBranch::SetComplexValue - Unsupported interface type"); + return NS_NOINTERFACE; +} + +NS_IMETHODIMP +nsPrefBranch::ClearUserPref(const char* aPrefName) { + NS_ENSURE_ARG(aPrefName); + + const PrefName& pref = GetPrefName(aPrefName); + return Preferences::ClearUser(pref.get()); +} + +NS_IMETHODIMP +nsPrefBranch::PrefHasUserValue(const char* aPrefName, bool* aRetVal) { + NS_ENSURE_ARG_POINTER(aRetVal); + NS_ENSURE_ARG(aPrefName); + + const PrefName& pref = GetPrefName(aPrefName); + *aRetVal = Preferences::HasUserValue(pref.get()); + return NS_OK; +} + +NS_IMETHODIMP +nsPrefBranch::PrefHasDefaultValue(const char* aPrefName, bool* aRetVal) { + NS_ENSURE_ARG_POINTER(aRetVal); + NS_ENSURE_ARG(aPrefName); + + const PrefName& pref = GetPrefName(aPrefName); + *aRetVal = Preferences::HasDefaultValue(pref.get()); + return NS_OK; +} + +NS_IMETHODIMP +nsPrefBranch::LockPref(const char* aPrefName) { + NS_ENSURE_ARG(aPrefName); + + const PrefName& pref = GetPrefName(aPrefName); + return Preferences::Lock(pref.get()); +} + +NS_IMETHODIMP +nsPrefBranch::PrefIsLocked(const char* aPrefName, bool* aRetVal) { + NS_ENSURE_ARG_POINTER(aRetVal); + NS_ENSURE_ARG(aPrefName); + + const PrefName& pref = GetPrefName(aPrefName); + *aRetVal = Preferences::IsLocked(pref.get()); + return NS_OK; +} + +NS_IMETHODIMP +nsPrefBranch::PrefIsSanitized(const char* aPrefName, bool* aRetVal) { + NS_ENSURE_ARG_POINTER(aRetVal); + NS_ENSURE_ARG(aPrefName); + + const PrefName& pref = GetPrefName(aPrefName); + *aRetVal = Preferences::IsSanitized(pref.get()); + return NS_OK; +} + +NS_IMETHODIMP +nsPrefBranch::UnlockPref(const char* aPrefName) { + NS_ENSURE_ARG(aPrefName); + + const PrefName& pref = GetPrefName(aPrefName); + return Preferences::Unlock(pref.get()); +} + +NS_IMETHODIMP +nsPrefBranch::ResetBranch(const char* aStartingAt) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsPrefBranch::DeleteBranch(const char* aStartingAt) { + ENSURE_PARENT_PROCESS("DeleteBranch", aStartingAt); + NS_ENSURE_ARG(aStartingAt); + + MOZ_ASSERT(NS_IsMainThread()); + + if (!HashTable()) { + return NS_ERROR_NOT_INITIALIZED; + } + + const PrefName& pref = GetPrefName(aStartingAt); + nsAutoCString branchName(pref.get()); + + // Add a trailing '.' if it doesn't already have one. + if (branchName.Length() > 1 && !StringEndsWith(branchName, "."_ns)) { + branchName += '.'; + } + + const nsACString& branchNameNoDot = + Substring(branchName, 0, branchName.Length() - 1); + + for (auto iter = HashTable()->modIter(); !iter.done(); iter.next()) { + // The first disjunct matches branches: e.g. a branch name "foo.bar." + // matches a name "foo.bar.baz" (but it won't match "foo.barrel.baz"). + // The second disjunct matches leaf nodes: e.g. a branch name "foo.bar." + // matches a name "foo.bar" (by ignoring the trailing '.'). + nsDependentCString name(iter.get()->Name()); + if (StringBeginsWith(name, branchName) || name.Equals(branchNameNoDot)) { + iter.remove(); + // The saved callback pref may be invalid now. + gCallbackPref = nullptr; + } + } + + Preferences::HandleDirty(); + return NS_OK; +} + +NS_IMETHODIMP +nsPrefBranch::GetChildList(const char* aStartingAt, + nsTArray& aChildArray) { + NS_ENSURE_ARG(aStartingAt); + + MOZ_ASSERT(NS_IsMainThread()); + + // This will contain a list of all the pref name strings. Allocated on the + // stack for speed. + AutoTArray prefArray; + + const PrefName& parent = GetPrefName(aStartingAt); + size_t parentLen = parent.Length(); + for (auto& pref : PrefsIter(HashTable(), gSharedMap)) { + if (strncmp(pref->Name(), parent.get(), parentLen) == 0) { + prefArray.AppendElement(pref->NameString()); + } + } + + // Now that we've built up the list, run the callback on all the matching + // elements. + aChildArray.SetCapacity(prefArray.Length()); + for (auto& element : prefArray) { + // we need to lop off mPrefRoot in case the user is planning to pass this + // back to us because if they do we are going to add mPrefRoot again. + aChildArray.AppendElement(Substring(element, mPrefRoot.Length())); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsPrefBranch::AddObserverImpl(const nsACString& aDomain, nsIObserver* aObserver, + bool aHoldWeak) { + UniquePtr pCallback; + + NS_ENSURE_ARG(aObserver); + + const nsCString& prefName = GetPrefName(aDomain); + + // Hold a weak reference to the observer if so requested. + if (aHoldWeak) { + nsCOMPtr weakRefFactory = + do_QueryInterface(aObserver); + if (!weakRefFactory) { + // The caller didn't give us a object that supports weak reference... + // tell them. + return NS_ERROR_INVALID_ARG; + } + + // Construct a PrefCallback with a weak reference to the observer. + pCallback = MakeUnique(prefName, weakRefFactory, this); + + } else { + // Construct a PrefCallback with a strong reference to the observer. + pCallback = MakeUnique(prefName, aObserver, this); + } + + mObservers.WithEntryHandle(pCallback.get(), [&](auto&& p) { + if (p) { + NS_WARNING("Ignoring duplicate observer."); + } else { + // We must pass a fully qualified preference name to the callback + // aDomain == nullptr is the only possible failure, and we trapped it with + // NS_ENSURE_ARG above. + Preferences::RegisterCallback(NotifyObserver, prefName, pCallback.get(), + Preferences::PrefixMatch, + /* isPriority */ false); + + p.Insert(std::move(pCallback)); + } + }); + + return NS_OK; +} + +NS_IMETHODIMP +nsPrefBranch::RemoveObserverImpl(const nsACString& aDomain, + nsIObserver* aObserver) { + NS_ENSURE_ARG(aObserver); + + nsresult rv = NS_OK; + + // If we're in the middle of a call to FreeObserverList, don't process this + // RemoveObserver call -- the observer in question will be removed soon, if + // it hasn't been already. + // + // It's important that we don't touch mObservers in any way -- even a Get() + // which returns null might cause the hashtable to resize itself, which will + // break the iteration in FreeObserverList. + if (mFreeingObserverList) { + return NS_OK; + } + + // Remove the relevant PrefCallback from mObservers and get an owning pointer + // to it. Unregister the callback first, and then let the owning pointer go + // out of scope and destroy the callback. + const nsCString& prefName = GetPrefName(aDomain); + PrefCallback key(prefName, aObserver, this); + mozilla::UniquePtr pCallback; + mObservers.Remove(&key, &pCallback); + if (pCallback) { + rv = Preferences::UnregisterCallback( + NotifyObserver, prefName, pCallback.get(), Preferences::PrefixMatch); + } + + return rv; +} + +NS_IMETHODIMP +nsPrefBranch::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + // Watch for xpcom shutdown and free our observers to eliminate any cyclic + // references. + if (!nsCRT::strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { + FreeObserverList(); + } + return NS_OK; +} + +/* static */ +void nsPrefBranch::NotifyObserver(const char* aNewPref, void* aData) { + PrefCallback* pCallback = (PrefCallback*)aData; + + nsCOMPtr observer = pCallback->GetObserver(); + if (!observer) { + // The observer has expired. Let's remove this callback. + pCallback->GetPrefBranch()->RemoveExpiredCallback(pCallback); + return; + } + + // Remove any root this string may contain so as to not confuse the observer + // by passing them something other than what they passed us as a topic. + uint32_t len = pCallback->GetPrefBranch()->GetRootLength(); + nsDependentCString suffix(aNewPref + len); + + observer->Observe(static_cast(pCallback->GetPrefBranch()), + NS_PREFBRANCH_PREFCHANGE_TOPIC_ID, + NS_ConvertASCIItoUTF16(suffix).get()); +} + +size_t nsPrefBranch::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { + size_t n = aMallocSizeOf(this); + + n += mPrefRoot.SizeOfExcludingThisIfUnshared(aMallocSizeOf); + + n += mObservers.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (const auto& entry : mObservers) { + const PrefCallback* data = entry.GetWeak(); + n += data->SizeOfIncludingThis(aMallocSizeOf); + } + + return n; +} + +void nsPrefBranch::FreeObserverList() { + // We need to prevent anyone from modifying mObservers while we're iterating + // over it. In particular, some clients will call RemoveObserver() when + // they're removed and destructed via the iterator; we set + // mFreeingObserverList to keep those calls from touching mObservers. + mFreeingObserverList = true; + for (auto iter = mObservers.Iter(); !iter.Done(); iter.Next()) { + auto callback = iter.UserData(); + Preferences::UnregisterCallback(nsPrefBranch::NotifyObserver, + callback->GetDomain(), callback, + Preferences::PrefixMatch); + iter.Remove(); + } + + nsCOMPtr observerService = services::GetObserverService(); + if (observerService) { + observerService->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + } + + mFreeingObserverList = false; +} + +void nsPrefBranch::RemoveExpiredCallback(PrefCallback* aCallback) { + MOZ_ASSERT(aCallback->IsExpired()); + mObservers.Remove(aCallback); +} + +nsresult nsPrefBranch::GetDefaultFromPropertiesFile(const char* aPrefName, + nsAString& aReturn) { + // The default value contains a URL to a .properties file. + + nsAutoCString propertyFileURL; + nsresult rv = Preferences::GetCString(aPrefName, propertyFileURL, + PrefValueKind::Default); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr bundleService = + components::StringBundle::Service(); + if (!bundleService) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr bundle; + rv = bundleService->CreateBundle(propertyFileURL.get(), + getter_AddRefs(bundle)); + if (NS_FAILED(rv)) { + return rv; + } + + return bundle->GetStringFromName(aPrefName, aReturn); +} + +nsPrefBranch::PrefName nsPrefBranch::GetPrefName( + const nsACString& aPrefName) const { + if (mPrefRoot.IsEmpty()) { + return PrefName(PromiseFlatCString(aPrefName)); + } + + return PrefName(mPrefRoot + aPrefName); +} + +//---------------------------------------------------------------------------- +// nsPrefLocalizedString +//---------------------------------------------------------------------------- + +nsPrefLocalizedString::nsPrefLocalizedString() = default; + +nsPrefLocalizedString::~nsPrefLocalizedString() = default; + +NS_IMPL_ISUPPORTS(nsPrefLocalizedString, nsIPrefLocalizedString, + nsISupportsString) + +nsresult nsPrefLocalizedString::Init() { + nsresult rv; + mUnicodeString = do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID, &rv); + + return rv; +} + +//---------------------------------------------------------------------------- +// nsRelativeFilePref +//---------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(nsRelativeFilePref, nsIRelativeFilePref) + +nsRelativeFilePref::nsRelativeFilePref() = default; + +nsRelativeFilePref::~nsRelativeFilePref() = default; + +NS_IMETHODIMP +nsRelativeFilePref::GetFile(nsIFile** aFile) { + NS_ENSURE_ARG_POINTER(aFile); + *aFile = mFile; + NS_IF_ADDREF(*aFile); + return NS_OK; +} + +NS_IMETHODIMP +nsRelativeFilePref::SetFile(nsIFile* aFile) { + mFile = aFile; + return NS_OK; +} + +NS_IMETHODIMP +nsRelativeFilePref::GetRelativeToKey(nsACString& aRelativeToKey) { + aRelativeToKey.Assign(mRelativeToKey); + return NS_OK; +} + +NS_IMETHODIMP +nsRelativeFilePref::SetRelativeToKey(const nsACString& aRelativeToKey) { + mRelativeToKey.Assign(aRelativeToKey); + return NS_OK; +} + +//=========================================================================== +// class Preferences and related things +//=========================================================================== + +namespace mozilla { + +#define INITIAL_PREF_FILES 10 + +static NS_DEFINE_CID(kZipReaderCID, NS_ZIPREADER_CID); + +void Preferences::HandleDirty() { + MOZ_ASSERT(XRE_IsParentProcess()); + + if (!HashTable() || !sPreferences) { + return; + } + + if (sPreferences->mProfileShutdown) { + NS_WARNING("Setting user pref after profile shutdown."); + return; + } + + if (!sPreferences->mDirty) { + sPreferences->mDirty = true; + + if (sPreferences->mCurrentFile && sPreferences->AllowOffMainThreadSave() && + !sPreferences->mSavePending) { + sPreferences->mSavePending = true; + static const int PREF_DELAY_MS = 500; + NS_DelayedDispatchToCurrentThread( + NewRunnableMethod("Preferences::SavePrefFileAsynchronous", + sPreferences.get(), + &Preferences::SavePrefFileAsynchronous), + PREF_DELAY_MS); + } + } +} + +static nsresult openPrefFile(nsIFile* aFile, PrefValueKind aKind); + +static nsresult parsePrefData(const nsCString& aData, PrefValueKind aKind); + +// clang-format off +static const char kPrefFileHeader[] = + "// Mozilla User Preferences" + NS_LINEBREAK + NS_LINEBREAK + "// DO NOT EDIT THIS FILE." + NS_LINEBREAK + "//" + NS_LINEBREAK + "// If you make changes to this file while the application is running," + NS_LINEBREAK + "// the changes will be overwritten when the application exits." + NS_LINEBREAK + "//" + NS_LINEBREAK + "// To change a preference value, you can either:" + NS_LINEBREAK + "// - modify it via the UI (e.g. via about:config in the browser); or" + NS_LINEBREAK + "// - set it within a user.js file in your profile." + NS_LINEBREAK + NS_LINEBREAK; +// clang-format on + +// Note: if sShutdown is true, sPreferences will be nullptr. +StaticRefPtr Preferences::sPreferences; +bool Preferences::sShutdown = false; + +// This globally enables or disables OMT pref writing, both sync and async. +static int32_t sAllowOMTPrefWrite = -1; + +// Write the preference data to a file. +class PreferencesWriter final { + public: + PreferencesWriter() = default; + + static nsresult Write(nsIFile* aFile, PrefSaveData& aPrefs) { + nsCOMPtr outStreamSink; + nsCOMPtr outStream; + uint32_t writeAmount; + nsresult rv; + + // Execute a "safe" save by saving through a tempfile. + rv = NS_NewSafeLocalFileOutputStream(getter_AddRefs(outStreamSink), aFile, + -1, 0600); + if (NS_FAILED(rv)) { + return rv; + } + + rv = NS_NewBufferedOutputStream(getter_AddRefs(outStream), + outStreamSink.forget(), 4096); + if (NS_FAILED(rv)) { + return rv; + } + + struct CharComparator { + bool LessThan(const nsCString& aA, const nsCString& aB) const { + return aA < aB; + } + + bool Equals(const nsCString& aA, const nsCString& aB) const { + return aA == aB; + } + }; + + // Sort the preferences to make a readable file on disk. + aPrefs.Sort(CharComparator()); + + // Write out the file header. + outStream->Write(kPrefFileHeader, sizeof(kPrefFileHeader) - 1, + &writeAmount); + + for (nsCString& pref : aPrefs) { + outStream->Write(pref.get(), pref.Length(), &writeAmount); + outStream->Write(NS_LINEBREAK, NS_LINEBREAK_LEN, &writeAmount); + } + + // Tell the safe output stream to overwrite the real prefs file. + // (It'll abort if there were any errors during writing.) + nsCOMPtr safeStream = do_QueryInterface(outStream); + MOZ_ASSERT(safeStream, "expected a safe output stream!"); + if (safeStream) { + rv = safeStream->Finish(); + } + +#ifdef DEBUG + if (NS_FAILED(rv)) { + NS_WARNING("failed to save prefs file! possible data loss"); + } +#endif + + return rv; + } + + static void Flush() { + MOZ_DIAGNOSTIC_ASSERT(sPendingWriteCount >= 0); + // SpinEventLoopUntil is unfortunate, but ultimately it's the best thing + // we can do here given the constraint that we need to ensure that + // the preferences on disk match what we have in memory. We could + // easily perform the write here ourselves by doing exactly what + // happens in PWRunnable::Run. This would be the right thing to do + // if we're stuck here because other unrelated runnables are taking + // a long time, and the wrong thing to do if PreferencesWriter::Write + // is what takes a long time, as we would be trading a SpinEventLoopUntil + // for a synchronous disk write, wherein we could not even spin the + // event loop. Given that PWRunnable generally runs on a thread pool, + // if we're stuck here, it's likely because of PreferencesWriter::Write + // and not some other runnable. Thus, spin away. + mozilla::SpinEventLoopUntil("PreferencesWriter::Flush"_ns, + []() { return sPendingWriteCount <= 0; }); + } + + // This is the data that all of the runnables (see below) will attempt + // to write. It will always have the most up to date version, or be + // null, if the up to date information has already been written out. + static Atomic sPendingWriteData; + + // This is the number of writes via PWRunnables which have been dispatched + // but not yet completed. This is intended to be used by Flush to ensure + // that there are no outstanding writes left incomplete, and thus our prefs + // on disk are in sync with what we have in memory. + static Atomic sPendingWriteCount; + + // See PWRunnable::Run for details on why we need this lock. + static StaticMutex sWritingToFile MOZ_UNANNOTATED; +}; + +Atomic PreferencesWriter::sPendingWriteData(nullptr); +Atomic PreferencesWriter::sPendingWriteCount(0); +StaticMutex PreferencesWriter::sWritingToFile; + +class PWRunnable : public Runnable { + public: + explicit PWRunnable(nsIFile* aFile) : Runnable("PWRunnable"), mFile(aFile) {} + + NS_IMETHOD Run() override { + // Preference writes are handled a bit strangely, in that a "newer" + // write is generally regarded as always better. For this reason, + // sPendingWriteData can be overwritten multiple times before anyone + // gets around to actually using it, minimizing writes. However, + // once we've acquired sPendingWriteData we've reached a + // "point of no return" and have to complete the write. + // + // Unfortunately, this design allows the following behaviour: + // + // 1. write1 is queued up + // 2. thread1 acquires write1 + // 3. write2 is queued up + // 4. thread2 acquires write2 + // 5. thread1 and thread2 concurrently clobber each other + // + // To avoid this, we use this lock to ensure that only one thread + // at a time is trying to acquire the write, and when it does, + // all other threads are prevented from acquiring writes until it + // completes the write. New writes are still allowed to be queued + // up in this time. + // + // Although it's atomic, the acquire needs to be guarded by the mutex + // to avoid reordering of writes -- we don't want an older write to + // run after a newer one. To avoid this causing too much waiting, we check + // if sPendingWriteData is already null before acquiring the mutex. If it + // is, then there's definitely no work to be done (or someone is in the + // middle of doing it for us). + // + // Note that every time a new write is queued up, a new write task is + // is also queued up, so there will always be a task that can see the newest + // write. + // + // Ideally this lock wouldn't be necessary, and the PreferencesWriter + // would be used more carefully, but it's hard to untangle all that. + nsresult rv = NS_OK; + if (PreferencesWriter::sPendingWriteData) { + StaticMutexAutoLock lock(PreferencesWriter::sWritingToFile); + // If we get a nullptr on the exchange, it means that somebody + // else has already processed the request, and we can just return. + UniquePtr prefs( + PreferencesWriter::sPendingWriteData.exchange(nullptr)); + if (prefs) { + rv = PreferencesWriter::Write(mFile, *prefs); + // Make a copy of these so we can have them in runnable lambda. + // nsIFile is only there so that we would never release the + // ref counted pointer off main thread. + nsresult rvCopy = rv; + nsCOMPtr fileCopy(mFile); + SchedulerGroup::Dispatch( + TaskCategory::Other, + NS_NewRunnableFunction("Preferences::WriterRunnable", + [fileCopy, rvCopy] { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + if (NS_FAILED(rvCopy)) { + Preferences::HandleDirty(); + } + })); + } + } + // We've completed the write to the best of our abilities, whether + // we had prefs to write or another runnable got to them first. If + // PreferencesWriter::Write failed, this is still correct as the + // write is no longer outstanding, and the above HandleDirty call + // will just start the cycle again. + PreferencesWriter::sPendingWriteCount--; + return rv; + } + + protected: + nsCOMPtr mFile; +}; + +// Although this is a member of Preferences, it measures sPreferences and +// several other global structures. +/* static */ +void Preferences::AddSizeOfIncludingThis(MallocSizeOf aMallocSizeOf, + PrefsSizes& aSizes) { + if (!sPreferences) { + return; + } + + aSizes.mMisc += aMallocSizeOf(sPreferences.get()); + + aSizes.mRootBranches += + static_cast(sPreferences->mRootBranch.get()) + ->SizeOfIncludingThis(aMallocSizeOf) + + static_cast(sPreferences->mDefaultRootBranch.get()) + ->SizeOfIncludingThis(aMallocSizeOf); +} + +class PreferenceServiceReporter final : public nsIMemoryReporter { + ~PreferenceServiceReporter() {} + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIMEMORYREPORTER + + protected: + static const uint32_t kSuspectReferentCount = 1000; +}; + +NS_IMPL_ISUPPORTS(PreferenceServiceReporter, nsIMemoryReporter) + +MOZ_DEFINE_MALLOC_SIZE_OF(PreferenceServiceMallocSizeOf) + +NS_IMETHODIMP +PreferenceServiceReporter::CollectReports( + nsIHandleReportCallback* aHandleReport, nsISupports* aData, + bool aAnonymize) { + MOZ_ASSERT(NS_IsMainThread()); + + MallocSizeOf mallocSizeOf = PreferenceServiceMallocSizeOf; + PrefsSizes sizes; + + Preferences::AddSizeOfIncludingThis(mallocSizeOf, sizes); + + if (HashTable()) { + sizes.mHashTable += HashTable()->shallowSizeOfIncludingThis(mallocSizeOf); + for (auto iter = HashTable()->iter(); !iter.done(); iter.next()) { + iter.get()->AddSizeOfIncludingThis(mallocSizeOf, sizes); + } + } + + sizes.mPrefNameArena += PrefNameArena().SizeOfExcludingThis(mallocSizeOf); + + for (CallbackNode* node = gFirstCallback; node; node = node->Next()) { + node->AddSizeOfIncludingThis(mallocSizeOf, sizes); + } + + if (gSharedMap) { + sizes.mMisc += mallocSizeOf(gSharedMap); + } + +#ifdef ACCESS_COUNTS + if (gAccessCounts) { + sizes.mMisc += gAccessCounts->ShallowSizeOfIncludingThis(mallocSizeOf); + } +#endif + + MOZ_COLLECT_REPORT("explicit/preferences/hash-table", KIND_HEAP, UNITS_BYTES, + sizes.mHashTable, "Memory used by libpref's hash table."); + + MOZ_COLLECT_REPORT("explicit/preferences/pref-values", KIND_HEAP, UNITS_BYTES, + sizes.mPrefValues, + "Memory used by PrefValues hanging off the hash table."); + + MOZ_COLLECT_REPORT("explicit/preferences/string-values", KIND_HEAP, + UNITS_BYTES, sizes.mStringValues, + "Memory used by libpref's string pref values."); + + MOZ_COLLECT_REPORT("explicit/preferences/root-branches", KIND_HEAP, + UNITS_BYTES, sizes.mRootBranches, + "Memory used by libpref's root branches."); + + MOZ_COLLECT_REPORT("explicit/preferences/pref-name-arena", KIND_HEAP, + UNITS_BYTES, sizes.mPrefNameArena, + "Memory used by libpref's arena for pref names."); + + MOZ_COLLECT_REPORT("explicit/preferences/callbacks/objects", KIND_HEAP, + UNITS_BYTES, sizes.mCallbacksObjects, + "Memory used by pref callback objects."); + + MOZ_COLLECT_REPORT("explicit/preferences/callbacks/domains", KIND_HEAP, + UNITS_BYTES, sizes.mCallbacksDomains, + "Memory used by pref callback domains (pref names and " + "prefixes)."); + + MOZ_COLLECT_REPORT("explicit/preferences/misc", KIND_HEAP, UNITS_BYTES, + sizes.mMisc, "Miscellaneous memory used by libpref."); + + if (gSharedMap) { + if (XRE_IsParentProcess()) { + MOZ_COLLECT_REPORT("explicit/preferences/shared-memory-map", KIND_NONHEAP, + UNITS_BYTES, gSharedMap->MapSize(), + "The shared memory mapping used to share a " + "snapshot of preference values across processes."); + } + } + + nsPrefBranch* rootBranch = + static_cast(Preferences::GetRootBranch()); + if (!rootBranch) { + return NS_OK; + } + + size_t numStrong = 0; + size_t numWeakAlive = 0; + size_t numWeakDead = 0; + nsTArray suspectPreferences; + // Count of the number of referents for each preference. + nsTHashMap prefCounter; + + for (const auto& entry : rootBranch->mObservers) { + auto* callback = entry.GetWeak(); + + if (callback->IsWeak()) { + nsCOMPtr callbackRef = do_QueryReferent(callback->mWeakRef); + if (callbackRef) { + numWeakAlive++; + } else { + numWeakDead++; + } + } else { + numStrong++; + } + + const uint32_t currentCount = prefCounter.Get(callback->GetDomain()) + 1; + prefCounter.InsertOrUpdate(callback->GetDomain(), currentCount); + + // Keep track of preferences that have a suspiciously large number of + // referents (a symptom of a leak). + if (currentCount == kSuspectReferentCount) { + suspectPreferences.AppendElement(callback->GetDomain()); + } + } + + for (uint32_t i = 0; i < suspectPreferences.Length(); i++) { + nsCString& suspect = suspectPreferences[i]; + const uint32_t totalReferentCount = prefCounter.Get(suspect); + + nsPrintfCString suspectPath( + "preference-service-suspect/" + "referent(pref=%s)", + suspect.get()); + + aHandleReport->Callback( + /* process = */ ""_ns, suspectPath, KIND_OTHER, UNITS_COUNT, + totalReferentCount, + "A preference with a suspiciously large number " + "referents (symptom of a leak)."_ns, + aData); + } + + MOZ_COLLECT_REPORT( + "preference-service/referent/strong", KIND_OTHER, UNITS_COUNT, numStrong, + "The number of strong referents held by the preference service."); + + MOZ_COLLECT_REPORT( + "preference-service/referent/weak/alive", KIND_OTHER, UNITS_COUNT, + numWeakAlive, + "The number of weak referents held by the preference service that are " + "still alive."); + + MOZ_COLLECT_REPORT( + "preference-service/referent/weak/dead", KIND_OTHER, UNITS_COUNT, + numWeakDead, + "The number of weak referents held by the preference service that are " + "dead."); + + return NS_OK; +} + +namespace { + +class AddPreferencesMemoryReporterRunnable : public Runnable { + public: + AddPreferencesMemoryReporterRunnable() + : Runnable("AddPreferencesMemoryReporterRunnable") {} + + NS_IMETHOD Run() override { + return RegisterStrongMemoryReporter(new PreferenceServiceReporter()); + } +}; + +} // namespace + +// A list of changed prefs sent from the parent via shared memory. +static nsTArray* gChangedDomPrefs; + +static const char kTelemetryPref[] = "toolkit.telemetry.enabled"; +static const char kChannelPref[] = "app.update.channel"; + +#ifdef MOZ_WIDGET_ANDROID + +static Maybe TelemetryPrefValue() { + // Leave it unchanged if it's already set. + // XXX: how could it already be set? + if (Preferences::GetType(kTelemetryPref) != nsIPrefBranch::PREF_INVALID) { + return Nothing(); + } + + // Determine the correct default for toolkit.telemetry.enabled. If this + // build has MOZ_TELEMETRY_ON_BY_DEFAULT *or* we're on the beta channel, + // telemetry is on by default, otherwise not. This is necessary so that + // beta users who are testing final release builds don't flipflop defaults. +# ifdef MOZ_TELEMETRY_ON_BY_DEFAULT + return Some(true); +# else + nsAutoCString channelPrefValue; + Unused << Preferences::GetCString(kChannelPref, channelPrefValue, + PrefValueKind::Default); + return Some(channelPrefValue.EqualsLiteral("beta")); +# endif +} + +/* static */ +void Preferences::SetupTelemetryPref() { + MOZ_ASSERT(XRE_IsParentProcess()); + + Maybe telemetryPrefValue = TelemetryPrefValue(); + if (telemetryPrefValue.isSome()) { + Preferences::SetBool(kTelemetryPref, *telemetryPrefValue, + PrefValueKind::Default); + } +} + +#else // !MOZ_WIDGET_ANDROID + +static bool TelemetryPrefValue() { + // For platforms with Unified Telemetry (here meaning not-Android), + // toolkit.telemetry.enabled determines whether we send "extended" data. + // We only want extended data from pre-release channels due to size. + + constexpr auto channel = MOZ_STRINGIFY(MOZ_UPDATE_CHANNEL) ""_ns; + + // Easy cases: Nightly, Aurora, Beta. + if (channel.EqualsLiteral("nightly") || channel.EqualsLiteral("aurora") || + channel.EqualsLiteral("beta")) { + return true; + } + +# ifndef MOZILLA_OFFICIAL + // Local developer builds: non-official builds on the "default" channel. + if (channel.EqualsLiteral("default")) { + return true; + } +# endif + + // Release Candidate builds: builds that think they are release builds, but + // are shipped to beta users. + if (channel.EqualsLiteral("release")) { + nsAutoCString channelPrefValue; + Unused << Preferences::GetCString(kChannelPref, channelPrefValue, + PrefValueKind::Default); + if (channelPrefValue.EqualsLiteral("beta")) { + return true; + } + } + + return false; +} + +/* static */ +void Preferences::SetupTelemetryPref() { + MOZ_ASSERT(XRE_IsParentProcess()); + + Preferences::SetBool(kTelemetryPref, TelemetryPrefValue(), + PrefValueKind::Default); + Preferences::Lock(kTelemetryPref); +} + +static void CheckTelemetryPref() { + MOZ_ASSERT(!XRE_IsParentProcess()); + + // Make sure the children got passed the right telemetry pref details. + DebugOnly value; + MOZ_ASSERT(NS_SUCCEEDED(Preferences::GetBool(kTelemetryPref, &value)) && + value == TelemetryPrefValue()); + MOZ_ASSERT(Preferences::IsLocked(kTelemetryPref)); +} + +#endif // MOZ_WIDGET_ANDROID + +/* static */ +already_AddRefed Preferences::GetInstanceForService() { + if (sPreferences) { + return do_AddRef(sPreferences); + } + + if (sShutdown) { + return nullptr; + } + + sPreferences = new Preferences(); + + MOZ_ASSERT(!HashTable()); + HashTable() = new PrefsHashTable(XRE_IsParentProcess() + ? kHashTableInitialLengthParent + : kHashTableInitialLengthContent); + +#ifdef DEBUG + gOnceStaticPrefsAntiFootgun = new AntiFootgunMap(); +#endif + +#ifdef ACCESS_COUNTS + MOZ_ASSERT(!gAccessCounts); + gAccessCounts = new AccessCountsHashTable(); +#endif + + nsresult rv = InitInitialObjects(/* isStartup */ true); + if (NS_FAILED(rv)) { + sPreferences = nullptr; + return nullptr; + } + + if (!XRE_IsParentProcess()) { + MOZ_ASSERT(gChangedDomPrefs); + for (unsigned int i = 0; i < gChangedDomPrefs->Length(); i++) { + Preferences::SetPreference(gChangedDomPrefs->ElementAt(i)); + } + delete gChangedDomPrefs; + gChangedDomPrefs = nullptr; + +#ifndef MOZ_WIDGET_ANDROID + CheckTelemetryPref(); +#endif + + } else { + // Check if there is a deployment configuration file. If so, set up the + // pref config machinery, which will actually read the file. + nsAutoCString lockFileName; + nsresult rv = Preferences::GetCString("general.config.filename", + lockFileName, PrefValueKind::User); + if (NS_SUCCEEDED(rv)) { + NS_CreateServicesFromCategory( + "pref-config-startup", + static_cast(static_cast(sPreferences)), + "pref-config-startup"); + } + + nsCOMPtr observerService = + services::GetObserverService(); + if (!observerService) { + sPreferences = nullptr; + return nullptr; + } + + observerService->AddObserver(sPreferences, + "profile-before-change-telemetry", true); + rv = observerService->AddObserver(sPreferences, "profile-before-change", + true); + + observerService->AddObserver(sPreferences, "suspend_process_notification", + true); + + if (NS_FAILED(rv)) { + sPreferences = nullptr; + return nullptr; + } + } + + const char* defaultPrefs = getenv("MOZ_DEFAULT_PREFS"); + if (defaultPrefs) { + parsePrefData(nsCString(defaultPrefs), PrefValueKind::Default); + } + + // Preferences::GetInstanceForService() can be called from GetService(), and + // RegisterStrongMemoryReporter calls GetService(nsIMemoryReporter). To + // avoid a potential recursive GetService() call, we can't register the + // memory reporter here; instead, do it off a runnable. + RefPtr runnable = + new AddPreferencesMemoryReporterRunnable(); + NS_DispatchToMainThread(runnable); + + return do_AddRef(sPreferences); +} + +/* static */ +bool Preferences::IsServiceAvailable() { return !!sPreferences; } + +/* static */ +bool Preferences::InitStaticMembers() { + MOZ_ASSERT(NS_IsMainThread() || ServoStyleSet::IsInServoTraversal()); + + if (MOZ_LIKELY(sPreferences)) { + return true; + } + + if (!sShutdown) { + MOZ_ASSERT(NS_IsMainThread()); + nsCOMPtr prefService = + do_GetService(NS_PREFSERVICE_CONTRACTID); + } + + return sPreferences != nullptr; +} + +/* static */ +void Preferences::Shutdown() { + if (!sShutdown) { + sShutdown = true; // Don't create the singleton instance after here. + sPreferences = nullptr; + StaticPrefs::ShutdownAlwaysPrefs(); + } +} + +Preferences::Preferences() + : mRootBranch(new nsPrefBranch("", PrefValueKind::User)), + mDefaultRootBranch(new nsPrefBranch("", PrefValueKind::Default)) {} + +Preferences::~Preferences() { + MOZ_ASSERT(!sPreferences); + + MOZ_ASSERT(!gCallbacksInProgress); + + CallbackNode* node = gFirstCallback; + while (node) { + CallbackNode* next_node = node->Next(); + delete node; + node = next_node; + } + gLastPriorityNode = gFirstCallback = nullptr; + + delete HashTable(); + HashTable() = nullptr; + +#ifdef DEBUG + delete gOnceStaticPrefsAntiFootgun; + gOnceStaticPrefsAntiFootgun = nullptr; +#endif + +#ifdef ACCESS_COUNTS + delete gAccessCounts; +#endif + + gSharedMap = nullptr; + + PrefNameArena().Clear(); +} + +NS_IMPL_ISUPPORTS(Preferences, nsIPrefService, nsIObserver, nsIPrefBranch, + nsISupportsWeakReference) + +/* static */ +void Preferences::SerializePreferences(nsCString& aStr, + bool aIsDestinationWebContentProcess) { + MOZ_RELEASE_ASSERT(InitStaticMembers()); + + aStr.Truncate(); + + for (auto iter = HashTable()->iter(); !iter.done(); iter.next()) { + Pref* pref = iter.get().get(); + if (!pref->IsTypeNone() && pref->HasAdvisablySizedValues()) { + pref->SerializeAndAppend(aStr, aIsDestinationWebContentProcess && + ShouldSanitizePreference(pref)); + } + } + + aStr.Append('\0'); +} + +/* static */ +void Preferences::DeserializePreferences(char* aStr, size_t aPrefsLen) { + MOZ_ASSERT(!XRE_IsParentProcess()); + + MOZ_ASSERT(!gChangedDomPrefs); + gChangedDomPrefs = new nsTArray(); + + char* p = aStr; + while (*p != '\0') { + dom::Pref pref; + p = Pref::Deserialize(p, &pref); + gChangedDomPrefs->AppendElement(pref); + } + + // We finished parsing on a '\0'. That should be the last char in the shared + // memory. (aPrefsLen includes the '\0'.) + MOZ_ASSERT(p == aStr + aPrefsLen - 1); + + MOZ_ASSERT(!gContentProcessPrefsAreInited); + gContentProcessPrefsAreInited = true; +} + +/* static */ +FileDescriptor Preferences::EnsureSnapshot(size_t* aSize) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + if (!gSharedMap) { + SharedPrefMapBuilder builder; + + nsTArray toRepopulate; + NameArena* newPrefNameArena = new NameArena(); + for (auto iter = HashTable()->modIter(); !iter.done(); iter.next()) { + if (!ShouldSanitizePreference(iter.get().get())) { + iter.get()->AddToMap(builder); + } else { + Pref* pref = iter.getMutable().release(); + pref->RelocateName(newPrefNameArena); + toRepopulate.AppendElement(pref); + } + } + + // Store the current value of `once`-mirrored prefs. After this point they + // will be immutable. + StaticPrefs::RegisterOncePrefs(builder); + + gSharedMap = new SharedPrefMap(std::move(builder)); + + // Once we've built a snapshot of the database, there's no need to continue + // storing dynamic copies of the preferences it contains. Once we reset the + // hashtable, preference lookups will fall back to the snapshot for any + // preferences not in the dynamic hashtable. + // + // And since the majority of the database is now contained in the snapshot, + // we can initialize the hashtable with the expected number of per-session + // changed preferences, rather than the expected total number of + // preferences. + HashTable()->clearAndCompact(); + Unused << HashTable()->reserve(kHashTableInitialLengthContent); + + delete sPrefNameArena; + sPrefNameArena = newPrefNameArena; + gCallbackPref = nullptr; + + for (uint32_t i = 0; i < toRepopulate.Length(); i++) { + auto pref = toRepopulate[i]; + auto p = HashTable()->lookupForAdd(pref->Name()); + MOZ_ASSERT(!p.found()); + Unused << HashTable()->add(p, pref); + } + } + + *aSize = gSharedMap->MapSize(); + return gSharedMap->CloneFileDescriptor(); +} + +/* static */ +void Preferences::InitSnapshot(const FileDescriptor& aHandle, size_t aSize) { + MOZ_ASSERT(!XRE_IsParentProcess()); + MOZ_ASSERT(!gSharedMap); + + gSharedMap = new SharedPrefMap(aHandle, aSize); + + StaticPrefs::InitStaticPrefsFromShared(); +} + +/* static */ +void Preferences::InitializeUserPrefs() { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(!sPreferences->mCurrentFile, "Should only initialize prefs once"); + + // Prefs which are set before we initialize the profile are silently + // discarded. This is stupid, but there are various tests which depend on + // this behavior. + sPreferences->ResetUserPrefs(); + + nsCOMPtr prefsFile = sPreferences->ReadSavedPrefs(); + sPreferences->ReadUserOverridePrefs(); + + sPreferences->mDirty = false; + + // Don't set mCurrentFile until we're done so that dirty flags work properly. + sPreferences->mCurrentFile = std::move(prefsFile); +} + +/* static */ +void Preferences::FinishInitializingUserPrefs() { + sPreferences->NotifyServiceObservers(NS_PREFSERVICE_READ_TOPIC_ID); +} + +NS_IMETHODIMP +Preferences::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* someData) { + if (MOZ_UNLIKELY(!XRE_IsParentProcess())) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsresult rv = NS_OK; + + if (!nsCRT::strcmp(aTopic, "profile-before-change")) { + // Normally prefs aren't written after this point, and so we kick off + // an asynchronous pref save so that I/O can be done in parallel with + // other shutdown. + if (AllowOffMainThreadSave()) { + SavePrefFile(nullptr); + } + + } else if (!nsCRT::strcmp(aTopic, "profile-before-change-telemetry")) { + // It's possible that a profile-before-change observer after ours + // set a pref. A blocking save here re-saves if necessary and also waits + // for any pending saves to complete. + SavePrefFileBlocking(); + MOZ_ASSERT(!mDirty, "Preferences should not be dirty"); + mProfileShutdown = true; + + } else if (!nsCRT::strcmp(aTopic, "reload-default-prefs")) { + // Reload the default prefs from file. + Unused << InitInitialObjects(/* isStartup */ false); + + } else if (!nsCRT::strcmp(aTopic, "suspend_process_notification")) { + // Our process is being suspended. The OS may wake our process later, + // or it may kill the process. In case our process is going to be killed + // from the suspended state, we save preferences before suspending. + rv = SavePrefFileBlocking(); + } + + return rv; +} + +NS_IMETHODIMP +Preferences::ReadDefaultPrefsFromFile(nsIFile* aFile) { + ENSURE_PARENT_PROCESS("Preferences::ReadDefaultPrefsFromFile", "all prefs"); + + if (!aFile) { + NS_ERROR("ReadDefaultPrefsFromFile requires a parameter"); + return NS_ERROR_INVALID_ARG; + } + + return openPrefFile(aFile, PrefValueKind::Default); +} + +NS_IMETHODIMP +Preferences::ReadUserPrefsFromFile(nsIFile* aFile) { + ENSURE_PARENT_PROCESS("Preferences::ReadUserPrefsFromFile", "all prefs"); + + if (!aFile) { + NS_ERROR("ReadUserPrefsFromFile requires a parameter"); + return NS_ERROR_INVALID_ARG; + } + + return openPrefFile(aFile, PrefValueKind::User); +} + +NS_IMETHODIMP +Preferences::ResetPrefs() { + ENSURE_PARENT_PROCESS("Preferences::ResetPrefs", "all prefs"); + + if (gSharedMap) { + return NS_ERROR_NOT_AVAILABLE; + } + + HashTable()->clearAndCompact(); + Unused << HashTable()->reserve(kHashTableInitialLengthParent); + + PrefNameArena().Clear(); + + return InitInitialObjects(/* isStartup */ false); +} + +nsresult Preferences::ResetUserPrefs() { + ENSURE_PARENT_PROCESS("Preferences::ResetUserPrefs", "all prefs"); + NS_ENSURE_TRUE(InitStaticMembers(), NS_ERROR_NOT_AVAILABLE); + MOZ_ASSERT(NS_IsMainThread()); + + Vector prefNames; + for (auto iter = HashTable()->modIter(); !iter.done(); iter.next()) { + Pref* pref = iter.get().get(); + + if (pref->HasUserValue()) { + if (!prefNames.append(pref->Name())) { + return NS_ERROR_OUT_OF_MEMORY; + } + + pref->ClearUserValue(); + if (!pref->HasDefaultValue()) { + iter.remove(); + } + } + } + + for (const char* prefName : prefNames) { + NotifyCallbacks(nsDependentCString(prefName)); + } + + Preferences::HandleDirty(); + return NS_OK; +} + +bool Preferences::AllowOffMainThreadSave() { + // Put in a preference that allows us to disable off main thread preference + // file save. + if (sAllowOMTPrefWrite < 0) { + bool value = false; + Preferences::GetBool("preferences.allow.omt-write", &value); + sAllowOMTPrefWrite = value ? 1 : 0; + } + + return !!sAllowOMTPrefWrite; +} + +nsresult Preferences::SavePrefFileBlocking() { + if (mDirty) { + return SavePrefFileInternal(nullptr, SaveMethod::Blocking); + } + + // If we weren't dirty to start, SavePrefFileInternal will early exit so + // there is no guarantee that we don't have oustanding async saves in the + // pipe. Since the contract of SavePrefFileOnMainThread is that the file on + // disk matches the preferences, we have to make sure those requests are + // completed. + + if (AllowOffMainThreadSave()) { + PreferencesWriter::Flush(); + } + + return NS_OK; +} + +nsresult Preferences::SavePrefFileAsynchronous() { + return SavePrefFileInternal(nullptr, SaveMethod::Asynchronous); +} + +NS_IMETHODIMP +Preferences::SavePrefFile(nsIFile* aFile) { + // This is the method accessible from service API. Make it off main thread. + return SavePrefFileInternal(aFile, SaveMethod::Asynchronous); +} + +/* static */ +void Preferences::SetPreference(const dom::Pref& aDomPref) { + MOZ_ASSERT(!XRE_IsParentProcess()); + NS_ENSURE_TRUE(InitStaticMembers(), (void)0); + + const nsCString& prefName = aDomPref.name(); + + Pref* pref; + auto p = HashTable()->lookupForAdd(prefName.get()); + if (!p) { + pref = new Pref(prefName); + if (!HashTable()->add(p, pref)) { + delete pref; + return; + } + } else { + pref = p->get(); + } + + bool valueChanged = false; + pref->FromDomPref(aDomPref, &valueChanged); + + // When the parent process clears a pref's user value we get a DomPref here + // with no default value and no user value. There are two possibilities. + // + // - There was an existing pref with only a user value. FromDomPref() will + // have just cleared that user value, so the pref can be removed. + // + // - There was no existing pref. FromDomPref() will have done nothing, and + // `pref` will be valueless. We will end up adding and removing the value + // needlessly, but that's ok because this case is rare. + // + if (!pref->HasDefaultValue() && !pref->HasUserValue() && + !pref->IsSanitized()) { + // If the preference exists in the shared map, we need to keep the dynamic + // entry around to mask it. + if (gSharedMap->Has(pref->Name())) { + pref->SetType(PrefType::None); + } else { + HashTable()->remove(prefName.get()); + } + pref = nullptr; + } + + // Note: we don't have to worry about HandleDirty() because we are setting + // prefs in the content process that have come from the parent process. + + if (valueChanged) { + if (pref) { + NotifyCallbacks(prefName, PrefWrapper(pref)); + } else { + NotifyCallbacks(prefName); + } + } +} + +/* static */ +void Preferences::GetPreference(dom::Pref* aDomPref, + const GeckoProcessType aDestinationProcessType, + const nsACString& aDestinationRemoteType) { + MOZ_ASSERT(XRE_IsParentProcess()); + bool destIsWebContent = + aDestinationProcessType == GeckoProcessType_Content && + (StringBeginsWith(aDestinationRemoteType, WEB_REMOTE_TYPE) || + StringBeginsWith(aDestinationRemoteType, PREALLOC_REMOTE_TYPE) || + StringBeginsWith(aDestinationRemoteType, PRIVILEGEDMOZILLA_REMOTE_TYPE)); + + Pref* pref = pref_HashTableLookup(aDomPref->name().get()); + if (pref && pref->HasAdvisablySizedValues()) { + pref->ToDomPref(aDomPref, destIsWebContent); + } +} + +#ifdef DEBUG +bool Preferences::ArePrefsInitedInContentProcess() { + MOZ_ASSERT(!XRE_IsParentProcess()); + return gContentProcessPrefsAreInited; +} +#endif + +NS_IMETHODIMP +Preferences::GetBranch(const char* aPrefRoot, nsIPrefBranch** aRetVal) { + if ((nullptr != aPrefRoot) && (*aPrefRoot != '\0')) { + // TODO: Cache this stuff and allow consumers to share branches (hold weak + // references, I think). + RefPtr prefBranch = + new nsPrefBranch(aPrefRoot, PrefValueKind::User); + prefBranch.forget(aRetVal); + } else { + // Special case: caching the default root. + nsCOMPtr root(sPreferences->mRootBranch); + root.forget(aRetVal); + } + + return NS_OK; +} + +NS_IMETHODIMP +Preferences::GetDefaultBranch(const char* aPrefRoot, nsIPrefBranch** aRetVal) { + if (!aPrefRoot || !aPrefRoot[0]) { + nsCOMPtr root(sPreferences->mDefaultRootBranch); + root.forget(aRetVal); + return NS_OK; + } + + // TODO: Cache this stuff and allow consumers to share branches (hold weak + // references, I think). + RefPtr prefBranch = + new nsPrefBranch(aPrefRoot, PrefValueKind::Default); + if (!prefBranch) { + return NS_ERROR_OUT_OF_MEMORY; + } + + prefBranch.forget(aRetVal); + return NS_OK; +} + +NS_IMETHODIMP +Preferences::ReadStats(nsIPrefStatsCallback* aCallback) { +#ifdef ACCESS_COUNTS + for (const auto& entry : *gAccessCounts) { + aCallback->Visit(entry.GetKey(), entry.GetData()); + } + + return NS_OK; +#else + return NS_ERROR_NOT_IMPLEMENTED; +#endif +} + +NS_IMETHODIMP +Preferences::ResetStats() { +#ifdef ACCESS_COUNTS + gAccessCounts->Clear(); + return NS_OK; +#else + return NS_ERROR_NOT_IMPLEMENTED; +#endif +} + +// We would much prefer to use C++ lambdas, but we cannot convert +// lambdas that capture (here, the underlying observer) to C pointer +// to functions. So, here we are, with icky C callbacks. Be aware +// that nothing is thread-safe here because there's a single global +// `nsIPrefObserver` instance. Use this from the main thread only. +nsIPrefObserver* PrefObserver = nullptr; + +void HandlePref(const char* aPrefName, PrefType aType, PrefValueKind aKind, + PrefValue aValue, bool aIsSticky, bool aIsLocked) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!PrefObserver) { + return; + } + + const char* kind = aKind == PrefValueKind::Default ? "Default" : "User"; + + switch (aType) { + case PrefType::String: + PrefObserver->OnStringPref(kind, aPrefName, aValue.mStringVal, aIsSticky, + aIsLocked); + break; + case PrefType::Int: + PrefObserver->OnIntPref(kind, aPrefName, aValue.mIntVal, aIsSticky, + aIsLocked); + break; + case PrefType::Bool: + PrefObserver->OnBoolPref(kind, aPrefName, aValue.mBoolVal, aIsSticky, + aIsLocked); + break; + default: + PrefObserver->OnError("Unexpected pref type."); + } +} + +void HandleError(const char* aMsg) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!PrefObserver) { + return; + } + + PrefObserver->OnError(aMsg); +} + +NS_IMETHODIMP +Preferences::ParsePrefsFromBuffer(const nsTArray& aBytes, + nsIPrefObserver* aObserver, + const char* aPathLabel) { + MOZ_ASSERT(NS_IsMainThread()); + + // We need a null-terminated buffer. + nsTArray data = aBytes.Clone(); + data.AppendElement(0); + + // Parsing as default handles both `pref` and `user_pref`. + PrefObserver = aObserver; + prefs_parser_parse(aPathLabel ? aPathLabel : "", + PrefValueKind::Default, (const char*)data.Elements(), + data.Length() - 1, HandlePref, HandleError); + PrefObserver = nullptr; + + return NS_OK; +} + +NS_IMETHODIMP +Preferences::GetDirty(bool* aRetVal) { + *aRetVal = mDirty; + return NS_OK; +} + +nsresult Preferences::NotifyServiceObservers(const char* aTopic) { + nsCOMPtr observerService = services::GetObserverService(); + if (!observerService) { + return NS_ERROR_FAILURE; + } + + auto subject = static_cast(this); + observerService->NotifyObservers(subject, aTopic, nullptr); + + return NS_OK; +} + +already_AddRefed Preferences::ReadSavedPrefs() { + nsCOMPtr file; + nsresult rv = + NS_GetSpecialDirectory(NS_APP_PREFS_50_FILE, getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + rv = openPrefFile(file, PrefValueKind::User); + if (rv == NS_ERROR_FILE_NOT_FOUND) { + // This is a normal case for new users. + rv = NS_OK; + } else if (NS_FAILED(rv)) { + // Save a backup copy of the current (invalid) prefs file, since all prefs + // from the error line to the end of the file will be lost (bug 361102). + // TODO we should notify the user about it (bug 523725). + Telemetry::ScalarSet( + Telemetry::ScalarID::PREFERENCES_PREFS_FILE_WAS_INVALID, true); + MakeBackupPrefFile(file); + } + + return file.forget(); +} + +void Preferences::ReadUserOverridePrefs() { + nsCOMPtr aFile; + nsresult rv = + NS_GetSpecialDirectory(NS_APP_PREFS_50_DIR, getter_AddRefs(aFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + aFile->AppendNative("user.js"_ns); + rv = openPrefFile(aFile, PrefValueKind::User); +} + +nsresult Preferences::MakeBackupPrefFile(nsIFile* aFile) { + // Example: this copies "prefs.js" to "Invalidprefs.js" in the same directory. + // "Invalidprefs.js" is removed if it exists, prior to making the copy. + nsAutoString newFilename; + nsresult rv = aFile->GetLeafName(newFilename); + NS_ENSURE_SUCCESS(rv, rv); + + newFilename.InsertLiteral(u"Invalid", 0); + nsCOMPtr newFile; + rv = aFile->GetParent(getter_AddRefs(newFile)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = newFile->Append(newFilename); + NS_ENSURE_SUCCESS(rv, rv); + + bool exists = false; + newFile->Exists(&exists); + if (exists) { + rv = newFile->Remove(false); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = aFile->CopyTo(nullptr, newFilename); + NS_ENSURE_SUCCESS(rv, rv); + + return rv; +} + +nsresult Preferences::SavePrefFileInternal(nsIFile* aFile, + SaveMethod aSaveMethod) { + ENSURE_PARENT_PROCESS("Preferences::SavePrefFileInternal", "all prefs"); + + // We allow different behavior here when aFile argument is not null, but it + // happens to be the same as the current file. It is not clear that we + // should, but it does give us a "force" save on the unmodified pref file + // (see the original bug 160377 when we added this.) + + if (nullptr == aFile) { + mSavePending = false; + + // Off main thread writing only if allowed. + if (!AllowOffMainThreadSave()) { + aSaveMethod = SaveMethod::Blocking; + } + + // The mDirty flag tells us if we should write to mCurrentFile. We only + // check this flag when the caller wants to write to the default. + if (!mDirty) { + return NS_OK; + } + + // Check for profile shutdown after mDirty because the runnables from + // HandleDirty() can still be pending. + if (mProfileShutdown) { + NS_WARNING("Cannot save pref file after profile shutdown."); + return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + } + + // It's possible that we never got a prefs file. + nsresult rv = NS_OK; + if (mCurrentFile) { + rv = WritePrefFile(mCurrentFile, aSaveMethod); + } + + // If we succeeded writing to mCurrentFile, reset the dirty flag. + if (NS_SUCCEEDED(rv)) { + mDirty = false; + } + return rv; + + } else { + // We only allow off main thread writes on mCurrentFile. + return WritePrefFile(aFile, SaveMethod::Blocking); + } +} + +nsresult Preferences::WritePrefFile(nsIFile* aFile, SaveMethod aSaveMethod) { + MOZ_ASSERT(XRE_IsParentProcess()); + + if (!HashTable()) { + return NS_ERROR_NOT_INITIALIZED; + } + + AUTO_PROFILER_LABEL("Preferences::WritePrefFile", OTHER); + + if (AllowOffMainThreadSave()) { + nsresult rv = NS_OK; + UniquePtr prefs = MakeUnique(pref_savePrefs()); + + // Put the newly constructed preference data into sPendingWriteData + // for the next request to pick up + prefs.reset(PreferencesWriter::sPendingWriteData.exchange(prefs.release())); + if (prefs) { + // There was a previous request that hasn't been processed, + // and this is the data it had. + return rv; + } + + // There were no previous requests. Dispatch one since sPendingWriteData has + // the up to date information. + nsCOMPtr target = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &rv); + if (NS_SUCCEEDED(rv)) { + bool async = aSaveMethod == SaveMethod::Asynchronous; + + // Increment sPendingWriteCount, even though it's redundant to track this + // in the case of a sync runnable; it just makes it easier to simply + // decrement this inside PWRunnable. We cannot use the constructor / + // destructor for increment/decrement, as on dispatch failure we might + // leak the runnable in order to not destroy it on the wrong thread, which + // would make us get stuck in an infinite SpinEventLoopUntil inside + // PreferencesWriter::Flush. Better that in future code we miss an + // increment of sPendingWriteCount and cause a simple crash due to it + // ending up negative. + PreferencesWriter::sPendingWriteCount++; + if (async) { + rv = target->Dispatch(new PWRunnable(aFile), + nsIEventTarget::DISPATCH_NORMAL); + } else { + rv = + SyncRunnable::DispatchToThread(target, new PWRunnable(aFile), true); + } + if (NS_FAILED(rv)) { + // If our dispatch failed, we should correct our bookkeeping to + // avoid shutdown hangs. + PreferencesWriter::sPendingWriteCount--; + } + return rv; + } + + // If we can't get the thread for writing, for whatever reason, do the main + // thread write after making some noise. + MOZ_ASSERT(false, "failed to get the target thread for OMT pref write"); + } + + // This will do a main thread write. It is safe to do it this way because + // AllowOffMainThreadSave() returns a consistent value for the lifetime of + // the parent process. + PrefSaveData prefsData = pref_savePrefs(); + return PreferencesWriter::Write(aFile, prefsData); +} + +static nsresult openPrefFile(nsIFile* aFile, PrefValueKind aKind) { + MOZ_ASSERT(XRE_IsParentProcess()); + + nsCString data; + MOZ_TRY_VAR(data, URLPreloader::ReadFile(aFile)); + + nsAutoString filenameUtf16; + aFile->GetLeafName(filenameUtf16); + NS_ConvertUTF16toUTF8 filename(filenameUtf16); + + nsAutoString path; + aFile->GetPath(path); + + Parser parser; + if (!parser.Parse(aKind, NS_ConvertUTF16toUTF8(path).get(), data)) { + return NS_ERROR_FILE_CORRUPTED; + } + + return NS_OK; +} + +static nsresult parsePrefData(const nsCString& aData, PrefValueKind aKind) { + const nsCString path = "$MOZ_DEFAULT_PREFS"_ns; + + Parser parser; + if (!parser.Parse(aKind, path.get(), aData)) { + return NS_ERROR_FILE_CORRUPTED; + } + + return NS_OK; +} + +static int pref_CompareFileNames(nsIFile* aFile1, nsIFile* aFile2, + void* /* unused */) { + nsAutoCString filename1, filename2; + aFile1->GetNativeLeafName(filename1); + aFile2->GetNativeLeafName(filename2); + + return Compare(filename2, filename1); +} + +// Load default pref files from a directory. The files in the directory are +// sorted reverse-alphabetically; a set of "special file names" may be +// specified which are loaded after all the others. +static nsresult pref_LoadPrefsInDir(nsIFile* aDir, + char const* const* aSpecialFiles, + uint32_t aSpecialFilesCount) { + MOZ_ASSERT(XRE_IsParentProcess()); + + nsresult rv, rv2; + + nsCOMPtr dirIterator; + + // This may fail in some normal cases, such as embedders who do not use a + // GRE. + rv = aDir->GetDirectoryEntries(getter_AddRefs(dirIterator)); + if (NS_FAILED(rv)) { + // If the directory doesn't exist, then we have no reason to complain. We + // loaded everything (and nothing) successfully. + if (rv == NS_ERROR_FILE_NOT_FOUND) { + rv = NS_OK; + } + return rv; + } + + nsCOMArray prefFiles(INITIAL_PREF_FILES); + nsCOMArray specialFiles(aSpecialFilesCount); + nsCOMPtr prefFile; + + while (NS_SUCCEEDED(dirIterator->GetNextFile(getter_AddRefs(prefFile))) && + prefFile) { + nsAutoCString leafName; + prefFile->GetNativeLeafName(leafName); + MOZ_ASSERT( + !leafName.IsEmpty(), + "Failure in default prefs: directory enumerator returned empty file?"); + + // Skip non-js files. + if (StringEndsWith(leafName, ".js"_ns, + nsCaseInsensitiveCStringComparator)) { + bool shouldParse = true; + + // Separate out special files. + for (uint32_t i = 0; i < aSpecialFilesCount; ++i) { + if (leafName.Equals(nsDependentCString(aSpecialFiles[i]))) { + shouldParse = false; + // Special files should be processed in order. We put them into the + // array by index, which can make the array sparse. + specialFiles.ReplaceObjectAt(prefFile, i); + } + } + + if (shouldParse) { + prefFiles.AppendObject(prefFile); + } + } + } + + if (prefFiles.Count() + specialFiles.Count() == 0) { + NS_WARNING("No default pref files found."); + if (NS_SUCCEEDED(rv)) { + rv = NS_SUCCESS_FILE_DIRECTORY_EMPTY; + } + return rv; + } + + prefFiles.Sort(pref_CompareFileNames, nullptr); + + uint32_t arrayCount = prefFiles.Count(); + uint32_t i; + for (i = 0; i < arrayCount; ++i) { + rv2 = openPrefFile(prefFiles[i], PrefValueKind::Default); + if (NS_FAILED(rv2)) { + NS_ERROR("Default pref file not parsed successfully."); + rv = rv2; + } + } + + arrayCount = specialFiles.Count(); + for (i = 0; i < arrayCount; ++i) { + // This may be a sparse array; test before parsing. + nsIFile* file = specialFiles[i]; + if (file) { + rv2 = openPrefFile(file, PrefValueKind::Default); + if (NS_FAILED(rv2)) { + NS_ERROR("Special default pref file not parsed successfully."); + rv = rv2; + } + } + } + + return rv; +} + +static nsresult pref_ReadPrefFromJar(nsZipArchive* aJarReader, + const char* aName) { + nsCString manifest; + MOZ_TRY_VAR(manifest, + URLPreloader::ReadZip(aJarReader, nsDependentCString(aName))); + + Parser parser; + if (!parser.Parse(PrefValueKind::Default, aName, manifest)) { + return NS_ERROR_FILE_CORRUPTED; + } + + return NS_OK; +} + +static nsresult pref_ReadDefaultPrefs(const RefPtr jarReader, + const char* path) { + UniquePtr find; + nsTArray prefEntries; + const char* entryName; + uint16_t entryNameLen; + + nsresult rv = jarReader->FindInit(path, getter_Transfers(find)); + NS_ENSURE_SUCCESS(rv, rv); + + while (NS_SUCCEEDED(find->FindNext(&entryName, &entryNameLen))) { + prefEntries.AppendElement(Substring(entryName, entryNameLen)); + } + + prefEntries.Sort(); + for (uint32_t i = prefEntries.Length(); i--;) { + rv = pref_ReadPrefFromJar(jarReader, prefEntries[i].get()); + if (NS_FAILED(rv)) { + NS_WARNING("Error parsing preferences."); + } + } + + return NS_OK; +} + +static nsCString PrefValueToString(const bool* b) { + return nsCString(*b ? "true" : "false"); +} +static nsCString PrefValueToString(const int* i) { + return nsPrintfCString("%d", *i); +} +static nsCString PrefValueToString(const uint32_t* u) { + return nsPrintfCString("%d", *u); +} +static nsCString PrefValueToString(const float* f) { + return nsPrintfCString("%f", *f); +} +static nsCString PrefValueToString(const nsACString* s) { + return nsCString(*s); +} +static nsCString PrefValueToString(const nsACString& s) { return nsCString(s); } + +// These preference getter wrappers allow us to look up the value for static +// preferences based on their native types, rather than manually mapping them to +// the appropriate Preferences::Get* functions. +// We define these methods in a struct which is made friend of Preferences in +// order to access private members. +struct Internals { + struct PreferenceReadMarker { + static constexpr Span MarkerTypeName() { + return MakeStringSpan("PreferenceRead"); + } + static void StreamJSONMarkerData( + baseprofiler::SpliceableJSONWriter& aWriter, + const ProfilerString8View& aPrefName, + const Maybe& aPrefKind, PrefType aPrefType, + const ProfilerString8View& aPrefValue) { + aWriter.StringProperty("prefName", aPrefName); + aWriter.StringProperty("prefKind", PrefValueKindToString(aPrefKind)); + aWriter.StringProperty("prefType", PrefTypeToString(aPrefType)); + aWriter.StringProperty("prefValue", aPrefValue); + } + static MarkerSchema MarkerTypeDisplay() { + using MS = MarkerSchema; + MS schema{MS::Location::MarkerChart, MS::Location::MarkerTable}; + schema.AddKeyLabelFormatSearchable("prefName", "Name", MS::Format::String, + MS::Searchable::Searchable); + schema.AddKeyLabelFormat("prefKind", "Kind", MS::Format::String); + schema.AddKeyLabelFormat("prefType", "Type", MS::Format::String); + schema.AddKeyLabelFormat("prefValue", "Value", MS::Format::String); + schema.SetTableLabel( + "{marker.data.prefName}: {marker.data.prefValue} " + "({marker.data.prefType})"); + return schema; + } + + private: + static Span PrefValueKindToString( + const Maybe& aKind) { + if (aKind) { + return *aKind == PrefValueKind::Default ? MakeStringSpan("Default") + : MakeStringSpan("User"); + } + return "Shared"; + } + + static Span PrefTypeToString(PrefType type) { + switch (type) { + case PrefType::None: + return "None"; + case PrefType::Int: + return "Int"; + case PrefType::Bool: + return "Bool"; + case PrefType::String: + return "String"; + default: + MOZ_ASSERT_UNREACHABLE("Unknown preference type."); + return "Unknown"; + } + } + }; + + template + static nsresult GetPrefValue(const char* aPrefName, T&& aResult, + PrefValueKind aKind) { + nsresult rv = NS_ERROR_UNEXPECTED; + NS_ENSURE_TRUE(Preferences::InitStaticMembers(), NS_ERROR_NOT_AVAILABLE); + + if (Maybe pref = pref_Lookup(aPrefName)) { + rv = pref->GetValue(aKind, std::forward(aResult)); + + if (profiler_feature_active(ProfilerFeature::PreferenceReads)) { + profiler_add_marker( + "PreferenceRead", baseprofiler::category::OTHER_PreferenceRead, {}, + PreferenceReadMarker{}, + ProfilerString8View::WrapNullTerminatedString(aPrefName), + Some(aKind), pref->Type(), PrefValueToString(aResult)); + } + } + + return rv; + } + + template + static nsresult GetSharedPrefValue(const char* aName, T* aResult) { + nsresult rv = NS_ERROR_UNEXPECTED; + + if (Maybe pref = pref_SharedLookup(aName)) { + rv = pref->GetValue(PrefValueKind::User, aResult); + + if (profiler_feature_active(ProfilerFeature::PreferenceReads)) { + profiler_add_marker( + "PreferenceRead", baseprofiler::category::OTHER_PreferenceRead, {}, + PreferenceReadMarker{}, + ProfilerString8View::WrapNullTerminatedString(aName), + Nothing() /* indicates Shared */, pref->Type(), + PrefValueToString(aResult)); + } + } + + return rv; + } + + template + static T GetPref(const char* aPrefName, T aFallback, + PrefValueKind aKind = PrefValueKind::User) { + T result = aFallback; + GetPrefValue(aPrefName, &result, aKind); + return result; + } + + template + static void MOZ_NEVER_INLINE AssignMirror(T& aMirror, V aValue) { + aMirror = aValue; + } + + static void MOZ_NEVER_INLINE AssignMirror(DataMutexString& aMirror, + nsCString&& aValue) { + auto lock = aMirror.Lock(); + lock->Assign(std::move(aValue)); + } + + static void MOZ_NEVER_INLINE AssignMirror(DataMutexString& aMirror, + const nsLiteralCString& aValue) { + auto lock = aMirror.Lock(); + lock->Assign(aValue); + } + + static void ClearMirror(DataMutexString& aMirror) { + auto lock = aMirror.Lock(); + lock->Assign(nsCString()); + } + + template + static void UpdateMirror(const char* aPref, void* aMirror) { + StripAtomic value; + + nsresult rv = GetPrefValue(aPref, &value, PrefValueKind::User); + if (NS_SUCCEEDED(rv)) { + AssignMirror(*static_cast(aMirror), + std::forward>(value)); + } else { + // GetPrefValue() can fail if the update is caused by the pref being + // deleted or if it fails to make a cast. This assertion is the only place + // where we safeguard these. In this case the mirror variable will be + // untouched, thus keeping the value it had prior to the change. + // (Note that this case won't happen for a deletion via DeleteBranch() + // unless bug 343600 is fixed, but it will happen for a deletion via + // ClearUserPref().) + NS_WARNING(nsPrintfCString("Pref changed failure: %s\n", aPref).get()); + MOZ_ASSERT(false); + } + } + + template + static nsresult RegisterCallback(void* aMirror, const nsACString& aPref) { + return Preferences::RegisterCallback(UpdateMirror, aPref, aMirror, + Preferences::ExactMatch, + /* isPriority */ true); + } +}; + +// Initialize default preference JavaScript buffers from appropriate TEXT +// resources. +/* static */ +nsresult Preferences::InitInitialObjects(bool aIsStartup) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!XRE_IsParentProcess()) { + MOZ_DIAGNOSTIC_ASSERT(gSharedMap); + if (aIsStartup) { + StaticPrefs::StartObservingAlwaysPrefs(); + } + return NS_OK; + } + + // Initialize static prefs before prefs from data files so that the latter + // will override the former. + StaticPrefs::InitAll(); + + // In the omni.jar case, we load the following prefs: + // - jar:$gre/omni.jar!/greprefs.js + // - jar:$gre/omni.jar!/defaults/pref/*.js + // + // In the non-omni.jar case, we load: + // - $gre/greprefs.js + // + // In both cases, we also load: + // - $gre/defaults/pref/*.js + // + // This is kept for bug 591866 (channel-prefs.js should not be in omni.jar) + // in the `$app == $gre` case; we load all files instead of channel-prefs.js + // only to have the same behaviour as `$app != $gre`, where this is required + // as a supported location for GRE preferences. + // + // When `$app != $gre`, we additionally load, in the omni.jar case: + // - jar:$app/omni.jar!/defaults/preferences/*.js + // - $app/defaults/preferences/*.js + // + // and in the non-omni.jar case: + // - $app/defaults/preferences/*.js + // + // When `$app == $gre`, we additionally load, in the omni.jar case: + // - jar:$gre/omni.jar!/defaults/preferences/*.js + // + // Thus, in the omni.jar case, we always load app-specific default + // preferences from omni.jar, whether or not `$app == $gre`. + + nsresult rv = NS_ERROR_FAILURE; + UniquePtr find; + nsTArray prefEntries; + const char* entryName; + uint16_t entryNameLen; + + RefPtr jarReader = Omnijar::GetReader(Omnijar::GRE); + if (jarReader) { +#ifdef MOZ_WIDGET_ANDROID + // Try to load an architecture-specific greprefs.js first. This will be + // present in FAT AAR builds of GeckoView on Android. + const char* abi = getenv("MOZ_ANDROID_CPU_ABI"); + if (abi) { + nsAutoCString path; + path.AppendPrintf("%s/greprefs.js", abi); + rv = pref_ReadPrefFromJar(jarReader, path.get()); + } + + if (NS_FAILED(rv)) { + // Fallback to toplevel greprefs.js if arch-specific load fails. + rv = pref_ReadPrefFromJar(jarReader, "greprefs.js"); + } +#else + // Load jar:$gre/omni.jar!/greprefs.js. + rv = pref_ReadPrefFromJar(jarReader, "greprefs.js"); +#endif + NS_ENSURE_SUCCESS(rv, rv); + + // Load jar:$gre/omni.jar!/defaults/pref/*.js. + rv = pref_ReadDefaultPrefs(jarReader, "defaults/pref/*.js$"); + NS_ENSURE_SUCCESS(rv, rv); + +#ifdef MOZ_BACKGROUNDTASKS + if (BackgroundTasks::IsBackgroundTaskMode()) { + rv = pref_ReadDefaultPrefs(jarReader, "defaults/backgroundtasks/*.js$"); + NS_ENSURE_SUCCESS(rv, rv); + } +#endif + +#ifdef MOZ_WIDGET_ANDROID + // Load jar:$gre/omni.jar!/defaults/pref/$MOZ_ANDROID_CPU_ABI/*.js. + nsAutoCString path; + path.AppendPrintf("jar:$gre/omni.jar!/defaults/pref/%s/*.js$", abi); + pref_ReadDefaultPrefs(jarReader, path.get()); + NS_ENSURE_SUCCESS(rv, rv); +#endif + } else { + // Load $gre/greprefs.js. + nsCOMPtr greprefsFile; + rv = NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(greprefsFile)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = greprefsFile->AppendNative("greprefs.js"_ns); + NS_ENSURE_SUCCESS(rv, rv); + + rv = openPrefFile(greprefsFile, PrefValueKind::Default); + if (NS_FAILED(rv)) { + NS_WARNING( + "Error parsing GRE default preferences. Is this an old-style " + "embedding app?"); + } + } + + // Load $gre/defaults/pref/*.js. + nsCOMPtr defaultPrefDir; + rv = NS_GetSpecialDirectory(NS_APP_PREF_DEFAULTS_50_DIR, + getter_AddRefs(defaultPrefDir)); + NS_ENSURE_SUCCESS(rv, rv); + + // These pref file names should not be used: we process them after all other + // application pref files for backwards compatibility. + static const char* specialFiles[] = { +#if defined(XP_MACOSX) + "macprefs.js" +#elif defined(XP_WIN) + "winpref.js" +#elif defined(XP_UNIX) + "unix.js" +# if defined(_AIX) + , + "aix.js" +# endif +#endif + }; + + rv = pref_LoadPrefsInDir(defaultPrefDir, specialFiles, + ArrayLength(specialFiles)); + if (NS_FAILED(rv)) { + NS_WARNING("Error parsing application default preferences."); + } + +#if defined(MOZ_WIDGET_GTK) + // Under Flatpak/Snap package, load /etc/firefox/defaults/pref/*.js. + if (mozilla::widget::IsRunningUnderFlatpakOrSnap()) { + nsCOMPtr defaultSnapPrefDir; + rv = NS_GetSpecialDirectory(NS_OS_SYSTEM_CONFIG_DIR, + getter_AddRefs(defaultSnapPrefDir)); + NS_ENSURE_SUCCESS(rv, rv); + defaultSnapPrefDir->AppendNative("defaults"_ns); + defaultSnapPrefDir->AppendNative("pref"_ns); + + rv = pref_LoadPrefsInDir(defaultSnapPrefDir, nullptr, 0); + if (NS_FAILED(rv)) { + NS_WARNING("Error parsing application default preferences under Snap."); + } + } +#endif + + // Load jar:$app/omni.jar!/defaults/preferences/*.js + // or jar:$gre/omni.jar!/defaults/preferences/*.js. + RefPtr appJarReader = Omnijar::GetReader(Omnijar::APP); + + // GetReader(Omnijar::APP) returns null when `$app == $gre`, in + // which case we look for app-specific default preferences in $gre. + if (!appJarReader) { + appJarReader = Omnijar::GetReader(Omnijar::GRE); + } + + if (appJarReader) { + rv = appJarReader->FindInit("defaults/preferences/*.js$", + getter_Transfers(find)); + NS_ENSURE_SUCCESS(rv, rv); + prefEntries.Clear(); + while (NS_SUCCEEDED(find->FindNext(&entryName, &entryNameLen))) { + prefEntries.AppendElement(Substring(entryName, entryNameLen)); + } + prefEntries.Sort(); + for (uint32_t i = prefEntries.Length(); i--;) { + rv = pref_ReadPrefFromJar(appJarReader, prefEntries[i].get()); + if (NS_FAILED(rv)) { + NS_WARNING("Error parsing preferences."); + } + } + +#ifdef MOZ_BACKGROUNDTASKS + if (BackgroundTasks::IsBackgroundTaskMode()) { + rv = appJarReader->FindInit("defaults/backgroundtasks/*.js$", + getter_Transfers(find)); + NS_ENSURE_SUCCESS(rv, rv); + prefEntries.Clear(); + while (NS_SUCCEEDED(find->FindNext(&entryName, &entryNameLen))) { + prefEntries.AppendElement(Substring(entryName, entryNameLen)); + } + prefEntries.Sort(); + for (uint32_t i = prefEntries.Length(); i--;) { + rv = pref_ReadPrefFromJar(appJarReader, prefEntries[i].get()); + if (NS_FAILED(rv)) { + NS_WARNING("Error parsing preferences."); + } + } + } +#endif + } + + nsCOMPtr dirSvc( + do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr list; + dirSvc->Get(NS_APP_PREFS_DEFAULTS_DIR_LIST, NS_GET_IID(nsISimpleEnumerator), + getter_AddRefs(list)); + if (list) { + bool hasMore; + while (NS_SUCCEEDED(list->HasMoreElements(&hasMore)) && hasMore) { + nsCOMPtr elem; + list->GetNext(getter_AddRefs(elem)); + if (!elem) { + continue; + } + + nsCOMPtr path = do_QueryInterface(elem); + if (!path) { + continue; + } + + // Do we care if a file provided by this process fails to load? + pref_LoadPrefsInDir(path, nullptr, 0); + } + } + + if (XRE_IsParentProcess()) { + SetupTelemetryPref(); + } + + if (aIsStartup) { + // Now that all prefs have their initial values, install the callbacks for + // `always`-mirrored static prefs. We do this now rather than in + // StaticPrefs::InitAll() so that the callbacks don't need to be traversed + // while we load prefs from data files. + StaticPrefs::StartObservingAlwaysPrefs(); + } + + NS_CreateServicesFromCategory(NS_PREFSERVICE_APPDEFAULTS_TOPIC_ID, nullptr, + NS_PREFSERVICE_APPDEFAULTS_TOPIC_ID); + + nsCOMPtr observerService = services::GetObserverService(); + NS_ENSURE_SUCCESS(rv, rv); + + observerService->NotifyObservers(nullptr, NS_PREFSERVICE_APPDEFAULTS_TOPIC_ID, + nullptr); + + return NS_OK; +} + +/* static */ +nsresult Preferences::GetBool(const char* aPrefName, bool* aResult, + PrefValueKind aKind) { + MOZ_ASSERT(aResult); + return Internals::GetPrefValue(aPrefName, aResult, aKind); +} + +/* static */ +nsresult Preferences::GetInt(const char* aPrefName, int32_t* aResult, + PrefValueKind aKind) { + MOZ_ASSERT(aResult); + return Internals::GetPrefValue(aPrefName, aResult, aKind); +} + +/* static */ +nsresult Preferences::GetFloat(const char* aPrefName, float* aResult, + PrefValueKind aKind) { + MOZ_ASSERT(aResult); + return Internals::GetPrefValue(aPrefName, aResult, aKind); +} + +/* static */ +nsresult Preferences::GetCString(const char* aPrefName, nsACString& aResult, + PrefValueKind aKind) { + aResult.SetIsVoid(true); + return Internals::GetPrefValue(aPrefName, aResult, aKind); +} + +/* static */ +nsresult Preferences::GetString(const char* aPrefName, nsAString& aResult, + PrefValueKind aKind) { + nsAutoCString result; + nsresult rv = Preferences::GetCString(aPrefName, result, aKind); + if (NS_SUCCEEDED(rv)) { + CopyUTF8toUTF16(result, aResult); + } + return rv; +} + +/* static */ +nsresult Preferences::GetLocalizedCString(const char* aPrefName, + nsACString& aResult, + PrefValueKind aKind) { + nsAutoString result; + nsresult rv = GetLocalizedString(aPrefName, result, aKind); + if (NS_SUCCEEDED(rv)) { + CopyUTF16toUTF8(result, aResult); + } + return rv; +} + +/* static */ +nsresult Preferences::GetLocalizedString(const char* aPrefName, + nsAString& aResult, + PrefValueKind aKind) { + NS_ENSURE_TRUE(InitStaticMembers(), NS_ERROR_NOT_AVAILABLE); + nsCOMPtr prefLocalString; + nsresult rv = GetRootBranch(aKind)->GetComplexValue( + aPrefName, NS_GET_IID(nsIPrefLocalizedString), + getter_AddRefs(prefLocalString)); + if (NS_SUCCEEDED(rv)) { + MOZ_ASSERT(prefLocalString, "Succeeded but the result is NULL"); + prefLocalString->GetData(aResult); + } + return rv; +} + +/* static */ +nsresult Preferences::GetComplex(const char* aPrefName, const nsIID& aType, + void** aResult, PrefValueKind aKind) { + NS_ENSURE_TRUE(InitStaticMembers(), NS_ERROR_NOT_AVAILABLE); + return GetRootBranch(aKind)->GetComplexValue(aPrefName, aType, aResult); +} + +/* static */ +bool Preferences::GetBool(const char* aPrefName, bool aFallback, + PrefValueKind aKind) { + return Internals::GetPref(aPrefName, aFallback, aKind); +} + +/* static */ +int32_t Preferences::GetInt(const char* aPrefName, int32_t aFallback, + PrefValueKind aKind) { + return Internals::GetPref(aPrefName, aFallback, aKind); +} + +/* static */ +uint32_t Preferences::GetUint(const char* aPrefName, uint32_t aFallback, + PrefValueKind aKind) { + return Internals::GetPref(aPrefName, aFallback, aKind); +} + +/* static */ +float Preferences::GetFloat(const char* aPrefName, float aFallback, + PrefValueKind aKind) { + return Internals::GetPref(aPrefName, aFallback, aKind); +} + +/* static */ +nsresult Preferences::SetCString(const char* aPrefName, + const nsACString& aValue, + PrefValueKind aKind) { + ENSURE_PARENT_PROCESS("SetCString", aPrefName); + NS_ENSURE_TRUE(InitStaticMembers(), NS_ERROR_NOT_AVAILABLE); + + if (aValue.Length() > MAX_PREF_LENGTH) { + return NS_ERROR_ILLEGAL_VALUE; + } + + // It's ok to stash a pointer to the temporary PromiseFlatCString's chars in + // pref because pref_SetPref() duplicates those chars. + PrefValue prefValue; + const nsCString& flat = PromiseFlatCString(aValue); + prefValue.mStringVal = flat.get(); + return pref_SetPref(nsDependentCString(aPrefName), PrefType::String, aKind, + prefValue, + /* isSticky */ false, + /* isLocked */ false, + /* fromInit */ false); +} + +/* static */ +nsresult Preferences::SetBool(const char* aPrefName, bool aValue, + PrefValueKind aKind) { + ENSURE_PARENT_PROCESS("SetBool", aPrefName); + NS_ENSURE_TRUE(InitStaticMembers(), NS_ERROR_NOT_AVAILABLE); + + PrefValue prefValue; + prefValue.mBoolVal = aValue; + return pref_SetPref(nsDependentCString(aPrefName), PrefType::Bool, aKind, + prefValue, + /* isSticky */ false, + /* isLocked */ false, + /* fromInit */ false); +} + +/* static */ +nsresult Preferences::SetInt(const char* aPrefName, int32_t aValue, + PrefValueKind aKind) { + ENSURE_PARENT_PROCESS("SetInt", aPrefName); + NS_ENSURE_TRUE(InitStaticMembers(), NS_ERROR_NOT_AVAILABLE); + + PrefValue prefValue; + prefValue.mIntVal = aValue; + return pref_SetPref(nsDependentCString(aPrefName), PrefType::Int, aKind, + prefValue, + /* isSticky */ false, + /* isLocked */ false, + /* fromInit */ false); +} + +/* static */ +nsresult Preferences::SetComplex(const char* aPrefName, const nsIID& aType, + nsISupports* aValue, PrefValueKind aKind) { + NS_ENSURE_TRUE(InitStaticMembers(), NS_ERROR_NOT_AVAILABLE); + return GetRootBranch(aKind)->SetComplexValue(aPrefName, aType, aValue); +} + +/* static */ +nsresult Preferences::Lock(const char* aPrefName) { + ENSURE_PARENT_PROCESS("Lock", aPrefName); + NS_ENSURE_TRUE(InitStaticMembers(), NS_ERROR_NOT_AVAILABLE); + + const auto& prefName = nsDependentCString(aPrefName); + + Pref* pref; + MOZ_TRY_VAR(pref, + pref_LookupForModify(prefName, [](const PrefWrapper& aPref) { + return !aPref.IsLocked(); + })); + + if (pref) { + pref->SetIsLocked(true); + NotifyCallbacks(prefName, PrefWrapper(pref)); + } + + return NS_OK; +} + +/* static */ +nsresult Preferences::Unlock(const char* aPrefName) { + ENSURE_PARENT_PROCESS("Unlock", aPrefName); + NS_ENSURE_TRUE(InitStaticMembers(), NS_ERROR_NOT_AVAILABLE); + + const auto& prefName = nsDependentCString(aPrefName); + + Pref* pref; + MOZ_TRY_VAR(pref, + pref_LookupForModify(prefName, [](const PrefWrapper& aPref) { + return aPref.IsLocked(); + })); + + if (pref) { + pref->SetIsLocked(false); + NotifyCallbacks(prefName, PrefWrapper(pref)); + } + + return NS_OK; +} + +/* static */ +bool Preferences::IsLocked(const char* aPrefName) { + NS_ENSURE_TRUE(InitStaticMembers(), false); + + Maybe pref = pref_Lookup(aPrefName); + return pref.isSome() && pref->IsLocked(); +} + +/* static */ +bool Preferences::IsSanitized(const char* aPrefName) { + NS_ENSURE_TRUE(InitStaticMembers(), false); + + Maybe pref = pref_Lookup(aPrefName); + return pref.isSome() && pref->IsSanitized(); +} + +/* static */ +nsresult Preferences::ClearUser(const char* aPrefName) { + ENSURE_PARENT_PROCESS("ClearUser", aPrefName); + NS_ENSURE_TRUE(InitStaticMembers(), NS_ERROR_NOT_AVAILABLE); + + const auto& prefName = nsDependentCString{aPrefName}; + auto result = pref_LookupForModify( + prefName, [](const PrefWrapper& aPref) { return aPref.HasUserValue(); }); + if (result.isErr()) { + return NS_OK; + } + + if (Pref* pref = result.unwrap()) { + pref->ClearUserValue(); + + if (!pref->HasDefaultValue()) { + MOZ_ASSERT( + !gSharedMap || !pref->IsSanitized() || !gSharedMap->Has(pref->Name()), + "A sanitized pref should never be in the shared pref map."); + if (!pref->IsSanitized() && + (!gSharedMap || !gSharedMap->Has(pref->Name()))) { + HashTable()->remove(aPrefName); + } else { + pref->SetType(PrefType::None); + } + + NotifyCallbacks(prefName); + } else { + NotifyCallbacks(prefName, PrefWrapper(pref)); + } + + Preferences::HandleDirty(); + } + return NS_OK; +} + +/* static */ +bool Preferences::HasUserValue(const char* aPrefName) { + NS_ENSURE_TRUE(InitStaticMembers(), false); + + Maybe pref = pref_Lookup(aPrefName); + return pref.isSome() && pref->HasUserValue(); +} + +/* static */ +bool Preferences::HasDefaultValue(const char* aPrefName) { + NS_ENSURE_TRUE(InitStaticMembers(), false); + + Maybe pref = pref_Lookup(aPrefName); + return pref.isSome() && pref->HasDefaultValue(); +} + +/* static */ +int32_t Preferences::GetType(const char* aPrefName) { + NS_ENSURE_TRUE(InitStaticMembers(), nsIPrefBranch::PREF_INVALID); + + if (!HashTable()) { + return PREF_INVALID; + } + + Maybe pref = pref_Lookup(aPrefName); + if (!pref.isSome()) { + return PREF_INVALID; + } + + switch (pref->Type()) { + case PrefType::String: + return PREF_STRING; + + case PrefType::Int: + return PREF_INT; + + case PrefType::Bool: + return PREF_BOOL; + + case PrefType::None: + if (IsPreferenceSanitized(aPrefName)) { + if (!sPrefTelemetryEventEnabled.exchange(true)) { + sPrefTelemetryEventEnabled = true; + Telemetry::SetEventRecordingEnabled("security"_ns, true); + } + + Telemetry::RecordEvent( + Telemetry::EventID::Security_Prefusage_Contentprocess, + mozilla::Some(aPrefName), mozilla::Nothing()); + + if (sCrashOnBlocklistedPref) { + MOZ_CRASH_UNSAFE_PRINTF( + "Should not access the preference '%s' in the Content Processes", + aPrefName); + } else { + return PREF_INVALID; + } + } + [[fallthrough]]; + + default: + MOZ_CRASH(); + } +} + +/* static */ +nsresult Preferences::AddStrongObserver(nsIObserver* aObserver, + const nsACString& aPref) { + MOZ_ASSERT(aObserver); + NS_ENSURE_TRUE(InitStaticMembers(), NS_ERROR_NOT_AVAILABLE); + return sPreferences->mRootBranch->AddObserver(aPref, aObserver, false); +} + +/* static */ +nsresult Preferences::AddWeakObserver(nsIObserver* aObserver, + const nsACString& aPref) { + MOZ_ASSERT(aObserver); + NS_ENSURE_TRUE(InitStaticMembers(), NS_ERROR_NOT_AVAILABLE); + return sPreferences->mRootBranch->AddObserver(aPref, aObserver, true); +} + +/* static */ +nsresult Preferences::RemoveObserver(nsIObserver* aObserver, + const nsACString& aPref) { + MOZ_ASSERT(aObserver); + if (sShutdown) { + MOZ_ASSERT(!sPreferences); + return NS_OK; // Observers have been released automatically. + } + NS_ENSURE_TRUE(sPreferences, NS_ERROR_NOT_AVAILABLE); + return sPreferences->mRootBranch->RemoveObserver(aPref, aObserver); +} + +template +static void AssertNotMallocAllocated(T* aPtr) { +#if defined(DEBUG) && defined(MOZ_MEMORY) + jemalloc_ptr_info_t info; + jemalloc_ptr_info((void*)aPtr, &info); + MOZ_ASSERT(info.tag == TagUnknown); +#endif +} + +/* static */ +nsresult Preferences::AddStrongObservers(nsIObserver* aObserver, + const char** aPrefs) { + MOZ_ASSERT(aObserver); + for (uint32_t i = 0; aPrefs[i]; i++) { + AssertNotMallocAllocated(aPrefs[i]); + + nsCString pref; + pref.AssignLiteral(aPrefs[i], strlen(aPrefs[i])); + nsresult rv = AddStrongObserver(aObserver, pref); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +/* static */ +nsresult Preferences::AddWeakObservers(nsIObserver* aObserver, + const char** aPrefs) { + MOZ_ASSERT(aObserver); + for (uint32_t i = 0; aPrefs[i]; i++) { + AssertNotMallocAllocated(aPrefs[i]); + + nsCString pref; + pref.AssignLiteral(aPrefs[i], strlen(aPrefs[i])); + nsresult rv = AddWeakObserver(aObserver, pref); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +/* static */ +nsresult Preferences::RemoveObservers(nsIObserver* aObserver, + const char** aPrefs) { + MOZ_ASSERT(aObserver); + if (sShutdown) { + MOZ_ASSERT(!sPreferences); + return NS_OK; // Observers have been released automatically. + } + NS_ENSURE_TRUE(sPreferences, NS_ERROR_NOT_AVAILABLE); + + for (uint32_t i = 0; aPrefs[i]; i++) { + nsresult rv = RemoveObserver(aObserver, nsDependentCString(aPrefs[i])); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +template +/* static */ +nsresult Preferences::RegisterCallbackImpl(PrefChangedFunc aCallback, + T& aPrefNode, void* aData, + MatchKind aMatchKind, + bool aIsPriority) { + NS_ENSURE_ARG(aCallback); + + NS_ENSURE_TRUE(InitStaticMembers(), NS_ERROR_NOT_AVAILABLE); + + auto node = new CallbackNode(aPrefNode, aCallback, aData, aMatchKind); + + if (aIsPriority) { + // Add to the start of the list. + node->SetNext(gFirstCallback); + gFirstCallback = node; + if (!gLastPriorityNode) { + gLastPriorityNode = node; + } + } else { + // Add to the start of the non-priority part of the list. + if (gLastPriorityNode) { + node->SetNext(gLastPriorityNode->Next()); + gLastPriorityNode->SetNext(node); + } else { + node->SetNext(gFirstCallback); + gFirstCallback = node; + } + } + + return NS_OK; +} + +/* static */ +nsresult Preferences::RegisterCallback(PrefChangedFunc aCallback, + const nsACString& aPrefNode, void* aData, + MatchKind aMatchKind, bool aIsPriority) { + return RegisterCallbackImpl(aCallback, aPrefNode, aData, aMatchKind, + aIsPriority); +} + +/* static */ +nsresult Preferences::RegisterCallbacks(PrefChangedFunc aCallback, + const char** aPrefs, void* aData, + MatchKind aMatchKind) { + return RegisterCallbackImpl(aCallback, aPrefs, aData, aMatchKind); +} + +/* static */ +nsresult Preferences::RegisterCallbackAndCall(PrefChangedFunc aCallback, + const nsACString& aPref, + void* aClosure, + MatchKind aMatchKind) { + MOZ_ASSERT(aCallback); + nsresult rv = RegisterCallback(aCallback, aPref, aClosure, aMatchKind); + if (NS_SUCCEEDED(rv)) { + (*aCallback)(PromiseFlatCString(aPref).get(), aClosure); + } + return rv; +} + +/* static */ +nsresult Preferences::RegisterCallbacksAndCall(PrefChangedFunc aCallback, + const char** aPrefs, + void* aClosure) { + MOZ_ASSERT(aCallback); + + nsresult rv = + RegisterCallbacks(aCallback, aPrefs, aClosure, MatchKind::ExactMatch); + if (NS_SUCCEEDED(rv)) { + for (const char** ptr = aPrefs; *ptr; ptr++) { + (*aCallback)(*ptr, aClosure); + } + } + return rv; +} + +template +/* static */ +nsresult Preferences::UnregisterCallbackImpl(PrefChangedFunc aCallback, + T& aPrefNode, void* aData, + MatchKind aMatchKind) { + MOZ_ASSERT(aCallback); + if (sShutdown) { + MOZ_ASSERT(!sPreferences); + return NS_OK; // Observers have been released automatically. + } + NS_ENSURE_TRUE(sPreferences, NS_ERROR_NOT_AVAILABLE); + + nsresult rv = NS_ERROR_FAILURE; + CallbackNode* node = gFirstCallback; + CallbackNode* prev_node = nullptr; + + while (node) { + if (node->Func() == aCallback && node->Data() == aData && + node->MatchKind() == aMatchKind && node->DomainIs(aPrefNode)) { + if (gCallbacksInProgress) { + // Postpone the node removal until after callbacks enumeration is + // finished. + node->ClearFunc(); + gShouldCleanupDeadNodes = true; + prev_node = node; + node = node->Next(); + } else { + node = pref_RemoveCallbackNode(node, prev_node); + } + rv = NS_OK; + } else { + prev_node = node; + node = node->Next(); + } + } + return rv; +} + +/* static */ +nsresult Preferences::UnregisterCallback(PrefChangedFunc aCallback, + const nsACString& aPrefNode, + void* aData, MatchKind aMatchKind) { + return UnregisterCallbackImpl(aCallback, aPrefNode, aData, + aMatchKind); +} + +/* static */ +nsresult Preferences::UnregisterCallbacks(PrefChangedFunc aCallback, + const char** aPrefs, void* aData, + MatchKind aMatchKind) { + return UnregisterCallbackImpl(aCallback, aPrefs, aData, aMatchKind); +} + +template +static void AddMirrorCallback(T* aMirror, const nsACString& aPref) { + MOZ_ASSERT(NS_IsMainThread()); + + Internals::RegisterCallback(aMirror, aPref); +} + +// Don't inline because it explodes compile times. +template +static MOZ_NEVER_INLINE void AddMirror(T* aMirror, const nsACString& aPref, + StripAtomic aDefault) { + *aMirror = Internals::GetPref(PromiseFlatCString(aPref).get(), aDefault); + AddMirrorCallback(aMirror, aPref); +} + +static MOZ_NEVER_INLINE void AddMirror(DataMutexString& aMirror, + const nsACString& aPref) { + auto lock = aMirror.Lock(); + nsCString result(*lock); + Internals::GetPrefValue(PromiseFlatCString(aPref).get(), result, + PrefValueKind::User); + lock->Assign(std::move(result)); + AddMirrorCallback(&aMirror, aPref); +} + +// The InitPref_*() functions below end in a `_` suffix because they are +// used by the PREF macro definition in InitAll() below. + +static void InitPref_bool(const nsCString& aName, bool aDefaultValue) { + MOZ_ASSERT(XRE_IsParentProcess()); + PrefValue value; + value.mBoolVal = aDefaultValue; + pref_SetPref(aName, PrefType::Bool, PrefValueKind::Default, value, + /* isSticky */ false, + /* isLocked */ false, + /* fromInit */ true); +} + +static void InitPref_int32_t(const nsCString& aName, int32_t aDefaultValue) { + MOZ_ASSERT(XRE_IsParentProcess()); + PrefValue value; + value.mIntVal = aDefaultValue; + pref_SetPref(aName, PrefType::Int, PrefValueKind::Default, value, + /* isSticky */ false, + /* isLocked */ false, + /* fromInit */ true); +} + +static void InitPref_uint32_t(const nsCString& aName, uint32_t aDefaultValue) { + InitPref_int32_t(aName, int32_t(aDefaultValue)); +} + +static void InitPref_float(const nsCString& aName, float aDefaultValue) { + MOZ_ASSERT(XRE_IsParentProcess()); + PrefValue value; + // Convert the value in a locale-independent way, including a trailing ".0" + // if necessary to distinguish floating-point from integer prefs when viewing + // them in about:config. + nsAutoCString defaultValue; + defaultValue.AppendFloat(aDefaultValue); + if (!defaultValue.Contains('.') && !defaultValue.Contains('e')) { + defaultValue.AppendLiteral(".0"); + } + value.mStringVal = defaultValue.get(); + pref_SetPref(aName, PrefType::String, PrefValueKind::Default, value, + /* isSticky */ false, + /* isLocked */ false, + /* fromInit */ true); +} + +static void InitPref_String(const nsCString& aName, const char* aDefaultValue) { + MOZ_ASSERT(XRE_IsParentProcess()); + PrefValue value; + value.mStringVal = aDefaultValue; + pref_SetPref(aName, PrefType::String, PrefValueKind::Default, value, + /* isSticky */ false, + /* isLocked */ false, + /* fromInit */ true); +} + +static void InitPref(const nsCString& aName, bool aDefaultValue) { + InitPref_bool(aName, aDefaultValue); +} +static void InitPref(const nsCString& aName, int32_t aDefaultValue) { + InitPref_int32_t(aName, aDefaultValue); +} +static void InitPref(const nsCString& aName, uint32_t aDefaultValue) { + InitPref_uint32_t(aName, aDefaultValue); +} +static void InitPref(const nsCString& aName, float aDefaultValue) { + InitPref_float(aName, aDefaultValue); +} + +template +static void InitAlwaysPref(const nsCString& aName, T* aCache, + StripAtomic aDefaultValue) { + // Only called in the parent process. Set/reset the pref value and the + // `always` mirror to the default value. + // `once` mirrors will be initialized lazily in InitOncePrefs(). + InitPref(aName, aDefaultValue); + *aCache = aDefaultValue; +} + +static void InitAlwaysPref(const nsCString& aName, DataMutexString& aCache, + const nsLiteralCString& aDefaultValue) { + // Only called in the parent process. Set/reset the pref value and the + // `always` mirror to the default value. + // `once` mirrors will be initialized lazily in InitOncePrefs(). + InitPref_String(aName, aDefaultValue.get()); + Internals::AssignMirror(aCache, aDefaultValue); +} + +static Atomic sOncePrefRead(false); +static StaticMutex sOncePrefMutex MOZ_UNANNOTATED; + +namespace StaticPrefs { + +void MaybeInitOncePrefs() { + if (MOZ_LIKELY(sOncePrefRead)) { + // `once`-mirrored prefs have already been initialized to their default + // value. + return; + } + StaticMutexAutoLock lock(sOncePrefMutex); + if (NS_IsMainThread()) { + InitOncePrefs(); + } else { + RefPtr runnable = NS_NewRunnableFunction( + "Preferences::MaybeInitOncePrefs", [&]() { InitOncePrefs(); }); + // This logic needs to run on the main thread + SyncRunnable::DispatchToThread(GetMainThreadSerialEventTarget(), runnable); + } + sOncePrefRead = true; +} + +// For mirrored prefs we generate a variable definition. +#define NEVER_PREF(name, cpp_type, value) +#define ALWAYS_PREF(name, base_id, full_id, cpp_type, default_value) \ + cpp_type sMirror_##full_id(default_value); +#define ALWAYS_DATAMUTEX_PREF(name, base_id, full_id, cpp_type, default_value) \ + cpp_type sMirror_##full_id("DataMutexString"); +#define ONCE_PREF(name, base_id, full_id, cpp_type, default_value) \ + cpp_type sMirror_##full_id(default_value); +#include "mozilla/StaticPrefListAll.h" +#undef NEVER_PREF +#undef ALWAYS_PREF +#undef ALWAYS_DATAMUTEX_PREF +#undef ONCE_PREF + +static void InitAll() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(XRE_IsParentProcess()); + + // For all prefs we generate some initialization code. + // + // The InitPref_*() functions have a type suffix to avoid ambiguity between + // prefs having int32_t and float default values. That suffix is not needed + // for the InitAlwaysPref() functions because they take a pointer parameter, + // which prevents automatic int-to-float coercion. +#define NEVER_PREF(name, cpp_type, value) \ + InitPref_##cpp_type(name ""_ns, value); +#define ALWAYS_PREF(name, base_id, full_id, cpp_type, value) \ + InitAlwaysPref(name ""_ns, &sMirror_##full_id, value); +#define ALWAYS_DATAMUTEX_PREF(name, base_id, full_id, cpp_type, value) \ + InitAlwaysPref(name ""_ns, sMirror_##full_id, value); +#define ONCE_PREF(name, base_id, full_id, cpp_type, value) \ + InitPref_##cpp_type(name ""_ns, value); +#include "mozilla/StaticPrefListAll.h" +#undef NEVER_PREF +#undef ALWAYS_PREF +#undef ALWAYS_DATAMUTEX_PREF +#undef ONCE_PREF +} + +static void StartObservingAlwaysPrefs() { + MOZ_ASSERT(NS_IsMainThread()); + + // Call AddMirror so that our mirrors for `always` prefs will stay updated. + // The call to AddMirror re-reads the current pref value into the mirror, so + // our mirror will now be up-to-date even if some of the prefs have changed + // since the call to InitAll(). +#define NEVER_PREF(name, cpp_type, value) +#define ALWAYS_PREF(name, base_id, full_id, cpp_type, value) \ + AddMirror(&sMirror_##full_id, name ""_ns, sMirror_##full_id); +#define ALWAYS_DATAMUTEX_PREF(name, base_id, full_id, cpp_type, value) \ + AddMirror(sMirror_##full_id, name ""_ns); +#define ONCE_PREF(name, base_id, full_id, cpp_type, value) +#include "mozilla/StaticPrefListAll.h" +#undef NEVER_PREF +#undef ALWAYS_PREF +#undef ALWAYS_DATAMUTEX_PREF +#undef ONCE_PREF +} + +static void InitOncePrefs() { + // For `once`-mirrored prefs we generate some initialization code. This is + // done in case the pref value was updated when reading pref data files. It's + // necessary because we don't have callbacks registered for `once`-mirrored + // prefs. + // + // In debug builds, we also install a mechanism that can check if the + // preference value is modified after `once`-mirrored prefs are initialized. + // In tests this would indicate a likely misuse of a `once`-mirrored pref and + // suggest that it should instead be `always`-mirrored. +#define NEVER_PREF(name, cpp_type, value) +#define ALWAYS_PREF(name, base_id, full_id, cpp_type, value) +#define ALWAYS_DATAMUTEX_PREF(name, base_id, full_id, cpp_type, value) +#ifdef DEBUG +# define ONCE_PREF(name, base_id, full_id, cpp_type, value) \ + { \ + MOZ_ASSERT(gOnceStaticPrefsAntiFootgun); \ + sMirror_##full_id = Internals::GetPref(name, cpp_type(value)); \ + auto checkPref = [&]() { \ + MOZ_ASSERT(sOncePrefRead); \ + cpp_type staticPrefValue = full_id(); \ + cpp_type preferenceValue = \ + Internals::GetPref(GetPrefName_##base_id(), cpp_type(value)); \ + MOZ_ASSERT(staticPrefValue == preferenceValue, \ + "Preference '" name \ + "' got modified since StaticPrefs::" #full_id \ + " was initialized. Consider using an `always` mirror kind " \ + "instead"); \ + }; \ + gOnceStaticPrefsAntiFootgun->insert( \ + std::pair(GetPrefName_##base_id(), \ + std::move(checkPref))); \ + } +#else +# define ONCE_PREF(name, base_id, full_id, cpp_type, value) \ + sMirror_##full_id = Internals::GetPref(name, cpp_type(value)); +#endif + +#include "mozilla/StaticPrefListAll.h" +#undef NEVER_PREF +#undef ALWAYS_PREF +#undef ALWAYS_DATAMUTEX_PREF +#undef ONCE_PREF +} + +static void ShutdownAlwaysPrefs() { + MOZ_ASSERT(NS_IsMainThread()); + + // We may need to do clean up for leak detection for some StaticPrefs. +#define NEVER_PREF(name, cpp_type, value) +#define ALWAYS_PREF(name, base_id, full_id, cpp_type, value) +#define ALWAYS_DATAMUTEX_PREF(name, base_id, full_id, cpp_type, value) \ + Internals::ClearMirror(sMirror_##full_id); +#define ONCE_PREF(name, base_id, full_id, cpp_type, value) +#include "mozilla/StaticPrefListAll.h" +#undef NEVER_PREF +#undef ALWAYS_PREF +#undef ALWAYS_DATAMUTEX_PREF +#undef ONCE_PREF +} + +} // namespace StaticPrefs + +static MOZ_MAYBE_UNUSED void SaveOncePrefToSharedMap( + SharedPrefMapBuilder& aBuilder, const nsACString& aName, bool aValue) { + auto oncePref = MakeUnique(aName); + oncePref->SetType(PrefType::Bool); + oncePref->SetIsSkippedByIteration(true); + bool valueChanged = false; + MOZ_ALWAYS_SUCCEEDS( + oncePref->SetDefaultValue(PrefType::Bool, PrefValue(aValue), + /* isSticky */ true, + /* isLocked */ true, &valueChanged)); + oncePref->AddToMap(aBuilder); +} + +static MOZ_MAYBE_UNUSED void SaveOncePrefToSharedMap( + SharedPrefMapBuilder& aBuilder, const nsACString& aName, int32_t aValue) { + auto oncePref = MakeUnique(aName); + oncePref->SetType(PrefType::Int); + oncePref->SetIsSkippedByIteration(true); + bool valueChanged = false; + MOZ_ALWAYS_SUCCEEDS( + oncePref->SetDefaultValue(PrefType::Int, PrefValue(aValue), + /* isSticky */ true, + /* isLocked */ true, &valueChanged)); + oncePref->AddToMap(aBuilder); +} + +static MOZ_MAYBE_UNUSED void SaveOncePrefToSharedMap( + SharedPrefMapBuilder& aBuilder, const nsACString& aName, uint32_t aValue) { + SaveOncePrefToSharedMap(aBuilder, aName, int32_t(aValue)); +} + +static MOZ_MAYBE_UNUSED void SaveOncePrefToSharedMap( + SharedPrefMapBuilder& aBuilder, const nsACString& aName, float aValue) { + auto oncePref = MakeUnique(aName); + oncePref->SetType(PrefType::String); + oncePref->SetIsSkippedByIteration(true); + nsAutoCString value; + value.AppendFloat(aValue); + bool valueChanged = false; + // It's ok to stash a pointer to the temporary PromiseFlatCString's chars in + // pref because pref_SetPref() duplicates those chars. + const nsCString& flat = PromiseFlatCString(value); + MOZ_ALWAYS_SUCCEEDS( + oncePref->SetDefaultValue(PrefType::String, PrefValue(flat.get()), + /* isSticky */ true, + /* isLocked */ true, &valueChanged)); + oncePref->AddToMap(aBuilder); +} + +#define ONCE_PREF_NAME(name) "$$$" name "$$$" + +namespace StaticPrefs { + +static void RegisterOncePrefs(SharedPrefMapBuilder& aBuilder) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_DIAGNOSTIC_ASSERT(!gSharedMap, + "Must be called before gSharedMap has been created"); + MaybeInitOncePrefs(); + + // For `once`-mirrored prefs we generate a save call, which saves the value + // as it was at parent startup. It is stored in a special (hidden and locked) + // entry in the global SharedPreferenceMap. In order for the entry to be + // hidden and not appear in about:config nor ever be stored to disk, we set + // its IsSkippedByIteration flag to true. We also distinguish it by adding a + // "$$$" prefix and suffix to the preference name. +#define NEVER_PREF(name, cpp_type, value) +#define ALWAYS_PREF(name, base_id, full_id, cpp_type, value) +#define ALWAYS_DATAMUTEX_PREF(name, base_id, full_id, cpp_type, value) +#define ONCE_PREF(name, base_id, full_id, cpp_type, value) \ + SaveOncePrefToSharedMap(aBuilder, ONCE_PREF_NAME(name) ""_ns, \ + cpp_type(sMirror_##full_id)); +#include "mozilla/StaticPrefListAll.h" +#undef NEVER_PREF +#undef ALWAYS_PREF +#undef ALWAYS_DATAMUTEX_PREF +#undef ONCE_PREF +} + +static void InitStaticPrefsFromShared() { + MOZ_ASSERT(!XRE_IsParentProcess()); + MOZ_DIAGNOSTIC_ASSERT(gSharedMap, + "Must be called once gSharedMap has been created"); + + // For mirrored static prefs we generate some initialization code. Each + // mirror variable is already initialized in the binary with the default + // value. If the pref value hasn't changed from the default in the main + // process (the common case) then the overwriting here won't change the + // mirror variable's value. + // + // Note that the MOZ_ASSERT calls below can fail in one obscure case: when a + // Firefox update occurs and we get a main process from the old binary (with + // static prefs {A,B,C,D}) plus a new content process from the new binary + // (with static prefs {A,B,C,D,E}). The content process' call to + // GetSharedPrefValue() for pref E will fail because the shared pref map was + // created by the main process, which doesn't have pref E. + // + // This silent failure is safe. The mirror variable for pref E is already + // initialized to the default value in the content process, and the main + // process cannot have changed pref E because it doesn't know about it! + // + // Nonetheless, it's useful to have the MOZ_ASSERT here for testing of debug + // builds, where this scenario involving inconsistent binaries should not + // occur. +#define NEVER_PREF(name, cpp_type, default_value) +#define ALWAYS_PREF(name, base_id, full_id, cpp_type, default_value) \ + { \ + StripAtomic val; \ + if (IsString::value && IsPreferenceSanitized(name)) { \ + if (!sPrefTelemetryEventEnabled.exchange(true)) { \ + sPrefTelemetryEventEnabled = true; \ + Telemetry::SetEventRecordingEnabled("security"_ns, true); \ + } \ + Telemetry::RecordEvent( \ + Telemetry::EventID::Security_Prefusage_Contentprocess, \ + mozilla::Some(name##_ns), mozilla::Nothing()); \ + MOZ_DIAGNOSTIC_ASSERT(!sCrashOnBlocklistedPref, \ + "Should not access the preference '" name \ + "' in Content Processes"); \ + } \ + DebugOnly rv = Internals::GetSharedPrefValue(name, &val); \ + MOZ_ASSERT(NS_SUCCEEDED(rv), "Failed accessing " name); \ + StaticPrefs::sMirror_##full_id = val; \ + } +#define ALWAYS_DATAMUTEX_PREF(name, base_id, full_id, cpp_type, default_value) \ + { \ + StripAtomic val; \ + if (IsString::value && IsPreferenceSanitized(name)) { \ + if (!sPrefTelemetryEventEnabled.exchange(true)) { \ + sPrefTelemetryEventEnabled = true; \ + Telemetry::SetEventRecordingEnabled("security"_ns, true); \ + } \ + Telemetry::RecordEvent( \ + Telemetry::EventID::Security_Prefusage_Contentprocess, \ + mozilla::Some(name##_ns), mozilla::Nothing()); \ + MOZ_DIAGNOSTIC_ASSERT(!sCrashOnBlocklistedPref, \ + "Should not access the preference '" name \ + "' in Content Processes"); \ + } \ + DebugOnly rv = Internals::GetSharedPrefValue(name, &val); \ + MOZ_ASSERT(NS_SUCCEEDED(rv), "Failed accessing " name); \ + Internals::AssignMirror(StaticPrefs::sMirror_##full_id, \ + std::forward>(val)); \ + } +#define ONCE_PREF(name, base_id, full_id, cpp_type, default_value) \ + { \ + cpp_type val; \ + if (IsString::value && IsPreferenceSanitized(name)) { \ + if (!sPrefTelemetryEventEnabled.exchange(true)) { \ + sPrefTelemetryEventEnabled = true; \ + Telemetry::SetEventRecordingEnabled("security"_ns, true); \ + } \ + Telemetry::RecordEvent( \ + Telemetry::EventID::Security_Prefusage_Contentprocess, \ + mozilla::Some(name##_ns), mozilla::Nothing()); \ + MOZ_DIAGNOSTIC_ASSERT(!sCrashOnBlocklistedPref, \ + "Should not access the preference '" name \ + "' in Content Processes"); \ + } \ + DebugOnly rv = \ + Internals::GetSharedPrefValue(ONCE_PREF_NAME(name), &val); \ + MOZ_ASSERT(NS_SUCCEEDED(rv), "Failed accessing " name); \ + StaticPrefs::sMirror_##full_id = val; \ + } +#include "mozilla/StaticPrefListAll.h" +#undef NEVER_PREF +#undef ALWAYS_PREF +#undef ALWAYS_DATAMUTEX_PREF +#undef ONCE_PREF + + // `once`-mirrored prefs have been set to their value in the step above and + // outside the parent process they are immutable. We set sOncePrefRead so + // that we can directly skip any lazy initializations. + sOncePrefRead = true; +} + +} // namespace StaticPrefs + +} // namespace mozilla + +#undef ENSURE_PARENT_PROCESS + +//=========================================================================== +// Module and factory stuff +//=========================================================================== + +NS_IMPL_COMPONENT_FACTORY(nsPrefLocalizedString) { + auto str = MakeRefPtr(); + if (NS_SUCCEEDED(str->Init())) { + return str.forget().downcast(); + } + return nullptr; +} + +namespace mozilla { + +void UnloadPrefsModule() { Preferences::Shutdown(); } + +} // namespace mozilla + +// Preference Sanitization Related Code --------------------------------------- + +#define PREF_LIST_ENTRY(s) \ + { s, (sizeof(s) / sizeof(char)) - 1 } +struct PrefListEntry { + const char* mPrefBranch; + size_t mLen; +}; + +// These prefs are not useful in child processes. +static const PrefListEntry sParentOnlyPrefBranchList[] = { + // Remove prefs with user data + PREF_LIST_ENTRY("datareporting.policy."), + PREF_LIST_ENTRY("browser.download.lastDir"), + PREF_LIST_ENTRY("browser.newtabpage.pinned"), + PREF_LIST_ENTRY("browser.uiCustomization.state"), + PREF_LIST_ENTRY("browser.urlbar"), + PREF_LIST_ENTRY("browser.urlbar.resultGroups"), + PREF_LIST_ENTRY("devtools.debugger.pending-selected-location"), + PREF_LIST_ENTRY("identity.fxaccounts.account.device.name"), + PREF_LIST_ENTRY("identity.fxaccounts.account.telemetry.sanitized_uid"), + PREF_LIST_ENTRY("identity.fxaccounts.lastSignedInUserHash"), + PREF_LIST_ENTRY("print_printer"), + PREF_LIST_ENTRY("services."), + + // Remove UUIDs + PREF_LIST_ENTRY("app.normandy.user_id"), + PREF_LIST_ENTRY("browser.newtabpage.activity-stream.impressionId"), + PREF_LIST_ENTRY("browser.pageActions.persistedActions"), + PREF_LIST_ENTRY("browser.startup.lastColdStartupCheck"), + PREF_LIST_ENTRY("dom.push.userAgentID"), + PREF_LIST_ENTRY("extensions.webextensions.uuids"), + PREF_LIST_ENTRY("privacy.userContext.extension"), + PREF_LIST_ENTRY("toolkit.telemetry.cachedClientID"), + + // Remove IDs that could be used to correlate across origins + PREF_LIST_ENTRY("app.update.lastUpdateTime."), + PREF_LIST_ENTRY( + "browser.contentblocking.cfr-milestone.milestone-shown-time"), + PREF_LIST_ENTRY("browser.contextual-services.contextId"), + PREF_LIST_ENTRY("browser.laterrun.bookkeeping.profileCreationTime"), + PREF_LIST_ENTRY("browser.newtabpage.activity-stream.discoverystream."), + PREF_LIST_ENTRY("browser.sessionstore.upgradeBackup.latestBuildID"), + PREF_LIST_ENTRY("browser.shell.mostRecentDateSetAsDefault"), + PREF_LIST_ENTRY("fission.experiment.max-origins.last-"), + PREF_LIST_ENTRY("idle.lastDailyNotification"), + PREF_LIST_ENTRY("media.gmp-gmpopenh264.lastUpdate"), + PREF_LIST_ENTRY("media.gmp-manager.lastCheck"), + PREF_LIST_ENTRY("places.database.lastMaintenance"), + PREF_LIST_ENTRY("privacy.purge_trackers.last_purge"), + PREF_LIST_ENTRY("storage.vacuum.last.places.sqlite"), + PREF_LIST_ENTRY("toolkit.startup.last_success"), + + // Remove fingerprintable things + PREF_LIST_ENTRY("browser.startup.homepage_override.buildID"), + PREF_LIST_ENTRY("extensions.lastAppBuildId"), + PREF_LIST_ENTRY("media.gmp-manager.buildID"), + PREF_LIST_ENTRY("toolkit.telemetry.previousBuildID"), +}; + +static const PrefListEntry sDynamicPrefOverrideList[]{ + PREF_LIST_ENTRY("apz.subtest"), + PREF_LIST_ENTRY("autoadmin.global_config_url"), // Bug 1780575 + PREF_LIST_ENTRY("browser.contentblocking.category"), + PREF_LIST_ENTRY("browser.search.region"), + PREF_LIST_ENTRY( + "browser.tabs.remote.testOnly.failPBrowserCreation.browsingContext"), + PREF_LIST_ENTRY("browser.translation.bing.authURL"), + PREF_LIST_ENTRY("browser.translation.bing.clientIdOverride"), + PREF_LIST_ENTRY("browser.translation.bing.translateArrayURL"), + PREF_LIST_ENTRY("browser.translation.bing.apiKeyOverride"), + PREF_LIST_ENTRY("browser.translation.yandex.apiKeyOverride"), + PREF_LIST_ENTRY("browser.translation.yandex.translateURLOverride"), + PREF_LIST_ENTRY("browser.uitour.testingOrigins"), + PREF_LIST_ENTRY("browser.urlbar.loglevel"), + PREF_LIST_ENTRY("browser.urlbar.opencompanionsearch.enabled"), + PREF_LIST_ENTRY("capability.policy"), + PREF_LIST_ENTRY("dom.securecontext.allowlist"), + PREF_LIST_ENTRY("extensions.foobaz"), + PREF_LIST_ENTRY( + "extensions.formautofill.creditCards.heuristics.testConfidence"), + PREF_LIST_ENTRY("general.appname.override"), + PREF_LIST_ENTRY("general.appversion.override"), + PREF_LIST_ENTRY("general.buildID.override"), + PREF_LIST_ENTRY("general.oscpu.override"), + PREF_LIST_ENTRY("general.useragent.override"), + PREF_LIST_ENTRY("general.platform.override"), + PREF_LIST_ENTRY("gfx.blacklist."), + PREF_LIST_ENTRY("font.system.whitelist"), + PREF_LIST_ENTRY("font.name."), + PREF_LIST_ENTRY("logging.config.LOG_FILE"), + PREF_LIST_ENTRY("marionette.log.level"), + PREF_LIST_ENTRY("media.audio_loopback_dev"), + PREF_LIST_ENTRY("media.decoder-doctor."), + PREF_LIST_ENTRY("media.cubeb.output_device"), + PREF_LIST_ENTRY("media.getusermedia.fake-camera-name"), + PREF_LIST_ENTRY("media.hls.server.url"), + PREF_LIST_ENTRY("media.peerconnection.nat_simulator.filtering_type"), + PREF_LIST_ENTRY("media.peerconnection.nat_simulator.mapping_type"), + PREF_LIST_ENTRY("media.peerconnection.nat_simulator.redirect_address"), + PREF_LIST_ENTRY("media.peerconnection.nat_simulator.redirect_targets"), + PREF_LIST_ENTRY("media.video_loopback_dev"), + PREF_LIST_ENTRY("media.webspeech.service.endpoint"), + PREF_LIST_ENTRY("network.gio.supported-protocols"), + PREF_LIST_ENTRY("network.security.ports.banned"), + PREF_LIST_ENTRY("nimbus.syncdatastore."), + PREF_LIST_ENTRY("pdfjs."), + PREF_LIST_ENTRY("print.printer_"), + PREF_LIST_ENTRY("print_printer"), + PREF_LIST_ENTRY("places.interactions.customBlocklist"), + PREF_LIST_ENTRY( + "services.settings.preview_enabled"), // This is really a boolean + // dynamic pref, but one Nightly + // user has it set as a string... + PREF_LIST_ENTRY("spellchecker.dictionary"), + PREF_LIST_ENTRY("test.char"), + PREF_LIST_ENTRY("Test.IPC."), + PREF_LIST_ENTRY("exists.thenDoesNot"), + PREF_LIST_ENTRY("type.String."), + PREF_LIST_ENTRY("toolkit.mozprotocol.url"), + PREF_LIST_ENTRY("toolkit.telemetry.log.level"), + PREF_LIST_ENTRY("ui."), +}; + +#undef PREF_LIST_ENTRY + +static bool ShouldSanitizePreference(const Pref* const aPref) { + // In the parent process, we use a heuristic to decide if a pref + // value should be sanitized before sending to subprocesses. + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + + const char* prefName = aPref->Name(); + + // If a pref starts with this magic string, it is a Once-Initialized pref + // from Static Prefs. It should* not be in the above list and while it looks + // like a dnyamically named pref, it is not. + // * nothing enforces this + if (strncmp(prefName, "$$$", 3) == 0) { + return false; + } + + // First check against the denylist. + // The services pref is an annoying one - it's much easier to blocklist + // the whole branch and then add this one check to let this one annoying + // pref through. + for (const auto& entry : sParentOnlyPrefBranchList) { + if (strncmp(entry.mPrefBranch, prefName, entry.mLen) == 0) { + const auto* p = prefName; // This avoids clang-format doing ugly things. + return !(strncmp("services.settings.clock_skew_seconds", p, 36) == 0 || + strncmp("services.settings.last_update_seconds", p, 37) == 0 || + strncmp("services.settings.server", p, 24) == 0); + } + } + + // Then check if it's a dynamically named string preference and not + // in the override list + if (aPref->Type() == PrefType::String && !aPref->HasDefaultValue()) { + for (const auto& entry : sDynamicPrefOverrideList) { + if (strncmp(entry.mPrefBranch, prefName, entry.mLen) == 0) { + return false; + } + } + return true; + } + + return false; +} + +// Forward Declaration - it's not defined in the .h, because we don't need to; +// it's only used here. +template +static bool IsPreferenceSanitized_Impl(const T& aPref); + +static bool IsPreferenceSanitized(const Pref* const aPref) { + return IsPreferenceSanitized_Impl(*aPref); +} + +static bool IsPreferenceSanitized(const PrefWrapper& aPref) { + return IsPreferenceSanitized_Impl(aPref); +} + +template +static bool IsPreferenceSanitized_Impl(const T& aPref) { + if (aPref.IsSanitized()) { + MOZ_DIAGNOSTIC_ASSERT(!XRE_IsParentProcess()); + MOZ_DIAGNOSTIC_ASSERT(XRE_IsContentProcess()); + return true; + } + return false; +} + +namespace mozilla { + +// This is the only Check Sanitization function exposed outside of +// Preferences.cpp, because this is the only one ever called from +// outside this file. +bool IsPreferenceSanitized(const char* aPrefName) { + // Perform this comparison (see notes above) early to avoid a lookup + // if we can avoid it. + if (strncmp(aPrefName, "$$$", 3) == 0) { + return false; + } + + if (!gContentProcessPrefsAreInited) { + return false; + } + + if (Maybe pref = pref_Lookup(aPrefName)) { + if (pref.isNothing()) { + return true; + } + return IsPreferenceSanitized(pref.value()); + } + + return true; +} + +Atomic sOmitBlocklistedPrefValues(false); +Atomic sCrashOnBlocklistedPref(false); + +void OnFissionBlocklistPrefChange(const char* aPref, void* aData) { + if (strcmp(aPref, kFissionEnforceBlockList) == 0) { + sCrashOnBlocklistedPref = + StaticPrefs::fission_enforceBlocklistedPrefsInSubprocesses(); + } else if (strcmp(aPref, kFissionOmitBlockListValues) == 0) { + sOmitBlocklistedPrefValues = + StaticPrefs::fission_omitBlocklistedPrefsInSubprocesses(); + } else { + MOZ_CRASH("Unknown pref passed to callback"); + } +} + +} // namespace mozilla + +// This file contains the C wrappers for the C++ static pref getters, as used +// by Rust code. +#include "init/StaticPrefsCGetters.cpp" diff --git a/modules/libpref/Preferences.h b/modules/libpref/Preferences.h new file mode 100644 index 0000000000..7540755422 --- /dev/null +++ b/modules/libpref/Preferences.h @@ -0,0 +1,551 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +// Documentation for libpref is in modules/libpref/docs/index.rst. + +#ifndef mozilla_Preferences_h +#define mozilla_Preferences_h + +#ifndef MOZILLA_INTERNAL_API +# error "This header is only usable from within libxul (MOZILLA_INTERNAL_API)." +#endif + +#include "mozilla/Atomics.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/StaticPtr.h" +#include "nsCOMPtr.h" +#include "nsIObserver.h" +#include "nsIPrefBranch.h" +#include "nsIPrefService.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsWeakReference.h" +#include "nsXULAppAPI.h" +#include +#include + +class nsIFile; + +// The callback function will get passed the pref name which triggered the call +// and the void* data which was passed to the registered callback function. +typedef void (*PrefChangedFunc)(const char* aPref, void* aData); + +class nsPrefBranch; + +namespace mozilla { + +struct RegisterCallbacksInternal; + +void UnloadPrefsModule(); + +class PreferenceServiceReporter; + +namespace dom { +class Pref; +class PrefValue; +} // namespace dom + +namespace ipc { +class FileDescriptor; +} // namespace ipc + +struct PrefsSizes; + +// Xlib.h defines Bool as a macro constant. Don't try to define this enum if +// it's already been included. +#ifndef Bool + +// Keep this in sync with PrefType in parser/src/lib.rs. +enum class PrefType : uint8_t { + None = 0, // only used when neither the default nor user value is set + String = 1, + Int = 2, + Bool = 3, +}; + +#endif + +#ifdef XP_UNIX +// We need to send two shared memory descriptors to every child process: +// +// 1) A read-only/write-protected snapshot of the initial state of the +// preference database. This memory is shared between all processes, and +// therefore cannot be modified once it has been created. +// +// 2) A set of changes on top of the snapshot, containing the current values of +// all preferences which have changed since it was created. +// +// Since the second set will be different for every process, and the first set +// cannot be modified, it is unfortunately not possible to combine them into a +// single file descriptor. +// +// XXX: bug 1440207 is about improving how fixed fds such as this are used. +static const int kPrefsFileDescriptor = 8; +static const int kPrefMapFileDescriptor = 9; +#endif + +// Keep this in sync with PrefType in parser/src/lib.rs. +enum class PrefValueKind : uint8_t { Default, User }; + +class Preferences final : public nsIPrefService, + public nsIObserver, + public nsIPrefBranch, + public nsSupportsWeakReference { + friend class ::nsPrefBranch; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIPREFSERVICE + NS_FORWARD_NSIPREFBRANCH(mRootBranch->) + NS_DECL_NSIOBSERVER + + Preferences(); + + // Returns true if the Preferences service is available, false otherwise. + static bool IsServiceAvailable(); + + // Initialize user prefs from prefs.js/user.js + static void InitializeUserPrefs(); + static void FinishInitializingUserPrefs(); + + // Returns the singleton instance which is addreffed. + static already_AddRefed GetInstanceForService(); + + // Finallizes global members. + static void Shutdown(); + + // Returns shared pref service instance NOTE: not addreffed. + static nsIPrefService* GetService() { + NS_ENSURE_TRUE(InitStaticMembers(), nullptr); + return sPreferences; + } + + // Returns shared pref branch instance. NOTE: not addreffed. + static nsIPrefBranch* GetRootBranch( + PrefValueKind aKind = PrefValueKind::User) { + NS_ENSURE_TRUE(InitStaticMembers(), nullptr); + return (aKind == PrefValueKind::Default) ? sPreferences->mDefaultRootBranch + : sPreferences->mRootBranch; + } + + // Gets the type of the pref. + static int32_t GetType(const char* aPrefName); + + // Fallible value getters. When `aKind` is `User` they will get the user + // value if possible, and fall back to the default value otherwise. + static nsresult GetBool(const char* aPrefName, bool* aResult, + PrefValueKind aKind = PrefValueKind::User); + static nsresult GetInt(const char* aPrefName, int32_t* aResult, + PrefValueKind aKind = PrefValueKind::User); + static nsresult GetUint(const char* aPrefName, uint32_t* aResult, + PrefValueKind aKind = PrefValueKind::User) { + return GetInt(aPrefName, reinterpret_cast(aResult), aKind); + } + static nsresult GetFloat(const char* aPrefName, float* aResult, + PrefValueKind aKind = PrefValueKind::User); + static nsresult GetCString(const char* aPrefName, nsACString& aResult, + PrefValueKind aKind = PrefValueKind::User); + static nsresult GetString(const char* aPrefName, nsAString& aResult, + PrefValueKind aKind = PrefValueKind::User); + static nsresult GetLocalizedCString( + const char* aPrefName, nsACString& aResult, + PrefValueKind aKind = PrefValueKind::User); + static nsresult GetLocalizedString(const char* aPrefName, nsAString& aResult, + PrefValueKind aKind = PrefValueKind::User); + static nsresult GetComplex(const char* aPrefName, const nsIID& aType, + void** aResult, + PrefValueKind aKind = PrefValueKind::User); + + // Infallible getters of user or default values, with fallback results on + // failure. When `aKind` is `User` they will get the user value if possible, + // and fall back to the default value otherwise. + static bool GetBool(const char* aPrefName, bool aFallback = false, + PrefValueKind aKind = PrefValueKind::User); + static int32_t GetInt(const char* aPrefName, int32_t aFallback = 0, + PrefValueKind aKind = PrefValueKind::User); + static uint32_t GetUint(const char* aPrefName, uint32_t aFallback = 0, + PrefValueKind aKind = PrefValueKind::User); + static float GetFloat(const char* aPrefName, float aFallback = 0.0f, + PrefValueKind aKind = PrefValueKind::User); + + // Value setters. These fail if run outside the parent process. + + static nsresult SetBool(const char* aPrefName, bool aValue, + PrefValueKind aKind = PrefValueKind::User); + static nsresult SetInt(const char* aPrefName, int32_t aValue, + PrefValueKind aKind = PrefValueKind::User); + static nsresult SetCString(const char* aPrefName, const nsACString& aValue, + PrefValueKind aKind = PrefValueKind::User); + + static nsresult SetUint(const char* aPrefName, uint32_t aValue, + PrefValueKind aKind = PrefValueKind::User) { + return SetInt(aPrefName, static_cast(aValue), aKind); + } + + static nsresult SetFloat(const char* aPrefName, float aValue, + PrefValueKind aKind = PrefValueKind::User) { + nsAutoCString value; + value.AppendFloat(aValue); + return SetCString(aPrefName, value, aKind); + } + + static nsresult SetCString(const char* aPrefName, const char* aValue, + PrefValueKind aKind = PrefValueKind::User) { + return Preferences::SetCString(aPrefName, nsDependentCString(aValue), + aKind); + } + + static nsresult SetString(const char* aPrefName, const char16ptr_t aValue, + PrefValueKind aKind = PrefValueKind::User) { + return Preferences::SetCString(aPrefName, NS_ConvertUTF16toUTF8(aValue), + aKind); + } + + static nsresult SetString(const char* aPrefName, const nsAString& aValue, + PrefValueKind aKind = PrefValueKind::User) { + return Preferences::SetCString(aPrefName, NS_ConvertUTF16toUTF8(aValue), + aKind); + } + + static nsresult SetComplex(const char* aPrefName, const nsIID& aType, + nsISupports* aValue, + PrefValueKind aKind = PrefValueKind::User); + + static nsresult Lock(const char* aPrefName); + static nsresult Unlock(const char* aPrefName); + static bool IsLocked(const char* aPrefName); + static bool IsSanitized(const char* aPrefName); + + // Clears user set pref. Fails if run outside the parent process. + static nsresult ClearUser(const char* aPrefName); + + // Whether the pref has a user value or not. + static bool HasUserValue(const char* aPref); + + // Whether the pref has a user value or not. + static bool HasDefaultValue(const char* aPref); + + // Adds/Removes the observer for the root pref branch. See nsIPrefBranch.idl + // for details. + static nsresult AddStrongObserver(nsIObserver* aObserver, + const nsACString& aPref); + static nsresult AddWeakObserver(nsIObserver* aObserver, + const nsACString& aPref); + static nsresult RemoveObserver(nsIObserver* aObserver, + const nsACString& aPref); + + template + static nsresult AddStrongObserver(nsIObserver* aObserver, + const char (&aPref)[N]) { + return AddStrongObserver(aObserver, nsLiteralCString(aPref)); + } + template + static nsresult AddWeakObserver(nsIObserver* aObserver, + const char (&aPref)[N]) { + return AddWeakObserver(aObserver, nsLiteralCString(aPref)); + } + template + static nsresult RemoveObserver(nsIObserver* aObserver, + const char (&aPref)[N]) { + return RemoveObserver(aObserver, nsLiteralCString(aPref)); + } + + // Adds/Removes two or more observers for the root pref branch. Pass to + // aPrefs an array of const char* whose last item is nullptr. + // Note: All preference strings *must* be statically-allocated string + // literals. + static nsresult AddStrongObservers(nsIObserver* aObserver, + const char** aPrefs); + static nsresult AddWeakObservers(nsIObserver* aObserver, const char** aPrefs); + static nsresult RemoveObservers(nsIObserver* aObserver, const char** aPrefs); + + // Registers/Unregisters the callback function for the aPref. + template + static nsresult RegisterCallback(PrefChangedFunc aCallback, + const nsACString& aPref, + T* aClosure = nullptr) { + return RegisterCallback(aCallback, aPref, aClosure, ExactMatch); + } + + template + static nsresult UnregisterCallback(PrefChangedFunc aCallback, + const nsACString& aPref, + T* aClosure = nullptr) { + return UnregisterCallback(aCallback, aPref, aClosure, ExactMatch); + } + + // Like RegisterCallback, but also calls the callback immediately for + // initialization. + template + static nsresult RegisterCallbackAndCall(PrefChangedFunc aCallback, + const nsACString& aPref, + T* aClosure = nullptr) { + return RegisterCallbackAndCall(aCallback, aPref, aClosure, ExactMatch); + } + + // Like RegisterCallback, but registers a callback for a prefix of multiple + // pref names, not a single pref name. + template + static nsresult RegisterPrefixCallback(PrefChangedFunc aCallback, + const nsACString& aPref, + T* aClosure = nullptr) { + return RegisterCallback(aCallback, aPref, aClosure, PrefixMatch); + } + + // Like RegisterPrefixCallback, but also calls the callback immediately for + // initialization. + template + static nsresult RegisterPrefixCallbackAndCall(PrefChangedFunc aCallback, + const nsACString& aPref, + T* aClosure = nullptr) { + return RegisterCallbackAndCall(aCallback, aPref, aClosure, PrefixMatch); + } + + // Unregister a callback registered with RegisterPrefixCallback or + // RegisterPrefixCallbackAndCall. + template + static nsresult UnregisterPrefixCallback(PrefChangedFunc aCallback, + const nsACString& aPref, + T* aClosure = nullptr) { + return UnregisterCallback(aCallback, aPref, aClosure, PrefixMatch); + } + + // Variants of the above which register a single callback to handle multiple + // preferences. + // + // The array of preference names must be null terminated. It may be + // dynamically allocated, but the caller is responsible for keeping it alive + // until the callback is unregistered. + // + // Also note that the exact same aPrefs pointer must be passed to the + // Unregister call as was passed to the Register call. + template + static nsresult RegisterCallbacks(PrefChangedFunc aCallback, + const char** aPrefs, + T* aClosure = nullptr) { + return RegisterCallbacks(aCallback, aPrefs, aClosure, ExactMatch); + } + static nsresult RegisterCallbacksAndCall(PrefChangedFunc aCallback, + const char** aPrefs, + void* aClosure = nullptr); + template + static nsresult UnregisterCallbacks(PrefChangedFunc aCallback, + const char** aPrefs, + T* aClosure = nullptr) { + return UnregisterCallbacks(aCallback, aPrefs, aClosure, ExactMatch); + } + template + static nsresult RegisterPrefixCallbacks(PrefChangedFunc aCallback, + const char** aPrefs, + T* aClosure = nullptr) { + return RegisterCallbacks(aCallback, aPrefs, aClosure, PrefixMatch); + } + template + static nsresult UnregisterPrefixCallbacks(PrefChangedFunc aCallback, + const char** aPrefs, + T* aClosure = nullptr) { + return UnregisterCallbacks(aCallback, aPrefs, aClosure, PrefixMatch); + } + + template + static nsresult RegisterCallback(PrefChangedFunc aCallback, + const char (&aPref)[N], + T* aClosure = nullptr) { + return RegisterCallback(aCallback, nsLiteralCString(aPref), aClosure, + ExactMatch); + } + + template + static nsresult UnregisterCallback(PrefChangedFunc aCallback, + const char (&aPref)[N], + T* aClosure = nullptr) { + return UnregisterCallback(aCallback, nsLiteralCString(aPref), aClosure, + ExactMatch); + } + + template + static nsresult RegisterCallbackAndCall(PrefChangedFunc aCallback, + const char (&aPref)[N], + T* aClosure = nullptr) { + return RegisterCallbackAndCall(aCallback, nsLiteralCString(aPref), aClosure, + ExactMatch); + } + + template + static nsresult RegisterPrefixCallback(PrefChangedFunc aCallback, + const char (&aPref)[N], + T* aClosure = nullptr) { + return RegisterCallback(aCallback, nsLiteralCString(aPref), aClosure, + PrefixMatch); + } + + template + static nsresult RegisterPrefixCallbackAndCall(PrefChangedFunc aCallback, + const char (&aPref)[N], + T* aClosure = nullptr) { + return RegisterCallbackAndCall(aCallback, nsLiteralCString(aPref), aClosure, + PrefixMatch); + } + + template + static nsresult UnregisterPrefixCallback(PrefChangedFunc aCallback, + const char (&aPref)[N], + T* aClosure = nullptr) { + return UnregisterCallback(aCallback, nsLiteralCString(aPref), aClosure, + PrefixMatch); + } + + // When a content process is created these methods are used to pass changed + // prefs in bulk from the parent process, via shared memory. + static void SerializePreferences(nsCString& aStr, + bool aIsDestinationWebContentProcess); + static void DeserializePreferences(char* aStr, size_t aPrefsLen); + + static mozilla::ipc::FileDescriptor EnsureSnapshot(size_t* aSize); + static void InitSnapshot(const mozilla::ipc::FileDescriptor&, size_t aSize); + + // When a single pref is changed in the parent process, these methods are + // used to pass the update to content processes. + static void GetPreference(dom::Pref* aPref, + const GeckoProcessType aDestinationProcessType, + const nsACString& aDestinationRemoteType); + static void SetPreference(const dom::Pref& aPref); + +#ifdef DEBUG + static bool ArePrefsInitedInContentProcess(); +#endif + + static void AddSizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf, + PrefsSizes& aSizes); + + static void HandleDirty(); + + // Explicitly choosing synchronous or asynchronous (if allowed) preferences + // file write. Only for the default file. The guarantee for the "blocking" + // is that when it returns, the file on disk reflect the current state of + // preferences. + nsresult SavePrefFileBlocking(); + nsresult SavePrefFileAsynchronous(); + + // If this is false, only blocking writes, on main thread are allowed. + bool AllowOffMainThreadSave(); + + private: + virtual ~Preferences(); + + nsresult NotifyServiceObservers(const char* aSubject); + + // Loads the prefs.js file from the profile, or creates a new one. Returns + // the prefs file if successful, or nullptr on failure. + already_AddRefed ReadSavedPrefs(); + + // Loads the user.js file from the profile if present. + void ReadUserOverridePrefs(); + + nsresult MakeBackupPrefFile(nsIFile* aFile); + + // Default pref file save can be blocking or not. + enum class SaveMethod { Blocking, Asynchronous }; + + // Off main thread is only respected for the default aFile value (nullptr). + nsresult SavePrefFileInternal(nsIFile* aFile, SaveMethod aSaveMethod); + nsresult WritePrefFile(nsIFile* aFile, SaveMethod aSaveMethod); + + nsresult ResetUserPrefs(); + + // Helpers for implementing + // Register(Prefix)Callback/Unregister(Prefix)Callback. + public: + // Public so the ValueObserver classes can use it. + enum MatchKind { + PrefixMatch, + ExactMatch, + }; + + private: + static void SetupTelemetryPref(); + static nsresult InitInitialObjects(bool aIsStartup); + + friend struct Internals; + + static nsresult RegisterCallback(PrefChangedFunc aCallback, + const nsACString& aPref, void* aClosure, + MatchKind aMatchKind, + bool aIsPriority = false); + static nsresult UnregisterCallback(PrefChangedFunc aCallback, + const nsACString& aPref, void* aClosure, + MatchKind aMatchKind); + static nsresult RegisterCallbackAndCall(PrefChangedFunc aCallback, + const nsACString& aPref, + void* aClosure, MatchKind aMatchKind); + + static nsresult RegisterCallbacks(PrefChangedFunc aCallback, + const char** aPrefs, void* aClosure, + MatchKind aMatchKind); + static nsresult UnregisterCallbacks(PrefChangedFunc aCallback, + const char** aPrefs, void* aClosure, + MatchKind aMatchKind); + + template + static nsresult RegisterCallbackImpl(PrefChangedFunc aCallback, T& aPref, + void* aClosure, MatchKind aMatchKind, + bool aIsPriority = false); + template + static nsresult UnregisterCallbackImpl(PrefChangedFunc aCallback, T& aPref, + void* aClosure, MatchKind aMatchKind); + + static nsresult RegisterCallback(PrefChangedFunc aCallback, const char* aPref, + void* aClosure, MatchKind aMatchKind, + bool aIsPriority = false) { + return RegisterCallback(aCallback, nsDependentCString(aPref), aClosure, + aMatchKind, aIsPriority); + } + static nsresult UnregisterCallback(PrefChangedFunc aCallback, + const char* aPref, void* aClosure, + MatchKind aMatchKind) { + return UnregisterCallback(aCallback, nsDependentCString(aPref), aClosure, + aMatchKind); + } + static nsresult RegisterCallbackAndCall(PrefChangedFunc aCallback, + const char* aPref, void* aClosure, + MatchKind aMatchKind) { + return RegisterCallbackAndCall(aCallback, nsDependentCString(aPref), + aClosure, aMatchKind); + } + + private: + nsCOMPtr mCurrentFile; + bool mDirty = false; + bool mProfileShutdown = false; + // We wait a bit after prefs are dirty before writing them. In this period, + // mDirty and mSavePending will both be true. + bool mSavePending = false; + + nsCOMPtr mRootBranch; + nsCOMPtr mDefaultRootBranch; + + static StaticRefPtr sPreferences; + static bool sShutdown; + + // Init static members. Returns true on success. + static bool InitStaticMembers(); +}; + +extern Atomic sOmitBlocklistedPrefValues; +extern Atomic sCrashOnBlocklistedPref; + +bool IsPreferenceSanitized(const char* aPref); + +const char kFissionEnforceBlockList[] = + "fission.enforceBlocklistedPrefsInSubprocesses"; +const char kFissionOmitBlockListValues[] = + "fission.omitBlocklistedPrefsInSubprocesses"; + +void OnFissionBlocklistPrefChange(const char* aPref, void* aData); + +} // namespace mozilla + +#endif // mozilla_Preferences_h diff --git a/modules/libpref/SharedPrefMap.cpp b/modules/libpref/SharedPrefMap.cpp new file mode 100644 index 0000000000..81f8d92485 --- /dev/null +++ b/modules/libpref/SharedPrefMap.cpp @@ -0,0 +1,236 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "SharedPrefMap.h" + +#include "mozilla/dom/ipc/MemMapSnapshot.h" + +#include "mozilla/BinarySearch.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/ipc/FileDescriptor.h" + +using namespace mozilla::loader; + +namespace mozilla { + +using namespace ipc; + +static inline size_t GetAlignmentOffset(size_t aOffset, size_t aAlign) { + auto mod = aOffset % aAlign; + return mod ? aAlign - mod : 0; +} + +SharedPrefMap::SharedPrefMap(const FileDescriptor& aMapFile, size_t aMapSize) { + auto result = mMap.initWithHandle(aMapFile, aMapSize); + MOZ_RELEASE_ASSERT(result.isOk()); + // We return literal nsCStrings pointing to the mapped data for preference + // names and string values, which means that we may still have references to + // the mapped data even after this instance is destroyed. That means that we + // need to keep the mapping alive until process shutdown, in order to be safe. + mMap.setPersistent(); +} + +SharedPrefMap::SharedPrefMap(SharedPrefMapBuilder&& aBuilder) { + auto result = aBuilder.Finalize(mMap); + MOZ_RELEASE_ASSERT(result.isOk()); + mMap.setPersistent(); +} + +mozilla::ipc::FileDescriptor SharedPrefMap::CloneFileDescriptor() const { + return mMap.cloneHandle(); +} + +bool SharedPrefMap::Has(const char* aKey) const { + size_t index; + return Find(aKey, &index); +} + +Maybe SharedPrefMap::Get(const char* aKey) const { + Maybe result; + + size_t index; + if (Find(aKey, &index)) { + result.emplace(Pref{this, &Entries()[index]}); + } + + return result; +} + +bool SharedPrefMap::Find(const char* aKey, size_t* aIndex) const { + const auto& keys = KeyTable(); + + return BinarySearchIf( + Entries(), 0, EntryCount(), + [&](const Entry& aEntry) { + return strcmp(aKey, keys.GetBare(aEntry.mKey)); + }, + aIndex); +} + +void SharedPrefMapBuilder::Add(const nsCString& aKey, const Flags& aFlags, + bool aDefaultValue, bool aUserValue) { + mEntries.AppendElement(Entry{ + aKey.get(), + mKeyTable.Add(aKey), + {aDefaultValue, aUserValue}, + uint8_t(PrefType::Bool), + aFlags.mHasDefaultValue, + aFlags.mHasUserValue, + aFlags.mIsSticky, + aFlags.mIsLocked, + aFlags.mIsSanitized, + aFlags.mIsSkippedByIteration, + }); +} + +void SharedPrefMapBuilder::Add(const nsCString& aKey, const Flags& aFlags, + int32_t aDefaultValue, int32_t aUserValue) { + ValueIdx index; + if (aFlags.mHasUserValue) { + index = mIntValueTable.Add(aDefaultValue, aUserValue); + } else { + index = mIntValueTable.Add(aDefaultValue); + } + + mEntries.AppendElement(Entry{ + aKey.get(), + mKeyTable.Add(aKey), + {index}, + uint8_t(PrefType::Int), + aFlags.mHasDefaultValue, + aFlags.mHasUserValue, + aFlags.mIsSticky, + aFlags.mIsLocked, + aFlags.mIsSanitized, + aFlags.mIsSkippedByIteration, + }); +} + +void SharedPrefMapBuilder::Add(const nsCString& aKey, const Flags& aFlags, + const nsCString& aDefaultValue, + const nsCString& aUserValue) { + ValueIdx index; + StringTableEntry defaultVal = mValueStringTable.Add(aDefaultValue); + if (aFlags.mHasUserValue) { + StringTableEntry userVal = mValueStringTable.Add(aUserValue); + index = mStringValueTable.Add(defaultVal, userVal); + } else { + index = mStringValueTable.Add(defaultVal); + } + + mEntries.AppendElement(Entry{ + aKey.get(), + mKeyTable.Add(aKey), + {index}, + uint8_t(PrefType::String), + aFlags.mHasDefaultValue, + aFlags.mHasUserValue, + aFlags.mIsSticky, + aFlags.mIsLocked, + aFlags.mIsSanitized, + aFlags.mIsSkippedByIteration, + }); +} + +Result SharedPrefMapBuilder::Finalize(loader::AutoMemMap& aMap) { + using Header = SharedPrefMap::Header; + + // Create an array of entry pointers for the entry array, and sort it by + // preference name prior to serialization, so that entries can be looked up + // using binary search. + nsTArray entries(mEntries.Length()); + for (auto& entry : mEntries) { + entries.AppendElement(&entry); + } + entries.Sort([](const Entry* aA, const Entry* aB) { + return strcmp(aA->mKeyString, aB->mKeyString); + }); + + Header header = {uint32_t(entries.Length())}; + + size_t offset = sizeof(header); + offset += GetAlignmentOffset(offset, alignof(Header)); + + offset += entries.Length() * sizeof(SharedPrefMap::Entry); + + header.mKeyStrings.mOffset = offset; + header.mKeyStrings.mSize = mKeyTable.Size(); + offset += header.mKeyStrings.mSize; + + offset += GetAlignmentOffset(offset, mIntValueTable.Alignment()); + header.mUserIntValues.mOffset = offset; + header.mUserIntValues.mSize = mIntValueTable.UserSize(); + offset += header.mUserIntValues.mSize; + + offset += GetAlignmentOffset(offset, mIntValueTable.Alignment()); + header.mDefaultIntValues.mOffset = offset; + header.mDefaultIntValues.mSize = mIntValueTable.DefaultSize(); + offset += header.mDefaultIntValues.mSize; + + offset += GetAlignmentOffset(offset, mStringValueTable.Alignment()); + header.mUserStringValues.mOffset = offset; + header.mUserStringValues.mSize = mStringValueTable.UserSize(); + offset += header.mUserStringValues.mSize; + + offset += GetAlignmentOffset(offset, mStringValueTable.Alignment()); + header.mDefaultStringValues.mOffset = offset; + header.mDefaultStringValues.mSize = mStringValueTable.DefaultSize(); + offset += header.mDefaultStringValues.mSize; + + header.mValueStrings.mOffset = offset; + header.mValueStrings.mSize = mValueStringTable.Size(); + offset += header.mValueStrings.mSize; + + MemMapSnapshot mem; + MOZ_TRY(mem.Init(offset)); + + auto headerPtr = mem.Get
(); + headerPtr[0] = header; + + auto* entryPtr = reinterpret_cast(&headerPtr[1]); + for (auto* entry : entries) { + *entryPtr = { + entry->mKey, + GetValue(*entry), + entry->mType, + entry->mHasDefaultValue, + entry->mHasUserValue, + entry->mIsSticky, + entry->mIsLocked, + entry->mIsSanitized, + entry->mIsSkippedByIteration, + }; + entryPtr++; + } + + auto ptr = mem.Get(); + + mKeyTable.Write({&ptr[header.mKeyStrings.mOffset], header.mKeyStrings.mSize}); + + mValueStringTable.Write( + {&ptr[header.mValueStrings.mOffset], header.mValueStrings.mSize}); + + mIntValueTable.WriteDefaultValues( + {&ptr[header.mDefaultIntValues.mOffset], header.mDefaultIntValues.mSize}); + mIntValueTable.WriteUserValues( + {&ptr[header.mUserIntValues.mOffset], header.mUserIntValues.mSize}); + + mStringValueTable.WriteDefaultValues( + {&ptr[header.mDefaultStringValues.mOffset], + header.mDefaultStringValues.mSize}); + mStringValueTable.WriteUserValues( + {&ptr[header.mUserStringValues.mOffset], header.mUserStringValues.mSize}); + + mKeyTable.Clear(); + mValueStringTable.Clear(); + mIntValueTable.Clear(); + mStringValueTable.Clear(); + mEntries.Clear(); + + return mem.Finalize(aMap); +} + +} // namespace mozilla diff --git a/modules/libpref/SharedPrefMap.h b/modules/libpref/SharedPrefMap.h new file mode 100644 index 0000000000..7c949e137d --- /dev/null +++ b/modules/libpref/SharedPrefMap.h @@ -0,0 +1,848 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 dom_ipc_SharedPrefMap_h +#define dom_ipc_SharedPrefMap_h + +#include "mozilla/AutoMemMap.h" +#include "mozilla/HashFunctions.h" +#include "mozilla/Preferences.h" +#include "mozilla/Result.h" +#include "mozilla/dom/ipc/StringTable.h" +#include "nsTHashMap.h" + +namespace mozilla { + +// The approximate number of preferences expected to be in an ordinary +// preferences database. +// +// This number is used to determine initial allocation sizes for data structures +// when building the shared preference map, and should be slightly higher than +// the expected number of preferences in an ordinary database to avoid +// unnecessary reallocations/rehashes. +constexpr size_t kExpectedPrefCount = 4000; + +class SharedPrefMapBuilder; + +// This class provides access to a compact, read-only copy of a preference +// database, backed by a shared memory buffer which can be shared between +// processes. All state data for the database is stored in the shared memory +// region, so individual instances require no dynamic memory allocation. +// +// Further, all strings returned from this API are nsLiteralCStrings with +// pointers into the shared memory region, which means that they can be copied +// into new nsCString instances without additional allocations. For instance, +// the following (where `pref` is a Pref object) will not cause any string +// copies, memory allocations, or atomic refcount changes: +// +// nsCString prefName(pref.NameString()); +// +// whereas if we returned a nsDependentCString or a dynamically allocated +// nsCString, it would. +// +// The set of entries is stored in sorted order by preference name, so look-ups +// are done by binary search. This means that look-ups have O(log n) complexity, +// rather than the O(1) complexity of a dynamic hashtable. Consumers should keep +// this in mind when planning their accesses. +// +// Important: The mapped memory created by this class is persistent. Once an +// instance has been initialized, the memory that it allocates can never be +// freed before process shutdown. Do not use it for short-lived mappings. +class SharedPrefMap { + using FileDescriptor = mozilla::ipc::FileDescriptor; + + friend class SharedPrefMapBuilder; + + // Describes a block of memory within the shared memory region. + struct DataBlock { + // The byte offset from the start of the shared memory region to the start + // of the block. + size_t mOffset; + // The size of the block, in bytes. This is typically used only for bounds + // checking in debug builds. + size_t mSize; + }; + + // Describes the contents of the shared memory region, which is laid-out as + // follows: + // + // - The Header struct + // + // - An array of Entry structs with mEntryCount elements, lexicographically + // sorted by preference name. + // + // - A set of data blocks, with offsets and sizes described by the DataBlock + // entries in the header, described below. + // + // Each entry stores its name string and values as indices into these blocks, + // as documented in the Entry struct, but with some important optimizations: + // + // - Boolean values are always stored inline. Both the default and user + // values can be retrieved directly from the entry. Other types have only + // one value index, and their values appear at the same indices in the + // default and user value arrays. + // + // Aside from reducing our memory footprint, this space-efficiency means + // that we can fit more entries in the CPU cache at once, and reduces the + // number of likely cache misses during lookups. + // + // - Key strings are stored in a separate string table from value strings. As + // above, this makes it more likely that the strings we need will be + // available in the CPU cache during lookups by not interleaving them with + // extraneous data. + // + // - Default and user values are stored in separate arrays. Entries with user + // values always appear before entries with default values in the value + // arrays, and entries without user values do not have entries in the user + // array at all. Since the same index is used for both arrays, this means + // that entries with a default value but no user value do not allocate any + // space to store their user value. + // + // - For preferences with no user value, the entries in the default value are + // de-duplicated. All preferences with the same default value (and no user + // value) point to the same index in the default value array. + // + // + // For example, a preference database containing: + // + // +---------+-------------------------------+-------------------------------+ + // | Name | Default Value | User Value | | + // +---------+---------------+---------------+-------------------------------+ + // | string1 | "meh" | "hem" | | + // | string2 | | "b" | | + // | string3 | "a" | | | + // | string4 | "foo" | | | + // | string5 | "foo" | | | + // | string6 | "meh" | | | + // +---------+---------------+---------------+-------------------------------+ + // | bool1 | false | true | | + // | bool2 | | false | | + // | bool3 | true | | | + // +---------+---------------+---------------+-------------------------------+ + // | int1 | 18 | 16 | | + // | int2 | | 24 | | + // | int3 | 42 | | | + // | int4 | 12 | | | + // | int5 | 12 | | | + // | int6 | 18 | | | + // +---------+---------------+---------------+-------------------------------+ + // + // Results in a database that looks like: + // + // +-------------------------------------------------------------------------+ + // | Header: | + // +-------------------------------------------------------------------------+ + // | mEntryCount = 15 | + // | ... | + // +-------------------------------------------------------------------------+ + // + // +-------------------------------------------------------------------------+ + // | Key strings: | + // +--------+----------------------------------------------------------------+ + // | Offset | Value | + // +--------+----------------------------------------------------------------+ + // | 0 | string1\0 | + // | 8 | string2\0 | + // | 16 | string3\0 | + // | 24 | string4\0 | + // | 32 | string5\0 | + // | 40 | string6\0 | + // | 48 | bool1\0 | + // | 54 | bool2\0 | + // | 60 | bool3\0 | + // | 66 | int1\0 | + // | 71 | int2\0 | + // | 76 | int3\0 | + // | 81 | int4\0 | + // | 86 | int6\0 | + // | 91 | int6\0 | + // +--------+----------------------------------------------------------------+ + // + // +-------------------------------------------------------------------------+ + // | Entries: | + // +---------------------+------+------------+------------+------------------+ + // | Key[1] | Type | HasDefault | HasUser | Value | + // +---------------------+------+------------+------------+------------------+ + // | K["bool1", 48, 5] | 3 | true | true | { false, true } | + // | K["bool2", 54, 5] | 3 | false | true | { 0, false } | + // | K["bool3", 60, 5] | 3 | true | false | { true, 0 } | + // | K["int1", 66, 4] | 2 | true | true | 0 | + // | K["int2", 71, 4] | 2 | false | true | 1 | + // | K["int3", 76, 4] | 2 | true | false | 2 | + // | K["int4", 81, 4] | 2 | true | false | 3 | + // | K["int5", 86, 4] | 2 | true | false | 3 | + // | K["int6", 91, 4] | 2 | true | false | 4 | + // | K["string1", 0, 6] | 1 | true | true | 0 | + // | K["string2", 8, 6] | 1 | false | true | 1 | + // | K["string3", 16, 6] | 1 | true | false | 2 | + // | K["string4", 24, 6] | 1 | true | false | 3 | + // | K["string5", 32, 6] | 1 | true | false | 3 | + // | K["string6", 40, 6] | 1 | true | false | 4 | + // +---------------------+------+------------+------------+------------------+ + // | [1]: Encoded as an offset into the key table and a length. Specified | + // | as K[string, offset, length] for clarity. | + // +-------------------------------------------------------------------------+ + // + // +------------------------------------+------------------------------------+ + // | User integer values | Default integer values | + // +-------+----------------------------+-------+----------------------------+ + // | Index | Contents | Index | Contents | + // +-------+----------------------------+-------+----------------------------+ + // | 0 | 16 | 0 | 18 | + // | 1 | 24 | 1 | | + // | | | 2 | 42 | + // | | | 3 | 12 | + // | | | 4 | 18 | + // +-------+----------------------------+-------+----------------------------+ + // | * Note: Tables are laid out sequentially in memory, but displayed | + // | here side-by-side for clarity. | + // +-------------------------------------------------------------------------+ + // + // +------------------------------------+------------------------------------+ + // | User string values | Default string values | + // +-------+----------------------------+-------+----------------------------+ + // | Index | Contents[1] | Index | Contents[1] | + // +-------+----------------------------+-------+----------------------------+ + // | 0 | V["hem", 0, 3] | 0 | V["meh", 4, 3] | + // | 1 | V["b", 8, 1] | 1 | | + // | | | 2 | V["a", 10, 1] | + // | | | 3 | V["foo", 12, 3] | + // | | | 4 | V["meh", 4, 3] | + // |-------+----------------------------+-------+----------------------------+ + // | [1]: Encoded as an offset into the value table and a length. Specified | + // | as V[string, offset, length] for clarity. | + // +-------------------------------------------------------------------------+ + // | * Note: Tables are laid out sequentially in memory, but displayed | + // | here side-by-side for clarity. | + // +-------------------------------------------------------------------------+ + // + // +-------------------------------------------------------------------------+ + // | Value strings: | + // +--------+----------------------------------------------------------------+ + // | Offset | Value | + // +--------+----------------------------------------------------------------+ + // | 0 | hem\0 | + // | 4 | meh\0 | + // | 8 | b\0 | + // | 10 | a\0 | + // | 12 | foo\0 | + // +--------+----------------------------------------------------------------+ + struct Header { + // The number of entries in this map. + uint32_t mEntryCount; + + // The StringTable data block for preference name strings, which act as keys + // in the map. + DataBlock mKeyStrings; + + // The int32_t arrays of user and default int preference values. Entries in + // the map store their values as indices into these arrays. + DataBlock mUserIntValues; + DataBlock mDefaultIntValues; + + // The StringTableEntry arrays of user and default string preference values. + // + // Strings are stored as StringTableEntry structs with character offsets + // into the mValueStrings string table and their corresponding lengths. + // + // Entries in the map, likewise, store their string values as indices into + // these arrays. + DataBlock mUserStringValues; + DataBlock mDefaultStringValues; + + // The StringTable data block for string preference values, referenced by + // the above two data blocks. + DataBlock mValueStrings; + }; + + using StringTableEntry = mozilla::dom::ipc::StringTableEntry; + + // Represents a preference value, as either a pair of boolean values, or an + // index into one of the above value arrays. + union Value { + Value(bool aDefaultValue, bool aUserValue) + : mDefaultBool(aDefaultValue), mUserBool(aUserValue) {} + + MOZ_IMPLICIT Value(uint16_t aIndex) : mIndex(aIndex) {} + + // The index of this entry in the value arrays. + // + // User and default preference values have the same indices in their + // respective arrays. However, entries without a user value are not + // guaranteed to have space allocated for them in the user value array, and + // likewise for preferences without default values in the default value + // array. This means that callers must only access value entries for entries + // which claim to have a value of that type. + uint16_t mIndex; + struct { + bool mDefaultBool; + bool mUserBool; + }; + }; + + // Represents a preference entry in the map, containing its name, type info, + // flags, and a reference to its value. + struct Entry { + // A pointer to the preference name in the KeyTable string table. + StringTableEntry mKey; + + // The preference's value, either as a pair of booleans, or an index into + // the value arrays. Please see the documentation for the Value struct + // above. + Value mValue; + + // The preference's type, as a PrefType enum value. This must *never* be + // PrefType::None for values in a shared array. + uint8_t mType : 2; + // True if the preference has a default value. Callers must not attempt to + // access the entry's default value if this is false. + uint8_t mHasDefaultValue : 1; + // True if the preference has a user value. Callers must not attempt to + // access the entry's user value if this is false. + uint8_t mHasUserValue : 1; + // True if the preference is sticky, as defined by the preference service. + uint8_t mIsSticky : 1; + // True if the preference is locked, as defined by the preference service. + uint8_t mIsLocked : 1; + // True if the preference is sanitized, as defined by the preference + // service. + uint8_t mIsSanitized : 1; + // True if the preference should be skipped while iterating over the + // SharedPrefMap. This is used to internally store Once StaticPrefs. + // This property is not visible to users the way sticky and locked are. + uint8_t mIsSkippedByIteration : 1; + }; + + public: + NS_INLINE_DECL_REFCOUNTING(SharedPrefMap) + + // A temporary wrapper class for accessing entries in the array. Instances of + // this class are valid as long as SharedPrefMap instance is alive, but + // generally should not be stored long term, or allocated on the heap. + // + // The class is implemented as two pointers, one to the SharedPrefMap + // instance, and one to the Entry that corresponds to the preference, and is + // meant to be cheaply returned by value from preference lookups and + // iterators. All property accessors lazily fetch the appropriate values from + // the shared memory region. + class MOZ_STACK_CLASS Pref final { + public: + const char* Name() const { return mMap->KeyTable().GetBare(mEntry->mKey); } + + nsCString NameString() const { return mMap->KeyTable().Get(mEntry->mKey); } + + PrefType Type() const { + MOZ_ASSERT(PrefType(mEntry->mType) != PrefType::None); + return PrefType(mEntry->mType); + } + + bool HasDefaultValue() const { return mEntry->mHasDefaultValue; } + bool HasUserValue() const { return mEntry->mHasUserValue; } + bool IsLocked() const { return mEntry->mIsLocked; } + bool IsSanitized() const { return mEntry->mIsSanitized; } + bool IsSticky() const { return mEntry->mIsSticky; } + bool IsSkippedByIteration() const { return mEntry->mIsSkippedByIteration; } + + bool GetBoolValue(PrefValueKind aKind = PrefValueKind::User) const { + MOZ_ASSERT(Type() == PrefType::Bool); + MOZ_ASSERT(aKind == PrefValueKind::Default ? HasDefaultValue() + : HasUserValue()); + + return aKind == PrefValueKind::Default ? mEntry->mValue.mDefaultBool + : mEntry->mValue.mUserBool; + } + + int32_t GetIntValue(PrefValueKind aKind = PrefValueKind::User) const { + MOZ_ASSERT(Type() == PrefType::Int); + MOZ_ASSERT(aKind == PrefValueKind::Default ? HasDefaultValue() + : HasUserValue()); + + return aKind == PrefValueKind::Default + ? mMap->DefaultIntValues()[mEntry->mValue.mIndex] + : mMap->UserIntValues()[mEntry->mValue.mIndex]; + } + + private: + const StringTableEntry& GetStringEntry(PrefValueKind aKind) const { + MOZ_ASSERT(Type() == PrefType::String); + MOZ_ASSERT(aKind == PrefValueKind::Default ? HasDefaultValue() + : HasUserValue()); + + return aKind == PrefValueKind::Default + ? mMap->DefaultStringValues()[mEntry->mValue.mIndex] + : mMap->UserStringValues()[mEntry->mValue.mIndex]; + } + + public: + nsCString GetStringValue(PrefValueKind aKind = PrefValueKind::User) const { + return mMap->ValueTable().Get(GetStringEntry(aKind)); + } + + const char* GetBareStringValue( + PrefValueKind aKind = PrefValueKind::User) const { + return mMap->ValueTable().GetBare(GetStringEntry(aKind)); + } + + // Returns the entry's index in the map, as understood by GetKeyAt() and + // GetValueAt(). + size_t Index() const { return mEntry - mMap->Entries().get(); } + + bool operator==(const Pref& aPref) const { return mEntry == aPref.mEntry; } + bool operator!=(const Pref& aPref) const { return !(*this == aPref); } + + // This is odd, but necessary in order for the C++ range iterator protocol + // to work here. + Pref& operator*() { return *this; } + + // Updates this wrapper to point to the next visible entry in the map. This + // should not be attempted unless Index() is less than the map's Count(). + Pref& operator++() { + do { + mEntry++; + } while (mEntry->mIsSkippedByIteration && Index() < mMap->Count()); + return *this; + } + + Pref(const Pref& aPref) = default; + + protected: + friend class SharedPrefMap; + + Pref(const SharedPrefMap* aPrefMap, const Entry* aEntry) + : mMap(aPrefMap), mEntry(aEntry) {} + + private: + const SharedPrefMap* const mMap; + const Entry* mEntry; + }; + + // Note: These constructors are infallible, because the preference database is + // critical to platform functionality, and we cannot operate without it. + SharedPrefMap(const FileDescriptor&, size_t); + explicit SharedPrefMap(SharedPrefMapBuilder&&); + + // Searches for the given preference in the map, and returns true if it + // exists. + bool Has(const char* aKey) const; + + bool Has(const nsCString& aKey) const { return Has(aKey.get()); } + + // Searches for the given preference in the map, and if it exists, returns + // a Some containing its details. + Maybe Get(const char* aKey) const; + + Maybe Get(const nsCString& aKey) const { return Get(aKey.get()); } + + private: + // Searches for an entry for the given key. If found, returns true, and + // places its index in the entry array in aIndex. + bool Find(const char* aKey, size_t* aIndex) const; + + public: + // Returns the number of entries in the map. + uint32_t Count() const { return EntryCount(); } + + // Returns the string entry at the given index. Keys are guaranteed to be + // sorted lexicographically. + // + // The given index *must* be less than the value returned by Count(). + // + // The returned value is a literal string which references the mapped memory + // region. + nsCString GetKeyAt(uint32_t aIndex) const { + MOZ_ASSERT(aIndex < Count()); + return KeyTable().Get(Entries()[aIndex].mKey); + } + + // Returns the value for the entry at the given index. + // + // The given index *must* be less than the value returned by Count(). + // + // The returned value is valid for the lifetime of this map instance. + const Pref GetValueAt(uint32_t aIndex) const { + MOZ_ASSERT(aIndex < Count()); + return UncheckedGetValueAt(aIndex); + } + + private: + // Returns a wrapper with a pointer to an entry without checking its bounds. + // This should only be used by range iterators, to check their end positions. + // + // Note: In debug builds, the RangePtr returned by entries will still assert + // that aIndex is no more than 1 past the last element in the array, since it + // also takes into account the ranged iteration use case. + Pref UncheckedGetValueAt(uint32_t aIndex) const { + return {this, (Entries() + aIndex).get()}; + } + + public: + // C++ range iterator protocol. begin() and end() return references to the + // first (non-skippable) and last entries in the array. The begin wrapper + // can be incremented until it matches the last element in the array, at which + // point it becomes invalid and the iteration is over. + Pref begin() const { + for (uint32_t aIndex = 0; aIndex < Count(); aIndex++) { + Pref pref = UncheckedGetValueAt(aIndex); + if (!pref.IsSkippedByIteration()) { + return pref; + } + } + return end(); + } + Pref end() const { return UncheckedGetValueAt(Count()); } + + // A cosmetic helper for range iteration. Returns a reference value from a + // pointer to this instance so that its .begin() and .end() methods can be + // accessed in a ranged for loop. `map->Iter()` is equivalent to `*map`, but + // makes its purpose slightly clearer. + const SharedPrefMap& Iter() const { return *this; } + + // Returns a copy of the read-only file descriptor which backs the shared + // memory region for this map. The file descriptor may be passed between + // processes, and used to construct new instances of SharedPrefMap with + // the same data as this instance. + FileDescriptor CloneFileDescriptor() const; + + // Returns the size of the mapped memory region. This size must be passed to + // the constructor when mapping the shared region in another process. + size_t MapSize() const { return mMap.size(); } + + protected: + ~SharedPrefMap() = default; + + private: + template + using StringTable = mozilla::dom::ipc::StringTable; + + // Type-safe getters for values in the shared memory region: + const Header& GetHeader() const { return mMap.get
()[0]; } + + RangedPtr Entries() const { + return {reinterpret_cast(&GetHeader() + 1), EntryCount()}; + } + + uint32_t EntryCount() const { return GetHeader().mEntryCount; } + + template + RangedPtr GetBlock(const DataBlock& aBlock) const { + return RangedPtr(&mMap.get()[aBlock.mOffset], + aBlock.mSize) + .ReinterpretCast(); + } + + RangedPtr DefaultIntValues() const { + return GetBlock(GetHeader().mDefaultIntValues); + } + RangedPtr UserIntValues() const { + return GetBlock(GetHeader().mUserIntValues); + } + + RangedPtr DefaultStringValues() const { + return GetBlock(GetHeader().mDefaultStringValues); + } + RangedPtr UserStringValues() const { + return GetBlock(GetHeader().mUserStringValues); + } + + StringTable KeyTable() const { + auto& block = GetHeader().mKeyStrings; + return {{&mMap.get()[block.mOffset], block.mSize}}; + } + + StringTable ValueTable() const { + auto& block = GetHeader().mValueStrings; + return {{&mMap.get()[block.mOffset], block.mSize}}; + } + + loader::AutoMemMap mMap; +}; + +// A helper class which builds the contiguous look-up table used by +// SharedPrefMap. Each preference in the final map is added to the builder, +// before it is finalized and transformed into a read-only snapshot. +class MOZ_RAII SharedPrefMapBuilder { + public: + SharedPrefMapBuilder() = default; + + // The set of flags for the preference, as documented in SharedPrefMap::Entry. + struct Flags { + uint8_t mHasDefaultValue : 1; + uint8_t mHasUserValue : 1; + uint8_t mIsSticky : 1; + uint8_t mIsLocked : 1; + uint8_t mIsSanitized : 1; + uint8_t mIsSkippedByIteration : 1; + }; + + void Add(const nsCString& aKey, const Flags& aFlags, bool aDefaultValue, + bool aUserValue); + + void Add(const nsCString& aKey, const Flags& aFlags, int32_t aDefaultValue, + int32_t aUserValue); + + void Add(const nsCString& aKey, const Flags& aFlags, + const nsCString& aDefaultValue, const nsCString& aUserValue); + + // Finalizes the binary representation of the map, writes it to a shared + // memory region, and then initializes the given AutoMemMap with a reference + // to the read-only copy of it. + // + // This should generally not be used directly by callers. The + // SharedPrefMapBuilder instance should instead be passed to the SharedPrefMap + // constructor as a move reference. + Result Finalize(loader::AutoMemMap& aMap); + + private: + using StringTableEntry = mozilla::dom::ipc::StringTableEntry; + template + using StringTableBuilder = mozilla::dom::ipc::StringTableBuilder; + + // An opaque descriptor of the index of a preference entry in a value array, + // which can be converted numeric index after the ValueTableBuilder is + // finalized. + struct ValueIdx { + // The relative index of the entry, based on its class. Entries for + // preferences with user values appear at the value arrays. Entries with + // only default values begin after the last entry with a user value. + uint16_t mIndex; + bool mHasUserValue; + }; + + // A helper class for building default and user value arrays for preferences. + // + // As described in the SharedPrefMap class, this helper optimizes the way that + // it builds its value arrays, in that: + // + // - It stores value entries for all preferences with user values before + // entries for preferences with only default values, and allocates no + // entries for preferences with only default values in the user value array. + // Since most preferences have only default values, this dramatically + // reduces the space required for value storage. + // + // - For preferences with only default values, it de-duplicates value entries, + // and returns the same indices for all preferences with the same value. + // + // One important complication of this approach is that it means we cannot know + // the final index of any entry with only a default value until all entries + // have been added to the builder, since it depends on the final number of + // user entries in the output. + // + // To deal with this, when entries are added, we return an opaque ValueIndex + // struct, from which we can calculate the final index after the map has been + // finalized. + template + class ValueTableBuilder { + public: + using ValueType = ValueType_; + + // Adds an entry for a preference with only a default value to the array, + // and returns an opaque descriptor for its index. + ValueIdx Add(const ValueType& aDefaultValue) { + auto index = uint16_t(mDefaultEntries.Count()); + + return mDefaultEntries.WithEntryHandle(aDefaultValue, [&](auto&& entry) { + entry.OrInsertWith([&] { return Entry{index, false, aDefaultValue}; }); + + return ValueIdx{entry->mIndex, false}; + }); + } + + // Adds an entry for a preference with a user value to the array. Regardless + // of whether the preference has a default value, space must be allocated + // for it. For preferences with no default value, the actual value which + // appears in the array at its value index is ignored. + ValueIdx Add(const ValueType& aDefaultValue, const ValueType& aUserValue) { + auto index = uint16_t(mUserEntries.Length()); + + mUserEntries.AppendElement(Entry{index, true, aDefaultValue, aUserValue}); + + return {index, true}; + } + + // Returns the final index for an entry based on its opaque index + // descriptor. This must only be called after the caller has finished adding + // entries to the builder. + uint16_t GetIndex(const ValueIdx& aIndex) const { + uint16_t base = aIndex.mHasUserValue ? 0 : UserCount(); + return base + aIndex.mIndex; + } + + // Writes out the array of default values at the block beginning at the + // given pointer. The block must be at least as large as the value returned + // by DefaultSize(). + void WriteDefaultValues(const RangedPtr& aBuffer) const { + auto buffer = aBuffer.ReinterpretCast(); + + for (const auto& entry : mUserEntries) { + buffer[entry.mIndex] = entry.mDefaultValue; + } + + size_t defaultsOffset = UserCount(); + for (const auto& data : mDefaultEntries.Values()) { + buffer[defaultsOffset + data.mIndex] = data.mDefaultValue; + } + } + + // Writes out the array of user values at the block beginning at the + // given pointer. The block must be at least as large as the value returned + // by UserSize(). + void WriteUserValues(const RangedPtr& aBuffer) const { + auto buffer = aBuffer.ReinterpretCast(); + + for (const auto& entry : mUserEntries) { + buffer[entry.mIndex] = entry.mUserValue; + } + } + + // These return the number of entries in the default and user value arrays, + // respectively. + uint32_t DefaultCount() const { + return UserCount() + mDefaultEntries.Count(); + } + uint32_t UserCount() const { return mUserEntries.Length(); } + + // These return the byte sizes of the default and user value arrays, + // respectively. + uint32_t DefaultSize() const { return DefaultCount() * sizeof(ValueType); } + uint32_t UserSize() const { return UserCount() * sizeof(ValueType); } + + void Clear() { + mUserEntries.Clear(); + mDefaultEntries.Clear(); + } + + static constexpr size_t Alignment() { return alignof(ValueType); } + + private: + struct Entry { + uint16_t mIndex; + bool mHasUserValue; + ValueType mDefaultValue; + ValueType mUserValue{}; + }; + + AutoTArray mUserEntries; + + nsTHashMap mDefaultEntries; + }; + + // A special-purpose string table builder for keys which are already + // guaranteed to be unique. Duplicate values will not be detected or + // de-duplicated. + template + class UniqueStringTableBuilder { + public: + using ElemType = CharType; + + explicit UniqueStringTableBuilder(size_t aCapacity) : mEntries(aCapacity) {} + + StringTableEntry Add(const nsTString& aKey) { + auto entry = mEntries.AppendElement( + Entry{mSize, uint32_t(aKey.Length()), aKey.get()}); + + mSize += entry->mLength + 1; + + return {entry->mOffset, entry->mLength}; + } + + void Write(const RangedPtr& aBuffer) { + auto buffer = aBuffer.ReinterpretCast(); + + for (auto& entry : mEntries) { + memcpy(&buffer[entry.mOffset], entry.mValue, + sizeof(ElemType) * (entry.mLength + 1)); + } + } + + uint32_t Count() const { return mEntries.Length(); } + + uint32_t Size() const { return mSize * sizeof(ElemType); } + + void Clear() { mEntries.Clear(); } + + static constexpr size_t Alignment() { return alignof(ElemType); } + + private: + struct Entry { + uint32_t mOffset; + uint32_t mLength; + const CharType* mValue; + }; + + nsTArray mEntries; + uint32_t mSize = 0; + }; + + // A preference value entry, roughly corresponding to the + // SharedPrefMap::Value struct, but with a temporary place-holder value rather + // than a final value index. + union Value { + Value(bool aDefaultValue, bool aUserValue) + : mDefaultBool(aDefaultValue), mUserBool(aUserValue) {} + + MOZ_IMPLICIT Value(const ValueIdx& aIndex) : mIndex(aIndex) {} + + // For Bool preferences, their default and user bool values. + struct { + bool mDefaultBool; + bool mUserBool; + }; + // For Int and String preferences, an opaque descriptor for their entries in + // their value arrays. This must be passed to the appropriate + // ValueTableBuilder to obtain the final index when the entry is serialized. + ValueIdx mIndex; + }; + + // A preference entry, to be converted to a SharedPrefMap::Entry struct during + // serialization. + struct Entry { + // The entry's preference name, as passed to Add(). The caller is + // responsible for keeping this pointer alive until the builder is + // finalized. + const char* mKeyString; + // The entry in mKeyTable corresponding to mKeyString. + StringTableEntry mKey; + Value mValue; + + uint8_t mType : 2; + uint8_t mHasDefaultValue : 1; + uint8_t mHasUserValue : 1; + uint8_t mIsSticky : 1; + uint8_t mIsLocked : 1; + uint8_t mIsSanitized : 1; + uint8_t mIsSkippedByIteration : 1; + }; + + // Converts a builder Value struct to a SharedPrefMap::Value struct for + // serialization. This must not be called before callers have finished adding + // entries to the value array builders. + SharedPrefMap::Value GetValue(const Entry& aEntry) const { + switch (PrefType(aEntry.mType)) { + case PrefType::Bool: + return {aEntry.mValue.mDefaultBool, aEntry.mValue.mUserBool}; + case PrefType::Int: + return {mIntValueTable.GetIndex(aEntry.mValue.mIndex)}; + case PrefType::String: + return {mStringValueTable.GetIndex(aEntry.mValue.mIndex)}; + default: + MOZ_ASSERT_UNREACHABLE("Invalid pref type"); + return {false, false}; + } + } + + UniqueStringTableBuilder mKeyTable{kExpectedPrefCount}; + StringTableBuilder mValueStringTable; + + ValueTableBuilder mIntValueTable; + ValueTableBuilder, StringTableEntry> + mStringValueTable; + + nsTArray mEntries{kExpectedPrefCount}; +}; + +} // namespace mozilla + +#endif // dom_ipc_SharedPrefMap_h diff --git a/modules/libpref/StaticPrefsBase.h b/modules/libpref/StaticPrefsBase.h new file mode 100644 index 0000000000..597a5beda2 --- /dev/null +++ b/modules/libpref/StaticPrefsBase.h @@ -0,0 +1,93 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 mozilla_StaticPrefsBase_h +#define mozilla_StaticPrefsBase_h + +#include + +#include "mozilla/Atomics.h" +#include "mozilla/DataMutex.h" +#include "nsString.h" + +namespace mozilla { + +class SharedPrefMapBuilder; + +// These typedefs are for use within init/StaticPrefList*.h. + +typedef const char* String; + +using DataMutexString = StaticDataMutex; + +template +struct IsString : std::false_type {}; + +template <> +struct IsString : std::true_type {}; + +template <> +struct IsString : std::true_type {}; + +typedef Atomic RelaxedAtomicBool; +typedef Atomic ReleaseAcquireAtomicBool; +typedef Atomic SequentiallyConsistentAtomicBool; + +typedef Atomic RelaxedAtomicInt32; +typedef Atomic ReleaseAcquireAtomicInt32; +typedef Atomic + SequentiallyConsistentAtomicInt32; + +typedef Atomic RelaxedAtomicUint32; +typedef Atomic ReleaseAcquireAtomicUint32; +typedef Atomic + SequentiallyConsistentAtomicUint32; + +// XXX: Atomic currently doesn't work (see bug 1552086). Once it's +// supported we will be able to use Atomic here. +typedef std::atomic AtomicFloat; + +template +struct StripAtomicImpl { + typedef T Type; +}; + +template +struct StripAtomicImpl> { + typedef T Type; +}; + +template +struct StripAtomicImpl> { + typedef T Type; +}; + +template <> +struct StripAtomicImpl { + typedef nsCString Type; +}; + +template +using StripAtomic = typename StripAtomicImpl::Type; + +template +struct IsAtomic : std::false_type {}; + +template +struct IsAtomic> : std::true_type {}; + +template +struct IsAtomic> : std::true_type {}; + +namespace StaticPrefs { + +void MaybeInitOncePrefs(); + +} // namespace StaticPrefs + +} // namespace mozilla + +#endif // mozilla_StaticPrefsBase_h diff --git a/modules/libpref/components.conf b/modules/libpref/components.conf new file mode 100644 index 0000000000..25f3aecbb7 --- /dev/null +++ b/modules/libpref/components.conf @@ -0,0 +1,31 @@ +# -*- 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/. + +Headers = [ + 'mozilla/Preferences.h', +] + +UnloadFunc = 'mozilla::UnloadPrefsModule' + +Classes = [ + { + 'name': 'Preferences', + 'js_name': 'prefs', + 'cid': '{91ca2441-050f-4f7c-9df8-75b40ea40156}', + 'contract_ids': ['@mozilla.org/preferences-service;1'], + 'interfaces': ['nsIPrefService', 'nsIPrefBranch'], + 'singleton': True, + 'type': 'mozilla::Preferences', + 'headers': ['mozilla/Preferences.h'], + 'constructor': 'mozilla::Preferences::GetInstanceForService', + 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_AND_UTILITY_PROCESS, + }, + { + 'cid': '{064d9cee-1dd2-11b2-83e3-d25ab0193c26}', + 'contract_ids': ['@mozilla.org/pref-localizedstring;1'], + 'type': 'nsPrefLocalizedString', + }, +] diff --git a/modules/libpref/docs/index.md b/modules/libpref/docs/index.md new file mode 100644 index 0000000000..0a6a1507c5 --- /dev/null +++ b/modules/libpref/docs/index.md @@ -0,0 +1,463 @@ +# libpref +libpref is a generic key/value store that is used to implement *prefs*, a term +that encompasses a variety of things. + +- Feature enable/disable flags (e.g. `xpinstall.signatures.required`). +- User preferences (e.g. things set from `about:preferences`) +- Internal application parameters (e.g. + `javascript.options.mem.nursery.max_kb`). +- Testing and debugging flags (e.g. `network.dns.native-is-localhost`). +- Things that might need locking in an enterprise installation. +- Application data (e.g. + `browser.onboarding.tour.onboarding-tour-addons.completed`, + `services.sync.clients.lastSync`). +- A cheap and dirty form of IPC(!) (some devtools prefs). + +Some of these (particularly the last two) are not an ideal use of libpref. + +The C++ API is in the `Preferences` class. The XPIDL API is in the +`nsIPrefService` and `nsIPrefBranch` interfaces. + +## High-level design + +### Keys +Keys (a.k.a. *pref names*) are 8-bit strings, and ASCII in practice. The +convention is to use a dotted segmented form, e.g. `foo.bar.baz`, but the +segments have no built-in meaning. + +Naming is inconsistent, e.g. segments have various forms: `foo_bar`, +`foo-bar`, `fooBar`, etc. Pref names for feature flags are likewise +inconsistent: `foo.enabled`, `foo.enable`, `foo.disable`, `fooEnabled`, +`enable-foo`, `foo.enabled.bar`, etc. + +The grouping of prefs into families, via pref name segments, is ad hoc. Some of +these families are closely related, e.g. there are many font prefs that are +present for every language script. + +Some prefs only make sense when considered in combination with other prefs. + +Many pref names are known at compile time, but some are computed at runtime. + +### Basic values +The basic types of pref values are bools, 32-bit ints, and 8-bit C strings. + +Strings are used to encode many types of data: identifiers, alphanumeric IDs, +UUIDs, SHA1 hashes, CSS color hex values, large integers that don't fit into +32-bit ints (e.g. timestamps), directory names, URLs, comma-separated lists, +space-separated lists, JSON blobs, etc. There is a 1 MiB length limit on string +values; longer strings are rejected outright. + +**Problem:** The C string encoding is unclear; some API functions deal with +unrestricted 8-bit strings (i.e. Latin1), but some require UTF-8. + +There is some API support for faking floats, by converting them from/to strings when getting/setting. + +**Problem:** confusion between ints and floats can lead to bugs. + +Each pref consists of a default value and/or a user value. Default values can +be initialized from file at startup, and can be added and modified at runtime +via the API. User values can be initialized from file at startup, and can be +added, modified and removed at runtime via the API and `about:config`. + +If both values are present the user value takes precedence for most operations, +though there are operations that specifically work on the default value. + +If a user value is set to the same value as the default value, the user value +is removed, unless the pref is marked as *sticky* at startup. + +**Problem:** it would be better to have a clear notion of "reset to default", +at least for prefs that have a default value. + +Prefs can be locked. This prevents them from being given a user value, or +hides the existing user value if there is one. + +### Complex values +There is API support for some complex values. + +`nsIFile` objects are handled by storing the filename as a string, similar to +how floats are faked by storing them as strings. + +`nsIPrefLocalizedString` objects are ones for which the default value +specifies a properties file that contains an entry whose name matches the +prefname. When gotten, the value from that entry is put into the user value. +When set, the given value just overwrites the user value, like a string pref. + +**Problem:** this is weird and unlike all the other pref types. + +`nsIRelativeFilePref` objects are only used in comm-central. + +### Pref Branches +XPIDL-based access to prefs is via `nsIPrefBranch`/`nsPrefBranch`, which +lets you specify a branch of the pref tree (e.g. `font.`) and pref names work +relative to that point. + +This API can be used from C++, but for C++ code there is also direct access +through the `Preferences` class, which uses absolute pref names. + +### Threads +For the most part, all the basic API functions only work on the main thread. +However, there are two exceptions to this. + +The narrow exception is that the Servo traversal thread is allowed to get pref +values. This only occurs when the main thread is paused, which makes it safe. +(Note: [bug 1474789](https://bugzilla.mozilla.org/show_bug.cgi?id=1474789) +indicates that this may not be true.) + +The broad exception is that static prefs can have a cached copy of a pref value +that can be accessed from other threads. See below. + +### Notifications +There is a notification API for being told when a pref's value changes. C++ +code can register a callback function and JS code can register an observer (via +`nsIObserver`, which requires XPCOM). In both cases, the registered entity +will be notified when the value of the named pref value changes, or when the +value of any pref matching a given prefix changes. E.g. all font pref changes +can be observed by adding a `font.` prefix-matching observer. + +See also the section on static prefs below. + +### Static prefs +There is a special kind of pref called a static pref. Static prefs are defined +in `StaticPrefList.yaml`. + +If a static pref is defined in both `StaticPrefList.yaml` and a pref data +file, the latter definition will take precedence. A pref shouldn't appear in +both `StaticPrefList.yaml` and `all.js`, but it may make sense for a pref +to appear in both `StaticPrefList.yaml` and an app-specific pref data file +such as `firefox.js`. + +Each static pref has a *mirror* kind. + +* `always`: A C++ *mirror variable* is associated with the pref. The variable + is always kept in sync with the pref value. This kind is common. +* `once`: A C++ mirror variable is associated with the pref. The variable is + synced once with the pref's value at startup, and then does not change. This + kind is less common, and mostly used for graphics prefs. +* `never`: No C++ mirror variable is associated with the pref. This is much + like a normal pref. + +An `always` or `once` static pref can only be used for prefs with +bool/int/float values, not strings or complex values. + +Each mirror variable is read-only, accessible via a getter function. + +Mirror variables have two benefits. First, they allow C++ and Rust code to get +the pref value directly from the variable instead of requiring a slow hash +table lookup, which is important for prefs that are consulted frequently. +Second, they allow C++ and Rust code to get the pref value off the main thread. +The mirror variable must have an atomic type if it is read off the main thread, +and assertions ensure this. + +Note that mirror variables could be implemented via vanilla callbacks without +API support, except for one detail: libpref gives their callbacks higher +priority than normal callbacks, ensuring that any static pref will be +up-to-date if read by a normal callback. + +**Problem:** It is not clear what should happen to a static pref's mirror +variable if the pref is deleted? Currently there is a missing +`NotifyCallbacks()` call so the mirror variable keeps its value from before +the deletion. The cleanest solution is probably to disallow static prefs from +being deleted. + +### Sanitized Prefs +We restrict certain prefs from entering certain subprocesses. In subprocesses, +a preference may be marked as 'Sanitized' to indicate that it may or may not +have a user value, but that value is not present in this process. In the +parent process no pref is marked as Sanitized. + +Pref Sanitization is overloaded for two purposes: + 1. To protect private user data that may be stored in preferences from a + Spectre adversary. + 2. To reduce IPC use for commonly modified preferences. + +A pref is sanitized from entering the web content process if it matches a +denylist _or_ it is a dynamically-named string preference. + +A pref is sanitized from entering other subprocesses if it matches the +denylist. + +### Loading and Saving +Default pref values are initialized from various pref data files. Notable ones +include: + +- `modules/libpref/init/all.js`, used by all products; +- `browser/app/profile/firefox.js`, used by Firefox desktop; +- `mobile/android/app/mobile.js`, used by Firefox mobile; +- `mail/app/profile/all-thunderbird.js`, used by Thunderbird (in comm-central); +- `suite/browser/browser-prefs.js`, used by SeaMonkey (in comm-central). + +In release builds these are all put into `omni.ja`. + +User pref values are initialized from `prefs.js` and (if present) +`user.js`, in the user's profile. This only happens once, in the parent +process. Note that `prefs.js` is managed by Firefox, and regularly +overwritten. `user.js` is created and managed by the user, and Firefox only +reads it. + +These files are not JavaScript; the `.js` suffix is present for historical +reasons. They are read by a custom parser within libpref. + +User pref file syntax is slightly more restrictive than default pref file +syntax. In user pref files `user_pref` definitions are allowed but `pref` and +`sticky_pref` definitions are not, and attributes (such as `locked`) are not +allowed. + +**Problem:** geckodriver has a separate prefs parser in the mozprofile crate. + +**Problem:** there is no versioning of these files, for either the syntax or +the data. This makes changing the file format difficult. + +There are API functions to save modified prefs, either synchronously or +asynchronously (via an off-main-thread runnable), either to the default file +(`prefs.js`) or to a named file. When saving to the default file, no action +will take place if no prefs have been modified. + +Also, whenever a pref is modified, we wait 500ms and then automatically do an +off-main-thread save to `prefs.js`. This provides an approximation of +[durability](https://en.wikipedia.org/wiki/ACID#Durability), but it is still +possible for something to go wrong (e.g. a parent process crash) and end up +with recently changed prefs not being saved. (If such a thing happens, it +compromises [atomicity](https://en.wikipedia.org/wiki/ACID#Atomicity), i.e. a +sequence of multiple related pref changes might only get partially written.) + +Only prefs whose values have changed from the default are saved to `prefs.js.` + +**Problem:** Each time prefs are saved, the entire file is overwritten -- 10s +or even 100s of KiBs -- even if only a single value has changed. This happens +at least every 5 minutes, due to sync. Furthermore, various prefs are changed +during and shortly after startup, which can result in 10s of MiBs of disk +activity. + +### about:support +about:support contains an "Important Modified Preferences" table. It contains +all prefs that (a) have had their value changed from the default, and (b) whose +prefix match a allowlist in `Troubleshoot.sys.mjs`. The allowlist matching is to +avoid exposing pref values that might be privacy-sensitive. + +**Problem:** The allowlist of prefixes is specified separately from the prefs +themselves. Having an attribute on a pref definition would be better. + +### Sync +On desktop, a pref is synced onto a device via Sync if there is an +accompanying `services.sync.prefs.sync.`-prefixed pref. I.e. the pref +`foo.bar` is synced if the pref `services.sync.prefs.sync.foo.bar` exists +and is true. + +Previously, one could push prefs onto a device even if a local +`services.sync.prefs.sync.`-prefixed pref was not present; however this +behavior changed in [bug 1538015](https://bugzilla.mozilla.org/show_bug.cgi?id=1538015) +to require the local prefixed pref to be present. The old (insecure) behavior +can be re-enabled by setting a single pref `services.sync.prefs.dangerously_allow_arbitrary` +to true on the target browser - subsequently any pref can be pushed there by +creating a *remote* `services.sync.prefs.sync.`-prefixed pref. + +In practice, only a small subset of prefs (about 70) have a `services.sync.prefs.sync.`-prefixed +pref by default. + +**Problem:** This is gross. An attribute on the pref definition would be +better, but it might be hard to change that at this point. + +The number of synced prefs is small because prefs are synced across versions; +any pref whose meaning might change shouldn't be synced. Also, we don't sync +prefs that may differ across different devices (such as a desktop machine +vs. a notebook). + +Prefs are not synced on mobile. + +### Rust +Static prefs mirror variables can be accessed from Rust code via the +`static_prefs::pref!` macro. Other prefs currently cannot be accessed. Parts +of libpref's C++ API could be made accessible to Rust code fairly +straightforwardly via C bindings, either hand-made or generated. + +### Cost of a pref +The cost of a single pref is low, but the cost of several thousand prefs is +reasonably high, and includes the following. + +- Parsing and initializing at startup. +- IPC costs at startup and on pref value changes. +- Disk writing costs of pref value changes, especially during startup. +- Memory usage for storing the prefs, callbacks and observers, and C++ mirror + variables. +- Complexity: most pref combinations are untested. Some can be set to a bogus + value by a curious user, which can have [serious effects](https://rejzor.wordpress.com/2015/06/14/improve-firefox-html5-video-playback-performance/) + (read the comments). Prefs can also have bugs. Real-life examples include + mistyped prefnames, `all.js` entries with incorrect types (e.g. confusing + int vs. float), both of which mean changing the pref value via about:config + or the API would have no effect (see [bug 1414150](https://bugzilla.mozilla.org/show_bug.cgi?id=1414150) for examples of + both). +- Sync cost, for synced prefs. + +### Guidelines +We have far too many prefs. This is at least partly because we have had, for a +long time, a culture of "when in doubt, add a pref". Also, we don't have any +system — either technical or cultural — for removing unnecessary prefs. See +[bug 90440] (https://bugzilla.mozilla.org/show_bug.cgi?id=90440) for a pref +that was unused for 17 years. + +In short, prefs are Firefox's equivalent of the Windows Registry: a dumping +ground for anything and everything. We should have guidelines for when to add a +pref. + +Here are some good reasons to add a pref. + +- *A user may genuinely want to change it.* E.g. it controls a feature that is + adjustable in about:preferences. +- *To enable/disable new features.* Once a feature is mature, consider removing + the pref. A pref expiry mechanism would help with this. +- *For certain testing/debugging flags.* Ideally, these would not be visible in + about:config. + +Here are some less good reasons to add a pref. + +- *I'm not confident about this numeric parameter (cache size, timeout, etc.)* + Get confident! In practice, few if any users will change it. Adding a pref + doesn't absolve you of the responsibility of finding a good default. Then + make it a code constant. +- *I need to experiment with different parameters during development.* This is + reasonable, but consider removing the pref before landing or once the feature + has matured. An expiry mechanism would help with this. +- *I sometimes fiddle with this value for debugging or testing.* + Is it worth exposing it to the whole world to save yourself a recompile every + once in a while? Consider making it a code constant. +- *Different values are needed on different platforms.* This can be done in + other ways, e.g. `#ifdef` in C++ code. + +These guidelines do not consider application data prefs (i.e. ones that +typically don't have a default value). They are quite different from the other +kinds. They arguably shouldn't prefs at all, and should be stored via some +other mechanism. + +## Low-level details +The key idea is that the prefs database consists of two pieces. The first is an +initial snapshot of pref values that is created when the first child process is +created. This snapshot is stored in immutable, shared memory, and shared by all +processes. + +Pref value changes that occur after this point are stored in a second hash +table. Each process has its own copy of this hash table. When pref values +change in the parent process, it performs IPC to inform child processes about +the changes, so they can update their copy. + +The motivation for this design is memory usage. It's not tenable for every +child process to have a full copy of the prefs database. + +Not all child processes need access to prefs. Those that do include web content +processes, the GPU process, and the RDD process. + +### Parent process startup +The parent process initially has only a hash table. + +Early in startup, the parent process loads all of the static prefs and default +prefs (mainly from `omni.ja`) into that hash table. The parent process also +registers C++ mirror variables for static prefs, initializes them, and +registers callbacks so they will be updated appropriately for all subsequent +updates. + +Slightly later in startup, the parent process loads all user prefs files, +mainly from the profile directory. + +When the first getter for a `once` static pref is called, all the `once` +static prefs have their mirror variables set and special frozen prefs are put +into the hash table. These frozen prefs are copies of the `once` prefs that +are given `$$$` prefixes and suffixes on their names. They are also marked +specially so they are ignored for all cases except when starting a new child +process. They exist so that all child processes can be given the same `once` +values as the parent process. + +### Child process startup (parent side) +When the first child process is created, the parent process serializes most of +its hash table into a shared, immutable snapshot. This snapshot is stored in a +shared memory region managed by a `SharedPrefMap` instance. + +Sanitized preferences (matching _either_ the denylist of the dynamically named +heuristic) are not included in the shared memory region. After building the +shared memory region, the parent process clears the hash table and then +re-enters sanitized prefs into it. Besides the sanitized prefs, the hash table +is subsequently used only to store changed pref values. + +When any child process is created, the parent process serializes all pref +values present in the hash table (i.e. those that have changed since the +snapshot was made) _except sanitized prefs__ and stores them in a second, +short-lived shared memory region. This represents the set of changes the child +process needs to apply on top of the snapshot, and allows it to build a hash +table which should exactly match the parent's, modulo the sanitized prefs. + +The parent process passes two file descriptors to the child process, one for +each region of memory. The snapshot is the same for all child processes. + +### Child process startup (child side) +Early in child process startup, the prefs service maps in and deserializes both +shared memory regions sent from the parent process, but defers further +initialization until requested by XPCOM initialization. Once that happens, +mirror variables are initialized for static prefs, but no default values are +set in the hash table, and no prefs files are loaded. + +Once the mirror variables have been initialized, we dispatch pref change +callbacks for any prefs in the shared snapshot which have user values or are +locked. This causes the mirror variables to be updated. + +After that, the changed pref values received from the parent process (via +`changedPrefsFd`) are added to the prefs database. Their values override the +values in the snapshot, and pref change callbacks are dispatched for them as +appropriate. `once` mirror variable are initialized from the special frozen +pref values. + +### Pref lookups +Each prefs database has both a hash table and a shared memory snapshot. A given +pref may have an entry in either or both of these. If a pref exists in both, +the hash table entry takes precedence. + +For pref lookups, the hash table is checked first, followed by the shared +snapshot. The entry in the hash table may have the type `None`, in which case +the pref is treated as if it did not exist. The entry in the static snapshot +never has the type `None`. + +For pref enumeration, both maps are enumerated, starting with the hash table. +While iterating over the hash table, any entry with the type `None` is +skipped. While iterating over the shared snapshot, any entry which also exists +in the hash table is skipped. The combined result of the two iterations +represents the full contents of the prefs database. + +### Pref changes +Pref changes can only be initiated in the parent process. All API methods that +modify prefs fail noisily (with `NS_ERROR`) if run outside the parent +process. + +Pref changes that happen before the initial snapshot have been made are simple, +and take place in the hash table. There is no shared snapshot to update, and no +child processes to synchronize with. + +Once a snapshot has been created, any changes need to happen in the hash table. + +If an entry for a changed pref already exists in the hash table, that entry can +be updated directly. Likewise for prefs that do not exist in either the hash +table or the shared snapshot: a new hash table entry can be created. + +More care is needed when a changed pref exists in the snapshot but not in the +hash table. In that case, we create a hash table entry with the same values as +the snapshot entry, and then update it... but *only* if the changes will have +an effect. If a caller attempts to set a pref to its existing value, we do not +want to waste memory creating an unnecessary hash table entry. + +Content processes must be told about any visible pref value changes. (A change +to a default value that is hidden by a user value is unimportant.) When this +happens, `ContentParent` detects the change (via an observer). Sanitized prefs +do not produce an update; and for string prefs it also checks the value(s) +don't exceed 4 KiB. If the checks pass, it sends an IPC message +(`PreferenceUpdate`) to the child process, and the child process updates the +pref (default and user value) accordingly. + +**Problem:** The denylist of prefixes is specified separately from the prefs +themselves. Having an attribute on a pref definition would be better. + +**Problem:** The 4 KiB limit can lead to inconsistencies between the parent +process and child processes. E.g. see +[bug 1303051](https://bugzilla.mozilla.org/show_bug.cgi?id=1303051#c28). + +### Pref deletions +Pref deletion is more complicated. If a pref to be deleted exists only in the +hash table of the parent process, its entry can simply be removed. If it exists +in the shared snapshot, however, its hash table entry needs to be kept (or +created), and its type changed to `None`. The presence of this entry masks +the snapshot entry, causing it to be ignored by pref enumerators. diff --git a/modules/libpref/greprefs.js b/modules/libpref/greprefs.js new file mode 100644 index 0000000000..c18e30b24e --- /dev/null +++ b/modules/libpref/greprefs.js @@ -0,0 +1,5 @@ +#filter dumbComments emptyLines + +// Do not add anything else to this file. Additions should go in all.js. + +#include init/all.js diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml new file mode 100644 index 0000000000..e23252cbe2 --- /dev/null +++ b/modules/libpref/init/StaticPrefList.yaml @@ -0,0 +1,15005 @@ +# 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/. */ + +# This file defines static prefs, i.e. those that are defined at startup and +# used entirely or mostly from C++ and/or Rust code. +# +# The file is separated into sections, where each section contains a group of +# prefs that all share the same first segment of their name -- all the "gfx.*" +# prefs are together, all the "network.*" prefs are together, etc. Sections +# must be kept in alphabetical order, but prefs within sections need not be. +# +# Basics +# ------ +# Any pref defined in one of the files included here should *not* be defined +# in a data file such as all.js; that would just be useless duplication. +# +# (Except under unusual circumstances where the value defined here must be +# overridden, e.g. for some Thunderbird prefs. In those cases the default +# value from the data file will override the static default value defined +# here.) +# +# Please follow the existing prefs naming convention when considering adding a +# new pref, and don't create a new pref group unless it's appropriate and there +# are likely to be multiple prefs within that group. (If you do, you'll need to +# update the `pref_groups` variable in modules/libpref/moz.build.) +# +# Definitions +# ----------- +# A pref definition looks like this: +# +# - name: # mandatory +# type: # mandatory +# value: # mandatory +# mirror: # mandatory +# do_not_use_directly: # optional +# include: # optional +# rust: # optional +# +# - `name` is the name of the pref, without double-quotes, as it appears +# in about:config. It is used in most libpref API functions (from both C++ +# and JS code). +# +# - `type` is one of `bool`, `int32_t`, `uint32_t`, `float`, an atomic version +# of one of those, `String` or `DataMutexString`. Note that float prefs are +# stored internally as strings. The C++ preprocessor doesn't like template +# syntax in a macro argument, so use the typedefs defined in +# StaticPrefsBase.h; for example, use `RelaxedAtomicBool` instead of +# `Atomic`. +# +# - `value` is the default value. Its type should be appropriate for +# , otherwise the generated code will fail to compile. A complex +# C++ numeric expressions like `60 * 60` (which the YAML parser cannot treat +# as an integer or float) is treated as a string and passed through without +# change, which is useful. +# +# - `mirror` indicates how the pref value is mirrored into a C++ variable. +# +# * `never`: There is no C++ mirror variable. The pref value can only be +# accessed via the standard libpref API functions. +# +# * `once`: The pref value is mirrored into a variable at startup; the +# mirror variable is left unchanged after that. (The exact point at which +# all `once` mirror variables are set is when the first `once` mirror +# variable is accessed, via its getter function.) This is mostly useful for +# graphics prefs where we often don't want a new pref value to apply until +# restart. Otherwise, this update policy is best avoided because its +# behaviour can cause confusion and bugs. +# +# * `always`: The mirror variable is always kept in sync with the pref value. +# This is the most common choice. +# +# When a mirror variable is present, a getter will be created that can access +# it. Using the getter function to read the pref's value has the two +# following advantages over the normal API functions. +# +# * A direct variable access is faster than a hash table lookup. +# +# * A mirror variable can be accessed off the main thread. If a pref *is* +# accessed off the main thread, it should have an atomic type. Assertions +# enforce this. +# +# Note that Rust code must access the mirror variable directly, rather than +# via the getter function. +# +# - `do_not_use_directly` indicates if `_DoNotUseDirectly` should be appended to +# the name of the getter function. This is simply a naming convention +# indicating that there is some other wrapper getter function that should be +# used in preference to the normal static pref getter. Defaults to `false` if +# not present. Cannot be used with a `never` mirror value, because there is +# no getter function in that case. +# +# - `include` names a header file that must be included for the pref value to +# compile correctly, e.g. because it refers to a code constant. System +# headers should be surrounded with angle brackets, e.g. ``. +# +# - `rust` indicates if the mirror variable is used by Rust code. If so, it +# will be usable via the `static_prefs::pref!` macro, e.g. +# `static_prefs::pref!("layout.css.font-display.enabled")`. +# +# The getter function's base name is the same as the pref's name, but with +# '.' or '-' chars converted to '_', to make a valid identifier. For example, +# the getter for `foo.bar_baz` is `foo_bar_baz()`. This is ugly but clear, +# and you can search for both the pref name and the getter using the regexp +# /foo.bar.baz/. Suffixes are added as follows: +# +# - If the `mirror` value is `once`, `_AtStartup` is appended, to indicate the +# value was obtained at startup. +# +# - If the `do_not_use_directly` value is true, `_DoNotUseDirectly` is +# appended. +# +# Preprocessor +# ------------ +# Note finally that this file is preprocessed by preprocessor.py, not the C++ +# preprocessor. As a result, the following things may be surprising. +# +# - YAML comments start with a '#', so putting a comment on the same line as a +# preprocessor directive is dubious. E.g. avoid lines like `#define X 3 # +# three` because the ` # three` will be part of `X`. +# +# - '@' use is required for substitutions to occur. E.g. with `#define FOO 1`, +# `FOO` won't be replaced with `1` unless it has '@' chars around it. +# +# - Spaces aren't permitted between the leading '#' and the name of a +# directive, e.g. `#ifdef XYZ` works but `# ifdef XYZ` does not. +# +# Please indent all prefs defined within #ifdef/#ifndef conditions. This +# improves readability, particular for conditional blocks that exceed a single +# screen. But note that the leading '-' in a definition must remain in the +# first column for it to be valid YAML. + +#ifdef RELEASE_OR_BETA +#define IS_NOT_RELEASE_OR_BETA false +#else +#define IS_NOT_RELEASE_OR_BETA true +#endif + +#ifdef NIGHTLY_BUILD +#define IS_NIGHTLY_BUILD true +#define IS_NOT_NIGHTLY_BUILD false +#else +#define IS_NIGHTLY_BUILD false +#define IS_NOT_NIGHTLY_BUILD true +#endif + +#if defined(NIGHTLY_BUILD) || defined(MOZ_DEV_EDITION) +#define IS_NIGHTLY_OR_DEV_EDITION true +#else +#define IS_NIGHTLY_OR_DEV_EDITION false +#endif + +#ifdef MOZILLA_OFFICIAL +#define IS_NOT_MOZILLA_OFFICIAL false +#else +#define IS_NOT_MOZILLA_OFFICIAL true +#endif + +#ifdef EARLY_BETA_OR_EARLIER +#define IS_EARLY_BETA_OR_EARLIER true +#define IS_NOT_EARLY_BETA_OR_EARLIER false +#else +#define IS_EARLY_BETA_OR_EARLIER false +#define IS_NOT_EARLY_BETA_OR_EARLIER true +#endif + +#ifdef ANDROID +#define IS_ANDROID true +#define IS_NOT_ANDROID false +#else +#define IS_ANDROID false +#define IS_NOT_ANDROID true +#endif + +#--------------------------------------------------------------------------- +# Prefs starting with "accessibility." +#--------------------------------------------------------------------------- + +- name: accessibility.accesskeycausesactivation + type: bool + value: true + mirror: always + +- name: accessibility.monoaudio.enable + type: RelaxedAtomicBool + value: false + mirror: always + +- name: accessibility.browsewithcaret + type: RelaxedAtomicBool + value: false + mirror: always + +- name: accessibility.AOM.enabled + type: bool + value: false + mirror: always + +- name: accessibility.ARIAReflection.enabled + type: bool + value: false + mirror: always + +# Whether form controls and images should be focusable with mouse, in content +# documents. +# +# This matches historical macOS / Safari behavior. +# +# * 0: never +# * 1: always +# * 2: on content documents +- name: accessibility.mouse_focuses_formcontrol + type: int32_t +#ifdef XP_MACOSX + value: 2 +#else + value: 1 +#endif + mirror: always + +# Whether to cache the entire accessibility trees of all content processes in +# the parent process. +- name: accessibility.cache.enabled + type: bool +#if defined(ANDROID) + value: true +#elif defined(XP_WIN) || defined(XP_LINUX) + value: @IS_EARLY_BETA_OR_EARLIER@ +#else + value: @IS_NOT_RELEASE_OR_BETA@ +#endif + mirror: once + +# Whether to avoid accessibility activation on Windows shortly after clipboard +# copy. +# +# Possible values are: +# * 0: never +# * 1: always +# * 2 (or others): when needed +- name: accessibility.windows.suppress-after-clipboard-copy + type: uint32_t + value: 2 + mirror: always + +#--------------------------------------------------------------------------- +# Prefs starting with "alerts." +#--------------------------------------------------------------------------- + +# Whether to use platform-specific backends for showing desktop notifications. +# If no such backend is available, or if the pref is false, then XUL +# notifications are used. +- name: alerts.useSystemBackend + type: bool +#ifdef XP_WIN + # Linux and macOS turn on system level notification as default, but Windows is + # disabled due to instability (dependencies of bug 1497425). + value: false +#else + value: true +#endif + mirror: always + + +#ifdef ANDROID + #--------------------------------------------------------------------------- + # Prefs starting with "android." + #--------------------------------------------------------------------------- + + # On Android, we want an opaque background to be visible under the page, + # so layout should not force a default background. +- name: android.widget_paints_background + type: RelaxedAtomicBool + value: true + mirror: always + +- name: android.touch_resampling.enabled + type: RelaxedAtomicBool + value: true + mirror: always + +#endif + +#--------------------------------------------------------------------------- +# Prefs starting with "apz." +# The apz prefs are explained in AsyncPanZoomController.cpp +#--------------------------------------------------------------------------- + +# amount we zoom in for a double tap gesture if we couldn't find any content +# based rect to zoom to +- name: apz.doubletapzoom.defaultzoomin + type: AtomicFloat + value: 1.2f + mirror: always + +- name: apz.scrollbarbuttonrepeat.enabled + type: RelaxedAtomicBool + value: true + mirror: always + +- name: apz.scrollend-event.content.enabled + type: RelaxedAtomicBool + value: true + mirror: always + +# After a user has executed a pan gesture, we may receive momentum phase pan +# gestures from the OS. This specifies how long we should wait following the +# pan end gesture for possible momentum phase pan gestures before sending the +# TransformEnd notification. +- name: apz.scrollend-event.content.delay_ms + type: RelaxedAtomicInt32 + value: 100 + mirror: always + +- name: apz.wr.activate_all_scroll_frames + type: RelaxedAtomicBool + value: false + mirror: always + +- name: apz.wr.activate_all_scroll_frames_when_fission + type: RelaxedAtomicBool + value: true + mirror: always + +- name: apz.prefer_jank_minimal_displayports + type: RelaxedAtomicBool + value: true + mirror: always + +- name: apz.allow_double_tap_zooming + type: RelaxedAtomicBool + value: true + mirror: always + +- name: apz.mac.enable_double_tap_zoom_touchpad_gesture + type: RelaxedAtomicBool + value: true + mirror: always + +- name: apz.allow_immediate_handoff + type: RelaxedAtomicBool + value: false + mirror: always + +- name: apz.allow_zooming + type: RelaxedAtomicBool + value: true + mirror: always + +- name: apz.max_zoom + type: AtomicFloat + value: 10.0f + mirror: always + +- name: apz.min_zoom + type: AtomicFloat + value: 0.25f + mirror: always + +- name: apz.allow_zooming_out + type: RelaxedAtomicBool + value: false + mirror: always + +- name: apz.android.chrome_fling_physics.friction + type: AtomicFloat + value: 0.015f + mirror: always + +- name: apz.android.chrome_fling_physics.inflexion + type: AtomicFloat + value: 0.35f + mirror: always + +- name: apz.android.chrome_fling_physics.stop_threshold + type: AtomicFloat + value: 0.1f + mirror: always + +- name: apz.autoscroll.enabled + type: RelaxedAtomicBool + value: true + mirror: always + +- name: apz.axis_lock.breakout_angle + type: AtomicFloat + value: float(M_PI / 8.0) # 22.5 degrees + mirror: always + include: + +- name: apz.axis_lock.breakout_threshold + type: AtomicFloat + value: 1.0f / 32.0f + mirror: always + +- name: apz.axis_lock.direct_pan_angle + type: AtomicFloat + value: float(M_PI / 3.0) # 60 degrees + mirror: always + include: + +- name: apz.axis_lock.lock_angle + type: AtomicFloat + value: float(M_PI / 6.0) # 30 degrees + mirror: always + include: + +# Whether to lock touch scrolling to one axis at a time. When a new +# axis lock mode is added, the APZCAxisLockCompatTester GTest shoud +# be updated to include the lock mode value. +# 0 = FREE (No locking at all) +# 1 = STANDARD (Once locked, remain locked until scrolling ends) +# 2 = STICKY (Allow lock to be broken, with hysteresis) +# 3 = DOMINANT_AXIS (Only allow movement on one axis at a time, only +# applies to touchpad scrolling) +- name: apz.axis_lock.mode + type: RelaxedAtomicInt32 +#if defined(XP_MACOSX) + value: 3 +#else + value: 2 +#endif + mirror: always + +- name: apz.content_response_timeout + type: RelaxedAtomicInt32 + value: 400 + mirror: always + +- name: apz.danger_zone_x + type: RelaxedAtomicInt32 + value: 50 + mirror: always + +- name: apz.danger_zone_y + type: RelaxedAtomicInt32 + value: 100 + mirror: always + +- name: apz.disable_for_scroll_linked_effects + type: RelaxedAtomicBool + value: false + mirror: always + +- name: apz.displayport_expiry_ms + type: RelaxedAtomicUint32 + value: 15000 + mirror: always + +- name: apz.drag.enabled + type: RelaxedAtomicBool + value: true + mirror: always + +- name: apz.drag.initial.enabled + type: RelaxedAtomicBool + value: true + mirror: always + +- name: apz.drag.touch.enabled + type: RelaxedAtomicBool + value: true + mirror: always + +- name: apz.enlarge_displayport_when_clipped + type: RelaxedAtomicBool + value: @IS_ANDROID@ + mirror: always + +# Test only. +- name: apz.fixed-margin-override.enabled + type: RelaxedAtomicBool + value: false + mirror: always + +# Test only. +- name: apz.fixed-margin-override.bottom + type: RelaxedAtomicInt32 + value: 0 + mirror: always + +# Test only. +- name: apz.fixed-margin-override.top + type: RelaxedAtomicInt32 + value: 0 + mirror: always + +- name: apz.fling_accel_base_mult + type: AtomicFloat + value: 1.0f + mirror: always + +- name: apz.fling_accel_supplemental_mult + type: AtomicFloat + value: 1.0f + mirror: always + +- name: apz.fling_accel_min_fling_velocity + type: AtomicFloat + value: 1.5f + mirror: always + +- name: apz.fling_accel_min_pan_velocity + type: AtomicFloat + value: 0.8f + mirror: always + +- name: apz.fling_accel_max_pause_interval_ms + type: RelaxedAtomicInt32 + value: 50 + mirror: always + +- name: apz.fling_curve_function_x1 + type: float + value: 0.0f + mirror: once + +- name: apz.fling_curve_function_x2 + type: float + value: 1.0f + mirror: once + +- name: apz.fling_curve_function_y1 + type: float + value: 0.0f + mirror: once + +- name: apz.fling_curve_function_y2 + type: float + value: 1.0f + mirror: once + +- name: apz.fling_curve_threshold_inches_per_ms + type: AtomicFloat + value: -1.0f + mirror: always + +- name: apz.fling_friction + type: AtomicFloat + value: 0.002f + mirror: always + +- name: apz.fling_min_velocity_threshold + type: AtomicFloat + value: 0.5f + mirror: always + +- name: apz.fling_stop_on_tap_threshold + type: AtomicFloat + value: 0.05f + mirror: always + +- name: apz.fling_stopped_threshold + type: AtomicFloat + value: 0.01f + mirror: always + +- name: apz.touch_acceleration_factor_x + type: float + value: 1.0f + mirror: always + +- name: apz.touch_acceleration_factor_y + type: float + value: 1.0f + mirror: always + +# new scrollbar code for desktop zooming +- name: apz.force_disable_desktop_zooming_scrollbars + type: RelaxedAtomicBool + value: false + mirror: always + +#ifdef MOZ_WIDGET_GTK +- name: apz.gtk.kinetic_scroll.enabled + type: RelaxedAtomicBool + value: true + mirror: always + +- name: apz.gtk.pangesture.enabled + type: RelaxedAtomicBool + value: true + mirror: always + +# Mode to use when receiving pan gesture input. +# +# * 0: Auto mode (uses the default behavior, subject to change). +# * 1: Page mode: Uses gtk deltas as a percentage of the page size to scroll. This mode matches: +# +# https://gitlab.gnome.org/GNOME/gtk/blob/c734c7e9188b56f56c3a504abee05fa40c5475ac/gtk/gtkrange.c#L3063-3074 +# +# * 2: Pixel mode: Uses gtk deltas as a fixed pixel multiplier. This mode matches e.g. GNOME web. +# +# https://webkit-search.igalia.com/webkit/rev/215039ef09d6bfd6e088175bfe30788d95b9705d/Source/WebKit/Shared/gtk/WebEventFactory.cpp#265-296 +# (multiplied then by pixelsPerLineStep which in GNOME-web is 40). +- name: apz.gtk.pangesture.delta_mode + type: uint32_t + value: 0 + mirror: always + +- name: apz.gtk.pangesture.page_delta_mode_multiplier + type: float + value: 1.0f + mirror: always + +- name: apz.gtk.pangesture.pixel_delta_mode_multiplier + type: float + value: 40.0f + mirror: always + +- name: apz.gtk.touchpad_pinch.enabled + type: RelaxedAtomicBool + value: true + mirror: always + +- name: apz.gtk.touchpad_pinch.three_fingers.enabled + type: RelaxedAtomicBool + value: false + mirror: always +#endif + +- name: apz.keyboard.enabled + type: bool + value: @IS_NOT_ANDROID@ + mirror: once + +- name: apz.keyboard.passive-listeners + type: RelaxedAtomicBool + value: @IS_NOT_ANDROID@ + mirror: always + +- name: apz.max_tap_time + type: RelaxedAtomicInt32 + value: 300 + mirror: always + +- name: apz.max_velocity_inches_per_ms + type: AtomicFloat + value: -1.0f + mirror: always + +- name: apz.max_velocity_queue_size + type: uint32_t + value: 5 + mirror: once + +- name: apz.min_skate_speed + type: AtomicFloat + value: 1.0f + mirror: always + +- name: apz.mvm.force-enabled + type: RelaxedAtomicBool + value: true + mirror: always + +- name: apz.one_touch_pinch.enabled + type: RelaxedAtomicBool + value: @IS_ANDROID@ + mirror: always + +- name: apz.overscroll.enabled + type: RelaxedAtomicBool +#if defined(XP_MACOSX) + value: true +#elif defined(XP_WIN) && defined(NIGHTLY_BUILD) + value: true +#else + value: false +#endif + mirror: always + +# The "test async scroll offset" (used via reftest-async-scroll +# or nsIDOMWindowUtils.setAsyncScrollOffset()) can be used to +# trigger overscroll. Used for tests only. +- name: apz.overscroll.test_async_scroll_offset.enabled + type: RelaxedAtomicBool + value: false + mirror: always + +- name: apz.overscroll.min_pan_distance_ratio + type: AtomicFloat + value: 1.0f + mirror: always + +- name: apz.overscroll.stop_distance_threshold + type: AtomicFloat + value: 5.0f + mirror: always + +- name: apz.overscroll.spring_stiffness + type: AtomicFloat + value: 200 + mirror: always + +- name: apz.overscroll.damping + type: AtomicFloat + value: 1.1 + mirror: always + +- name: apz.overscroll.max_velocity + type: AtomicFloat + value: 10 + mirror: always + +- name: apz.paint_skipping.enabled + type: RelaxedAtomicBool + value: true + mirror: always + +# Fetch displayport updates early from the message queue. +- name: apz.pinch_lock.mode + type: RelaxedAtomicInt32 + value: 2 + mirror: always + +- name: apz.pinch_lock.scroll_lock_threshold + type: AtomicFloat + value: 1.0f / 16.0f # 1/16 inches + mirror: always + +- name: apz.pinch_lock.span_breakout_threshold + type: AtomicFloat + value: 1.0f / 32.0f # 1/32 inches + mirror: always + +- name: apz.pinch_lock.span_lock_threshold + type: AtomicFloat + value: 1.0f / 32.0f # 1/32 inches + mirror: always + +- name: apz.pinch_lock.buffer_max_age + type: int32_t + value: 80 # milliseconds + mirror: once + +- name: apz.popups.enabled + type: RelaxedAtomicBool + value: true + mirror: always + +# Whether to print the APZC tree for debugging. +- name: apz.printtree + type: RelaxedAtomicBool + value: false + mirror: always + +- name: apz.record_checkerboarding + type: RelaxedAtomicBool + value: @IS_NIGHTLY_BUILD@ + mirror: always + +- name: apz.second_tap_tolerance + type: AtomicFloat + value: 0.5f + mirror: always + +- name: apz.test.fails_with_native_injection + type: RelaxedAtomicBool + value: false + mirror: always + +- name: apz.test.logging_enabled + type: RelaxedAtomicBool + value: false + mirror: always + +- name: apz.touch_move_tolerance + type: AtomicFloat + value: 0.1f + mirror: always + +- name: apz.touch_start_tolerance + type: AtomicFloat + value: 0.1f + mirror: always + +- name: apz.velocity_bias + type: AtomicFloat + value: 0.0f + mirror: always + +- name: apz.velocity_relevance_time_ms + type: RelaxedAtomicUint32 + value: 100 + mirror: always + +- name: apz.windows.force_disable_direct_manipulation + type: RelaxedAtomicBool + value: false + mirror: always + +- name: apz.windows.use_direct_manipulation + type: RelaxedAtomicBool + value: true + mirror: always + +- name: apz.windows.check_for_pan_gesture_conversion + type: RelaxedAtomicBool + value: true + mirror: always + +- name: apz.x_skate_highmem_adjust + type: AtomicFloat + value: 0.0f + mirror: always + +- name: apz.x_skate_size_multiplier + type: AtomicFloat + value: 1.25f + mirror: always + +- name: apz.x_stationary_size_multiplier + type: AtomicFloat + value: 1.5f + mirror: always + +- name: apz.y_skate_highmem_adjust + type: AtomicFloat + value: 0.0f + mirror: always + +- name: apz.y_skate_size_multiplier + type: AtomicFloat +#if defined(MOZ_WIDGET_ANDROID) + value: 1.5f +#else + value: 3.5f +#endif + mirror: always + +- name: apz.y_stationary_size_multiplier + type: AtomicFloat +#if defined(MOZ_WIDGET_ANDROID) + value: 1.5f +#else + value: 3.5f +#endif + mirror: always + +- name: apz.zoom_animation_duration_ms + type: RelaxedAtomicInt32 +#if defined(MOZ_WIDGET_ANDROID) + value: 250 +#else + value: 350 +#endif + mirror: always + +- name: apz.scale_repaint_delay_ms + type: RelaxedAtomicInt32 + value: 500 + mirror: always + +#--------------------------------------------------------------------------- +# Prefs starting with "beacon." +#--------------------------------------------------------------------------- + +# Is support for Navigator.sendBeacon enabled? +- name: beacon.enabled + type: bool + value: true + mirror: always + +#--------------------------------------------------------------------------- +# Prefs starting with "bidi." +#--------------------------------------------------------------------------- + +# Whether delete and backspace should immediately delete characters not +# visually adjacent to the caret, or adjust the visual position of the caret +# on the first keypress and delete the character on a second keypress +- name: bidi.edit.delete_immediately + type: bool + value: true + mirror: always + +# Bidi caret movement style: +# 0 = logical +# 1 = visual +# 2 = visual, but logical during selection +- name: bidi.edit.caret_movement_style + type: int32_t +#if !defined(XP_LINUX) && defined(NIGHTLY_BUILD) + value: 1 +#else + value: 2 # See Bug 1638240 +#endif + mirror: always + +# Bidi numeral style: +# 0 = nominalnumeralBidi * +# 1 = regularcontextnumeralBidi +# 2 = hindicontextnumeralBidi +# 3 = arabicnumeralBidi +# 4 = hindinumeralBidi +# 5 = persiancontextnumeralBidi +# 6 = persiannumeralBidi +- name: bidi.numeral + type: RelaxedAtomicUint32 + value: 0 + mirror: always + +# Bidi text type +# 1 = charsettexttypeBidi * +# 2 = logicaltexttypeBidi +# 3 = visualtexttypeBidi +- name: bidi.texttype + type: RelaxedAtomicUint32 + value: 1 + mirror: always + +# Bidi direction +# 1 = directionLTRBidi * +# 2 = directionRTLBidi +- name: bidi.direction + type: RelaxedAtomicUint32 + value: 1 + mirror: always + +# Setting this pref to |true| forces Bidi UI menu items and keyboard shortcuts +# to be exposed, and enables the directional caret hook. By default, only +# expose it for bidi-associated system locales. +- name: bidi.browser.ui + type: bool + value: false + mirror: always + +#--------------------------------------------------------------------------- +# Prefs starting with "browser." +#--------------------------------------------------------------------------- + +- name: browser.active_color + type: String + value: "#EE0000" + mirror: never + +- name: browser.active_color.dark + type: String + value: "#FF6666" + mirror: never + +- name: browser.anchor_color + type: String + value: "#0000EE" + mirror: never + +# If you change this, you probably also want to change +# nsXPLookAndFeel::GenericDarkColor for MozNativehyperlinktext. +- name: browser.anchor_color.dark + type: String + value: "#8C8CFF" + mirror: never + +# See http://dev.w3.org/html5/spec/forms.html#attr-fe-autofocus +- name: browser.autofocus + type: bool + value: true + mirror: always + +- name: browser.cache.offline.enable + type: bool + value: @IS_NOT_EARLY_BETA_OR_EARLIER@ + mirror: always + +- name: browser.cache.disk.enable + type: RelaxedAtomicBool + value: true + mirror: always + +- name: browser.cache.memory.enable + type: RelaxedAtomicBool + value: true + mirror: always + +# Limit of recent metadata we keep in memory for faster access, in KB. +- name: browser.cache.disk.metadata_memory_limit + type: RelaxedAtomicUint32 + value: 250 # 0.25 MB + mirror: always + +# Does the user want smart-sizing? +- name: browser.cache.disk.smart_size.enabled + type: RelaxedAtomicBool + value: true + mirror: always + +# Disk cache capacity in kilobytes. It's used only when +# browser.cache.disk.smart_size.enabled == false +- name: browser.cache.disk.capacity + type: RelaxedAtomicUint32 + value: 256000 + mirror: always + +# -1 = determine dynamically, 0 = none, n = memory capacity in kilobytes. +- name: browser.cache.memory.capacity + type: RelaxedAtomicInt32 + value: -1 + mirror: always + +# When smartsizing is disabled we could potentially fill all disk space by +# cache data when the disk capacity is not set correctly. To avoid that we +# check the free space every time we write some data to the cache. The free +# space is checked against two limits. Once the soft limit is reached we start +# evicting the least useful entries, when we reach the hard limit writing to +# the entry fails. +- name: browser.cache.disk.free_space_soft_limit + type: RelaxedAtomicUint32 + value: 5 * 1024 # 5MB + mirror: always + +- name: browser.cache.disk.free_space_hard_limit + type: RelaxedAtomicUint32 + value: 1024 # 1MB + mirror: always + +# The number of chunks we preload ahead of read. One chunk currently has +# 256kB. +- name: browser.cache.disk.preload_chunk_count + type: RelaxedAtomicUint32 + value: 4 # 1 MB of read ahead + mirror: always + +# Max-size (in KB) for entries in disk cache. Set to -1 for no limit. +# (Note: entries bigger than 1/8 of disk-cache are never cached) +- name: browser.cache.disk.max_entry_size + type: RelaxedAtomicUint32 + value: 50 * 1024 # 50 MB + mirror: always + +# Max-size (in KB) for entries in memory cache. Set to -1 for no limit. +# (Note: entries bigger than than 90% of the mem-cache are never cached.) +- name: browser.cache.memory.max_entry_size + type: RelaxedAtomicInt32 + value: 5 * 1024 + mirror: always + +# Memory limit (in kB) for new cache data not yet written to disk. Writes to +# the cache are buffered and written to disk on background with low priority. +# With a slow persistent storage these buffers may grow when data is coming +# fast from the network. When the amount of unwritten data is exceeded, new +# writes will simply fail. We have two buckets, one for important data +# (priority) like html, css, fonts and js, and one for other data like images, +# video, etc. +# Note: 0 means no limit. +- name: browser.cache.disk.max_chunks_memory_usage + type: RelaxedAtomicUint32 + value: 40 * 1024 + mirror: always +- name: browser.cache.disk.max_priority_chunks_memory_usage + type: RelaxedAtomicUint32 + value: 40 * 1024 + mirror: always + + +# Number of seconds the cache spends writing pending data and closing files +# after shutdown has been signalled. Past that time data is not written and +# files are left open for the OS to clean up. +- name: browser.cache.max_shutdown_io_lag + type: RelaxedAtomicUint32 + value: 2 + mirror: always + +# After the max_shutdown_io_lag has passed, we will attempt to cancel +# blocking IO (on windows). The CacheIOThread may pick up more blocking +# tasks so we want to cancel those too. The main thread will be woken +# up every shutdown_io_time_between_cancellations_ms to cancel the IO +# on the other thread. +- name: browser.cache.shutdown_io_time_between_cancellations_ms + type: RelaxedAtomicUint32 + value: 5 + mirror: always + +# A percentage limit for media content type in the disk cache. When some entries +# need to be evicted and media is over the limit, it's evicted first. +- name: browser.cache.disk.content_type_media_limit + type: RelaxedAtomicInt32 + value: 50 + mirror: always + +# How often to validate document in cache +# 0 = once-per-session, +# 1 = each-time, +# 2 = never, +# 3 = when-appropriate/automatically +- name: browser.cache.check_doc_frequency + type: RelaxedAtomicUint32 + value: 3 + mirror: always + +# Compression level for cached JavaScript bytecode +# 0 = do not compress, +# 1 = minimal compression, +# 9 = maximal compression +- name: browser.cache.jsbc_compression_level + type: RelaxedAtomicUint32 + value: 0 + mirror: always + +- name: browser.contentblocking.database.enabled + type: bool + value: false + mirror: always + +# How many recent block/unblock actions per origins we remember in the +# Content Blocking log for each top-level window. +- name: browser.contentblocking.originlog.length + type: uint32_t + value: 32 + mirror: always + +- name: browser.display.background_color + type: String + value: "#FFFFFF" + mirror: never + +- name: browser.display.background_color.dark + type: String + value: "#1C1B22" + mirror: never + +# This preference is a bit confusing because we use the opposite +# string value in the colors dialog to indicate to users how FF HCM +# will behave. +# With resect to document colors, these values mean: +# 0 = "default" = always, except in high contrast mode +# 1 = "always" +# 2 = "never" +# +# On windows, we set this to 0, which means FF HCM will mirror OS HCM. +# Everywhere else, we set this to 1, disabling FF HCM. +- name: browser.display.document_color_use + type: RelaxedAtomicUint32 +#if defined(XP_WIN) + value: 0 +#else + value: 1 +#endif + mirror: always + rust: true + +# 0 = always native +# 1 = never native +# other = default +- name: browser.display.windows.non_native_menus + type: RelaxedAtomicUint32 + value: 2 + mirror: always + rust: true + +# This pref dictates whether or not backplates and background images +# are to be drawn, when in high-contrast mode: +# false: do not draw backplates or render background images +# true: render background images and draw backplates +# This condition is only considered when high-contrast mode is enabled +# in Firefox, ie. when the user has: +# (1) mUseAccessibilityMode set to true (Widows high-contrast mode is on) +# AND browser.display.document_color_use set to 0 +# (only with high-contrast themes) OR +# (2) browser.display.document_color_use set to 2 (always) +- name: browser.display.permit_backplate + type: RelaxedAtomicBool + value: true + mirror: always + rust: true + +# Whether we should suppress the background-image of the canvas (the root +# frame) if we're in forced colors mode. +# +# This is important because some sites use background-image with a plain color +# and it causes undesirable results in high-contrast mode. +# +# See bug 1614921 for example. +- name: browser.display.suppress_canvas_background_image_on_forced_colors + type: bool + value: true + mirror: always + +- name: browser.display.focus_ring_on_anything + type: bool + value: false + mirror: always + +- name: browser.display.focus_ring_width + type: uint32_t + value: 1 + mirror: always + +- name: browser.display.focus_background_color + type: String + value: "#117722" + mirror: never + +# Focus ring border style. +# 0 = solid border, 1 = dotted border +- name: browser.display.focus_ring_style + type: uint32_t + value: 1 + mirror: always + +- name: browser.display.focus_text_color + type: String + value: "#ffffff" + mirror: never + +- name: browser.display.foreground_color + type: String + value: "#000000" + mirror: never + +- name: browser.display.foreground_color.dark + type: String + value: "#FBFBFE" + mirror: never + +# Determines the behavior of OS zoom settings. +# +# 0: doesn't affect rendering at all +# 1: affects full zoom (dpi, effectively). +# 2: affects text zoom. +# +# Default is (1): Historical behavior on Linux, matches other browsers on +# Windows, and generally creates more consistent rendering. +- name: browser.display.os-zoom-behavior + type: RelaxedAtomicInt32 + value: 1 + mirror: always + rust: true + +# Whether focus rings are always shown by default. +# +# This is the initial value of nsWindowRoot::mShowFocusRings, but it can be +# overridden by system preferences. +- name: browser.display.show_focus_rings + type: bool + value: false + mirror: always + +# Whether we should always enable focus rings after focus was moved by keyboard. +# +# This behavior matches both historical and GTK / Windows focus behavior. +# +# :focus-visible is intended to provide better heuristics than this. +- name: browser.display.always_show_rings_after_key_focus + type: bool + value: false + mirror: always + +# In theory: 0 = never, 1 = quick, 2 = always, though we always just use it as +# a bool! +- name: browser.display.use_document_fonts + type: RelaxedAtomicInt32 + value: 1 + mirror: always + rust: true + +- name: browser.display.use_focus_colors + type: bool + value: false + mirror: always + +- name: browser.display.use_system_colors + type: RelaxedAtomicBool +#ifdef XP_WIN + value: true +#else + value: false +#endif + mirror: always + +- name: browser.dom.window.dump.enabled + type: RelaxedAtomicBool + value: @IS_NOT_MOZILLA_OFFICIAL@ + mirror: always + +# See bug 1710926 +- name: browser.download.improvements_to_download_panel + type: bool + value: true + mirror: always + +# See bug 1738574 +- name: browser.download.start_downloads_in_tmp_dir + type: bool + value: false + mirror: always + +# See bug 1747343 +- name: browser.download.always_ask_before_handling_new_types + type: bool + value: false + mirror: always + +# See bug 1731668 +- name: browser.download.enable_spam_prevention + type: bool + value: false + mirror: always + +# See bug 1772569 +- name: browser.download.open_pdf_attachments_inline + type: bool + value: false + mirror: always + +- name: browser.download.sanitize_non_media_extensions + type: bool + value: true + mirror: always + +# Image document's automatic image sizing. +- name: browser.enable_automatic_image_resizing + type: bool + value: true + mirror: always + +# Image document's click-to-resize. +- name: browser.enable_click_image_resizing + type: bool + value: @IS_NOT_ANDROID@ + mirror: always + +- name: browser.find.ignore_ruby_annotations + type: bool + value: true + mirror: always + +#if defined(XP_MACOSX) +# Whether pressing Esc will exit fullscreen. +- name: browser.fullscreen.exit_on_escape + type: bool + value: true + mirror: always +#endif + +# The max url length we'll store in history. +# +# The default value is mostly a guess based on various facts: +# +# * IE didn't support urls longer than 2083 chars +# * Sitemaps protocol used to support a maximum of 2048 chars +# * Various SEO guides suggest to not go over 2000 chars +# * Various apps/services are known to have issues over 2000 chars +# * RFC 2616 - HTTP/1.1 suggests being cautious about depending +# on URI lengths above 255 bytes +# +- name: browser.history.maxUrlLength + type: uint32_t + value: 2000 + mirror: always + +# Max size of push/replaceState data parameter +- name: browser.history.maxStateObjectSize + type: int32_t + value: 16777216 + mirror: always + +# True to collect wireframes upon navigations / pushState +- name: browser.history.collectWireframes + type: bool + value: false + mirror: always + +# The minimum area for a rect to be included in a wireframe, in CSS pixels. +# +# The current value of 50 is pretty arbitrary, and will be tuned as we refine +# and test the wireframing capability. +- name: browser.history.wireframeAreaThreshold + type: uint32_t + value: 50 + mirror: always + +#if defined(XP_WIN) || defined(XP_LINUX) + # Notify TabUnloader or send the memory pressure if the memory resource + # notification is signaled AND the available commit space is lower than + # this value. +- name: browser.low_commit_space_threshold_mb + type: RelaxedAtomicUint32 + value: 200 + mirror: always +#endif + +#ifdef XP_LINUX + # On Linux we also check available memory in comparison to total memory, + # and use this percent value (out of 100) to determine if we are in a + # low memory scenario. +- name: browser.low_commit_space_threshold_percent + type: RelaxedAtomicUint32 + value: 5 + mirror: always +#endif + +# Render animations and videos as a solid color +- name: browser.measurement.render_anims_and_video_solid + type: RelaxedAtomicBool + value: false + mirror: always + +- name: browser.navigation.requireUserInteraction + type: bool + value: false + mirror: always + +# Indicates if about:newtab shows content (enabled) or just blank. +- name: browser.newtabpage.enabled + type: bool + value: true + mirror: always + +# Open PDFs in Edge with the --app flag if it is the default. +- name: browser.pdf.launchDefaultEdgeAsApp + type: bool + value: true + mirror: always + +# Maximium delay between keystrokes that will be considered typing (milliseconds). +- name: browser.places.interactions.typing_timeout_ms + type: RelaxedAtomicUint32 + value: 3000 + mirror: always + +# Maximum delay between scroll input events that will be considered a scrolling interaction (milliseconds). +- name: browser.places.interactions.scrolling_timeout_ms + type: RelaxedAtomicUint32 + value: 5000 + mirror: always + +# Number of seconds till the sponsored session is timeout. +- name: browser.places.sponsoredSession.timeoutSecs + type: RelaxedAtomicUint32 + value: 3600 + mirror: always + +# Whether to start the private browsing mode at application startup +- name: browser.privatebrowsing.autostart + type: bool + value: false + mirror: always + +# Force usage of in-memory (rather than file on disk) media cache for video streaming when private browsing +- name: browser.privatebrowsing.forceMediaMemoryCache + type: bool + value: false + mirror: always + +# Communicates the toolbar color to platform (for e.g., prefers-color-scheme). +# +# Returns whether the toolbar is dark (0), light (1), or system (2). +# +# Default to "light" on macOS / Windows, and "system" elsewhere. The theming +# code sets it appropriately. +- name: browser.theme.toolbar-theme + type: RelaxedAtomicUint32 +#if defined(XP_WIN) || defined(XP_MACOSX) + value: 1 +#else + value: 2 +#endif + mirror: always + +# Communicates the preferred content theme color to platform (for e.g., +# prefers-color-scheme). +# +# dark (0), light (1), system (2), or toolbar (3). +# +# Default to "toolbar", the theming code sets it appropriately. +- name: browser.theme.content-theme + type: RelaxedAtomicUint32 + value: 2 + mirror: always + rust: true + +# Blocked plugin content +- name: browser.safebrowsing.blockedURIs.enabled + type: bool + value: true + mirror: always + +# Malware protection +- name: browser.safebrowsing.malware.enabled + type: bool + value: true + mirror: always + +# Password protection +- name: browser.safebrowsing.passwords.enabled + type: bool + value: false + mirror: always + +# Phishing protection +- name: browser.safebrowsing.phishing.enabled + type: bool + value: true + mirror: always + +# Maximum size for an array to store the safebrowsing prefixset. +- name: browser.safebrowsing.prefixset_max_array_size + type: RelaxedAtomicUint32 + value: 512*1024 + mirror: always + +# SessionStore prefs +# Maximum number of bytes of DOMSessionStorage data we collect per origin. +- name: browser.sessionstore.dom_storage_limit + type: uint32_t + value: 2048 + mirror: always + +# Maximum number of characters of form field data per field we collect. +- name: browser.sessionstore.dom_form_limit + type: uint32_t + value: 1024*1024*2 + mirror: always + +# Maximum number of characters of form data we collect per origin. +- name: browser.sessionstore.dom_form_max_limit + type: uint32_t + value: 1024*1024*50 + mirror: always + +# Minimal interval between two save operations in milliseconds (while the user is active). +- name: browser.sessionstore.interval + type: RelaxedAtomicUint32 + value: 15000 + mirror: always + +# Platform collection of data for session store +- name: browser.sessionstore.platform_collection + type: bool +#if defined(ANDROID) || defined(MOZ_THUNDERBIRD) + value: false +#else + value: true +#endif + mirror: once + +# Platform collection of session storage data for session store +- name: browser.sessionstore.collect_session_storage + type: bool + value: @IS_NOT_ANDROID@ + mirror: once + +# Platform collection of zoom data for session store +- name: browser.sessionstore.collect_zoom + type: bool + value: @IS_NOT_ANDROID@ + mirror: once + +# Causes SessionStore to ignore non-final update messages from +# browser tabs that were not caused by a flush from the parent. +# This is a testing flag and should not be used by end-users. +- name: browser.sessionstore.debug.no_auto_updates + type: RelaxedAtomicBool + value: false + mirror: always + +# +- name: browser.swipe.navigation-icon-move-distance + type: uint32_t +#if defined(XP_LINUX) || defined(XP_WIN) + value: 100 +#else + value: 0 +#endif + mirror: always + +# Whether we should draw the tabs on top of the titlebar. +# +# no (0), yes (1), or default (2), which is true everywhere except Linux. +- name: browser.tabs.inTitlebar + type: int32_t + value: 2 + mirror: always + +# If set, use DocumentChannel to directly initiate loads entirely +# from parent-process BrowsingContexts +- name: browser.tabs.documentchannel.parent-controlled + type: bool + value: false + mirror: always + +# Testing-only pref which makes data: URIs be loaded in a "web" content process +# instead of within a process based on the URI's loader. +- name: browser.tabs.remote.dataUriInDefaultWebProcess + type: bool + value: false + mirror: always + +# Testing-only pref to force system-triggered about:blank loads to not change +# content processes. This is used for performance tests which load an +# about:blank document between navigations for historical reasons to avoid +# unnecessary process switches. +- name: browser.tabs.remote.systemTriggeredAboutBlankAnywhere + type: bool + value: false + mirror: always + +# Testing-only pref to cause PBrowser creation for a specific BrowsingContext to +# fail, to test the errored codepath. +- name: browser.tabs.remote.testOnly.failPBrowserCreation.enabled + type: bool + value: false + mirror: always + +- name: browser.tabs.remote.desktopbehavior + type: bool + value: false + mirror: always + +- name: browser.tabs.remote.force-paint + type: bool + value: true + mirror: always + +# When this pref is enabled document loads with a mismatched +# Cross-Origin-Embedder-Policy header will fail to load +- name: browser.tabs.remote.useCrossOriginEmbedderPolicy + type: RelaxedAtomicBool + value: true + mirror: always + +# This pref makes `credentialless` a valid value for +# Cross-Origin-Embedder-Policy header +- name: browser.tabs.remote.coep.credentialless + type: RelaxedAtomicBool + value: @IS_NIGHTLY_BUILD@ + mirror: always + do_not_use_directly: true + +# When this pref is enabled top level loads with a mismatched +# Cross-Origin-Opener-Policy header will be loaded in a separate process. +- name: browser.tabs.remote.useCrossOriginOpenerPolicy + type: RelaxedAtomicBool + value: true + mirror: always + +# When this pref is enabled then we use a separate content process for +# top-level load of file:// URIs +- name: browser.tabs.remote.separateFileUriProcess + type: RelaxedAtomicBool +#if !defined(ANDROID) + value: true +#else + value: false +#endif + mirror: always + +# Pref to control whether we use a separate privileged content process +# for certain mozilla webpages (which are listed in the pref +# browser.tabs.remote.separatedMozillaDomains). +- name: browser.tabs.remote.separatePrivilegedMozillaWebContentProcess + type: bool + value: false + mirror: always + +# Whether or not process selection for subframes will prefer re-using an +# existing content process over creating a new one. Enabling this pref should +# reduce the number of processes allocated for non-first-party domains if +# dom.ipc.processCount.webIsolated > 1. +- name: browser.tabs.remote.subframesPreferUsed + type: bool + value: true + mirror: always + +# When this pref is enabled, opaque response is only allowed to enter the +# content process if it's a response for media (audio, image, video), CSS, or +# JavaScript. +- name: browser.opaqueResponseBlocking + type: RelaxedAtomicBool +#if defined(ANDROID) + value: false +#else + value: @IS_NIGHTLY_BUILD@ +#endif + mirror: always + +# When this pref is enabled, the JS validator will be enabled for +# ORB. +- name: browser.opaqueResponseBlocking.javascriptValidator + type: bool + value: @IS_NIGHTLY_BUILD@ + mirror: always + +# When this pref is enabled, and elements will create +# synthetic documents when the resource type they're loading is an image. +- name: browser.opaqueResponseBlocking.syntheticBrowsingContext + type: bool + value: true + mirror: once + +# When this pref is enabled, and elements will filter the +# browsing contexts created for the synthetic browsing contexts for the +# synthetic documents when browser.opaqueResponseBlocking.syntheticBrowsingContext +# is enabled from `Window.frames`, `Window.length` and named targeting. +- name: browser.opaqueResponseBlocking.syntheticBrowsingContext.filter + type: bool + value: true + mirror: once + do_not_use_directly: true + +# When true, zooming will be enabled on all sites, even ones that declare +# user-scalable=no. +- name: browser.ui.zoom.force-user-scalable + type: RelaxedAtomicBool + value: false + mirror: always + +- name: browser.underline_anchors + type: bool + value: true + mirror: always + +- name: browser.viewport.desktopWidth + type: RelaxedAtomicInt32 + value: 980 + mirror: always + +- name: browser.visited_color + type: String + value: "#551A8B" + mirror: never + +# If you change this, you probably also want to change +# nsXPLookAndFeel::GenericDarkColor for MozNativevisitedhyperlinktext. +- name: browser.visited_color.dark + type: String + value: "#FFADFF" + mirror: never + +# When true, soft reloads (including location.reload()) +# will only froce validate the top level document, subresources will +# be loaded normally as-if users normally navigated to the page. +- name: browser.soft_reload.only_force_validate_top_level_document + type: bool + value: true + mirror: always + +#--------------------------------------------------------------------------- +# Prefs starting with "canvas." +#--------------------------------------------------------------------------- + +# Limit for the canvas image cache. 0 means unlimited. +- name: canvas.image.cache.limit + type: int32_t + value: 0 + mirror: always + +# Add support for canvas path objects +- name: canvas.path.enabled + type: RelaxedAtomicBool + value: true + mirror: always + +- name: canvas.capturestream.enabled + type: bool + value: true + mirror: always + +# Is support for CanvasRenderingContext2D.filter enabled? +- name: canvas.filters.enabled + type: bool + value: true + mirror: always + +# Provide ability to turn on support for canvas focus rings. +- name: canvas.focusring.enabled + type: bool + value: true + mirror: always + +# Is support for CanvasRenderingContext2D's createConicGradient API enabled? +- name: canvas.createConicGradient.enabled + type: RelaxedAtomicBool + value: true + mirror: always + +#--------------------------------------------------------------------------- +# Prefs starting with "channelclassifier." +#--------------------------------------------------------------------------- + +- name: channelclassifier.allowlist_example + type: bool + value: false + mirror: always + +#--------------------------------------------------------------------------- +# Prefs starting with "clipboard." +#--------------------------------------------------------------------------- + +# Clipboard behavior. +- name: clipboard.autocopy + type: bool +#if !defined(ANDROID) && !defined(XP_MACOSX) && defined(XP_UNIX) + value: true +#else + value: false +#endif + mirror: always + +#ifdef XP_WIN + # allow to copy clipboard data to Clipboard History/Cloud + # (used on sensitive data in about:logins and Private Browsing) +- name: clipboard.copyPrivateDataToClipboardCloudOrHistory + type: bool + value: false + mirror: always +#endif + +#--------------------------------------------------------------------------- +# Prefs starting with "consoleservice." +#--------------------------------------------------------------------------- + +#if defined(ANDROID) + # Disable sending console to logcat on release builds. +- name: consoleservice.logcat + type: RelaxedAtomicBool + value: @IS_NOT_RELEASE_OR_BETA@ + mirror: always +#endif + +#--------------------------------------------------------------------------- +# Prefs starting with "content." +#--------------------------------------------------------------------------- + +- name: content.cors.disable + type: bool + value: false + mirror: always + +# Back off timer notification after count. +# -1 means never. +- name: content.notify.backoffcount + type: int32_t + value: -1 + mirror: always + +# Notification interval in microseconds. +# The notification interval has a dramatic effect on how long it takes to +# initially display content for slow connections. The current value +# provides good incremental display of content without causing an increase +# in page load time. If this value is set below 1/10 of a second it starts +# to impact page load performance. +# See bugzilla bug 72138 for more info. +- name: content.notify.interval + type: int32_t + value: 120000 + mirror: always + +# Do we notify based on time? +- name: content.notify.ontimer + type: bool + value: true + mirror: always + +# How many times to deflect in interactive mode. +- name: content.sink.interactive_deflect_count + type: int32_t + value: 0 + mirror: always + +# How many times to deflect in perf mode. +- name: content.sink.perf_deflect_count + type: int32_t + value: 200 + mirror: always + +# Parse mode for handling pending events. +# 0 = don't check for pending events +# 1 = don't deflect if there are pending events +# 2 = bail if there are pending events +- name: content.sink.pending_event_mode + type: int32_t +#ifdef XP_WIN + value: 1 +#else + value: 0 +#endif + mirror: always + +# How often to probe for pending events. 1 = every token. +- name: content.sink.event_probe_rate + type: int32_t + value: 1 + mirror: always + +# How long to stay off the event loop in interactive mode (microseconds). +- name: content.sink.interactive_parse_time + type: int32_t + value: 3000 + mirror: always + +# How long to stay off the event loop in perf mode. +- name: content.sink.perf_parse_time + type: int32_t + value: 30000 + mirror: always + +# How long to be in interactive mode after an event. +- name: content.sink.interactive_time + type: uint32_t + value: 750000 + mirror: always + +# How long to stay in perf mode after initial loading. +- name: content.sink.initial_perf_time + type: uint32_t + value: 2000000 + mirror: always + +# Should we switch between perf-mode and interactive-mode? +# 0 = Switch +# 1 = Interactive mode +# 2 = Perf mode +- name: content.sink.enable_perf_mode + type: int32_t + value: 0 + mirror: always + +#--------------------------------------------------------------------------- +# Prefs starting with "converter." +#--------------------------------------------------------------------------- + +# Whether we include ruby annotation in the text despite whether it +# is requested. This was true because we didn't explicitly strip out +# annotations. Set false by default to provide a better behavior, but +# we want to be able to pref-off it if user doesn't like it. +- name: converter.html2txt.always_include_ruby + type: bool + value: false + mirror: always + +#--------------------------------------------------------------------------- +# Prefs starting with "cookiebanners." +#--------------------------------------------------------------------------- + +# Controls the cookie banner handling mode in normal browsing. +# 0: Disables all cookie banner handling. +# 1: Reject-all if possible, otherwise do nothing. +# 2: Reject-all if possible, otherwise accept-all. +- name: cookiebanners.service.mode + type: uint32_t + value: 0 + mirror: always + +# Controls the cookie banner handling mode in private browsing. Same mode +# options as the normal browsing pref above. +- name: cookiebanners.service.mode.privateBrowsing + type: uint32_t + value: 0 + mirror: always + +# Enables use of global CookieBannerRules, which apply to all sites. This is +# used for click rules that can handle common Consent Management Providers +# (CMP). +# Enabling this (when the cookie handling feature is enabled) may negatively +# impact site performance since it requires us to run rule-defined query +# selectors for every page. +- name: cookiebanners.service.enableGlobalRules + type: bool + value: false + mirror: always + +# Enables the cookie banner cookie injector. The cookie banner cookie injector +# depends on the `cookiebanners.service.mode` pref above. +- name: cookiebanners.cookieInjector.enabled + type: bool + value: true + mirror: always + + +# By default, how many seconds in the future cookies should expire after they +# have been injected. Defaults to 12 months. Individual cookie rules may +# override this. +- name: cookiebanners.cookieInjector.defaultExpiryRelative + type: uint32_t + value: 31536000 + mirror: always + +# Enables the cookie banner auto clicking. The cookie banner auto clicking +# depends on the `cookiebanners.service.mode` pref above. +- name: cookiebanners.bannerClicking.enabled + type: bool + value: true + mirror: always + +# The maximum time in ms for detecting banner and button elements for cookie +# banner auto clicking. +- name: cookiebanners.bannerClicking.timeout + type: uint32_t + value: 3000 + mirror: always + +# Whether or not banner auto clicking test mode is enabled. +- name: cookiebanners.bannerClicking.testing + type: bool + value: false + mirror: always + +#--------------------------------------------------------------------------- +# Prefs starting with "datareporting." +#--------------------------------------------------------------------------- + +- name: datareporting.healthreport.uploadEnabled + type: RelaxedAtomicBool + value: false + mirror: always + rust: true + +#--------------------------------------------------------------------------- +# Prefs starting with "device." +#--------------------------------------------------------------------------- + +# Is support for the device sensors API enabled? +- name: device.sensors.enabled + type: bool + value: true + mirror: always + +# KaiOS-only, see https://bugzilla.mozilla.org/show_bug.cgi?id=1699707#c10 +- name: device.sensors.ambientLight.enabled + type: bool + value: false + mirror: always + +- name: device.sensors.motion.enabled + type: bool + value: true + mirror: always + +- name: device.sensors.orientation.enabled + type: bool + value: true + mirror: always + +# KaiOS-only, see https://bugzilla.mozilla.org/show_bug.cgi?id=1699707#c10 +- name: device.sensors.proximity.enabled + type: bool + value: false + mirror: always + +- name: device.sensors.test.events + type: bool + value: false + mirror: always + +#--------------------------------------------------------------------------- +# Prefs starting with "devtools." +#--------------------------------------------------------------------------- + +- name: devtools.console.stdout.chrome + type: RelaxedAtomicBool + value: @IS_NOT_MOZILLA_OFFICIAL@ + mirror: always + +- name: devtools.console.stdout.content + type: RelaxedAtomicBool + value: false + mirror: always + +- name: devtools.toolbox.force-chrome-prefs + type: RelaxedAtomicBool + value: true + mirror: always + +#--------------------------------------------------------------------------- +# Prefs starting with "docshell." +#--------------------------------------------------------------------------- + +# Used to indicate whether session history listeners should be notified +# about content viewer eviction. Used only for testing. +- name: docshell.shistory.testing.bfevict + type: bool + value: false + mirror: always + +# If true, pages with an opener won't be bfcached. +- name: docshell.shistory.bfcache.require_no_opener + type: bool + value: @IS_ANDROID@ + mirror: always + +# If true, page with beforeunload or unload event listeners can be bfcached. +- name: docshell.shistory.bfcache.allow_unload_listeners + type: bool + value: @IS_ANDROID@ + mirror: always + +# If true, page with beforeunload event listeners can be bfcached. +# This only works when sessionHistoryInParent is enabled. +- name: docshell.shistory.bfcache.ship_allow_beforeunload_listeners + type: bool + value: @IS_EARLY_BETA_OR_EARLIER@ + mirror: always + +#--------------------------------------------------------------------------- +# Prefs starting with "dom." +#--------------------------------------------------------------------------- + +# Whether CanvasRenderingContext2D.mozTextStyle is enabled. +- name: dom.mozTextStyle.enabled + type: bool + value: @IS_NOT_EARLY_BETA_OR_EARLIER@ + mirror: always + +# Allow cut/copy +- name: dom.allow_cut_copy + type: bool + value: true + mirror: always + +- name: dom.allow_XUL_XBL_for_file + type: bool + value: false + mirror: always + +# Checks if offscreen animation throttling is enabled. +- name: dom.animations.offscreen-throttling + type: bool + value: true + mirror: always + +# Is support for automatically removing replaced filling animations enabled? +- name: dom.animations-api.autoremove.enabled + type: bool + value: true + mirror: always + +# Is support for composite operations from the Web Animations API enabled? +- name: dom.animations-api.compositing.enabled + type: bool + value: true + mirror: always + +# Is support for the core interfaces of Web Animations API enabled? +- name: dom.animations-api.core.enabled + type: bool + value: true + mirror: always + +# Is support for Document.getAnimations() and Element.getAnimations() +# supported? +- name: dom.animations-api.getAnimations.enabled + type: bool + value: true + mirror: always + +# Is support for animations from the Web Animations API without 0%/100% +# keyframes enabled? +- name: dom.animations-api.implicit-keyframes.enabled + type: bool + value: true + mirror: always + +# Is support for timelines from the Web Animations API enabled? +- name: dom.animations-api.timelines.enabled + type: bool + value: true + mirror: always + +# Synchronize transform animations with geometric animations on the +# main thread. +- name: dom.animations.mainthread-synchronization-with-geometric-animations + type: bool + value: @IS_NOT_NIGHTLY_BUILD@ + mirror: always + +# Is support for AudioWorklet enabled? +- name: dom.audioworklet.enabled + type: bool + value: true + mirror: always + +# Is support for Navigator.getBattery enabled? +- name: dom.battery.enabled + type: bool + value: true + mirror: always + +# Block multiple external protocol URLs in iframes per single event. +- name: dom.block_external_protocol_in_iframes + type: bool + value: true + mirror: always + +# Block sandboxed BrowsingContexts from navigating to external protocols. +- name: dom.block_external_protocol_navigation_from_sandbox + type: bool + value: true + mirror: always + +# Block Insecure downloads from Secure Origins +- name: dom.block_download_insecure + type: bool + value: true + mirror: always + +# Block all downloads in iframes with the sandboxed attribute +- name: dom.block_download_in_sandboxed_iframes + type: bool + value: true + mirror: always + +# Block multiple window.open() per single event. +- name: dom.block_multiple_popups + type: bool + value: true + mirror: always + +# The maximum number of popup that is allowed to be opened. Set to -1 for no +# limit. +- name: dom.popup_maximum + type: int32_t + value: 20 + mirror: always + +# Whether window.location.reload() and window.history.go(0) should be blocked +# if called directly from a window resize event handler. +# +# This used to be necessary long ago to prevent terrible UX when using stuff +# like TypeAheadFind (bug 258917), but it also causes compat issues on mobile +# (bug 1570566). +- name: dom.block_reload_from_resize_event_handler + type: bool + value: false + mirror: always + +# SW Cache API +- name: dom.caches.enabled + type: RelaxedAtomicBool + value: true + mirror: always + +# Exposes window.caches and skips SecureContext check. +# dom.serviceWorkers.testing.enabled also includes the same effect. +- name: dom.caches.testing.enabled + type: RelaxedAtomicBool + value: false + mirror: always + +# Disable capture attribute for input elements; only supported on GeckoView. +- name: dom.capture.enabled + type: bool + value: false + mirror: always + +# Allow control characters appear in composition string. +# When this is false, control characters except +# CHARACTER TABULATION (horizontal tab) are removed from +# both composition string and data attribute of compositionupdate +# and compositionend events. +- name: dom.compositionevent.allow_control_characters + type: bool + value: false + mirror: always + +- name: dom.crypto.randomUUID.enabled + type: RelaxedAtomicBool + value: true + mirror: always + +# Is support for CSSPseudoElement enabled? +- name: dom.css_pseudo_element.enabled + type: bool + value: false + mirror: always + +# After how many seconds we allow external protocol URLs in iframe when not in +# single events +- name: dom.delay.block_external_protocol_in_iframes + type: uint32_t + value: 10 # in seconds + mirror: always + +# Whether the above pref has any effect at all. +# Make sure cases like bug 1795380 work before trying to turn this off. See +# bug 1680721 for some other context that might be relevant. +- name: dom.delay.block_external_protocol_in_iframes.enabled + type: bool + value: true + mirror: always + +# HTML element +- name: dom.dialog_element.enabled + type: bool + value: true + mirror: always + +# Only propagate the open window click permission if the setTimeout() is equal +# to or less than this value. +- name: dom.disable_open_click_delay + type: int32_t + value: 1000 + mirror: always + +- name: dom.disable_open_during_load + type: bool + value: false + mirror: always + +- name: dom.disable_beforeunload + type: bool + value: false + mirror: always + +- name: dom.require_user_interaction_for_beforeunload + type: bool + value: true + mirror: always + +# Enable/disable Gecko specific edit commands +- name: dom.document.edit_command.contentReadOnly.enabled + type: bool + value: @IS_NOT_EARLY_BETA_OR_EARLIER@ + mirror: always + +- name: dom.document.edit_command.insertBrOnReturn.enabled + type: bool + value: @IS_NOT_EARLY_BETA_OR_EARLIER@ + mirror: always + +# If set this to true, `Document.execCommand` may be performed nestedly. +# Otherwise, nested calls just return false. +- name: dom.document.exec_command.nested_calls_allowed + type: bool + value: false + mirror: always + +- name: dom.enable_window_print + type: bool + value: @IS_NOT_ANDROID@ + mirror: always + +# Only intended for fuzzing purposes, this will break mozPrintCallback, etc. +- name: dom.window_print.fuzzing.block_while_printing + type: bool + value: false + mirror: always + +- name: dom.element.transform-getters.enabled + type: bool + value: false + mirror: always + +# Controls whether the "focus fixup rule" is enabled. Subject to minor changes +# based on https://github.com/whatwg/html/pull/8392 and +# https://github.com/whatwg/html/issues/8225 +- name: dom.focus.fixup + type: bool + value: true + mirror: always + +- name: dom.mouse_capture.enabled + type: bool + value: true + mirror: always + +# Is support for Performance.mozMemory enabled? +- name: dom.enable_memory_stats + type: bool + value: false + mirror: always + +# Enable Performance API +# Whether nonzero values can be returned from performance.timing.* +- name: dom.enable_performance + type: RelaxedAtomicBool + value: true + mirror: always + +# Enable Performance Observer API +- name: dom.enable_performance_observer + type: RelaxedAtomicBool + value: true + mirror: always + +# Whether resource timing will be gathered and returned by performance.GetEntries* +- name: dom.enable_resource_timing + type: bool + value: true + mirror: always + +# Whether event timing will be gathered and returned by performance observer* +- name: dom.enable_event_timing + type: RelaxedAtomicBool + value: true + mirror: always + +# Whether performance.GetEntries* will contain an entry for the active document +- name: dom.enable_performance_navigation_timing + type: bool + value: true + mirror: always + +# Whether the scheduler interface will be exposed +- name: dom.enable_web_task_scheduling + type: RelaxedAtomicBool + value: @IS_NIGHTLY_BUILD@ + mirror: always + +# If this is true, it's allowed to fire "cut", "copy" and "paste" events. +# Additionally, "input" events may expose clipboard content when inputType +# is "insertFromPaste" or something. +- name: dom.event.clipboardevents.enabled + type: bool + value: true + mirror: always + +# Whether touch event listeners are passive by default. +- name: dom.event.default_to_passive_touch_listeners + type: bool + value: true + mirror: always + +# Whether wheel listeners are passive by default. +- name: dom.event.default_to_passive_wheel_listeners + type: bool + value: true + mirror: always + +- name: dom.event.dragexit.enabled + type: bool + value: @IS_NOT_NIGHTLY_BUILD@ + mirror: always + +# Whether WheelEvent should return pixels instead of lines for +# WheelEvent.deltaX/Y/Z, when deltaMode hasn't been checked. +# +# Other browsers don't use line deltas and websites forget to check for it, see +# bug 1392460. +- name: dom.event.wheel-deltaMode-lines.disabled + type: bool + value: true + mirror: always + +# Mostly for debugging. Whether we should do the same as +# dom.event.wheel-deltaMode-lines.disabled, but unconditionally rather than +# only when deltaMode hasn't been checked. +- name: dom.event.wheel-deltaMode-lines.always-disabled + type: bool + value: false + mirror: always + +# A blocklist (list of domains) for the +# dom.event.wheel-deltaMode-lines.disabled behavior, in case potential +# unforeseen problems with it arrive. +- name: dom.event.wheel-deltaMode-lines.always-enabled + type: String + value: "" + mirror: never + +#if defined(XP_MACOSX) +# Whether to disable treating ctrl click as right click +- name: dom.event.treat_ctrl_click_as_right_click.disabled + type: bool + value: @IS_NIGHTLY_BUILD@ + mirror: always +#endif + +# Whether .offset{X,Y} for events targeted at SVG nodes returns bounds relative +# to the outer SVG. +- name: dom.events.offset-in-svg-relative-to-svg-root + type: bool + value: true + mirror: always + +# Disable clipboard.read(), clipboard.write() and ClipboardItem by default +- name: dom.events.asyncClipboard.clipboardItem + type: bool + value: false + mirror: always + +# Skips checking permission and user activation when accessing the clipboard. +# Should only be enabled in tests. +# Access with Clipboard::IsTestingPrefEnabled(). +- name: dom.events.testing.asyncClipboard + type: bool + value: false + mirror: always + do_not_use_directly: true + +# Control whether `navigator.clipboard.readText()` is exposed to content. +# Currently not supported by GeckoView, see bug 1776829. +- name: dom.events.asyncClipboard.readText + type: bool + value: false + mirror: always + do_not_use_directly: true + +# This pref controls whether or not the `protected` dataTransfer state is +# enabled. If the `protected` dataTransfer stae is disabled, then the +# DataTransfer will be read-only whenever it should be protected, and will not +# be disconnected after a drag event is completed. +- name: dom.events.dataTransfer.protected.enabled + type: bool + value: false + mirror: always + +# Whether to hide normal files (i.e. non-images) in dataTransfer inside +# the content process. +- name: dom.events.dataTransfer.mozFile.enabled + type: bool + value: false + mirror: always + +- name: dom.events.dataTransfer.imageAsFile.enabled + type: bool + value: false + mirror: always + +# User interaction timer interval, in ms +- name: dom.events.user_interaction_interval + type: uint32_t + value: 5000 + mirror: always + +# Whether to try to compress touchmove events on IPC layer. +- name: dom.events.compress.touchmove + type: bool + value: true + mirror: always + +# In addition to the above IPC layer compresison, allow touchmove +# events to be further coalesced in the child side after they +# are sent. +- name: dom.events.coalesce.touchmove + type: bool + value: true + mirror: always + +# Allow mousemove events to be coalesced in the child side after they are sent. +- name: dom.events.coalesce.mousemove + type: bool + value: true + mirror: always + +# Whether to expose test interfaces of various sorts +- name: dom.expose_test_interfaces + type: bool + value: false + mirror: always + +- name: dom.fetchObserver.enabled + type: RelaxedAtomicBool + value: false + mirror: always + +# Allow the content process to create a File from a path. This is allowed just +# on parent process, on 'file' Content process, or for testing. +- name: dom.file.createInChild + type: RelaxedAtomicBool + value: false + mirror: always + +# Whether or not File Handle is enabled. +- name: dom.fileHandle.enabled + type: RelaxedAtomicBool + value: false + mirror: always + +# Support @autocomplete values for form autofill feature. +- name: dom.forms.autocomplete.formautofill + type: bool + value: false + mirror: always + +# Only trusted submit event could trigger form submission. +- name: dom.forms.submit.trusted_event_only + type: bool + value: false + mirror: always + +# This pref just controls whether we format the number with grouping separator +# characters when the internal value is set or updated. It does not stop the +# user from typing in a number and using grouping separators. +- name: dom.forms.number.grouping + type: bool + value: false + mirror: always + +# Whether fullscreen should make the rest of the document inert. +# This matches other browsers but historically not Gecko. +- name: dom.fullscreen.modal + type: bool + value: true + mirror: always + +# Whether the Gamepad API is enabled +- name: dom.gamepad.enabled + type: bool + value: true + mirror: always + +# Is Gamepad Extension API enabled? +- name: dom.gamepad.extensions.enabled + type: bool + value: true + mirror: always + +# Is LightIndicator API enabled in Gamepad Extension API? +- name: dom.gamepad.extensions.lightindicator + type: bool + value: false + mirror: always + +# Is MultiTouch API enabled in Gamepad Extension API? +- name: dom.gamepad.extensions.multitouch + type: bool + value: false + mirror: always + +# Is Gamepad vibrate haptic feedback function enabled? +- name: dom.gamepad.haptic_feedback.enabled + type: bool + value: true + mirror: always + +- name: dom.gamepad.non_standard_events.enabled + type: bool + value: @IS_NOT_RELEASE_OR_BETA@ + mirror: always + +- name: dom.gamepad.test.enabled + type: RelaxedAtomicBool + value: false + mirror: always + +# W3C draft ImageCapture API +- name: dom.imagecapture.enabled + type: bool + value: false + mirror: always + +# +# +# See https://github.com/whatwg/html/pull/3752 +- name: dom.image-lazy-loading.enabled + type: RelaxedAtomicBool + value: true + mirror: always + +# The root margin for image lazy loading, defined as four (value, percentage) +# pairs. +- name: dom.image-lazy-loading.root-margin.top + type: float + value: 600 + mirror: always + +- name: dom.image-lazy-loading.root-margin.top.percentage + type: bool + value: false + mirror: always + +- name: dom.image-lazy-loading.root-margin.bottom + type: float + value: 600 + mirror: always + +- name: dom.image-lazy-loading.root-margin.bottom.percentage + type: bool + value: false + mirror: always + +- name: dom.image-lazy-loading.root-margin.left + type: float + value: 600 + mirror: always + +- name: dom.image-lazy-loading.root-margin.left.percentage + type: bool + value: false + mirror: always + +- name: dom.image-lazy-loading.root-margin.right + type: float + value: 600 + mirror: always + +- name: dom.image-lazy-loading.root-margin.right.percentage + type: bool + value: false + mirror: always + +# Enable indexedDB in private browsing mode with encryption +- name: dom.indexedDB.privateBrowsing.enabled + type: RelaxedAtomicBool + value: false + mirror: always + +# Whether or not indexedDB test mode is enabled. +- name: dom.indexedDB.testing + type: RelaxedAtomicBool + value: false + mirror: always + +# Whether or not indexedDB experimental features are enabled. +- name: dom.indexedDB.experimental + type: RelaxedAtomicBool + value: false + mirror: always + +# Whether or not indexedDB preprocessing is enabled. +- name: dom.indexedDB.preprocessing + type: RelaxedAtomicBool + value: false + mirror: always + +- name: dom.input_events.beforeinput.enabled + type: bool + value: true + mirror: always + +# Whether innerWidth / innerHeight return rounded or fractional sizes. +# +# NOTE(emilio): Fractional sizes are not web-compatible, see the regressions +# from bug 1676843, but we want to expose the fractional sizes (probably in +# another API) one way or another, see [1], so we're keeping the code for the +# time being. +# +# [1]: https://github.com/w3c/csswg-drafts/issues/5260 +- name: dom.innerSize.rounded + type: bool + value: true + mirror: always + +# Whether we conform to Input Events Level 1 or Input Events Level 2. +# true: conforming to Level 1 +# false: conforming to Level 2 +- name: dom.input_events.conform_to_level_1 + type: bool + value: true + mirror: always + +# Whether we allow BrowsingContextGroup to suspend input events +- name: dom.input_events.canSuspendInBCG.enabled + type: bool + value: false + mirror: always + +# The maximum time (milliseconds) we reserve for handling input events in each +# frame. +- name: dom.input_event_queue.duration.max + type: uint32_t + value: 8 + mirror: always + +# Enable not moving the cursor to end when a text input or textarea has .value +# set to the value it already has. By default, enabled. +- name: dom.input.skip_cursor_move_for_same_value_set + type: bool + value: true + mirror: always + +- name: dom.ipc.cancel_content_js_when_navigating + type: bool + value: true + mirror: always + +# How often to check for CPOW timeouts (ms). CPOWs are only timed +# out by the hang monitor. +- name: dom.ipc.cpow.timeout + type: uint32_t + value: 500 + mirror: always + +#ifdef MOZ_ENABLE_FORKSERVER +- name: dom.ipc.forkserver.enable + type: bool + value: false + mirror: once +#endif + +#ifdef MOZ_WIDGET_GTK +# +# Avoid the use of GTK in content processes if possible, by running +# them in headless mode, to conserve resources (e.g., connections to +# the X server). See the usage in `ContentParent.cpp` for the full +# definition of "if possible". +# +# This does not affect sandbox policies; content processes may still +# dynamically connect to the display server for, e.g., WebGL. +- name: dom.ipc.avoid-gtk + type: bool + value: true + mirror: always +#endif + +# Whether or not to collect a paired minidump when force-killing a +# content process. +- name: dom.ipc.tabs.createKillHardCrashReports + type: bool + value: @IS_NOT_RELEASE_OR_BETA@ + mirror: once + +# Allow Flash async drawing mode in 64-bit release builds. +- name: dom.ipc.plugins.asyncdrawing.enabled + type: RelaxedAtomicBool + value: true + mirror: always + +# How long we wait before unloading an idle plugin process. +- name: dom.ipc.plugins.unloadTimeoutSecs + type: RelaxedAtomicUint32 + value: 30 + mirror: always + +- name: dom.ipc.plugins.allow_dxgi_surface + type: bool + value: true + mirror: always + +# Enable e10s hang monitoring (slow script checking and plugin hang detection). +- name: dom.ipc.processHangMonitor + type: bool + value: true + mirror: once + +# Whether we report such process hangs +- name: dom.ipc.reportProcessHangs + type: RelaxedAtomicBool +# Don't report hangs in DEBUG builds. They're too slow and often a +# debugger is attached. +#ifdef DEBUG + value: false +#else + value: true +#endif + mirror: always + +- name: dom.ipc.tabs.disabled + type: bool + value: false + mirror: always + +# Process launch delay (in milliseconds). +- name: dom.ipc.processPrelaunch.delayMs + type: uint32_t +# This number is fairly arbitrary ... the intention is to put off +# launching another app process until the last one has finished +# loading its content, to reduce CPU/memory/IO contention. + value: 1000 + mirror: always + +- name: dom.ipc.processPrelaunch.startupDelayMs + type: uint32_t +# delay starting content processes for a short time after browser start +# to provide time for the UI to come up + value: 1000 + mirror: always + +# Process preallocation cache +# Only used in fission; in e10s we use 1 always +- name: dom.ipc.processPrelaunch.fission.number + type: uint32_t + value: 3 + mirror: always + +# Limit preallocated processes below this memory size (in MB) +- name: dom.ipc.processPrelaunch.lowmem_mb + type: uint32_t + value: 4096 + mirror: always + +- name: dom.ipc.processPriorityManager.enabled + type: bool + value: false + mirror: always + +- name: dom.ipc.processPriorityManager.testMode + type: bool + value: false + mirror: always + +- name: dom.ipc.processPriorityManager.backgroundPerceivableGracePeriodMS + type: uint32_t +#if defined(MOZ_WIDGET_ANDROID) && defined(NIGHTLY_BUILD) + value: 3000 +#else + value: 0 +#endif + mirror: always + +- name: dom.ipc.processPriorityManager.backgroundGracePeriodMS + type: uint32_t +#if defined(MOZ_WIDGET_ANDROID) && defined(NIGHTLY_BUILD) + value: 3000 +#else + value: 0 +#endif + mirror: always + +#ifdef XP_WIN +- name: dom.ipc.processPriorityManager.backgroundUsesEcoQoS + type: bool + value: false + mirror: always +#endif + +# Is support for HTMLElement.autocapitalize enabled? +- name: dom.forms.autocapitalize + type: bool + value: @IS_NIGHTLY_BUILD@ + mirror: always + +# Support for input type=month, type=week. By default, disabled. +- name: dom.forms.datetime.others + type: bool + value: @IS_ANDROID@ + mirror: always + +# Is support for HTMLElement.enterKeyHint enabled? +- name: dom.forms.enterkeyhint + type: bool + value: true + mirror: always + +# Is support for HTMLElement.inputMode enabled? +- name: dom.forms.inputmode + type: bool + value: true + mirror: always + +- name: dom.forms.always_allow_pointer_events.enabled + type: bool + value: @IS_NIGHTLY_BUILD@ + mirror: always + +# Is support for HTMLInputElement.showPicker enabled? +- name: dom.input.showPicker + type: bool + value: true + mirror: always + +# Whether to allow or disallow web apps to cancel `beforeinput` events caused +# by MozEditableElement#setUserInput() which is used by autocomplete, autofill +# and password manager. +- name: dom.input_event.allow_to_cancel_set_user_input + type: bool + value: false + mirror: always + +# How long a content process can take before closing its IPC channel +# after shutdown is initiated. If the process exceeds the timeout, +# we fear the worst and kill it. +- name: dom.ipc.tabs.shutdownTimeoutSecs + type: RelaxedAtomicUint32 +#if !defined(DEBUG) && !defined(MOZ_ASAN) && !defined(MOZ_VALGRIND) && !defined(MOZ_TSAN) + value: 20 +#else + value: 0 +#endif + mirror: always + +# Whether a native event loop should be used in the content process. +- name: dom.ipc.useNativeEventProcessing.content + type: RelaxedAtomicBool +#if defined(XP_WIN) || defined(XP_MACOSX) + value: false +#else + value: true +#endif + mirror: always + +# If this is true, TextEventDispatcher dispatches keydown and keyup events +# even during composition (keypress events are never fired during composition +# even if this is true). +- name: dom.keyboardevent.dispatch_during_composition + type: bool + value: true + mirror: always + +# Enable/disable KeyboardEvent.initKeyEvent function +- name: dom.keyboardevent.init_key_event.enabled + type: bool + value: false + mirror: always + +# Enable/disable KeyboardEvent.initKeyEvent function in addons even if it's +# disabled. +- name: dom.keyboardevent.init_key_event.enabled_in_addons + type: bool + value: true + mirror: always + +# If this is true, keypress events for non-printable keys are dispatched only +# for event listeners of the system event group in web content. +- name: dom.keyboardevent.keypress.dispatch_non_printable_keys_only_system_group_in_content + type: bool + value: true + mirror: always + +# If this is true, "keypress" event's keyCode value and charCode value always +# become same if the event is not created/initialized by JS. +- name: dom.keyboardevent.keypress.set_keycode_and_charcode_to_same_value + type: bool + value: true + mirror: always + +# Whether "W3C Web Manifest" processing is enabled +- name: dom.manifest.enabled + type: bool + value: true + mirror: always + +# Enable mapped array buffer by default. +- name: dom.mapped_arraybuffer.enabled + type: bool + value: true + mirror: always + +# This pref is used to enable/disable the `document.autoplayPolicy` API which +# returns a enum string which presents current autoplay policy and can change +# overtime based on user session activity. +- name: dom.media.autoplay.autoplay-policy-api + type: bool + value: false + mirror: always + +# Autoplay Policy Detection https://w3c.github.io/autoplay/ +- name: dom.media.autoplay-policy-detection.enabled + type: RelaxedAtomicBool + value: @IS_NIGHTLY_BUILD@ + mirror: always + +# Media Session API +- name: dom.media.mediasession.enabled + type: bool + value: true + mirror: always + +# WebCodecs API +- name: dom.media.webcodecs.enabled + type: RelaxedAtomicBool + value: false + mirror: always + +# Number of seconds of very quiet or silent audio before considering the audio +# inaudible. +- name: dom.media.silence_duration_for_audibility + type: AtomicFloat + value: 2.0f + mirror: always + +# Enable meta-viewport support in remote APZ-enabled frames. +- name: dom.meta-viewport.enabled + type: RelaxedAtomicBool + value: false + mirror: always + +# Timeout clamp in ms for timeouts we clamp. +- name: dom.min_timeout_value + type: RelaxedAtomicInt32 + value: 4 + mirror: always + +# Timeout clamp in ms for background windows. +- name: dom.min_background_timeout_value + type: int32_t + value: 1000 + mirror: always + +# Timeout clamp in ms for background windows when throttling isn't enabled. +- name: dom.min_background_timeout_value_without_budget_throttling + type: int32_t + value: 1000 + mirror: always + +# Are missing-property use counters for certain DOM attributes enabled? +- name: dom.missing_prop_counters.enabled + type: bool + value: true + mirror: always + +# Is support for module scripts ( + + diff --git a/modules/libpref/test/gtest/Basics.cpp b/modules/libpref/test/gtest/Basics.cpp new file mode 100644 index 0000000000..1bf8561a0d --- /dev/null +++ b/modules/libpref/test/gtest/Basics.cpp @@ -0,0 +1,58 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "gtest/gtest.h" +#include "mozilla/Preferences.h" + +using namespace mozilla; + +TEST(PrefsBasics, Errors) +{ + Preferences::SetBool("foo.bool", true, PrefValueKind::Default); + Preferences::SetBool("foo.bool", false, PrefValueKind::User); + ASSERT_EQ(Preferences::GetBool("foo.bool", false, PrefValueKind::Default), + true); + ASSERT_EQ(Preferences::GetBool("foo.bool", true, PrefValueKind::User), false); + + Preferences::SetInt("foo.int", -66, PrefValueKind::Default); + Preferences::SetInt("foo.int", -77, PrefValueKind::User); + ASSERT_EQ(Preferences::GetInt("foo.int", 1, PrefValueKind::Default), -66); + ASSERT_EQ(Preferences::GetInt("foo.int", 1, PrefValueKind::User), -77); + + Preferences::SetUint("foo.uint", 88, PrefValueKind::Default); + Preferences::SetUint("foo.uint", 99, PrefValueKind::User); + ASSERT_EQ(Preferences::GetUint("foo.uint", 1, PrefValueKind::Default), 88U); + ASSERT_EQ(Preferences::GetUint("foo.uint", 1, PrefValueKind::User), 99U); + + Preferences::SetFloat("foo.float", 3.33f, PrefValueKind::Default); + Preferences::SetFloat("foo.float", 4.44f, PrefValueKind::User); + ASSERT_FLOAT_EQ( + Preferences::GetFloat("foo.float", 1.0f, PrefValueKind::Default), 3.33f); + ASSERT_FLOAT_EQ(Preferences::GetFloat("foo.float", 1.0f, PrefValueKind::User), + 4.44f); +} + +TEST(PrefsBasics, Serialize) +{ + // Ensure that at least this one preference exists + Preferences::SetBool("foo.bool", true, PrefValueKind::Default); + ASSERT_EQ(Preferences::GetBool("foo.bool", false, PrefValueKind::Default), + true); + + nsCString str; + Preferences::SerializePreferences(str, true); + fprintf(stderr, "%s\n", str.Data()); + // Assert that some prefs were not sanitized + ASSERT_NE(nullptr, strstr(str.Data(), "B--:")); + ASSERT_NE(nullptr, strstr(str.Data(), "I--:")); + ASSERT_NE(nullptr, strstr(str.Data(), "S--:")); + // Assert that something was sanitized + ASSERT_NE( + nullptr, + strstr( + str.Data(), + "I-S:56/datareporting.policy.dataSubmissionPolicyAcceptedVersion")); +} diff --git a/modules/libpref/test/gtest/Parser.cpp b/modules/libpref/test/gtest/Parser.cpp new file mode 100644 index 0000000000..972d32cfca --- /dev/null +++ b/modules/libpref/test/gtest/Parser.cpp @@ -0,0 +1,496 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "gtest/gtest.h" +#include "mozilla/ArrayUtils.h" +#include "Preferences.h" + +using namespace mozilla; + +// Keep this in sync with the declaration in Preferences.cpp. +// +// It's declared here to avoid polluting Preferences.h with test-only stuff. +void TestParseError(PrefValueKind aKind, const char* aText, + nsCString& aErrorMsg); + +TEST(PrefsParser, Errors) +{ + nsAutoCStringN<128> actualErrorMsg; + +// Use a macro rather than a function so that the line number reported by +// gtest on failure is useful. +#define P(kind_, text_, expectedErrorMsg_) \ + do { \ + TestParseError(kind_, text_, actualErrorMsg); \ + ASSERT_STREQ(expectedErrorMsg_, actualErrorMsg.get()); \ + } while (0) + +#define DEFAULT(text_, expectedErrorMsg_) \ + P(PrefValueKind::Default, text_, expectedErrorMsg_) + +#define USER(text_, expectedErrorMsg_) \ + P(PrefValueKind::User, text_, expectedErrorMsg_) + + // clang-format off + + //------------------------------------------------------------------------- + // Valid syntax. (Other testing of more typical valid syntax and semantics is + // done in modules/libpref/test/unit/test_parser.js.) + //------------------------------------------------------------------------- + + // Normal prefs. + DEFAULT(R"( +pref("bool", true); +sticky_pref("int", 123); +user_pref("string", "value"); + )", + "" + ); + + // Totally empty input. + DEFAULT("", ""); + + // Whitespace-only input. + DEFAULT(R"( + + )" "\v \t \v \f", + "" + ); + + // Comment-only inputs. + DEFAULT(R"(// blah)", ""); + DEFAULT(R"(# blah)", ""); + DEFAULT(R"(/* blah */)", ""); + + //------------------------------------------------------------------------- + // All the lexing errors. (To be pedantic, some of the integer literal + // overflows are triggered in the parser, but put them all here so they're all + // in the one spot.) + //------------------------------------------------------------------------- + + // Integer overflow errors. + DEFAULT(R"( +pref("int.ok", 2147483647); +pref("int.overflow", 2147483648); +pref("int.ok", +2147483647); +pref("int.overflow", +2147483648); +pref("int.ok", -2147483648); +pref("int.overflow", -2147483649); +pref("int.overflow", 4294967296); +pref("int.overflow", +4294967296); +pref("int.overflow", -4294967296); +pref("int.overflow", 4294967297); +pref("int.overflow", 1234567890987654321); + )", + "test:3: prefs parse error: integer literal overflowed\n" + "test:5: prefs parse error: integer literal overflowed\n" + "test:7: prefs parse error: integer literal overflowed\n" + "test:8: prefs parse error: integer literal overflowed\n" + "test:9: prefs parse error: integer literal overflowed\n" + "test:10: prefs parse error: integer literal overflowed\n" + "test:11: prefs parse error: integer literal overflowed\n" + "test:12: prefs parse error: integer literal overflowed\n" + ); + + // Other integer errors. + DEFAULT(R"( +pref("int.unexpected", 100foo); +pref("int.ok", 0); + )", + "test:2: prefs parse error: unexpected character in integer literal\n" + ); + + // \x00 is not allowed. + DEFAULT(R"( +pref("string.bad-x-escape", "foo\x00bar"); +pref("int.ok", 0); + )", + "test:2: prefs parse error: \\x00 is not allowed\n" + ); + + // Various bad things after \x: end of string, punctuation, space, newline, + // EOF. + DEFAULT(R"( +pref("string.bad-x-escape", "foo\x"); +pref("string.bad-x-escape", "foo\x,bar"); +pref("string.bad-x-escape", "foo\x 12"); +pref("string.bad-x-escape", "foo\x +12"); +pref("string.bad-x-escape", "foo\x)", + "test:2: prefs parse error: malformed \\x escape sequence\n" + "test:3: prefs parse error: malformed \\x escape sequence\n" + "test:4: prefs parse error: malformed \\x escape sequence\n" + "test:5: prefs parse error: malformed \\x escape sequence\n" + "test:7: prefs parse error: malformed \\x escape sequence\n" + ); + + // Not enough hex digits. + DEFAULT(R"( +pref("string.bad-x-escape", "foo\x1"); +pref("int.ok", 0); + )", + "test:2: prefs parse error: malformed \\x escape sequence\n" + ); + + // Invalid hex digit. + DEFAULT(R"( +pref("string.bad-x-escape", "foo\x1G"); +pref("int.ok", 0); + )", + "test:2: prefs parse error: malformed \\x escape sequence\n" + ); + + // \u0000 is not allowed. + // (The string literal is broken in two so that MSVC doesn't complain about + // an invalid universal-character-name.) + DEFAULT(R"( +pref("string.bad-u-escape", "foo\)" R"(u0000 bar"); +pref("int.ok", 0); + )", + "test:2: prefs parse error: \\u0000 is not allowed\n" + ); + + // Various bad things after \u: end of string, punctuation, space, newline, + // EOF. + DEFAULT(R"( +pref("string.bad-u-escape", "foo\u"); +pref("string.bad-u-escape", "foo\u,bar"); +pref("string.bad-u-escape", "foo\u 1234"); +pref("string.bad-u-escape", "foo\u +1234"); +pref("string.bad-u-escape", "foo\u)", + "test:2: prefs parse error: malformed \\u escape sequence\n" + "test:3: prefs parse error: malformed \\u escape sequence\n" + "test:4: prefs parse error: malformed \\u escape sequence\n" + "test:5: prefs parse error: malformed \\u escape sequence\n" + "test:7: prefs parse error: malformed \\u escape sequence\n" + ); + + // Not enough hex digits. + DEFAULT(R"( +pref("string.bad-u-escape", "foo\u1"); +pref("string.bad-u-escape", "foo\u12"); +pref("string.bad-u-escape", "foo\u123"); +pref("int.ok", 0); + )", + "test:2: prefs parse error: malformed \\u escape sequence\n" + "test:3: prefs parse error: malformed \\u escape sequence\n" + "test:4: prefs parse error: malformed \\u escape sequence\n" + ); + + // Invalid hex digit. + DEFAULT(R"( +pref("string.bad-u-escape", "foo\u1G34"); +pref("int.ok", 0); + )", + "test:2: prefs parse error: malformed \\u escape sequence\n" + ); + + // High surrogate not followed by low surrogate. + // (The string literal is broken in two so that MSVC doesn't complain about + // an invalid universal-character-name.) + DEFAULT(R"( +pref("string.bad-u-surrogate", "foo\)" R"(ud83c,blah"); +pref("int.ok", 0); + )", + "test:2: prefs parse error: expected low surrogate after high surrogate\n" + ); + + // High surrogate followed by invalid low surrogate. + // (The string literal is broken in two so that MSVC doesn't complain about + // an invalid universal-character-name.) + DEFAULT(R"( +pref("string.bad-u-surrogate", "foo\)" R"(ud83c\u1234"); +pref("int.ok", 0); + )", + "test:2: prefs parse error: invalid low surrogate after high surrogate\n" + ); + + // Low surrogate not preceded by high surrogate. + // (The string literal is broken in two so that MSVC doesn't complain about + // an invalid universal-character-name.) + DEFAULT(R"( +pref("string.bad-u-surrogate", "foo\)" R"(udc00"); +pref("int.ok", 0); + )", + "test:2: prefs parse error: expected high surrogate before low surrogate\n" + ); + + // Unlike in JavaScript, \b, \f, \t, \v aren't allowed. + DEFAULT(R"( +pref("string.bad-escape", "foo\b"); +pref("string.bad-escape", "foo\f"); +pref("string.bad-escape", "foo\t"); +pref("string.bad-escape", "foo\v"); +pref("int.ok", 0); + )", + "test:2: prefs parse error: unexpected escape sequence character after '\\'\n" + "test:3: prefs parse error: unexpected escape sequence character after '\\'\n" + "test:4: prefs parse error: unexpected escape sequence character after '\\'\n" + "test:5: prefs parse error: unexpected escape sequence character after '\\'\n" + ); + + // Various bad things after \: non-special letter, number, punctuation, + // space, newline, EOF. + DEFAULT(R"( +pref("string.bad-escape", "foo\Q"); +pref("string.bad-escape", "foo\1"); +pref("string.bad-escape", "foo\,"); +pref("string.bad-escape", "foo\ n"); +pref("string.bad-escape", "foo\ +n"); +pref("string.bad-escape", "foo\)", + "test:2: prefs parse error: unexpected escape sequence character after '\\'\n" + "test:3: prefs parse error: unexpected escape sequence character after '\\'\n" + "test:4: prefs parse error: unexpected escape sequence character after '\\'\n" + "test:5: prefs parse error: unexpected escape sequence character after '\\'\n" + "test:6: prefs parse error: unexpected escape sequence character after '\\'\n" + "test:8: prefs parse error: unexpected escape sequence character after '\\'\n" + ); + + // Unterminated string literals. + + // Simple case. + DEFAULT(R"( +pref("string.unterminated-string", "foo + )", + "test:3: prefs parse error: unterminated string literal\n" + ); + + // Alternative case; `int` comes after the string and is seen as a keyword. + // The parser then skips to the ';', so no error about the unterminated + // string is issued. + DEFAULT(R"( +pref("string.unterminated-string", "foo); +pref("int.ok", 0); + )", + "test:3: prefs parse error: unknown keyword\n" + ); + + // Mismatched quotes (1). + DEFAULT(R"( +pref("string.unterminated-string", "foo'); + )", + "test:3: prefs parse error: unterminated string literal\n" + ); + + // Mismatched quotes (2). + DEFAULT(R"( +pref("string.unterminated-string", 'foo"); + )", + "test:3: prefs parse error: unterminated string literal\n" + ); + + // Unknown keywords. + DEFAULT(R"( +foo; +preff("string.bad-keyword", true); +ticky_pref("string.bad-keyword", true); +User_pref("string.bad-keyword", true); +pref("string.bad-keyword", TRUE); + )", + "test:2: prefs parse error: unknown keyword\n" + "test:3: prefs parse error: unknown keyword\n" + "test:4: prefs parse error: unknown keyword\n" + "test:5: prefs parse error: unknown keyword\n" + "test:6: prefs parse error: unknown keyword\n" + ); + + // Unterminated C-style comment. + DEFAULT(R"( +/* comment + )", + "test:3: prefs parse error: unterminated /* comment\n" + ); + + // Malformed comments (single slashes), followed by whitespace, newline, EOF. + DEFAULT(R"( +/ comment; +/ +; /)", + "test:2: prefs parse error: expected '/' or '*' after '/'\n" + "test:3: prefs parse error: expected '/' or '*' after '/'\n" + "test:4: prefs parse error: expected '/' or '*' after '/'\n" + ); + + // C++-style comment ending in EOF (1). + DEFAULT(R"( +// comment)", + "" + ); + + // C++-style comment ending in EOF (2). + DEFAULT(R"( +//)", + "" + ); + + // Various unexpected characters. + DEFAULT(R"( +pref("unexpected.chars", &true); +pref("unexpected.chars" : true); +@pref("unexpected.chars", true); +pref["unexpected.chars": true]; + )", + "test:2: prefs parse error: unexpected character\n" + "test:3: prefs parse error: unexpected character\n" + "test:4: prefs parse error: unexpected character\n" + "test:5: prefs parse error: unexpected character\n" + ); + + //------------------------------------------------------------------------- + // All the parsing errors. + //------------------------------------------------------------------------- + + DEFAULT(R"( +"pref"("parse.error": true); +pref1("parse.error": true); +pref(123: true); +pref("parse.error" true); +pref("parse.error", pref); +pref("parse.error", -true); +pref("parse.error", +"value"); +pref("parse.error", true,); +pref("parse.error", true; +pref("parse.error", true, sticky, locked; +pref("parse.error", true) +pref("int.ok", 1); +pref("parse.error", true))", + "test:2: prefs parse error: expected pref specifier at start of pref definition\n" + "test:3: prefs parse error: expected '(' after pref specifier\n" + "test:4: prefs parse error: expected pref name after '('\n" + "test:5: prefs parse error: expected ',' after pref name\n" + "test:6: prefs parse error: expected pref value after ','\n" + "test:7: prefs parse error: expected integer literal after '-'\n" + "test:8: prefs parse error: expected integer literal after '+'\n" + "test:9: prefs parse error: expected pref attribute after ','\n" + "test:10: prefs parse error: expected ',' or ')' after pref value\n" + "test:11: prefs parse error: expected ',' or ')' after pref attribute\n" + "test:13: prefs parse error: expected ';' after ')'\n" + "test:14: prefs parse error: expected ';' after ')'\n" + ); + + USER(R"( +pref("parse.error", true); +sticky_pref("parse.error", true); +user_pref("int.ok", 1); + )", + "test:2: prefs parse error: expected 'user_pref' at start of pref definition\n" + "test:3: prefs parse error: expected 'user_pref' at start of pref definition\n" + ); + + USER(R"( +user_pref("parse.error", true; +user_pref("int.ok", 1); + )", + "test:2: prefs parse error: expected ')' after pref value\n" + ); + + // Parse errors involving unexpected EOF. + + DEFAULT(R"( +pref)", + "test:2: prefs parse error: expected '(' after pref specifier\n" + ); + + DEFAULT(R"( +pref()", + "test:2: prefs parse error: expected pref name after '('\n" + ); + + DEFAULT(R"( +pref("parse.error")", + "test:2: prefs parse error: expected ',' after pref name\n" + ); + + DEFAULT(R"( +pref("parse.error",)", + "test:2: prefs parse error: expected pref value after ','\n" + ); + + DEFAULT(R"( +pref("parse.error", -)", + "test:2: prefs parse error: expected integer literal after '-'\n" + ); + + DEFAULT(R"( +pref("parse.error", +)", + "test:2: prefs parse error: expected integer literal after '+'\n" + ); + + DEFAULT(R"( +pref("parse.error", true)", + "test:2: prefs parse error: expected ',' or ')' after pref value\n" + ); + + USER(R"( +user_pref("parse.error", true)", + "test:2: prefs parse error: expected ')' after pref value\n" + ); + + DEFAULT(R"( +pref("parse.error", true,)", + "test:2: prefs parse error: expected pref attribute after ','\n" + ); + + DEFAULT(R"( +pref("parse.error", true, sticky)", + "test:2: prefs parse error: expected ',' or ')' after pref attribute\n" + ); + + DEFAULT(R"( +pref("parse.error", true))", + "test:2: prefs parse error: expected ';' after ')'\n" + ); + + // This is something we saw in practice with the old parser, which allowed + // repeated semicolons. + DEFAULT(R"( +pref("parse.error", true);; +pref("parse.error", true, locked);;; +pref("parse.error", true, sticky, locked);;;; +pref("int.ok", 0); + )", + "test:2: prefs parse error: expected pref specifier at start of pref definition\n" + "test:3: prefs parse error: expected pref specifier at start of pref definition\n" + "test:3: prefs parse error: expected pref specifier at start of pref definition\n" + "test:4: prefs parse error: expected pref specifier at start of pref definition\n" + "test:4: prefs parse error: expected pref specifier at start of pref definition\n" + "test:4: prefs parse error: expected pref specifier at start of pref definition\n" + ); + + //------------------------------------------------------------------------- + // Invalid syntax after various newline combinations, for the purpose of + // testing that line numbers are correct. + //------------------------------------------------------------------------- + + // In all of the following we have a \n, a \r, a \r\n, and then an error, so + // the error is on line 4. (Note: these ones don't use raw string literals + // because MSVC somehow swallows any \r that appears in them.) + + DEFAULT("\n \r \r\n bad", + "test:4: prefs parse error: unknown keyword\n" + ); + + DEFAULT("#\n#\r#\r\n bad", + "test:4: prefs parse error: unknown keyword\n" + ); + + DEFAULT("//\n//\r//\r\n bad", + "test:4: prefs parse error: unknown keyword\n" + ); + + DEFAULT("/*\n \r \r\n*/ bad", + "test:4: prefs parse error: unknown keyword\n" + ); + + // Note: the escape sequences do *not* affect the line number. + DEFAULT("pref(\"foo\\n\n foo\\r\r foo\\r\\n\r\n foo\", bad);", + "test:4: prefs parse error: unknown keyword\n" + ); + + // clang-format on +} diff --git a/modules/libpref/test/gtest/moz.build b/modules/libpref/test/gtest/moz.build new file mode 100644 index 0000000000..de16de26db --- /dev/null +++ b/modules/libpref/test/gtest/moz.build @@ -0,0 +1,25 @@ +# -*- 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/. + +Library("libpreftests") + +LOCAL_INCLUDES += [ + "../..", +] + +UNIFIED_SOURCES = [ + "Basics.cpp", + "Parser.cpp", +] + +# THE MOCK_METHOD2 macro from gtest triggers this clang warning and it's hard +# to work around, so we just ignore it. +if CONFIG["CC_TYPE"] == "clang": + CXXFLAGS += ["-Wno-inconsistent-missing-override"] + +FINAL_LIBRARY = "xul-gtest" + +REQUIRES_UNIFIED_BUILD = True diff --git a/modules/libpref/test/python.ini b/modules/libpref/test/python.ini new file mode 100644 index 0000000000..dd54269c68 --- /dev/null +++ b/modules/libpref/test/python.ini @@ -0,0 +1,4 @@ +[DEFAULT] +subsuite = mozbuild + +[test_generate_static_pref_list.py] diff --git a/modules/libpref/test/test_generate_static_pref_list.py b/modules/libpref/test/test_generate_static_pref_list.py new file mode 100644 index 0000000000..118aa47ec6 --- /dev/null +++ b/modules/libpref/test/test_generate_static_pref_list.py @@ -0,0 +1,471 @@ +# 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/. + +import sys +import unittest +from os import path + +import mozpack.path as mozpath +import mozunit +import yaml + +try: + from StringIO import StringIO +except ImportError: + from io import StringIO + +sys.path.append(path.join(path.dirname(__file__), "..")) +from init.generate_static_pref_list import generate_code + +test_data_path = mozpath.abspath(mozpath.dirname(__file__)) +test_data_path = mozpath.join(test_data_path, "data") + +# A single good input with lots of different combinations. +good_input = """ +- name: my.bool + type: bool + value: false + mirror: never + +- name: my.int + type: int32_t + value: -123 + mirror: once + do_not_use_directly: false + rust: false + +- mirror: always + value: 999 + type: uint32_t + name: my.uint + rust: true + +- name: my.float # A comment. + type: float # A comment. + do_not_use_directly: true # A comment. + value: 0.0f # A comment. + mirror: once # A comment. + rust: true # A comment. + +# A comment. +- name: my.string + type: String + value: foo"bar # The double quote needs escaping. + mirror: never + include: foobar.h + +# A comment. +- name: my.string2 + type: String + value: "foobar" # This string is quoted. + mirror: never + +# A comment. +- name: my.atomic.bool + type: RelaxedAtomicBool + value: true + mirror: always + rust: true + +# A comment. +- name: my.datamutex.string + type: DataMutexString + value: "foobar" # This string is quoted. + mirror: always + +# YAML+Python interprets `10 + 10 * 20` as a string, and so it is printed +# unchanged. +- name: my.atomic.int + type: ReleaseAcquireAtomicInt32 + value: 10 + 10 * 20 + mirror: always + do_not_use_directly: true # A comment. + +# YAML+Python changes `0x44` to `68` because it interprets the value as an +# integer. +- name: my.atomic.uint + type: SequentiallyConsistentAtomicUint32 + value: 0x44 + mirror: once + +# YAML+Python changes `.4455667` to `0.4455667` because it interprets the value +# as a float. +- name: my-dashed.atomic.float + type: AtomicFloat + value: .4455667 + mirror: never + include: +""" + +# The corresponding code for good_input. +good = {} + +good[ + "static_pref_list_all_h" +] = """\ +// This file was generated by generate_static_pref_list.py from (string input). DO NOT EDIT. + +#include "mozilla/StaticPrefList_my.h" +#include "mozilla/StaticPrefList_my_dashed.h" +""" + +good[ + "static_prefs_all_h" +] = """\ +// This file was generated by generate_static_pref_list.py from (string input). DO NOT EDIT. + +#include "mozilla/StaticPrefs_my.h" +#include "mozilla/StaticPrefs_my_dashed.h" +""" + +good["static_pref_list_group_h"] = { + "my": """\ +// This file was generated by generate_static_pref_list.py from (string input). DO NOT EDIT. + +NEVER_PREF("my.bool", bool, false) + +ONCE_PREF( + "my.int", + my_int, + my_int_AtStartup, + int32_t, -123 +) + +ALWAYS_PREF( + "my.uint", + my_uint, + my_uint, + uint32_t, 999 +) + +ONCE_PREF( + "my.float", + my_float, + my_float_AtStartup_DoNotUseDirectly, + float, 0.0f +) + +NEVER_PREF("my.string", String, "foo\\"bar") + +NEVER_PREF("my.string2", String, "foobar") + +ALWAYS_PREF( + "my.atomic.bool", + my_atomic_bool, + my_atomic_bool, + RelaxedAtomicBool, true +) + +ALWAYS_DATAMUTEX_PREF( + "my.datamutex.string", + my_datamutex_string, + my_datamutex_string, + DataMutexString, "foobar"_ns +) + +ALWAYS_PREF( + "my.atomic.int", + my_atomic_int, + my_atomic_int_DoNotUseDirectly, + ReleaseAcquireAtomicInt32, 10 + 10 * 20 +) + +ONCE_PREF( + "my.atomic.uint", + my_atomic_uint, + my_atomic_uint_AtStartup, + SequentiallyConsistentAtomicUint32, 68 +) +""", + "my_dashed": """\ +// This file was generated by generate_static_pref_list.py from (string input). DO NOT EDIT. + +NEVER_PREF("my-dashed.atomic.float", AtomicFloat, 0.4455667) +""", +} + +good["static_prefs_group_h"] = { + "my": """\ +// This file was generated by generate_static_pref_list.py from (string input). DO NOT EDIT. +// Include it to gain access to StaticPrefs::my_*. + +#ifndef mozilla_StaticPrefs_my_h +#define mozilla_StaticPrefs_my_h + +#include "foobar.h" + +#include "mozilla/StaticPrefListBegin.h" +#include "mozilla/StaticPrefList_my.h" +#include "mozilla/StaticPrefListEnd.h" + +#endif // mozilla_StaticPrefs_my_h +""" +} + +good[ + "static_prefs_c_getters_cpp" +] = """\ +// This file was generated by generate_static_pref_list.py from (string input). DO NOT EDIT. + +extern "C" uint32_t StaticPrefs_my_uint() { + return mozilla::StaticPrefs::my_uint(); +} + +extern "C" float StaticPrefs_my_float_AtStartup_DoNotUseDirectly() { + return mozilla::StaticPrefs::my_float_AtStartup_DoNotUseDirectly(); +} + +extern "C" bool StaticPrefs_my_atomic_bool() { + return mozilla::StaticPrefs::my_atomic_bool(); +} +""" + +good[ + "static_prefs_rs" +] = """\ +// This file was generated by generate_static_pref_list.py from (string input). DO NOT EDIT. + +extern "C" { + pub fn StaticPrefs_my_uint() -> u32; + pub fn StaticPrefs_my_float_AtStartup_DoNotUseDirectly() -> f32; + pub fn StaticPrefs_my_atomic_bool() -> bool; +} + +#[macro_export] +macro_rules! pref { + ("my.uint") => (unsafe { $crate::StaticPrefs_my_uint() }); + ("my.float") => (unsafe { $crate::StaticPrefs_my_float_AtStartup_DoNotUseDirectly() }); + ("my.atomic.bool") => (unsafe { $crate::StaticPrefs_my_atomic_bool() }); +} +""" + +# A lot of bad inputs, each with an accompanying error message. Listed in order +# of the relevant `error` calls within generate_static_pref_list.py. +bad_inputs = [ + ( + """ +- invalidkey: 3 +""", + "invalid key `invalidkey`", + ), + ( + """ +- type: int32_t +""", + "missing `name` key", + ), + ( + """ +- name: 99 +""", + "non-string `name` value `99`", + ), + ( + """ +- name: name_with_no_dot +""", + "`name` value `name_with_no_dot` lacks a '.'", + ), + ( + """ +- name: pref.is.defined.more.than.once + type: bool + value: false + mirror: never +- name: pref.is.defined.more.than.once + type: int32_t + value: 111 + mirror: always +""", + "`pref.is.defined.more.than.once` pref is defined more than once", + ), + ( + """ +- name: your.pref + type: bool + value: false + mirror: never +- name: my.pref + type: bool + value: false + mirror: never +""", + "`my.pref` pref must come before `your.pref` pref", + ), + ( + """ +- name: missing.type.key + value: false + mirror: never +""", + "missing `type` key for pref `missing.type.key`", + ), + ( + """ +- name: invalid.type.value + type: const char* + value: true + mirror: never +""", + "invalid `type` value `const char*` for pref `invalid.type.value`", + ), + ( + """ +- name: missing.value.key + type: int32_t + mirror: once +""", + "missing `value` key for pref `missing.value.key`", + ), + ( + """ +- name: non-string.value + type: String + value: 3.45 + mirror: once +""", + "non-string `value` value `3.45` for `String` pref `non-string.value`; add double quotes", + ), + ( + """ +- name: invalid.boolean.value + type: bool + value: true || false + mirror: once +""", + "invalid boolean value `true || false` for pref `invalid.boolean.value`", + ), + ( + """ +- name: missing.mirror.key + type: int32_t + value: 3 +""", + "missing `mirror` key for pref `missing.mirror.key`", + ), + ( + """ +- name: invalid.mirror.value + type: bool + value: true + mirror: sometimes +""", + "invalid `mirror` value `sometimes` for pref `invalid.mirror.value`", + ), + ( + """ +- name: non-boolean.do_not_use_directly.value + type: bool + value: true + mirror: always + do_not_use_directly: 0 +""", + "non-boolean `do_not_use_directly` value `0` for pref " + "`non-boolean.do_not_use_directly.value`", + ), + ( + """ +- name: do_not_use_directly.uselessly.set + type: int32_t + value: 0 + mirror: never + do_not_use_directly: true +""", + "`do_not_use_directly` uselessly set with `mirror` value `never` for " + "pref `do_not_use_directly.uselessly.set`", + ), + ( + """ +- name: non-string.include.value + type: bool + value: true + mirror: always + include: 33 +""", + "non-string `include` value `33` for pref `non-string.include.value`", + ), + ( + """ +- name: include.value.starts.with + type: float + value: M_PI + mirror: never + include: ` for " + "pref `include.value.starts.with`", + ), + ( + """ +- name: non-boolean.rust.value + type: bool + value: true + mirror: always + rust: 1 +""", + "non-boolean `rust` value `1` for pref `non-boolean.rust.value`", + ), + ( + """ +- name: rust.uselessly.set + type: int32_t + value: 0 + mirror: never + rust: true +""", + "`rust` uselessly set with `mirror` value `never` for pref " + "`rust.uselessly.set`", + ), +] + + +class TestGenerateStaticPrefList(unittest.TestCase): + """ + Unit tests for generate_static_pref_list.py. + """ + + def test_good(self): + "Test various pieces of good input." + inp = StringIO(good_input) + pref_list = yaml.safe_load(inp) + code = generate_code(pref_list, "(string input)") + + self.assertEqual(good["static_pref_list_all_h"], code["static_pref_list_all_h"]) + + self.assertEqual(good["static_prefs_all_h"], code["static_prefs_all_h"]) + + self.assertEqual( + good["static_pref_list_group_h"]["my"], + code["static_pref_list_group_h"]["my"], + ) + self.assertEqual( + good["static_pref_list_group_h"]["my_dashed"], + code["static_pref_list_group_h"]["my_dashed"], + ) + + self.assertEqual( + good["static_prefs_group_h"]["my"], code["static_prefs_group_h"]["my"] + ) + + self.assertEqual( + good["static_prefs_c_getters_cpp"], code["static_prefs_c_getters_cpp"] + ) + + self.assertEqual(good["static_prefs_rs"], code["static_prefs_rs"]) + + def test_bad(self): + "Test various pieces of bad input." + + for (input_string, expected) in bad_inputs: + inp = StringIO(input_string) + try: + pref_list = yaml.safe_load(inp) + generate_code(pref_list, "(string input") + self.assertEqual(0, 1) + except ValueError as e: + self.assertEqual(str(e), expected) + + +if __name__ == "__main__": + mozunit.main() diff --git a/modules/libpref/test/unit/data/testParser.js b/modules/libpref/test/unit/data/testParser.js new file mode 100644 index 0000000000..be29430b2a --- /dev/null +++ b/modules/libpref/test/unit/data/testParser.js @@ -0,0 +1,98 @@ +// Note: this file tests only valid syntax (of default pref files, not user +// pref files). See modules/libpref/test/gtest/Parser.cpp for tests of invalid +// syntax. + +# +# comment + # comment £ +// +// comment + // comment £ +/**/ + /* comment £ */ +/* comment + * and +some + # more + // comment */ +// comment /* +# comment /* +/* /* /* /* /* ...no nesting of C-style comments... */ + +// The following four lines have whitespace: \t, \v, \f, \r + + + + + +// This comment has the same whitespace: +# This comment has other stuff: \n \r \t \v \f \r \a \b \? \' \" \\ \@ +/* This comment has more stuff: \x61 \u0061 \u1234 \uXYZ */ + +/*0*/ pref /*1*/ ( /*2*/ "comment1" /*3*/ , /*4*/ true /*5*/ ) /*6*/ ; /*7*/ + +pref # foo +( // foo +"comment2" /* +foo +*/,/*foo*/ +true#foo +)// +; /*7*/ + +pref + ( + "spaced-out" + , + true + ) + ; + +pref("pref", true); +sticky_pref("sticky_pref", true); +user_pref("user_pref", true); +pref("sticky_pref2", true, sticky); +pref("locked_pref", true, locked); +pref("locked_sticky_pref", true, locked, sticky,sticky, + locked, locked, locked); + +pref("bool.true", true); +pref("bool.false", false); + +pref("int.0", 0); +pref("int.1", 1); +pref("int.123", 123); +pref("int.+234", +234); +pref("int.+ 345", + 345); +// Note that both the prefname and value have tabs in them +pref("int.-0", -0); +pref("int.-1", -1); +pref("int.- /* hmm */ 456", - /* hmm */ 456); +pref("int.-\n567", - +567); +pref("int.INT_MAX-1", 2147483646); +pref("int.INT_MAX", 2147483647); +pref("int.INT_MIN+2", -2147483646); +pref("int.INT_MIN+1", -2147483647); +pref("int.INT_MIN", -2147483648); +//pref("int.overflow.max", 2147483648); // overflows to -2147483648 +//pref("int.overflow.min", -2147483649); // overflows to 2147483647 +//pref("int.overflow.other", 4000000000); // overflows to -294967296 +//pref("int.overflow.another", 5000000000000000); // overflows to 937459712 + +pref("string.empty", ""); +pref("string.abc", "abc"); +pref("string.long", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); +pref('string.single-quotes', '"abc"'); +pref("string.double-quotes", "'abc'"); +pref("string.weird-chars", "  "); +pref("string.escapes", "\" \' \\ \n \r"); +pref("string.x-escapes1", "Mozilla0\x4d\x6F\x7a\x69\x6c\x6C\x610"); +pref("string.x-escapes2", "A\x41 A_umlaut\xc4 y_umlaut\xff"); +pref("string.u-escapes1", "A\u0041 A_umlaut\u00c4 y_umlaut\u00ff0"); +pref("string.u-escapes2", "S_acute\u015a y_grave\u1Ef3"); +// CYCLONE is 0x1f300, GRINNING FACE is 0x1f600. We have to represent them via +// surrogate pairs. +pref("string.u-surrogates", + "cyclone\uD83C\uDF00 grinning_face\uD83D\uDE00"); + diff --git a/modules/libpref/test/unit/data/testPref.js b/modules/libpref/test/unit/data/testPref.js new file mode 100644 index 0000000000..0864e7ce8b --- /dev/null +++ b/modules/libpref/test/unit/data/testPref.js @@ -0,0 +1,6 @@ +user_pref("testPref.bool1", true); +user_pref("testPref.bool2", false); +user_pref("testPref.int1", 23); +user_pref("testPref.int2", -1236); +user_pref("testPref.char1", "_testPref"); +user_pref("testPref.char2", "älskar"); \ No newline at end of file diff --git a/modules/libpref/test/unit/data/testPrefLocked.js b/modules/libpref/test/unit/data/testPrefLocked.js new file mode 100644 index 0000000000..001b65122b --- /dev/null +++ b/modules/libpref/test/unit/data/testPrefLocked.js @@ -0,0 +1,2 @@ +pref("testPref.unlocked.int", 333); +pref("testPref.locked.int", 444, locked); diff --git a/modules/libpref/test/unit/data/testPrefLockedUser.js b/modules/libpref/test/unit/data/testPrefLockedUser.js new file mode 100644 index 0000000000..4da40b7abc --- /dev/null +++ b/modules/libpref/test/unit/data/testPrefLockedUser.js @@ -0,0 +1,3 @@ +// testPrefLocked.js defined this pref as a locked pref. +// Changing a locked pref has no effect. +user_pref("testPref.locked.int", 555); diff --git a/modules/libpref/test/unit/data/testPrefSticky.js b/modules/libpref/test/unit/data/testPrefSticky.js new file mode 100644 index 0000000000..ef1a02adfd --- /dev/null +++ b/modules/libpref/test/unit/data/testPrefSticky.js @@ -0,0 +1,2 @@ +pref("testPref.unsticky.bool", true); +pref("testPref.sticky.bool", false, sticky); diff --git a/modules/libpref/test/unit/data/testPrefStickyUser.js b/modules/libpref/test/unit/data/testPrefStickyUser.js new file mode 100644 index 0000000000..d929c670ad --- /dev/null +++ b/modules/libpref/test/unit/data/testPrefStickyUser.js @@ -0,0 +1,5 @@ +// testPrefSticky.js defined this pref as sticky. Once a sticky +// pref has been changed, it's written as a user_pref(). +// So this test file reflects that scenario. +// Note the default in testPrefSticky.js is also false. +user_pref("testPref.sticky.bool", false); diff --git a/modules/libpref/test/unit/data/testPrefUTF8.js b/modules/libpref/test/unit/data/testPrefUTF8.js new file mode 100644 index 0000000000..a409c4eb72 --- /dev/null +++ b/modules/libpref/test/unit/data/testPrefUTF8.js @@ -0,0 +1,6 @@ +user_pref("testPref.bool1", true); +user_pref("testPref.bool2", false); +user_pref("testPref.int1", 23); +user_pref("testPref.int2", -1236); +user_pref("testPref.char1", "_testPref"); +user_pref("testPref.char2", "älskar"); diff --git a/modules/libpref/test/unit/extdata/testExt.js b/modules/libpref/test/unit/extdata/testExt.js new file mode 100644 index 0000000000..17c6849692 --- /dev/null +++ b/modules/libpref/test/unit/extdata/testExt.js @@ -0,0 +1,2 @@ +pref("testPref.bool2", true); +pref("testExtPref.bool", true); diff --git a/modules/libpref/test/unit/head_libPrefs.js b/modules/libpref/test/unit/head_libPrefs.js new file mode 100644 index 0000000000..ef7044a641 --- /dev/null +++ b/modules/libpref/test/unit/head_libPrefs.js @@ -0,0 +1,37 @@ +/* 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/. */ + +const NS_APP_USER_PROFILE_50_DIR = "ProfD"; + +function do_check_throws(f, result, stack) { + if (!stack) { + stack = Components.stack.caller; + } + + try { + f(); + } catch (exc) { + equal(exc.result, result, "Correct exception was thrown"); + return; + } + ok(false, "expected result " + result + ", none thrown"); +} + +// Register current test directory as provider for the profile directory. +var provider = { + getFile(prop, persistent) { + persistent.value = true; + if (prop == NS_APP_USER_PROFILE_50_DIR) { + return Services.dirsvc.get("CurProcD", Ci.nsIFile); + } + throw Components.Exception( + "Tried to get test directory '" + prop + "'", + Cr.NS_ERROR_FAILURE + ); + }, + QueryInterface: ChromeUtils.generateQI(["nsIDirectoryServiceProvider"]), +}; +Services.dirsvc + .QueryInterface(Ci.nsIDirectoryService) + .registerProvider(provider); diff --git a/modules/libpref/test/unit/test_bug1354613.js b/modules/libpref/test/unit/test_bug1354613.js new file mode 100644 index 0000000000..e609c6805a --- /dev/null +++ b/modules/libpref/test/unit/test_bug1354613.js @@ -0,0 +1,21 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/licenses/publicdomain/ */ + +function run_test() { + const BRANCH = "foo."; + const PREF_NAME = "testPref"; + const FULL_PREF_NAME = BRANCH + PREF_NAME; + + const FLOAT = 9.674; + const FUDGE = 0.001; + + let prefs = Services.prefs.getDefaultBranch(null); + + /* Test with a non-default branch */ + prefs.setCharPref(FULL_PREF_NAME, FLOAT); + let pb = Services.prefs.getBranch(BRANCH); + + let floatPref = pb.getFloatPref(PREF_NAME); + Assert.ok(FLOAT + FUDGE >= floatPref); + Assert.ok(FLOAT - FUDGE <= floatPref); +} diff --git a/modules/libpref/test/unit/test_bug345529.js b/modules/libpref/test/unit/test_bug345529.js new file mode 100644 index 0000000000..05fce35f57 --- /dev/null +++ b/modules/libpref/test/unit/test_bug345529.js @@ -0,0 +1,25 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/licenses/publicdomain/ */ + +// Regression test for bug 345529 - crash removing an observer during an +// nsPref:changed notification. +function run_test() { + const PREF_NAME = "testPref"; + + var observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + + observe: function observe(aSubject, aTopic, aState) { + Services.prefs.removeObserver(PREF_NAME, observer); + }, + }; + Services.prefs.addObserver(PREF_NAME, observer); + + Services.prefs.setCharPref(PREF_NAME, "test0"); + // This second call isn't needed on a clean profile: it makes sure + // the observer gets called even if the pref already had the value + // "test0" before this test. + Services.prefs.setCharPref(PREF_NAME, "test1"); + + Assert.ok(true); +} diff --git a/modules/libpref/test/unit/test_bug506224.js b/modules/libpref/test/unit/test_bug506224.js new file mode 100644 index 0000000000..033c5acc6e --- /dev/null +++ b/modules/libpref/test/unit/test_bug506224.js @@ -0,0 +1,25 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/licenses/publicdomain/ */ + +function run_test() { + const PREF_NAME = "testPref"; + + var prefs = Services.prefs.getDefaultBranch(null); + var userprefs = Services.prefs.getBranch(null); + + prefs.setCharPref(PREF_NAME, "test0"); + prefs.lockPref(PREF_NAME); + Assert.equal("test0", userprefs.getCharPref(PREF_NAME)); + Assert.equal(false, userprefs.prefHasUserValue(PREF_NAME)); + + var file = do_get_profile(); + file.append("prefs.js"); + Services.prefs.savePrefFile(file); + + prefs.unlockPref(PREF_NAME); + prefs.setCharPref(PREF_NAME, "test1"); + Services.prefs.readUserPrefsFromFile(file); + + Assert.equal("test1", userprefs.getCharPref(PREF_NAME)); + Assert.equal(false, userprefs.prefHasUserValue(PREF_NAME)); +} diff --git a/modules/libpref/test/unit/test_bug577950.js b/modules/libpref/test/unit/test_bug577950.js new file mode 100644 index 0000000000..f9106a349f --- /dev/null +++ b/modules/libpref/test/unit/test_bug577950.js @@ -0,0 +1,17 @@ +/* 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/. */ + +function run_test() { + var observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + + observe: function observe(aSubject, aTopic, aState) { + // Don't do anything. + }, + }; + + /* Set the same pref twice. This shouldn't leak. */ + Services.prefs.addObserver("UserPref.nonexistent.setIntPref", observer); + Services.prefs.addObserver("UserPref.nonexistent.setIntPref", observer); +} diff --git a/modules/libpref/test/unit/test_bug790374.js b/modules/libpref/test/unit/test_bug790374.js new file mode 100644 index 0000000000..683d3ecd22 --- /dev/null +++ b/modules/libpref/test/unit/test_bug790374.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/licenses/publicdomain/ */ + +function run_test() { + const PREF_NAME = "testPref"; + + var prefs = Services.prefs.getDefaultBranch(null); + var userprefs = Services.prefs.getBranch(null); + + /* First, test to make sure we can parse a float from a string properly. */ + prefs.setCharPref(PREF_NAME, "9.674"); + prefs.lockPref(PREF_NAME); + var myFloat = 9.674; + var fudge = 0.001; + var floatPref = userprefs.getFloatPref(PREF_NAME); + Assert.ok(myFloat + fudge >= floatPref); + Assert.ok(myFloat - fudge <= floatPref); + + /* Now test some failure conditions. */ + prefs.unlockPref(PREF_NAME); + prefs.setCharPref(PREF_NAME, ""); + prefs.lockPref(PREF_NAME); + do_check_throws(function() { + userprefs.getFloatPref(PREF_NAME); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + + prefs.unlockPref(PREF_NAME); + prefs.setCharPref(PREF_NAME, "18.0a1"); + prefs.lockPref(PREF_NAME); + + do_check_throws(function() { + userprefs.getFloatPref(PREF_NAME); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + + prefs.unlockPref(PREF_NAME); + prefs.setCharPref(PREF_NAME, "09.25.2012"); + prefs.lockPref(PREF_NAME); + + do_check_throws(function() { + userprefs.getFloatPref(PREF_NAME); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + + prefs.unlockPref(PREF_NAME); + prefs.setCharPref(PREF_NAME, "aString"); + prefs.lockPref(PREF_NAME); + + do_check_throws(function() { + userprefs.getFloatPref(PREF_NAME); + }, Cr.NS_ERROR_ILLEGAL_VALUE); +} diff --git a/modules/libpref/test/unit/test_changeType.js b/modules/libpref/test/unit/test_changeType.js new file mode 100644 index 0000000000..9cd24174d4 --- /dev/null +++ b/modules/libpref/test/unit/test_changeType.js @@ -0,0 +1,164 @@ +/* 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/. */ + +/* Tests for changing the type of a preference (bug 985998) */ + +function run_test() { + var ps = Services.prefs; + + let defaultBranch = ps.getDefaultBranch(""); + let userBranch = ps.getBranch(""); + + // Prefs that only have a default value -- we can't change their type. + defaultBranch.setBoolPref("TypeTest.default.bool", true); + defaultBranch.setIntPref("TypeTest.default.int", 23); + defaultBranch.setCharPref("TypeTest.default.char", "hey"); + + Assert.equal(userBranch.getBoolPref("TypeTest.default.bool"), true); + Assert.equal(userBranch.getIntPref("TypeTest.default.int"), 23); + Assert.equal(userBranch.getCharPref("TypeTest.default.char"), "hey"); + + // Prefs that only have a user value -- we can change their type, but only + // when we set the user value. + userBranch.setBoolPref("TypeTest.user.bool", false); + userBranch.setIntPref("TypeTest.user.int", 24); + userBranch.setCharPref("TypeTest.user.char", "hi"); + + Assert.equal(userBranch.getBoolPref("TypeTest.user.bool"), false); + Assert.equal(userBranch.getIntPref("TypeTest.user.int"), 24); + Assert.equal(userBranch.getCharPref("TypeTest.user.char"), "hi"); + + // Prefs that have both a default and a user value -- we can't change their + // type. + defaultBranch.setBoolPref("TypeTest.both.bool", true); + userBranch.setBoolPref("TypeTest.both.bool", false); + defaultBranch.setIntPref("TypeTest.both.int", 25); + userBranch.setIntPref("TypeTest.both.int", 26); + defaultBranch.setCharPref("TypeTest.both.char", "yo"); + userBranch.setCharPref("TypeTest.both.char", "ya"); + + Assert.equal(userBranch.getBoolPref("TypeTest.both.bool"), false); + Assert.equal(userBranch.getIntPref("TypeTest.both.int"), 26); + Assert.equal(userBranch.getCharPref("TypeTest.both.char"), "ya"); + + // We only have a default value, and we try to set a default value of a + // different type --> fails. + do_check_throws(function() { + defaultBranch.setCharPref("TypeTest.default.bool", "boo"); + }, Cr.NS_ERROR_UNEXPECTED); + do_check_throws(function() { + defaultBranch.setIntPref("TypeTest.default.bool", 5); + }, Cr.NS_ERROR_UNEXPECTED); + do_check_throws(function() { + defaultBranch.setCharPref("TypeTest.default.int", "boo"); + }, Cr.NS_ERROR_UNEXPECTED); + do_check_throws(function() { + defaultBranch.setBoolPref("TypeTest.default.int", true); + }, Cr.NS_ERROR_UNEXPECTED); + do_check_throws(function() { + defaultBranch.setBoolPref("TypeTest.default.char", true); + }, Cr.NS_ERROR_UNEXPECTED); + do_check_throws(function() { + defaultBranch.setIntPref("TypeTest.default.char", 6); + }, Cr.NS_ERROR_UNEXPECTED); + + // We only have a default value, and we try to set a user value of a + // different type --> fails. + do_check_throws(function() { + userBranch.setCharPref("TypeTest.default.bool", "boo"); + }, Cr.NS_ERROR_UNEXPECTED); + do_check_throws(function() { + userBranch.setIntPref("TypeTest.default.bool", 5); + }, Cr.NS_ERROR_UNEXPECTED); + do_check_throws(function() { + userBranch.setCharPref("TypeTest.default.int", "boo"); + }, Cr.NS_ERROR_UNEXPECTED); + do_check_throws(function() { + userBranch.setBoolPref("TypeTest.default.int", true); + }, Cr.NS_ERROR_UNEXPECTED); + do_check_throws(function() { + userBranch.setBoolPref("TypeTest.default.char", true); + }, Cr.NS_ERROR_UNEXPECTED); + do_check_throws(function() { + userBranch.setIntPref("TypeTest.default.char", 6); + }, Cr.NS_ERROR_UNEXPECTED); + + // We only have a user value, and we try to set a default value of a + // different type --> fails. + do_check_throws(function() { + defaultBranch.setCharPref("TypeTest.user.bool", "boo"); + }, Cr.NS_ERROR_UNEXPECTED); + do_check_throws(function() { + defaultBranch.setIntPref("TypeTest.user.bool", 5); + }, Cr.NS_ERROR_UNEXPECTED); + do_check_throws(function() { + defaultBranch.setCharPref("TypeTest.user.int", "boo"); + }, Cr.NS_ERROR_UNEXPECTED); + do_check_throws(function() { + defaultBranch.setBoolPref("TypeTest.user.int", true); + }, Cr.NS_ERROR_UNEXPECTED); + do_check_throws(function() { + defaultBranch.setBoolPref("TypeTest.user.char", true); + }, Cr.NS_ERROR_UNEXPECTED); + do_check_throws(function() { + defaultBranch.setIntPref("TypeTest.user.char", 6); + }, Cr.NS_ERROR_UNEXPECTED); + + // We only have a user value, and we try to set a user value of a + // different type --> SUCCEEDS. + userBranch.setCharPref("TypeTest.user.bool", "boo"); + Assert.equal(userBranch.getCharPref("TypeTest.user.bool"), "boo"); + userBranch.setIntPref("TypeTest.user.bool", 5); + Assert.equal(userBranch.getIntPref("TypeTest.user.bool"), 5); + userBranch.setCharPref("TypeTest.user.int", "boo"); + Assert.equal(userBranch.getCharPref("TypeTest.user.int"), "boo"); + userBranch.setBoolPref("TypeTest.user.int", true); + Assert.equal(userBranch.getBoolPref("TypeTest.user.int"), true); + userBranch.setBoolPref("TypeTest.user.char", true); + Assert.equal(userBranch.getBoolPref("TypeTest.user.char"), true); + userBranch.setIntPref("TypeTest.user.char", 6); + Assert.equal(userBranch.getIntPref("TypeTest.user.char"), 6); + + // We have both a default value and user value, and we try to set a default + // value of a different type --> fails. + do_check_throws(function() { + defaultBranch.setCharPref("TypeTest.both.bool", "boo"); + }, Cr.NS_ERROR_UNEXPECTED); + do_check_throws(function() { + defaultBranch.setIntPref("TypeTest.both.bool", 5); + }, Cr.NS_ERROR_UNEXPECTED); + do_check_throws(function() { + defaultBranch.setCharPref("TypeTest.both.int", "boo"); + }, Cr.NS_ERROR_UNEXPECTED); + do_check_throws(function() { + defaultBranch.setBoolPref("TypeTest.both.int", true); + }, Cr.NS_ERROR_UNEXPECTED); + do_check_throws(function() { + defaultBranch.setBoolPref("TypeTest.both.char", true); + }, Cr.NS_ERROR_UNEXPECTED); + do_check_throws(function() { + defaultBranch.setIntPref("TypeTest.both.char", 6); + }, Cr.NS_ERROR_UNEXPECTED); + + // We have both a default value and user value, and we try to set a user + // value of a different type --> fails. + do_check_throws(function() { + userBranch.setCharPref("TypeTest.both.bool", "boo"); + }, Cr.NS_ERROR_UNEXPECTED); + do_check_throws(function() { + userBranch.setIntPref("TypeTest.both.bool", 5); + }, Cr.NS_ERROR_UNEXPECTED); + do_check_throws(function() { + userBranch.setCharPref("TypeTest.both.int", "boo"); + }, Cr.NS_ERROR_UNEXPECTED); + do_check_throws(function() { + userBranch.setBoolPref("TypeTest.both.int", true); + }, Cr.NS_ERROR_UNEXPECTED); + do_check_throws(function() { + userBranch.setBoolPref("TypeTest.both.char", true); + }, Cr.NS_ERROR_UNEXPECTED); + do_check_throws(function() { + userBranch.setIntPref("TypeTest.both.char", 6); + }, Cr.NS_ERROR_UNEXPECTED); +} diff --git a/modules/libpref/test/unit/test_defaultValues.js b/modules/libpref/test/unit/test_defaultValues.js new file mode 100644 index 0000000000..632b2ffe13 --- /dev/null +++ b/modules/libpref/test/unit/test_defaultValues.js @@ -0,0 +1,59 @@ +/* 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/. */ + +/* Tests for providing a default value to get{Bool,Char,Float,Int}Pref */ + +function run_test() { + const ps = Services.prefs; + let prefName = "test.default.values.bool"; + do_check_throws(function() { + ps.getBoolPref(prefName); + }, Cr.NS_ERROR_UNEXPECTED); + strictEqual(ps.getBoolPref(prefName, false), false); + strictEqual(ps.getBoolPref(prefName, true), true); + ps.setBoolPref(prefName, true); + strictEqual(ps.getBoolPref(prefName), true); + strictEqual(ps.getBoolPref(prefName, false), true); + strictEqual(ps.getBoolPref(prefName, true), true); + + prefName = "test.default.values.char"; + do_check_throws(function() { + ps.getCharPref(prefName); + }, Cr.NS_ERROR_UNEXPECTED); + strictEqual(ps.getCharPref(prefName, ""), ""); + strictEqual(ps.getCharPref(prefName, "string"), "string"); + ps.setCharPref(prefName, "foo"); + strictEqual(ps.getCharPref(prefName), "foo"); + strictEqual(ps.getCharPref(prefName, "string"), "foo"); + + prefName = "test.default.values.string"; + do_check_throws(function() { + ps.getCharPref(prefName); + }, Cr.NS_ERROR_UNEXPECTED); + strictEqual(ps.getStringPref(prefName, ""), ""); + strictEqual(ps.getStringPref(prefName, "éèçàê€"), "éèçàê€"); + ps.setStringPref(prefName, "éèçàê€"); + strictEqual(ps.getStringPref(prefName), "éèçàê€"); + strictEqual(ps.getStringPref(prefName, "string"), "éèçàê€"); + + prefName = "test.default.values.float"; + do_check_throws(function() { + ps.getFloatPref(prefName); + }, Cr.NS_ERROR_UNEXPECTED); + strictEqual(ps.getFloatPref(prefName, 3.5), 3.5); + strictEqual(ps.getFloatPref(prefName, 0), 0); + ps.setCharPref(prefName, 1.75); + strictEqual(ps.getFloatPref(prefName), 1.75); + strictEqual(ps.getFloatPref(prefName, 3.5), 1.75); + + prefName = "test.default.values.int"; + do_check_throws(function() { + ps.getIntPref(prefName); + }, Cr.NS_ERROR_UNEXPECTED); + strictEqual(ps.getIntPref(prefName, 3), 3); + strictEqual(ps.getIntPref(prefName, 0), 0); + ps.setIntPref(prefName, 42); + strictEqual(ps.getIntPref(prefName), 42); + strictEqual(ps.getIntPref(prefName, 3), 42); +} diff --git a/modules/libpref/test/unit/test_dirtyPrefs.js b/modules/libpref/test/unit/test_dirtyPrefs.js new file mode 100644 index 0000000000..8ffda953e2 --- /dev/null +++ b/modules/libpref/test/unit/test_dirtyPrefs.js @@ -0,0 +1,69 @@ +/* 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/. */ + +/* Tests for handling of the preferences 'dirty' flag (bug 985998) */ + +function run_test() { + const ps = Services.prefs; + + let defaultBranch = ps.getDefaultBranch(""); + let userBranch = ps.getBranch(""); + + let prefFile = do_get_profile(); + prefFile.append("prefs.js"); + + //* *************************************************************************// + // prefs are not dirty after a write + ps.savePrefFile(null); + Assert.ok(!ps.dirty); + + // set a new a user value, we should become dirty + userBranch.setBoolPref("DirtyTest.new.bool", true); + Assert.ok(ps.dirty); + ps.savePrefFile(null); + // Overwrite a pref with the same value => not dirty + userBranch.setBoolPref("DirtyTest.new.bool", true); + Assert.ok(!ps.dirty); + + // Repeat for the other two types + userBranch.setIntPref("DirtyTest.new.int", 1); + Assert.ok(ps.dirty); + ps.savePrefFile(null); + // Overwrite a pref with the same value => not dirty + userBranch.setIntPref("DirtyTest.new.int", 1); + Assert.ok(!ps.dirty); + + userBranch.setCharPref("DirtyTest.new.char", "oop"); + Assert.ok(ps.dirty); + ps.savePrefFile(null); + // Overwrite a pref with the same value => not dirty + userBranch.setCharPref("DirtyTest.new.char", "oop"); + Assert.ok(!ps.dirty); + + // change *type* of a user value -> dirty + userBranch.setBoolPref("DirtyTest.new.char", false); + Assert.ok(ps.dirty); + ps.savePrefFile(null); + + // Set a default pref => not dirty (defaults don't go into prefs.js) + defaultBranch.setBoolPref("DirtyTest.existing.bool", true); + Assert.ok(!ps.dirty); + // Fail to change type of a pref with default value -> not dirty + do_check_throws(function() { + userBranch.setCharPref("DirtyTest.existing.bool", "boo"); + }, Cr.NS_ERROR_UNEXPECTED); + Assert.ok(!ps.dirty); + + // Set user value same as default, not dirty + userBranch.setBoolPref("DirtyTest.existing.bool", true); + Assert.ok(!ps.dirty); + // User value different from default, dirty + userBranch.setBoolPref("DirtyTest.existing.bool", false); + Assert.ok(ps.dirty); + ps.savePrefFile(null); + // Back to default value, dirty again + userBranch.setBoolPref("DirtyTest.existing.bool", true); + Assert.ok(ps.dirty); + ps.savePrefFile(null); +} diff --git a/modules/libpref/test/unit/test_libPrefs.js b/modules/libpref/test/unit/test_libPrefs.js new file mode 100644 index 0000000000..8e95a6e30c --- /dev/null +++ b/modules/libpref/test/unit/test_libPrefs.js @@ -0,0 +1,441 @@ +/* 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/. */ + +const PREF_INVALID = 0; +const PREF_BOOL = 128; +const PREF_INT = 64; +const PREF_STRING = 32; + +const MAX_PREF_LENGTH = 1 * 1024 * 1024; + +function makeList(a) { + var o = {}; + for (var i = 0; i < a.length; i++) { + o[a[i]] = ""; + } + return o; +} + +function run_test() { + const ps = Services.prefs; + + //* *************************************************************************// + // Nullsafety + + do_check_throws(function() { + ps.getPrefType(null); + }, Cr.NS_ERROR_INVALID_ARG); + do_check_throws(function() { + ps.getBoolPref(null); + }, Cr.NS_ERROR_INVALID_ARG); + do_check_throws(function() { + ps.setBoolPref(null, false); + }, Cr.NS_ERROR_INVALID_ARG); + do_check_throws(function() { + ps.getIntPref(null); + }, Cr.NS_ERROR_INVALID_ARG); + do_check_throws(function() { + ps.setIntPref(null, 0); + }, Cr.NS_ERROR_INVALID_ARG); + do_check_throws(function() { + ps.getCharPref(null); + }, Cr.NS_ERROR_INVALID_ARG); + do_check_throws(function() { + ps.setCharPref(null, null); + }, Cr.NS_ERROR_INVALID_ARG); + do_check_throws(function() { + ps.getStringPref(null); + }, Cr.NS_ERROR_INVALID_ARG); + do_check_throws(function() { + ps.setStringPref(null, null); + }, Cr.NS_ERROR_INVALID_ARG); + do_check_throws(function() { + ps.clearUserPref(null); + }, Cr.NS_ERROR_INVALID_ARG); + do_check_throws(function() { + ps.prefHasUserValue(null); + }, Cr.NS_ERROR_INVALID_ARG); + do_check_throws(function() { + ps.lockPref(null); + }, Cr.NS_ERROR_INVALID_ARG); + do_check_throws(function() { + ps.prefIsLocked(null); + }, Cr.NS_ERROR_INVALID_ARG); + do_check_throws(function() { + ps.unlockPref(null); + }, Cr.NS_ERROR_INVALID_ARG); + do_check_throws(function() { + ps.deleteBranch(null); + }, Cr.NS_ERROR_INVALID_ARG); + do_check_throws(function() { + ps.getChildList(null); + }, Cr.NS_ERROR_INVALID_ARG); + + //* *************************************************************************// + // Nonexisting user preferences + + Assert.equal(ps.prefHasUserValue("UserPref.nonexistent.hasUserValue"), false); + ps.clearUserPref("UserPref.nonexistent.clearUserPref"); // shouldn't throw + Assert.equal( + ps.getPrefType("UserPref.nonexistent.getPrefType"), + PREF_INVALID + ); + Assert.equal(ps.root, ""); + + // bool... + do_check_throws(function() { + ps.getBoolPref("UserPref.nonexistent.getBoolPref"); + }, Cr.NS_ERROR_UNEXPECTED); + ps.setBoolPref("UserPref.nonexistent.setBoolPref", false); + Assert.equal(ps.getBoolPref("UserPref.nonexistent.setBoolPref"), false); + + // int... + do_check_throws(function() { + ps.getIntPref("UserPref.nonexistent.getIntPref"); + }, Cr.NS_ERROR_UNEXPECTED); + ps.setIntPref("UserPref.nonexistent.setIntPref", 5); + Assert.equal(ps.getIntPref("UserPref.nonexistent.setIntPref"), 5); + + // char + do_check_throws(function() { + ps.getCharPref("UserPref.nonexistent.getCharPref"); + }, Cr.NS_ERROR_UNEXPECTED); + ps.setCharPref("UserPref.nonexistent.setCharPref", "_test"); + Assert.equal(ps.getCharPref("UserPref.nonexistent.setCharPref"), "_test"); + + //* *************************************************************************// + // Existing user Prefs and data integrity test (round-trip match) + + ps.setBoolPref("UserPref.existing.bool", true); + ps.setIntPref("UserPref.existing.int", 23); + ps.setCharPref("UserPref.existing.char", "hey"); + + // getPref should return the pref value + Assert.equal(ps.getBoolPref("UserPref.existing.bool"), true); + Assert.equal(ps.getIntPref("UserPref.existing.int"), 23); + Assert.equal(ps.getCharPref("UserPref.existing.char"), "hey"); + + // setPref should not complain and should change the value of the pref + ps.setBoolPref("UserPref.existing.bool", false); + Assert.equal(ps.getBoolPref("UserPref.existing.bool"), false); + ps.setIntPref("UserPref.existing.int", 24); + Assert.equal(ps.getIntPref("UserPref.existing.int"), 24); + ps.setCharPref("UserPref.existing.char", "hej dÃ¥!"); + Assert.equal(ps.getCharPref("UserPref.existing.char"), "hej dÃ¥!"); + + // prefHasUserValue should return true now + Assert.ok(ps.prefHasUserValue("UserPref.existing.bool")); + Assert.ok(ps.prefHasUserValue("UserPref.existing.int")); + Assert.ok(ps.prefHasUserValue("UserPref.existing.char")); + + // clearUserPref should remove the pref + ps.clearUserPref("UserPref.existing.bool"); + Assert.ok(!ps.prefHasUserValue("UserPref.existing.bool")); + ps.clearUserPref("UserPref.existing.int"); + Assert.ok(!ps.prefHasUserValue("UserPref.existing.int")); + ps.clearUserPref("UserPref.existing.char"); + Assert.ok(!ps.prefHasUserValue("UserPref.existing.char")); + + //* *************************************************************************// + // Large value test + + let largeStr = new Array(MAX_PREF_LENGTH + 1).join("x"); + ps.setCharPref("UserPref.large.char", largeStr); + largeStr += "x"; + do_check_throws(function() { + ps.setCharPref("UserPref.large.char", largeStr); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + + //* *************************************************************************// + // getPrefType test + + // bool... + ps.setBoolPref("UserPref.getPrefType.bool", true); + Assert.equal(ps.getPrefType("UserPref.getPrefType.bool"), PREF_BOOL); + + // int... + ps.setIntPref("UserPref.getPrefType.int", -234); + Assert.equal(ps.getPrefType("UserPref.getPrefType.int"), PREF_INT); + + // char... + ps.setCharPref("UserPref.getPrefType.char", "testing1..2"); + Assert.equal(ps.getPrefType("UserPref.getPrefType.char"), PREF_STRING); + + //* *************************************************************************// + // getBranch tests + + Assert.equal(ps.root, ""); + + // bool ... + ps.setBoolPref("UserPref.root.boolPref", true); + let pb_1 = ps.getBranch("UserPref.root."); + Assert.equal(pb_1.getBoolPref("boolPref"), true); + let pb_2 = ps.getBranch("UserPref.root.boolPref"); + Assert.equal(pb_2.getBoolPref(""), true); + pb_2.setBoolPref(".anotherPref", false); + let pb_3 = ps.getBranch("UserPref.root.boolPre"); + Assert.equal(pb_3.getBoolPref("f.anotherPref"), false); + + // int ... + ps.setIntPref("UserPref.root.intPref", 23); + pb_1 = ps.getBranch("UserPref.root."); + Assert.equal(pb_1.getIntPref("intPref"), 23); + pb_2 = ps.getBranch("UserPref.root.intPref"); + Assert.equal(pb_2.getIntPref(""), 23); + pb_2.setIntPref(".anotherPref", 69); + pb_3 = ps.getBranch("UserPref.root.intPre"); + Assert.equal(pb_3.getIntPref("f.anotherPref"), 69); + + // char... + ps.setCharPref("UserPref.root.charPref", "_char"); + pb_1 = ps.getBranch("UserPref.root."); + Assert.equal(pb_1.getCharPref("charPref"), "_char"); + pb_2 = ps.getBranch("UserPref.root.charPref"); + Assert.equal(pb_2.getCharPref(""), "_char"); + pb_2.setCharPref(".anotherPref", "_another"); + pb_3 = ps.getBranch("UserPref.root.charPre"); + Assert.equal(pb_3.getCharPref("f.anotherPref"), "_another"); + + //* *************************************************************************// + // getChildlist tests + + // get an already set prefBranch + let pb1 = ps.getBranch("UserPref.root."); + let prefList = pb1.getChildList(""); + Assert.equal(prefList.length, 6); + + // check for specific prefs in the array : the order is not important + Assert.ok("boolPref" in makeList(prefList)); + Assert.ok("intPref" in makeList(prefList)); + Assert.ok("charPref" in makeList(prefList)); + Assert.ok("boolPref.anotherPref" in makeList(prefList)); + Assert.ok("intPref.anotherPref" in makeList(prefList)); + Assert.ok("charPref.anotherPref" in makeList(prefList)); + + //* *************************************************************************// + // Default branch tests + + // bool... + pb1 = ps.getDefaultBranch(""); + pb1.setBoolPref("DefaultPref.bool", true); + Assert.equal(pb1.getBoolPref("DefaultPref.bool"), true); + Assert.ok(!pb1.prefHasUserValue("DefaultPref.bool")); + ps.setBoolPref("DefaultPref.bool", false); + Assert.ok(pb1.prefHasUserValue("DefaultPref.bool")); + Assert.equal(ps.getBoolPref("DefaultPref.bool"), false); + + // int... + pb1 = ps.getDefaultBranch(""); + pb1.setIntPref("DefaultPref.int", 100); + Assert.equal(pb1.getIntPref("DefaultPref.int"), 100); + Assert.ok(!pb1.prefHasUserValue("DefaultPref.int")); + ps.setIntPref("DefaultPref.int", 50); + Assert.ok(pb1.prefHasUserValue("DefaultPref.int")); + Assert.equal(ps.getIntPref("DefaultPref.int"), 50); + + // char... + pb1 = ps.getDefaultBranch(""); + pb1.setCharPref("DefaultPref.char", "_default"); + Assert.equal(pb1.getCharPref("DefaultPref.char"), "_default"); + Assert.ok(!pb1.prefHasUserValue("DefaultPref.char")); + ps.setCharPref("DefaultPref.char", "_user"); + Assert.ok(pb1.prefHasUserValue("DefaultPref.char")); + Assert.equal(ps.getCharPref("DefaultPref.char"), "_user"); + + //* *************************************************************************// + // pref Locking/Unlocking tests + + // locking and unlocking a nonexistent pref should throw + do_check_throws(function() { + ps.lockPref("DefaultPref.nonexistent"); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + do_check_throws(function() { + ps.unlockPref("DefaultPref.nonexistent"); + }, Cr.NS_ERROR_ILLEGAL_VALUE); + + // getting a locked pref branch should return the "default" value + Assert.ok(!ps.prefIsLocked("DefaultPref.char")); + ps.lockPref("DefaultPref.char"); + Assert.equal(ps.getCharPref("DefaultPref.char"), "_default"); + Assert.ok(ps.prefIsLocked("DefaultPref.char")); + + // getting an unlocked pref branch should return the "user" value + ps.unlockPref("DefaultPref.char"); + Assert.equal(ps.getCharPref("DefaultPref.char"), "_user"); + Assert.ok(!ps.prefIsLocked("DefaultPref.char")); + + // setting the "default" value to a user pref branch should + // make prefHasUserValue return false (documented behavior) + ps.setCharPref("DefaultPref.char", "_default"); + Assert.ok(!pb1.prefHasUserValue("DefaultPref.char")); + + // unlocking and locking multiple times shouldn't throw + ps.unlockPref("DefaultPref.char"); + ps.lockPref("DefaultPref.char"); + ps.lockPref("DefaultPref.char"); + + //* *************************************************************************// + // resetBranch test + + // NOT IMPLEMENTED YET in module/libpref. So we're not testing ! + // uncomment the following if resetBranch ever gets implemented. + /* ps.resetBranch("DefaultPref"); + do_check_eq(ps.getBoolPref("DefaultPref.bool"), true); + do_check_eq(ps.getIntPref("DefaultPref.int"), 100); + do_check_eq(ps.getCharPref("DefaultPref.char"), "_default");*/ + + //* *************************************************************************// + // deleteBranch tests + + // TODO : Really, this should throw!, by documentation. + // do_check_throws(function() { + // ps.deleteBranch("UserPref.nonexistent.deleteBranch");}, Cr.NS_ERROR_UNEXPECTED); + + ps.deleteBranch("DefaultPref"); + let pb = ps.getBranch("DefaultPref"); + pb1 = ps.getDefaultBranch("DefaultPref"); + + // getting prefs on deleted user branches should throw + do_check_throws(function() { + pb.getBoolPref("DefaultPref.bool"); + }, Cr.NS_ERROR_UNEXPECTED); + do_check_throws(function() { + pb.getIntPref("DefaultPref.int"); + }, Cr.NS_ERROR_UNEXPECTED); + do_check_throws(function() { + pb.getCharPref("DefaultPref.char"); + }, Cr.NS_ERROR_UNEXPECTED); + + // getting prefs on deleted default branches should throw + do_check_throws(function() { + pb1.getBoolPref("DefaultPref.bool"); + }, Cr.NS_ERROR_UNEXPECTED); + do_check_throws(function() { + pb1.getIntPref("DefaultPref.int"); + }, Cr.NS_ERROR_UNEXPECTED); + do_check_throws(function() { + pb1.getCharPref("DefaultPref.char"); + }, Cr.NS_ERROR_UNEXPECTED); + + //* *************************************************************************// + // savePrefFile & readPrefFile tests + + // set some prefs + ps.setBoolPref("ReadPref.bool", true); + ps.setIntPref("ReadPref.int", 230); + ps.setCharPref("ReadPref.char", "hello"); + + // save those prefs in a file + let savePrefFile = do_get_cwd(); + savePrefFile.append("data"); + savePrefFile.append("savePref.js"); + + if (savePrefFile.exists()) { + savePrefFile.remove(false); + } + savePrefFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666); + ps.savePrefFile(savePrefFile); + ps.resetPrefs(); + + // load a preexisting pref file + let prefFile = do_get_file("data/testPref.js"); + ps.readUserPrefsFromFile(prefFile); + + // former prefs should have been replaced/lost + do_check_throws(function() { + pb.getBoolPref("ReadPref.bool"); + }, Cr.NS_ERROR_UNEXPECTED); + do_check_throws(function() { + pb.getIntPref("ReadPref.int"); + }, Cr.NS_ERROR_UNEXPECTED); + do_check_throws(function() { + pb.getCharPref("ReadPref.char"); + }, Cr.NS_ERROR_UNEXPECTED); + + // loaded prefs should read ok. + pb = ps.getBranch("testPref."); + Assert.equal(pb.getBoolPref("bool1"), true); + Assert.equal(pb.getBoolPref("bool2"), false); + Assert.equal(pb.getIntPref("int1"), 23); + Assert.equal(pb.getIntPref("int2"), -1236); + Assert.equal(pb.getCharPref("char1"), "_testPref"); + Assert.equal(pb.getCharPref("char2"), "älskar"); + + // loading our former savePrefFile should allow us to read former prefs + + // Hack alert: on Windows nsLocalFile caches the size of savePrefFile from + // the .create() call above as 0. We call .exists() to reset the cache. + savePrefFile.exists(); + + ps.readUserPrefsFromFile(savePrefFile); + // cleanup the file now we don't need it + savePrefFile.remove(false); + Assert.equal(ps.getBoolPref("ReadPref.bool"), true); + Assert.equal(ps.getIntPref("ReadPref.int"), 230); + Assert.equal(ps.getCharPref("ReadPref.char"), "hello"); + + // ... and still be able to access "prior-to-readUserPrefs" preferences + Assert.equal(pb.getBoolPref("bool1"), true); + Assert.equal(pb.getBoolPref("bool2"), false); + Assert.equal(pb.getIntPref("int1"), 23); + + //* *************************************************************************// + // preference Observers + + class PrefObserver { + /** + * Creates and registers a pref observer. + * + * @param prefBranch The preference branch instance to observe. + * @param expectedName The pref name we expect to receive. + * @param expectedValue The int pref value we expect to receive. + */ + constructor(prefBranch, expectedName, expectedValue) { + this.pb = prefBranch; + this.name = expectedName; + this.value = expectedValue; + + prefBranch.addObserver(expectedName, this); + } + + QueryInterface(aIID) { + if (aIID.equals(Ci.nsIObserver) || aIID.equals(Ci.nsISupports)) { + return this; + } + throw Components.Exception("", Cr.NS_NOINTERFACE); + } + + observe(aSubject, aTopic, aState) { + Assert.equal(aTopic, "nsPref:changed"); + Assert.equal(aState, this.name); + Assert.equal(this.pb.getIntPref(aState), this.value); + pb.removeObserver(aState, this); + + // notification received, we may go on... + do_test_finished(); + } + } + + // Indicate that we'll have 3 more async tests pending so that they all + // actually get a chance to run. + do_test_pending(); + do_test_pending(); + do_test_pending(); + + let observer = new PrefObserver(ps, "ReadPref.int", 76); + ps.setIntPref("ReadPref.int", 76); + + // removed observer should not fire + ps.removeObserver("ReadPref.int", observer); + ps.setIntPref("ReadPref.int", 32); + + // let's test observers once more with a non-root prefbranch + pb = ps.getBranch("ReadPref."); + observer = new PrefObserver(pb, "int", 76); + ps.setIntPref("ReadPref.int", 76); + + // Let's try that again with different pref. + observer = new PrefObserver(pb, "another_int", 76); + ps.setIntPref("ReadPref.another_int", 76); +} diff --git a/modules/libpref/test/unit/test_locked_file_prefs.js b/modules/libpref/test/unit/test_locked_file_prefs.js new file mode 100644 index 0000000000..4e15f9e3e2 --- /dev/null +++ b/modules/libpref/test/unit/test_locked_file_prefs.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/licenses/publicdomain/ */ + +// This file tests the `locked` attribute in default pref files. + +const ps = Services.prefs; + +add_test(function notChangedFromAPI() { + ps.resetPrefs(); + ps.readDefaultPrefsFromFile(do_get_file("data/testPrefLocked.js")); + Assert.strictEqual(ps.getIntPref("testPref.unlocked.int"), 333); + Assert.strictEqual(ps.getIntPref("testPref.locked.int"), 444); + + // Unlocked pref: can set the user value, which is used upon reading. + ps.setIntPref("testPref.unlocked.int", 334); + Assert.ok(ps.prefHasUserValue("testPref.unlocked.int"), "has a user value"); + Assert.strictEqual(ps.getIntPref("testPref.unlocked.int"), 334); + + // Locked pref: can set the user value, but the default value is used upon + // reading. + ps.setIntPref("testPref.locked.int", 445); + Assert.ok(ps.prefHasUserValue("testPref.locked.int"), "has a user value"); + Assert.strictEqual(ps.getIntPref("testPref.locked.int"), 444); + + // After unlocking, the user value is used. + ps.unlockPref("testPref.locked.int"); + Assert.ok(ps.prefHasUserValue("testPref.locked.int"), "has a user value"); + Assert.strictEqual(ps.getIntPref("testPref.locked.int"), 445); + + run_next_test(); +}); + +add_test(function notChangedFromUserPrefs() { + ps.resetPrefs(); + ps.readDefaultPrefsFromFile(do_get_file("data/testPrefLocked.js")); + ps.readUserPrefsFromFile(do_get_file("data/testPrefLockedUser.js")); + + Assert.strictEqual(ps.getIntPref("testPref.unlocked.int"), 333); + Assert.strictEqual(ps.getIntPref("testPref.locked.int"), 444); + + run_next_test(); +}); diff --git a/modules/libpref/test/unit/test_parsePrefs.js b/modules/libpref/test/unit/test_parsePrefs.js new file mode 100644 index 0000000000..14e335bcec --- /dev/null +++ b/modules/libpref/test/unit/test_parsePrefs.js @@ -0,0 +1,136 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/licenses/publicdomain/ */ + +"use strict"; + +// // This undoes some of the configuration from +// Services.dirsvc +// .QueryInterface(Ci.nsIDirectoryService) +// .unregisterProvider(provider); + +class PrefObserver { + constructor() { + this.events = []; + } + + onStringPref(...args) { + // // What happens with an exception here? + // throw new Error(`foo ${args[1]}`); + this.events.push(["string", ...args]); + } + + onIntPref(...args) { + this.events.push(["int", ...args]); + } + + onBoolPref(...args) { + this.events.push(["bool", ...args]); + } + + onError(message) { + this.events.push(["error", message]); + } +} + +const TESTS = { + "data/testPrefSticky.js": [ + ["bool", "Default", "testPref.unsticky.bool", true, false, false], + ["bool", "Default", "testPref.sticky.bool", false, true, false], + ], + "data/testPrefStickyUser.js": [ + ["bool", "User", "testPref.sticky.bool", false, false, false], + ], + "data/testPrefLocked.js": [ + ["int", "Default", "testPref.unlocked.int", 333, false, false], + ["int", "Default", "testPref.locked.int", 444, false, true], + ], + // data/testPrefLockedUser is ASCII. + "data/testPrefLockedUser.js": [ + ["int", "User", "testPref.locked.int", 555, false, false], + ], + // data/testPref is ISO-8859. + "data/testPref.js": [ + ["bool", "User", "testPref.bool1", true, false, false], + ["bool", "User", "testPref.bool2", false, false, false], + ["int", "User", "testPref.int1", 23, false, false], + ["int", "User", "testPref.int2", -1236, false, false], + ["string", "User", "testPref.char1", "_testPref", false, false], + ["string", "User", "testPref.char2", "älskar", false, false], + ], + // data/testParsePrefs is data/testPref.js as UTF-8. + "data/testPrefUTF8.js": [ + ["bool", "User", "testPref.bool1", true, false, false], + ["bool", "User", "testPref.bool2", false, false, false], + ["int", "User", "testPref.int1", 23, false, false], + ["int", "User", "testPref.int2", -1236, false, false], + ["string", "User", "testPref.char1", "_testPref", false, false], + // Observe that this is the ISO-8859/Latin-1 encoding of the UTF-8 bytes. + // (Note that this source file is encoded as UTF-8.) This appears to just + // be how libpref handles this case. This test serves as documentation of + // this infelicity. + ["string", "User", "testPref.char2", "älskar", false, false], + ], +}; + +add_task(async function test_success() { + for (let [path, expected] of Object.entries(TESTS)) { + let prefObserver = new PrefObserver(); + + let prefsFile = do_get_file(path); + let data = await IOUtils.read(prefsFile.path); + + Services.prefs.parsePrefsFromBuffer(data, prefObserver, path); + Assert.deepEqual( + prefObserver.events, + expected, + `Observations from ${path} are as expected` + ); + } +}); + +add_task(async function test_exceptions() { + const { AddonTestUtils } = ChromeUtils.import( + "resource://testing-common/AddonTestUtils.jsm" + ); + + let s = `user_pref("testPref.bool1", true); + user_pref("testPref.bool2", false); + user_pref("testPref.int1", 23); + user_pref("testPref.int2", -1236); + `; + + let onErrorCount = 0; + let addPrefCount = 0; + + let marker = "2fc599a1-433b-4de7-bd63-3e69c3bbad5b"; + let addPref = (...args) => { + addPrefCount += 1; + throw new Error(`${marker}${JSON.stringify(args)}${marker}`); + }; + + let { messages } = await AddonTestUtils.promiseConsoleOutput(() => { + Services.prefs.parsePrefsFromBuffer(new TextEncoder().encode(s), { + onStringPref: addPref, + onIntPref: addPref, + onBoolPref: addPref, + onError(message) { + onErrorCount += 1; + console.error(message); + }, + }); + }); + + Assert.equal(addPrefCount, 4); + Assert.equal(onErrorCount, 0); + + // This is fragile but mercifully simple. + Assert.deepEqual( + messages.map(m => JSON.parse(m.message.split(marker)[1])), + [ + ["User", "testPref.bool1", true, false, false], + ["User", "testPref.bool2", false, false, false], + ["User", "testPref.int1", 23, false, false], + ["User", "testPref.int2", -1236, false, false], + ] + ); +}); diff --git a/modules/libpref/test/unit/test_parser.js b/modules/libpref/test/unit/test_parser.js new file mode 100644 index 0000000000..27a5c86905 --- /dev/null +++ b/modules/libpref/test/unit/test_parser.js @@ -0,0 +1,107 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/licenses/publicdomain/ */ + +function run_test() { + const ps = Services.prefs; + + ps.resetPrefs(); + ps.readDefaultPrefsFromFile(do_get_file("data/testParser.js")); + + Assert.equal(ps.getBoolPref("comment1"), true); + Assert.equal(ps.getBoolPref("comment2"), true); + Assert.equal(ps.getBoolPref("spaced-out"), true); + + Assert.equal(ps.getBoolPref("pref"), true); + Assert.equal(ps.getBoolPref("sticky_pref"), true); + Assert.equal(ps.getBoolPref("user_pref"), true); + Assert.equal(ps.getBoolPref("sticky_pref2"), true); + Assert.equal(ps.getBoolPref("locked_pref"), true); + Assert.equal(ps.getBoolPref("locked_sticky_pref"), true); + Assert.equal(ps.prefIsLocked("locked_pref"), true); + Assert.equal(ps.prefIsLocked("locked_sticky_pref"), true); + + Assert.equal(ps.getBoolPref("bool.true"), true); + Assert.equal(ps.getBoolPref("bool.false"), false); + + Assert.equal(ps.getIntPref("int.0"), 0); + Assert.equal(ps.getIntPref("int.1"), 1); + Assert.equal(ps.getIntPref("int.123"), 123); + Assert.equal(ps.getIntPref("int.+234"), 234); + Assert.equal(ps.getIntPref("int.+ 345"), 345); + Assert.equal(ps.getIntPref("int.-0"), -0); + Assert.equal(ps.getIntPref("int.-1"), -1); + Assert.equal(ps.getIntPref("int.- /* hmm */\t456"), -456); + Assert.equal(ps.getIntPref("int.-\n567"), -567); + Assert.equal(ps.getIntPref("int.INT_MAX-1"), 2147483646); + Assert.equal(ps.getIntPref("int.INT_MAX"), 2147483647); + Assert.equal(ps.getIntPref("int.INT_MIN+2"), -2147483646); + Assert.equal(ps.getIntPref("int.INT_MIN+1"), -2147483647); + Assert.equal(ps.getIntPref("int.INT_MIN"), -2147483648); + + Assert.equal(ps.getCharPref("string.empty"), ""); + Assert.equal(ps.getCharPref("string.abc"), "abc"); + Assert.equal( + ps.getCharPref("string.long"), + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + ); + Assert.equal(ps.getCharPref("string.single-quotes"), '"abc"'); + Assert.equal(ps.getCharPref("string.double-quotes"), "'abc'"); + Assert.equal( + ps.getCharPref("string.weird-chars"), + "\x0d \x09 \x0b \x0c \x06 \x16" + ); + Assert.equal(ps.getCharPref("string.escapes"), "\" ' \\ \n \r"); + + // This one is ASCII, so we can use getCharPref() and getStringPref + // interchangeably. + Assert.equal( + ps.getCharPref("string.x-escapes1"), + "Mozilla0\x4d\x6F\x7a\x69\x6c\x6C\x610" + ); + Assert.equal(ps.getStringPref("string.x-escapes1"), "Mozilla0Mozilla0"); + + // This one has chars with value > 127, so it's not valid UTF8, so we can't + // use getStringPref on it. + Assert.equal( + ps.getCharPref("string.x-escapes2"), + "AA A_umlaut\xc4 y_umlaut\xff" + ); + + // The following strings use \uNNNN escapes, which are UTF16 code points. + // libpref stores them internally as UTF8 byte sequences. In each case we get + // the string in two ways: + // - getStringPref() interprets it as UTF8, which is then converted to UTF16 + // in JS. I.e. code points that are multiple bytes in UTF8 become a single + // 16-bit char in JS (except for the non-BMP chars, which become a 16-bit + // surrogate pair). + // - getCharPref() interprets it as Latin1, which is then converted to UTF16 + // in JS. I.e. code points that are multiple bytes in UTF8 become multiple + // 16-bit chars in JS. + + Assert.equal( + ps.getStringPref("string.u-escapes1"), + "A\u0041 A_umlaut\u00c4 y_umlaut\u00ff0" + ); + Assert.equal( + ps.getCharPref("string.u-escapes1"), + "A\x41 A_umlaut\xc3\x84 y_umlaut\xc3\xbf0" + ); + + Assert.equal( + ps.getStringPref("string.u-escapes2"), + "S_acute\u015a y_grave\u1Ef3" + ); + Assert.equal( + ps.getCharPref("string.u-escapes2"), + "S_acute\xc5\x9a y_grave\xe1\xbb\xb3" + ); + + Assert.equal( + ps.getStringPref("string.u-surrogates"), + "cyclone\uD83C\uDF00 grinning_face\uD83D\uDE00" + ); + Assert.equal( + ps.getCharPref("string.u-surrogates"), + "cyclone\xF0\x9F\x8C\x80 grinning_face\xF0\x9F\x98\x80" + ); +} diff --git a/modules/libpref/test/unit/test_stickyprefs.js b/modules/libpref/test/unit/test_stickyprefs.js new file mode 100644 index 0000000000..d1e16e0f0f --- /dev/null +++ b/modules/libpref/test/unit/test_stickyprefs.js @@ -0,0 +1,187 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/licenses/publicdomain/ */ + +const ps = Services.prefs; + +// A little helper to reset the service and load one pref file. +function resetAndLoadDefaults() { + ps.resetPrefs(); + ps.readDefaultPrefsFromFile(do_get_file("data/testPrefSticky.js")); +} + +// A little helper to reset the service and load two pref files. +function resetAndLoadAll() { + ps.resetPrefs(); + ps.readDefaultPrefsFromFile(do_get_file("data/testPrefSticky.js")); + ps.readUserPrefsFromFile(do_get_file("data/testPrefStickyUser.js")); +} + +// A little helper that saves the current state to a file in the profile +// dir, then resets the service and re-reads the file it just saved. +// Used to test what gets actually written - things the pref service decided +// not to write don't exist at all after this call. +function saveAndReload() { + let file = do_get_profile(); + file.append("prefs.js"); + ps.savePrefFile(file); + + // Now reset the pref service and re-read what we saved. + ps.resetPrefs(); + + // Hack alert: on Windows nsLocalFile caches the size of savePrefFile from + // the .create() call above as 0. We call .exists() to reset the cache. + file.exists(); + + ps.readUserPrefsFromFile(file); +} + +// A sticky pref should not be written if the value is unchanged. +add_test(function notWrittenWhenUnchanged() { + resetAndLoadDefaults(); + Assert.strictEqual(ps.getBoolPref("testPref.unsticky.bool"), true); + Assert.strictEqual(ps.getBoolPref("testPref.sticky.bool"), false); + + // write prefs - but we haven't changed the sticky one, so it shouldn't be written. + saveAndReload(); + // sticky should not have been written to the new file. + try { + ps.getBoolPref("testPref.sticky.bool"); + Assert.ok(false, "expected failure reading this pref"); + } catch (ex) { + Assert.ok(ex, "exception reading regular pref"); + } + run_next_test(); +}); + +// Loading a sticky `pref` then a `user_pref` for the same pref means it should +// always be written. +add_test(function writtenOnceLoadedWithoutChange() { + // Load the same pref file *as well as* a pref file that has a user_pref for + // our sticky with the default value. It should be re-written without us + // touching it. + resetAndLoadAll(); + // reset and re-read what we just wrote - it should be written. + saveAndReload(); + Assert.strictEqual( + ps.getBoolPref("testPref.sticky.bool"), + false, + "user_pref was written with default value" + ); + run_next_test(); +}); + +// If a sticky pref is explicicitly changed, even to the default, it is written. +add_test(function writtenOnceLoadedWithChangeNonDefault() { + // Load the same pref file *as well as* a pref file that has a user_pref for + // our sticky - then change the pref. It should be written. + resetAndLoadAll(); + // Set a new val and check we wrote it. + ps.setBoolPref("testPref.sticky.bool", false); + saveAndReload(); + Assert.strictEqual( + ps.getBoolPref("testPref.sticky.bool"), + false, + "user_pref was written with custom value" + ); + run_next_test(); +}); + +// If a sticky pref is changed to the non-default value, it is written. +add_test(function writtenOnceLoadedWithChangeNonDefault() { + // Load the same pref file *as well as* a pref file that has a user_pref for + // our sticky - then change the pref. It should be written. + resetAndLoadAll(); + // Set a new val and check we wrote it. + ps.setBoolPref("testPref.sticky.bool", true); + saveAndReload(); + Assert.strictEqual( + ps.getBoolPref("testPref.sticky.bool"), + true, + "user_pref was written with custom value" + ); + run_next_test(); +}); + +// Test that prefHasUserValue always returns true whenever there is a sticky +// value, even when that value matches the default. This is mainly for +// about:config semantics - prefs with a sticky value always remain bold and +// always offer "reset" (which fully resets and drops the sticky value as if +// the pref had never changed.) +add_test(function hasUserValue() { + // sticky pref without user value. + resetAndLoadDefaults(); + Assert.strictEqual(ps.getBoolPref("testPref.sticky.bool"), false); + Assert.ok( + !ps.prefHasUserValue("testPref.sticky.bool"), + "should not initially reflect a user value" + ); + + ps.setBoolPref("testPref.sticky.bool", false); + Assert.ok( + ps.prefHasUserValue("testPref.sticky.bool"), + "should reflect a user value after set to default" + ); + + ps.setBoolPref("testPref.sticky.bool", true); + Assert.ok( + ps.prefHasUserValue("testPref.sticky.bool"), + "should reflect a user value after change to non-default" + ); + + ps.clearUserPref("testPref.sticky.bool"); + Assert.ok( + !ps.prefHasUserValue("testPref.sticky.bool"), + "should reset to no user value" + ); + ps.setBoolPref("testPref.sticky.bool", false, "expected default"); + + // And make sure the pref immediately reflects a user value after load. + resetAndLoadAll(); + Assert.strictEqual(ps.getBoolPref("testPref.sticky.bool"), false); + Assert.ok( + ps.prefHasUserValue("testPref.sticky.bool"), + "should have a user value when loaded value is the default" + ); + run_next_test(); +}); + +// Test that clearUserPref removes the "sticky" value. +add_test(function clearUserPref() { + // load things such that we have a sticky value which is the same as the + // default. + resetAndLoadAll(); + ps.clearUserPref("testPref.sticky.bool"); + + // Once we save prefs the sticky pref should no longer be written. + saveAndReload(); + try { + ps.getBoolPref("testPref.sticky.bool"); + Assert.ok(false, "expected failure reading this pref"); + } catch (ex) { + Assert.ok(ex, "pref doesn't have a sticky value"); + } + run_next_test(); +}); + +// Test that a pref observer gets a notification fired when a sticky pref +// has it's value changed to the same value as the default. The reason for +// this behaviour is that later we might have other code that cares about a +// pref being sticky (IOW, we notify due to the "state" of the pref changing +// even if the value has not) +add_test(function observerFires() { + // load things so there's no sticky value. + resetAndLoadDefaults(); + + function observe(subject, topic, data) { + Assert.equal(data, "testPref.sticky.bool"); + ps.removeObserver("testPref.sticky.bool", observe); + run_next_test(); + } + ps.addObserver("testPref.sticky.bool", observe); + + ps.setBoolPref( + "testPref.sticky.bool", + ps.getBoolPref("testPref.sticky.bool") + ); + // and the observer will fire triggering the next text. +}); diff --git a/modules/libpref/test/unit/test_warnings.js b/modules/libpref/test/unit/test_warnings.js new file mode 100644 index 0000000000..33adb22579 --- /dev/null +++ b/modules/libpref/test/unit/test_warnings.js @@ -0,0 +1,62 @@ +/* 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/. */ + +function makeBuffer(length) { + return new Array(length + 1).join("x"); +} + +/** + * @resolves |true| if execution proceeded without warning, + * |false| if there was a warning. + */ +function checkWarning(pref, buffer) { + return new Promise(resolve => { + let complete = false; + let listener = { + observe(event) { + let message = event.message; + if ( + !( + message.startsWith("Warning: attempting to write") && + message.includes(pref) + ) + ) { + return; + } + if (complete) { + return; + } + complete = true; + info("Warning while setting " + pref); + Services.console.unregisterListener(listener); + resolve(true); + }, + }; + do_timeout(1000, function() { + if (complete) { + return; + } + complete = true; + info("No warning while setting " + pref); + Services.console.unregisterListener(listener); + resolve(false); + }); + Services.console.registerListener(listener); + Services.prefs.setCharPref(pref, buffer); + }); +} + +add_task(async function() { + // Simple change, shouldn't cause a warning + info("Checking that a simple change doesn't cause a warning"); + let buf = makeBuffer(100); + let warned = await checkWarning("string.accept", buf); + Assert.ok(!warned); + + // Large change, should cause a warning + info("Checking that a large change causes a warning"); + buf = makeBuffer(32 * 1024); + warned = await checkWarning("string.warn", buf); + Assert.ok(warned); +}); diff --git a/modules/libpref/test/unit/xpcshell.ini b/modules/libpref/test/unit/xpcshell.ini new file mode 100644 index 0000000000..0778b8659f --- /dev/null +++ b/modules/libpref/test/unit/xpcshell.ini @@ -0,0 +1,28 @@ +[DEFAULT] +head = head_libPrefs.js +support-files = + data/testPref.js + extdata/testExt.js + +[test_warnings.js] +[test_bug345529.js] +[test_bug506224.js] +[test_bug577950.js] +[test_bug790374.js] +[test_stickyprefs.js] +skip-if = (os == "win" && socketprocess_networking) +support-files = data/testPrefSticky.js data/testPrefStickyUser.js +[test_locked_file_prefs.js] +skip-if = (os == "win" && socketprocess_networking) +support-files = data/testPrefLocked.js data/testPrefLockedUser.js +[test_changeType.js] +[test_defaultValues.js] +[test_dirtyPrefs.js] +[test_libPrefs.js] +[test_bug1354613.js] +[test_parser.js] +support-files = data/testParser.js +[test_parsePrefs.js] +support-files = + data/testPrefUTF8.js +head = diff --git a/modules/libpref/test/unit_ipc/test_existing_prefs.js b/modules/libpref/test/unit_ipc/test_existing_prefs.js new file mode 100644 index 0000000000..4efce784b0 --- /dev/null +++ b/modules/libpref/test/unit_ipc/test_existing_prefs.js @@ -0,0 +1,20 @@ +function isParentProcess() { + let appInfo = Cc["@mozilla.org/xre/app-info;1"]; + return ( + !appInfo || + Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT + ); +} + +function run_test() { + if (!isParentProcess()) { + do_load_child_test_harness(); + + var pb = Services.prefs; + pb.setBoolPref("Test.IPC.bool.new", true); + pb.setIntPref("Test.IPC.int.new", 23); + pb.setCharPref("Test.IPC.char.new", "hey"); + + run_test_in_child("test_observed_prefs.js"); + } +} diff --git a/modules/libpref/test/unit_ipc/test_initial_prefs.js b/modules/libpref/test/unit_ipc/test_initial_prefs.js new file mode 100644 index 0000000000..9c5e8991cf --- /dev/null +++ b/modules/libpref/test/unit_ipc/test_initial_prefs.js @@ -0,0 +1,14 @@ +function isParentProcess() { + return Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; +} + +function run_test() { + if (!isParentProcess()) { + const pb = Services.prefs; + pb.setBoolPref("Test.IPC.bool", true); + pb.setIntPref("Test.IPC.int", 23); + pb.setCharPref("Test.IPC.char", "hey"); + + run_test_in_child("test_existing_prefs.js"); + } +} diff --git a/modules/libpref/test/unit_ipc/test_large_pref.js b/modules/libpref/test/unit_ipc/test_large_pref.js new file mode 100644 index 0000000000..79c1330c36 --- /dev/null +++ b/modules/libpref/test/unit_ipc/test_large_pref.js @@ -0,0 +1,104 @@ +/* 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/. */ + +// Large preferences should not be set in the child process. +// Non-string preferences are not tested here, because their behavior +// should not be affected by this filtering. +// + +function isParentProcess() { + return Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; +} + +function makeBuffer(length) { + let string = "x"; + while (string.length < length) { + string = string + string; + } + if (string.length > length) { + string = string.substring(length - string.length); + } + return string; +} + +// from prefapi.h +const MAX_ADVISABLE_PREF_LENGTH = 4 * 1024; + +const largeString = makeBuffer(MAX_ADVISABLE_PREF_LENGTH + 1); +const smallString = makeBuffer(4); + +const testValues = [ + { name: "None", value: undefined }, + { name: "Small", value: smallString }, + { name: "Large", value: largeString }, +]; + +function prefName(def, user) { + return "Test.IPC.default" + def.name + "User" + user.name; +} + +function expectedPrefValue(def, user) { + if (user.value) { + return user.value; + } + return def.value; +} + +function run_test() { + const pb = Services.prefs; + let defaultBranch = pb.getDefaultBranch(""); + + let isParent = isParentProcess(); + if (isParent) { + // Preferences with large values will still appear in the shared memory + // snapshot that we share with all processes. They should not, however, be + // sent with the list of changes on top of the snapshot. + // + // So, make sure we've generated the initial snapshot before we set the + // preference values by launching a child process with an empty test. + sendCommand(""); + + // Set all combinations of none, small and large, for default and user prefs. + for (let def of testValues) { + for (let user of testValues) { + let currPref = prefName(def, user); + if (def.value) { + defaultBranch.setCharPref(currPref, def.value); + } + if (user.value) { + pb.setCharPref(currPref, user.value); + } + } + } + + run_test_in_child("test_large_pref.js"); + } + + // Check that each preference is set or not set, as appropriate. + for (let def of testValues) { + for (let user of testValues) { + if (!def.value && !user.value) { + continue; + } + let pref_name = prefName(def, user); + if (isParent || (def.name != "Large" && user.name != "Large")) { + Assert.equal(pb.getCharPref(pref_name), expectedPrefValue(def, user)); + } else { + // This is the child, and either the default or user value is + // large, so the preference should not be set. + let prefExists; + try { + let val = pb.getCharPref(pref_name); + prefExists = val.length > 128; + } catch (e) { + prefExists = false; + } + ok( + !prefExists, + "Pref " + pref_name + " should not be set in the child" + ); + } + } + } +} diff --git a/modules/libpref/test/unit_ipc/test_locked_prefs.js b/modules/libpref/test/unit_ipc/test_locked_prefs.js new file mode 100644 index 0000000000..037427e495 --- /dev/null +++ b/modules/libpref/test/unit_ipc/test_locked_prefs.js @@ -0,0 +1,39 @@ +/* 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/. */ + +// Locked status should be communicated to children. + +function isParentProcess() { + return Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; +} + +function run_test() { + const pb = Services.prefs; + + let bprefname = "Test.IPC.locked.bool"; + let iprefname = "Test.IPC.locked.int"; + let sprefname = "Test.IPC.locked.string"; + + let isParent = isParentProcess(); + if (isParent) { + pb.setBoolPref(bprefname, true); + pb.lockPref(bprefname); + + pb.setIntPref(iprefname, true); + pb.lockPref(iprefname); + + pb.setStringPref(sprefname, true); + pb.lockPref(sprefname); + pb.unlockPref(sprefname); + + run_test_in_child("test_locked_prefs.js"); + } + + ok(pb.prefIsLocked(bprefname), bprefname + " should be locked in the child"); + ok(pb.prefIsLocked(iprefname), iprefname + " should be locked in the child"); + ok( + !pb.prefIsLocked(sprefname), + sprefname + " should be unlocked in the child" + ); +} diff --git a/modules/libpref/test/unit_ipc/test_observed_prefs.js b/modules/libpref/test/unit_ipc/test_observed_prefs.js new file mode 100644 index 0000000000..9f6984d555 --- /dev/null +++ b/modules/libpref/test/unit_ipc/test_observed_prefs.js @@ -0,0 +1,12 @@ +function isParentProcess() { + return Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; +} + +function run_test() { + if (!isParentProcess()) { + const pb = Services.prefs; + Assert.equal(pb.getBoolPref("Test.IPC.bool.new"), true); + Assert.equal(pb.getIntPref("Test.IPC.int.new"), 23); + Assert.equal(pb.getCharPref("Test.IPC.char.new"), "hey"); + } +} diff --git a/modules/libpref/test/unit_ipc/test_sharedMap.js b/modules/libpref/test/unit_ipc/test_sharedMap.js new file mode 100644 index 0000000000..b21320e111 --- /dev/null +++ b/modules/libpref/test/unit_ipc/test_sharedMap.js @@ -0,0 +1,362 @@ +/* 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/. */ +"use strict"; + +// This file tests the functionality of the preference service when using a +// shared memory snapshot. In this configuration, a snapshot of the initial +// state of the preferences database is made when we first spawn a child +// process, and changes after that point are stored as entries in a dynamic hash +// table, on top of the snapshot. + +const { XPCShellContentUtils } = ChromeUtils.importESModule( + "resource://testing-common/XPCShellContentUtils.sys.mjs" +); + +XPCShellContentUtils.init(this); + +let contentPage; + +const { prefs } = Services; +const defaultPrefs = prefs.getDefaultBranch(""); + +const FRAME_SCRIPT_INIT = ` + var { prefs } = Services; + var defaultPrefs = prefs.getDefaultBranch(""); +`; + +function try_(fn) { + try { + return fn(); + } catch (e) { + return undefined; + } +} + +function getPref(pref) { + let flags = { + locked: try_(() => prefs.prefIsLocked(pref)), + hasUser: try_(() => prefs.prefHasUserValue(pref)), + }; + + switch (prefs.getPrefType(pref)) { + case prefs.PREF_INT: + return { + ...flags, + type: "Int", + user: try_(() => prefs.getIntPref(pref)), + default: try_(() => defaultPrefs.getIntPref(pref)), + }; + case prefs.PREF_BOOL: + return { + ...flags, + type: "Bool", + user: try_(() => prefs.getBoolPref(pref)), + default: try_(() => defaultPrefs.getBoolPref(pref)), + }; + case prefs.PREF_STRING: + return { + ...flags, + type: "String", + user: try_(() => prefs.getStringPref(pref)), + default: try_(() => defaultPrefs.getStringPref(pref)), + }; + } + return {}; +} + +function getPrefs(prefNames) { + let result = {}; + for (let pref of prefNames) { + result[pref] = getPref(pref); + } + result.childList = prefs.getChildList(""); + return result; +} + +function checkPref( + pref, + proc, + val, + type, + userVal, + defaultVal, + expectedFlags = {} +) { + info(`Check "${pref}" ${proc} value`); + + equal(val.type, type, `Expected type for "${pref}"`); + equal(val.user, userVal, `Expected user value for "${pref}"`); + + // We only send changes to the content process when they'll make a visible + // difference, so ignore content process default values when we have a defined + // user value. + if (proc !== "content" || val.user === undefined) { + equal(val.default, defaultVal, `Expected default value for "${pref}"`); + } + + for (let [flag, value] of Object.entries(expectedFlags)) { + equal(val[flag], value, `Expected ${flag} value for "${pref}"`); + } +} + +function getPrefList() { + return prefs.getChildList(""); +} + +const TESTS = { + "exists.thenDoesNot": { + beforeContent(PREF) { + prefs.setBoolPref(PREF, true); + + ok(getPrefList().includes(PREF), `Parent list includes "${PREF}"`); + }, + contentStartup(PREF, val, childList) { + ok(getPrefList().includes(PREF), `Parent list includes "${PREF}"`); + ok(childList.includes(PREF), `Child list includes "${PREF}"`); + + prefs.clearUserPref(PREF); + ok( + !getPrefList().includes(PREF), + `Parent list doesn't include "${PREF}"` + ); + }, + contentUpdate1(PREF, val, childList) { + ok( + !getPrefList().includes(PREF), + `Parent list doesn't include "${PREF}"` + ); + ok(!childList.includes(PREF), `Child list doesn't include "${PREF}"`); + + prefs.setCharPref(PREF, "foo"); + ok(getPrefList().includes(PREF), `Parent list includes "${PREF}"`); + checkPref(PREF, "parent", getPref(PREF), "String", "foo"); + }, + contentUpdate2(PREF, val, childList) { + ok(getPrefList().includes(PREF), `Parent list includes "${PREF}"`); + ok(childList.includes(PREF), `Child list includes "${PREF}"`); + + checkPref(PREF, "parent", getPref(PREF), "String", "foo"); + checkPref(PREF, "child", val, "String", "foo"); + }, + }, + "doesNotExists.thenDoes": { + contentStartup(PREF, val, childList) { + ok( + !getPrefList().includes(PREF), + `Parent list doesn't include "${PREF}"` + ); + ok(!childList.includes(PREF), `Child list doesn't include "${PREF}"`); + + prefs.setIntPref(PREF, 42); + ok(getPrefList().includes(PREF), `Parent list includes "${PREF}"`); + }, + contentUpdate1(PREF, val, childList) { + ok(getPrefList().includes(PREF), `Parent list includes "${PREF}"`); + ok(childList.includes(PREF), `Child list includes "${PREF}"`); + + checkPref(PREF, "parent", getPref(PREF), "Int", 42); + checkPref(PREF, "child", val, "Int", 42); + }, + }, +}; + +const PREFS = [ + { type: "Bool", values: [true, false, true] }, + { type: "Int", values: [24, 42, 73] }, + { type: "String", values: ["meh", "hem", "hrm"] }, +]; + +for (let { type, values } of PREFS) { + let set = `set${type}Pref`; + + function prefTest(opts) { + function check( + pref, + proc, + val, + { + expectedVal, + defaultVal = undefined, + expectedDefault = defaultVal, + expectedFlags = {}, + } + ) { + checkPref( + pref, + proc, + val, + type, + expectedVal, + expectedDefault, + expectedFlags + ); + } + + function updatePref( + PREF, + { userVal = undefined, defaultVal = undefined, flags = {} } + ) { + info(`Update "${PREF}"`); + if (userVal !== undefined) { + prefs[set](PREF, userVal); + } + if (defaultVal !== undefined) { + defaultPrefs[set](PREF, defaultVal); + } + if (flags.locked === true) { + prefs.lockPref(PREF); + } else if (flags.locked === false) { + prefs.unlockPref(PREF); + } + } + + return { + beforeContent(PREF) { + updatePref(PREF, opts.initial); + check(PREF, "parent", getPref(PREF), opts.initial); + }, + contentStartup(PREF, contentVal) { + check(PREF, "content", contentVal, opts.initial); + check(PREF, "parent", getPref(PREF), opts.initial); + + updatePref(PREF, opts.change1); + check(PREF, "parent", getPref(PREF), opts.change1); + }, + contentUpdate1(PREF, contentVal) { + check(PREF, "content", contentVal, opts.change1); + check(PREF, "parent", getPref(PREF), opts.change1); + + if (opts.change2) { + updatePref(PREF, opts.change2); + check(PREF, "parent", getPref(PREF), opts.change2); + } + }, + contentUpdate2(PREF, contentVal) { + if (opts.change2) { + check(PREF, "content", contentVal, opts.change2); + check(PREF, "parent", getPref(PREF), opts.change2); + } + }, + }; + } + + for (let i of [0, 1]) { + let userVal = values[i]; + let defaultVal = values[+!i]; + + TESTS[`type.${type}.${i}.default`] = prefTest({ + initial: { defaultVal, expectedVal: defaultVal }, + change1: { defaultVal: values[2], expectedVal: values[2] }, + }); + + TESTS[`type.${type}.${i}.user`] = prefTest({ + initial: { userVal, expectedVal: userVal }, + change1: { defaultVal: values[2], expectedVal: userVal }, + change2: { + userVal: values[2], + expectedDefault: values[2], + expectedVal: values[2], + }, + }); + + TESTS[`type.${type}.${i}.both`] = prefTest({ + initial: { userVal, defaultVal, expectedVal: userVal }, + change1: { defaultVal: values[2], expectedVal: userVal }, + change2: { + userVal: values[2], + expectedDefault: values[2], + expectedVal: values[2], + }, + }); + + TESTS[`type.${type}.${i}.both.thenLock`] = prefTest({ + initial: { userVal, defaultVal, expectedVal: userVal }, + change1: { + expectedDefault: defaultVal, + expectedVal: defaultVal, + flags: { locked: true }, + expectFlags: { locked: true }, + }, + }); + + TESTS[`type.${type}.${i}.both.thenUnlock`] = prefTest({ + initial: { + userVal, + defaultVal, + expectedVal: defaultVal, + flags: { locked: true }, + expectedFlags: { locked: true }, + }, + change1: { + expectedDefault: defaultVal, + expectedVal: userVal, + flags: { locked: false }, + expectFlags: { locked: false }, + }, + }); + + TESTS[`type.${type}.${i}.both.locked`] = prefTest({ + initial: { + userVal, + defaultVal, + expectedVal: defaultVal, + flags: { locked: true }, + expectedFlags: { locked: true }, + }, + change1: { + userVal: values[2], + expectedDefault: defaultVal, + expectedVal: defaultVal, + expectedFlags: { locked: true }, + }, + change2: { + defaultVal: values[2], + expectedDefault: defaultVal, + expectedVal: defaultVal, + expectedFlags: { locked: true }, + }, + }); + } +} + +add_task(async function test_sharedMap_prefs() { + let prefValues = {}; + + async function runChecks(op) { + for (let [pref, ops] of Object.entries(TESTS)) { + if (ops[op]) { + info(`Running ${op} for "${pref}"`); + await ops[op]( + pref, + prefValues[pref] || undefined, + prefValues.childList || undefined + ); + } + } + } + + await runChecks("beforeContent"); + + contentPage = await XPCShellContentUtils.loadContentPage("about:blank", { + remote: true, + }); + registerCleanupFunction(() => contentPage.close()); + + contentPage.addFrameScriptHelper(FRAME_SCRIPT_INIT); + contentPage.addFrameScriptHelper(try_); + contentPage.addFrameScriptHelper(getPref); + + let prefNames = Object.keys(TESTS); + prefValues = await contentPage.spawn(prefNames, getPrefs); + + await runChecks("contentStartup"); + + prefValues = await contentPage.spawn(prefNames, getPrefs); + + await runChecks("contentUpdate1"); + + prefValues = await contentPage.spawn(prefNames, getPrefs); + + await runChecks("contentUpdate2"); +}); diff --git a/modules/libpref/test/unit_ipc/test_sharedMap_static_prefs.js b/modules/libpref/test/unit_ipc/test_sharedMap_static_prefs.js new file mode 100644 index 0000000000..fa6a3e4fa1 --- /dev/null +++ b/modules/libpref/test/unit_ipc/test_sharedMap_static_prefs.js @@ -0,0 +1,76 @@ +/* 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/. */ +"use strict"; + +// Tests that static preferences in the content process +// correctly handle values which are different from their +// statically-defined defaults. +// +// Since we can't access static preference values from JS, this tests relies on +// assertions in debug builds to detect mismatches. The default and user +// values of two preferences are changed (respectively) before a content +// process is started. Once the content process is launched, the +// preference service asserts that the values stored in all static prefs +// match their current values as known to the preference service. If +// there's a mismatch, the shell will crash, and the test will fail. +// +// For sanity, we also check that the dynamically retrieved preference +// values in the content process match our expectations, though this is +// not strictly part of the test. + +const PREF1_NAME = "dom.webcomponents.shadowdom.report_usage"; +const PREF1_VALUE = false; + +const PREF2_NAME = "dom.mutation-events.cssom.disabled"; +const PREF2_VALUE = true; + +const { XPCShellContentUtils } = ChromeUtils.importESModule( + "resource://testing-common/XPCShellContentUtils.sys.mjs" +); + +XPCShellContentUtils.init(this); + +const { prefs } = Services; +const defaultPrefs = prefs.getDefaultBranch(""); + +add_task(async function test_sharedMap_static_prefs() { + equal( + prefs.getBoolPref(PREF1_NAME), + PREF1_VALUE, + `Expected initial value for ${PREF1_NAME}` + ); + equal( + prefs.getBoolPref(PREF2_NAME), + PREF2_VALUE, + `Expected initial value for ${PREF2_NAME}` + ); + + defaultPrefs.setBoolPref(PREF1_NAME, !PREF1_VALUE); + prefs.setBoolPref(PREF2_NAME, !PREF2_VALUE); + + equal( + prefs.getBoolPref(PREF1_NAME), + !PREF1_VALUE, + `Expected updated value for ${PREF1_NAME}` + ); + equal( + prefs.getBoolPref(PREF2_NAME), + !PREF2_VALUE, + `Expected updated value for ${PREF2_NAME}` + ); + + let contentPage = await XPCShellContentUtils.loadContentPage("about:blank", { + remote: true, + }); + registerCleanupFunction(() => contentPage.close()); + + /* eslint-disable no-shadow */ + let values = await contentPage.spawn([PREF1_NAME, PREF2_NAME], prefs => { + return prefs.map(pref => Services.prefs.getBoolPref(pref)); + }); + /* eslint-enable no-shadow */ + + equal(values[0], !PREF1_VALUE, `Expected content value for ${PREF1_NAME}`); + equal(values[1], !PREF2_VALUE, `Expected content value for ${PREF2_NAME}`); +}); diff --git a/modules/libpref/test/unit_ipc/test_update_prefs.js b/modules/libpref/test/unit_ipc/test_update_prefs.js new file mode 100644 index 0000000000..51e2c458c5 --- /dev/null +++ b/modules/libpref/test/unit_ipc/test_update_prefs.js @@ -0,0 +1,34 @@ +function isParentProcess() { + return Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; +} + +function run_test() { + if (isParentProcess()) { + do_load_child_test_harness(); + + var pb = Services.prefs; + + // these prefs are set after the child has been created. + pb.setBoolPref("Test.IPC.bool.new", true); + pb.setIntPref("Test.IPC.int.new", 23); + pb.setCharPref("Test.IPC.char.new", "hey"); + + run_test_in_child("test_observed_prefs.js", testPrefClear); + } +} + +function testPrefClear() { + var pb = Services.prefs; + pb.clearUserPref("Test.IPC.bool.new"); + + sendCommand( + 'var pb = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);\n' + + 'pb.prefHasUserValue("Test.IPC.bool.new");\n', + checkWasCleared + ); +} + +function checkWasCleared(existsStr) { + Assert.equal(existsStr, "false"); + do_test_finished(); +} diff --git a/modules/libpref/test/unit_ipc/test_user_default_prefs.js b/modules/libpref/test/unit_ipc/test_user_default_prefs.js new file mode 100644 index 0000000000..ea8ff8be57 --- /dev/null +++ b/modules/libpref/test/unit_ipc/test_user_default_prefs.js @@ -0,0 +1,72 @@ +const pb = Services.prefs; + +// This pref is chosen somewhat arbitrarily --- we just need one +// that's guaranteed to have a default value. +const kPrefName = "intl.accept_languages"; // of type char, which we +// assume below +var initialValue = null; + +function check_child_pref_info_eq(continuation) { + sendCommand( + 'var pb = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch);\n' + + // Returns concatenation "[value],[isUser]" + 'pb.getCharPref("' + + kPrefName + + '")+ "," +' + + 'pb.prefHasUserValue("' + + kPrefName + + '");', + function(info) { + let [value, isUser] = info.split(","); + Assert.equal(pb.getCharPref(kPrefName), value); + Assert.equal(pb.prefHasUserValue(kPrefName), isUser == "true"); + continuation(); + } + ); +} + +function run_test() { + // We finish in clean_up() + do_test_pending(); + + initialValue = pb.getCharPref(kPrefName); + + test_user_setting(); +} + +function test_user_setting() { + // We rely on setting this before the content process starts up. + // When it starts up, it should recognize this as a user pref, not + // a default pref. + pb.setCharPref(kPrefName, "i-imaginarylanguage"); + // NB: processing of the value-change notification in the child + // process triggered by the above set happens-before the remaining + // code here + check_child_pref_info_eq(function() { + Assert.equal(pb.prefHasUserValue(kPrefName), true); + + test_cleared_is_default(); + }); +} + +function test_cleared_is_default() { + pb.clearUserPref(kPrefName); + // NB: processing of the value-change notification in the child + // process triggered by the above set happens-before the remaining + // code here + check_child_pref_info_eq(function() { + Assert.equal(pb.prefHasUserValue(kPrefName), false); + + clean_up(); + }); +} + +function clean_up() { + pb.setCharPref(kPrefName, initialValue); + // NB: processing of the value-change notification in the child + // process triggered by the above set happens-before the remaining + // code here + check_child_pref_info_eq(function() { + do_test_finished(); + }); +} diff --git a/modules/libpref/test/unit_ipc/xpcshell.ini b/modules/libpref/test/unit_ipc/xpcshell.ini new file mode 100644 index 0000000000..e2a444f774 --- /dev/null +++ b/modules/libpref/test/unit_ipc/xpcshell.ini @@ -0,0 +1,14 @@ +[DEFAULT] +head = +skip-if = toolkit == 'android' + +[test_existing_prefs.js] +[test_initial_prefs.js] +[test_large_pref.js] +[test_locked_prefs.js] +[test_observed_prefs.js] +[test_update_prefs.js] +[test_sharedMap.js] +[test_sharedMap_static_prefs.js] +skip-if = !debug # Relies on debug assertions to catch failure cases. +[test_user_default_prefs.js] -- cgit v1.2.3