Bug 440908 - Add support for `sticky` and `locked` attributes to default prefs. r=glandium draft
authorNicholas Nethercote <nnethercote@mozilla.com>
Fri, 02 Mar 2018 15:31:40 +1100
changeset 763484 ec4ed3a0a8190164725d65d62c7b3e5272d32b68
parent 763483 cb7ea3f90bf89734f76e7d5b3e992fc82c92b178
child 763485 9b9384ee8abf22ed35fc6cff2ec0308b16fb3030
push id101468
push usernnethercote@mozilla.com
push dateTue, 06 Mar 2018 02:40:44 +0000
reviewersglandium
bugs440908
milestone60.0a1
Bug 440908 - Add support for `sticky` and `locked` attributes to default prefs. r=glandium Sticky prefs are already specifiable with `sticky_pref`, but this is a more general attribute mechanism. The ability to specify a locked pref in the data file is new. The patch also adds nsIPrefService.readDefaultPrefsFromFile, to match the existing nsIPrefService.readUserPrefsFromFile method, and converts a number of the existing tests to use it. MozReview-Commit-ID: 9LLMBJVZfg7
modules/libpref/Preferences.cpp
modules/libpref/nsIPrefService.idl
modules/libpref/parser/src/lib.rs
modules/libpref/test/gtest/Parser.cpp
modules/libpref/test/unit/data/testParser.js
modules/libpref/test/unit/test_parser.js
modules/libpref/test/unit/test_stickyprefs.js
services/sync/tests/unit/test_prefs_store.js
--- a/modules/libpref/Preferences.cpp
+++ b/modules/libpref/Preferences.cpp
@@ -591,41 +591,47 @@ public:
     }
 
     mHasUserValue = false;
     mHasChangedSinceInit = true;
   }
 
   nsresult SetDefaultValue(PrefType aType,
                            PrefValue aValue,
+                           bool aIsSticky,
+                           bool aIsLocked,
                            bool aFromFile,
-                           bool aIsSticky,
                            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() && !ValueMatches(PrefValueKind::Default, aType, aValue)) {
-      mDefaultValue.Replace(Type(), aType, aValue);
-      mHasDefaultValue = true;
-      if (!aFromFile) {
-        mHasChangedSinceInit = true;
+    if (!IsLocked()) {
+      if (aIsLocked) {
+        SetIsLocked(true);
       }
-      if (aIsSticky) {
-        mIsSticky = true;
+      if (!ValueMatches(PrefValueKind::Default, aType, aValue)) {
+        mDefaultValue.Replace(Type(), aType, aValue);
+        mHasDefaultValue = true;
+        if (!aFromFile) {
+          mHasChangedSinceInit = 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.
       }
-      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 aFromFile,
                         bool* aValueChanged)
@@ -939,16 +945,17 @@ pref_HashTableLookup(const char* aPrefNa
 }
 
 static nsresult
 pref_SetPref(const char* aPrefName,
              PrefType aType,
              PrefValueKind aKind,
              PrefValue aValue,
              bool aIsSticky,
+             bool aIsLocked,
              bool aFromFile)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   if (!gHashTable) {
     return NS_ERROR_OUT_OF_MEMORY;
   }
 
@@ -961,19 +968,20 @@ pref_SetPref(const char* aPrefName,
   if (pref->IsTypeNone()) {
     // New entry. Set the type.
     pref->SetType(aType);
   }
 
   bool valueChanged = false;
   nsresult rv;
   if (aKind == PrefValueKind::Default) {
-    rv =
-      pref->SetDefaultValue(aType, aValue, aFromFile, aIsSticky, &valueChanged);
+    rv = pref->SetDefaultValue(
+      aType, aValue, aIsSticky, aIsLocked, aFromFile, &valueChanged);
   } else {
+    MOZ_ASSERT(!aIsLocked); // `locked` is disallowed in user pref files
     rv = pref->SetUserValue(aType, aValue, aFromFile, &valueChanged);
   }
   if (NS_FAILED(rv)) {
     NS_WARNING(
       nsPrintfCString(
         "Rejected attempt to change type of pref %s's %s value from %s to %s",
         aPrefName,
         (aKind == PrefValueKind::Default) ? "default" : "user",
@@ -1071,47 +1079,50 @@ static nsDataHashtable<nsCStringHashKey,
 
 extern "C" {
 
 // Keep this in sync with PrefFn in prefs_parser/src/lib.rs.
 typedef void (*PrefsParserPrefFn)(const char* aPrefName,
                                   PrefType aType,
                                   PrefValueKind aKind,
                                   PrefValue aValue,
-                                  bool aIsSticky);
+                                  bool aIsSticky,
+                                  bool aIsLocked);
 
 // Keep this in sync with ErrorFn in prefs_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 prefs_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(const nsCString& aName,
+             PrefValueKind aKind,
              const char* aPath,
              const TimeStamp& aStartTime,
              const nsCString& aBuf)
   {
     sNumPrefs = 0;
     bool ok = prefs_parser_parse(
-      aPath, aBuf.get(), aBuf.Length(), HandlePref, HandleError);
+      aPath, aKind, aBuf.get(), aBuf.Length(), HandlePref, HandleError);
     if (!ok) {
       return false;
     }
 
     uint32_t loadTime_us = (TimeStamp::Now() - aStartTime).ToMicroseconds();
 
     // Most prefs files are read before telemetry initializes, so we have to
     // save these measurements now and send them to telemetry later.
@@ -1123,21 +1134,27 @@ public:
     return true;
   }
 
 private:
   static void HandlePref(const char* aPrefName,
                          PrefType aType,
                          PrefValueKind aKind,
                          PrefValue aValue,
-                         bool aIsSticky)
+                         bool aIsSticky,
+                         bool aIsLocked)
   {
     sNumPrefs++;
-    pref_SetPref(
-      aPrefName, aType, aKind, aValue, aIsSticky, /* fromFile */ true);
+    pref_SetPref(aPrefName,
+                 aType,
+                 aKind,
+                 aValue,
+                 aIsSticky,
+                 aIsLocked,
+                 /* fromFile */ true);
   }
 
   static void HandleError(const char* aMsg)
   {
     nsresult rv;
     nsCOMPtr<nsIConsoleService> console =
       do_GetService("@mozilla.org/consoleservice;1", &rv);
     if (NS_SUCCEEDED(rv)) {
@@ -1159,34 +1176,36 @@ uint32_t Parser::sNumPrefs = 0;
 
 // The following code is test code for the gtest.
 
 static void
 TestParseErrorHandlePref(const char* aPrefName,
                          PrefType aType,
                          PrefValueKind aKind,
                          PrefValue aValue,
-                         bool aIsSticky)
+                         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(const char* aText, nsCString& aErrorMsg)
+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);
@@ -2444,17 +2463,17 @@ Preferences::HandleDirty()
                                    sPreferences.get(),
                                    &Preferences::SavePrefFileAsynchronous),
         PREF_DELAY_MS);
     }
   }
 }
 
 static nsresult
-openPrefFile(nsIFile* aFile);
+openPrefFile(nsIFile* aFile, PrefValueKind aKind);
 
 static const char kTelemetryPref[] = "toolkit.telemetry.enabled";
 static const char kChannelPref[] = "app.update.channel";
 
 // clang-format off
 static const char kPrefFileHeader[] =
   "// Mozilla User Preferences"
   NS_LINEBREAK
@@ -3152,26 +3171,39 @@ Preferences::Observe(nsISupports* aSubje
     // 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);
+  return openPrefFile(aFile, PrefValueKind::User);
 }
 
 NS_IMETHODIMP
 Preferences::ResetPrefs()
 {
   ENSURE_PARENT_PROCESS("Preferences::ResetPrefs", "all prefs");
 
   gHashTable->ClearAndPrepareForLength(PREF_HASHTABLE_INITIAL_LENGTH);
@@ -3408,17 +3440,17 @@ Preferences::ReadSavedPrefs()
 {
   nsCOMPtr<nsIFile> 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);
+  rv = openPrefFile(file, PrefValueKind::User);
   if (rv == NS_ERROR_FILE_NOT_FOUND) {
     // This is a normal case for new users.
     Telemetry::ScalarSet(
       Telemetry::ScalarID::PREFERENCES_CREATED_NEW_USER_PREFS_FILE, true);
     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).
@@ -3437,17 +3469,17 @@ Preferences::ReadUserOverridePrefs()
   nsCOMPtr<nsIFile> aFile;
   nsresult rv =
     NS_GetSpecialDirectory(NS_APP_PREFS_50_DIR, getter_AddRefs(aFile));
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return;
   }
 
   aFile->AppendNative(NS_LITERAL_CSTRING("user.js"));
-  rv = openPrefFile(aFile);
+  rv = openPrefFile(aFile, PrefValueKind::User);
   if (rv != NS_ERROR_FILE_NOT_FOUND) {
     // If the file exists and was at least partially read, record that in
     // telemetry as it may be a sign of pref injection.
     Telemetry::ScalarSet(Telemetry::ScalarID::PREFERENCES_READ_USER_JS, true);
   }
 }
 
 nsresult
@@ -3579,33 +3611,33 @@ Preferences::WritePrefFile(nsIFile* aFil
   // 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)
+openPrefFile(nsIFile* aFile, PrefValueKind aKind)
 {
   TimeStamp startTime = TimeStamp::Now();
 
   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(
-        filename, NS_ConvertUTF16toUTF8(path).get(), startTime, data)) {
+        filename, aKind, NS_ConvertUTF16toUTF8(path).get(), startTime, data)) {
     return NS_ERROR_FILE_CORRUPTED;
   }
 
   return NS_OK;
 }
 
 static int
 pref_CompareFileNames(nsIFile* aFile1, nsIFile* aFile2, void* /* unused */)
@@ -3696,29 +3728,29 @@ pref_LoadPrefsInDir(nsIFile* aDir,
     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]);
+    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);
+      rv2 = openPrefFile(file, PrefValueKind::Default);
       if (NS_FAILED(rv2)) {
         NS_ERROR("Special default pref file not parsed successfully.");
         rv = rv2;
       }
     }
   }
 
   return rv;
@@ -3729,17 +3761,21 @@ pref_ReadPrefFromJar(nsZipArchive* aJarR
 {
   TimeStamp startTime = TimeStamp::Now();
 
   nsCString manifest;
   MOZ_TRY_VAR(manifest,
               URLPreloader::ReadZip(aJarReader, nsDependentCString(aName)));
 
   Parser parser;
-  if (!parser.Parse(nsDependentCString(aName), aName, startTime, manifest)) {
+  if (!parser.Parse(nsDependentCString(aName),
+                    PrefValueKind::Default,
+                    aName,
+                    startTime,
+                    manifest)) {
     return NS_ERROR_FILE_CORRUPTED;
   }
 
   return NS_OK;
 }
 
 // Initialize default preference JavaScript buffers from appropriate TEXT
 // resources.
@@ -3809,17 +3845,17 @@ Preferences::InitInitialObjects()
     // Load $gre/greprefs.js.
     nsCOMPtr<nsIFile> greprefsFile;
     rv = NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(greprefsFile));
     NS_ENSURE_SUCCESS(rv, Err("NS_GetSpecialDirectory(NS_GRE_DIR) failed"));
 
     rv = greprefsFile->AppendNative(NS_LITERAL_CSTRING("greprefs.js"));
     NS_ENSURE_SUCCESS(rv, Err("greprefsFile->AppendNative() failed"));
 
-    rv = openPrefFile(greprefsFile);
+    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<nsIFile> defaultPrefDir;
@@ -3938,25 +3974,24 @@ Preferences::InitInitialObjects()
   developerBuild = !strcmp(NS_STRINGIFY(MOZ_UPDATE_CHANNEL), "default");
 #endif
 
   // Release Candidate builds are builds that think they are release builds, but
   // are shipped to beta users. We still need extended data from these users.
   bool releaseCandidateOnBeta = false;
   if (!strcmp(NS_STRINGIFY(MOZ_UPDATE_CHANNEL), "release")) {
     nsAutoCString updateChannelPrefValue;
-    Preferences::GetCString(kChannelPref, updateChannelPrefValue,
-                            PrefValueKind::Default);
+    Preferences::GetCString(
+      kChannelPref, updateChannelPrefValue, PrefValueKind::Default);
     releaseCandidateOnBeta = updateChannelPrefValue.EqualsLiteral("beta");
   }
 
   if (!strcmp(NS_STRINGIFY(MOZ_UPDATE_CHANNEL), "nightly") ||
       !strcmp(NS_STRINGIFY(MOZ_UPDATE_CHANNEL), "aurora") ||
-      !strcmp(NS_STRINGIFY(MOZ_UPDATE_CHANNEL), "beta") ||
-      developerBuild ||
+      !strcmp(NS_STRINGIFY(MOZ_UPDATE_CHANNEL), "beta") || developerBuild ||
       releaseCandidateOnBeta) {
     Preferences::SetBoolInAnyProcess(
       kTelemetryPref, true, PrefValueKind::Default);
   } else {
     Preferences::SetBoolInAnyProcess(
       kTelemetryPref, false, PrefValueKind::Default);
   }
   Preferences::LockInAnyProcess(kTelemetryPref);
@@ -4096,16 +4131,17 @@ Preferences::SetCStringInAnyProcess(cons
   PrefValue prefValue;
   const nsCString& flat = PromiseFlatCString(aValue);
   prefValue.mStringVal = flat.get();
   return pref_SetPref(aPrefName,
                       PrefType::String,
                       aKind,
                       prefValue,
                       /* isSticky */ false,
+                      /* isLocked */ false,
                       /* fromFile */ false);
 }
 
 /* static */ nsresult
 Preferences::SetCString(const char* aPrefName,
                         const nsACString& aValue,
                         PrefValueKind aKind)
 {
@@ -4122,16 +4158,17 @@ Preferences::SetBoolInAnyProcess(const c
 
   PrefValue prefValue;
   prefValue.mBoolVal = aValue;
   return pref_SetPref(aPrefName,
                       PrefType::Bool,
                       aKind,
                       prefValue,
                       /* isSticky */ false,
+                      /* isLocked */ false,
                       /* fromFile */ false);
 }
 
 /* static */ nsresult
 Preferences::SetBool(const char* aPrefName, bool aValue, PrefValueKind aKind)
 {
   ENSURE_PARENT_PROCESS("SetBool", aPrefName);
   return SetBoolInAnyProcess(aPrefName, aValue, aKind);
@@ -4146,16 +4183,17 @@ Preferences::SetIntInAnyProcess(const ch
 
   PrefValue prefValue;
   prefValue.mIntVal = aValue;
   return pref_SetPref(aPrefName,
                       PrefType::Int,
                       aKind,
                       prefValue,
                       /* isSticky */ false,
+                      /* isLocked */ false,
                       /* fromFile */ false);
 }
 
 /* static */ nsresult
 Preferences::SetInt(const char* aPrefName, int32_t aValue, PrefValueKind aKind)
 {
   ENSURE_PARENT_PROCESS("SetInt", aPrefName);
   return SetIntInAnyProcess(aPrefName, aValue, aKind);
--- a/modules/libpref/nsIPrefService.idl
+++ b/modules/libpref/nsIPrefService.idl
@@ -40,17 +40,17 @@ interface nsIPrefService : nsISupports
    * @param aFile The file to be written.
    *
    * @note
    * If nullptr is passed in for the aFile parameter the preference data is
    * written out to the current preferences file (usually prefs.js.)
    *
    * @throws Error File failed to write.
    *
-   * @see readUserPrefs
+   * @see readUserPrefsFromFile
    * @see nsIFile
    */
   void savePrefFile(in nsIFile aFile);
 
   /**
    * Call to get a Preferences "Branch" which accesses user preference data.
    * Using a Set method on this object will always create or set a user
    * preference value. When using a Get method a user set value will be
@@ -97,24 +97,30 @@ interface nsIPrefService : nsISupports
 
   /**
    * The preference service is 'dirty' if there are changes to user preferences
    * that have not been written to disk
    */
   readonly attribute boolean dirty;
 
   /**
-   * Read in the preferences specified in a user preference file. This method
-   * does not clear user preferences that were already set.
+   * Read in the preferences specified in a default preference file. This
+   * method does not clear preferences that were already set, but it may
+   * overwrite existing preferences.
    *
    * @param aFile The file to be read.
    *
    * @throws Error File failed to read or contained invalid data.
    * @note This method is intended for internal unit testing only!
    */
+  void readDefaultPrefsFromFile(in nsIFile aFile);
+
+  /**
+   * Like readDefaultPrefsFromFile, but for a user prefs file.
+   */
   void readUserPrefsFromFile(in nsIFile aFile);
 };
 
 %{C++
 
 #define NS_PREFSERVICE_CID                             \
   { /* {1cd91b88-1dd2-11b2-92e1-ed22ed298000} */       \
     0x91ca2441,                                        \
--- a/modules/libpref/parser/src/lib.rs
+++ b/modules/libpref/parser/src/lib.rs
@@ -1,31 +1,36 @@
 /* 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 crate implements a prefs file parser.
 //!
-//! Pref files have the following grammar.
+//! Pref files have the following grammar. Note that there are slight
+//! differences between the grammar for a default prefs files and a user prefs
+//! file.
 //!
 //! <pref-file>   = <pref>*
-//! <pref>        = <pref-spec> "(" <pref-name> "," <pref-value> ")" ";"
+//! <pref>        = <pref-spec> "(" <pref-name> "," <pref-value> <pref-attrs> ")" ";"
 //! <pref-spec>   = "user_pref" | "pref" | "sticky_pref"
 //! <pref-name>   = <string-literal>
 //! <pref-value>  = <string-literal> | "true" | "false" | <int-value>
 //! <int-value>   = <sign>? <int-literal>
 //! <sign>        = "+" | "-"
 //! <int-literal> = [0-9]+ (and cannot be followed by [A-Za-z_])
 //! <string-literal> =
 //!   A single or double-quoted string, with the following escape sequences
 //!   allowed: \", \', \\, \n, \r, \xNN, \uNNNN, where \xNN gives a raw byte
 //!   value that is copied directly into an 8-bit string value, and \uNNNN
 //!   gives a UTF-16 code unit that is converted to UTF-8 before being copied
 //!   into an 8-bit string value. \x00 and \u0000 are disallowed because they
 //!   would cause C++ code handling such strings to misbehave.
+//! <pref-attrs>  = ("," <pref-attr>)*      // in default pref files
+//!               = <empty>                 // in user pref files
+//! <pref-attr>   = "sticky" | "locked"     // default pref files only
 //!
 //! Comments can take three forms:
 //! - # Python-style comments
 //! - // C++ style comments
 //! - /* C style comments (non-nested) */
 //!
 //! Non-end-of-line whitespace chars are \t, \v, \f, and space.
 //!
@@ -89,17 +94,17 @@ use std::os::raw::{c_char, c_uchar};
 pub enum PrefType {
     None,
     String,
     Int,
     Bool,
 }
 
 /// Keep this in sync with PrefValueKind in Preferences.h.
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, PartialEq)]
 #[repr(u8)]
 pub enum PrefValueKind {
     Default,
     User
 }
 
 /// Keep this in sync with PrefValue in Preferences.cpp.
 #[repr(C)]
@@ -107,43 +112,43 @@ pub union PrefValue {
     string_val: *const c_char,
     int_val: i32,
     bool_val: bool,
 }
 
 /// Keep this in sync with PrefsParserPrefFn in Preferences.cpp.
 type PrefFn = unsafe extern "C" fn(pref_name: *const c_char, pref_type: PrefType,
                                    pref_value_kind: PrefValueKind, pref_value: PrefValue,
-                                   is_sticky: bool);
+                                   is_sticky: bool, is_locked: bool);
 
 /// Keep this in sync with PrefsParserErrorFn in Preferences.cpp.
 type ErrorFn = unsafe extern "C" fn(msg: *const c_char);
 
 /// Parse the contents of a prefs file.
 ///
 /// `buf` is a null-terminated string. `len` is its length, excluding the
 /// null terminator.
 ///
 /// `pref_fn` is called once for each successfully parsed pref.
 ///
 /// `error_fn` is called once for each parse error detected.
 ///
 /// Keep this in sync with the prefs_parser_parse() declaration in
 /// Preferences.cpp.
 #[no_mangle]
-pub extern "C" fn prefs_parser_parse(path: *const c_char, buf: *const c_char, len: usize,
-                                     pref_fn: PrefFn, error_fn: ErrorFn) -> bool {
+pub extern "C" fn prefs_parser_parse(path: *const c_char, kind: PrefValueKind, buf: *const c_char,
+                                     len: usize, pref_fn: PrefFn, error_fn: ErrorFn) -> bool {
     let path = unsafe { std::ffi::CStr::from_ptr(path).to_string_lossy().into_owned() };
 
     // Make sure `buf` ends in a '\0', and include that in the length, because
     // it represents EOF.
     let buf = unsafe { std::slice::from_raw_parts(buf as *const c_uchar, len + 1) };
     assert!(buf.last() == Some(&EOF));
 
-    let mut parser = Parser::new(&path, &buf, pref_fn, error_fn);
+    let mut parser = Parser::new(&path, kind, &buf, pref_fn, error_fn);
     parser.parse()
 }
 
 //---------------------------------------------------------------------------
 // The implementation
 //---------------------------------------------------------------------------
 
 #[derive(Clone, Copy, Debug, PartialEq)]
@@ -152,16 +157,18 @@ enum Token {
     SingleChar(u8),
 
     // Keywords
     Pref,       // pref
     StickyPref, // sticky_pref
     UserPref,   // user_pref
     True,       // true
     False,      // false
+    Sticky,     // sticky
+    Locked,     // locked
 
     // String literal, e.g. '"string"'. The value is stored elsewhere.
     String,
 
     // Unsigned integer literal, e.g. '123'. Although libpref uses i32 values,
     // any '-' and '+' before an integer literal are treated as separate
     // tokens, so these token values are always positive. Furthermore, we
     // tokenize int literals as u32 so that 2147483648 (which doesn't fit into
@@ -267,46 +274,51 @@ const SPECIAL_STRING_CHARS: [bool; 256] 
 /* 250+ */ _______, _______, _______, _______, _______, _______
 ];
 
 struct KeywordInfo {
   string: &'static [u8],
   token: Token,
 }
 
-const KEYWORD_INFOS: &[KeywordInfo; 5] = &[
+const KEYWORD_INFOS: &[KeywordInfo; 7] = &[
   // These are ordered by frequency.
   KeywordInfo { string: b"pref",        token: Token::Pref },
   KeywordInfo { string: b"true",        token: Token::True },
   KeywordInfo { string: b"false",       token: Token::False },
   KeywordInfo { string: b"user_pref",   token: Token::UserPref },
+  KeywordInfo { string: b"sticky",      token: Token::Sticky },
+  KeywordInfo { string: b"locked",      token: Token::Locked },
   KeywordInfo { string: b"sticky_pref", token: Token::StickyPref },
 ];
 
 struct Parser<'t> {
-    path: &'t str,      // Path to the file being parsed. Used in error messages.
-    buf: &'t [u8],      // Text being parsed.
-    i: usize,           // Index of next char to be read.
-    line_num: u32,      // Current line number within the text.
-    pref_fn: PrefFn,    // Callback for processing each pref.
-    error_fn: ErrorFn,  // Callback for parse errors.
-    has_errors: bool,   // Have we encountered errors?
+    path: &'t str,       // Path to the file being parsed. Used in error messages.
+    kind: PrefValueKind, // Default prefs file or user prefs file?
+    buf: &'t [u8],       // Text being parsed.
+    i: usize,            // Index of next char to be read.
+    line_num: u32,       // Current line number within the text.
+    pref_fn: PrefFn,     // Callback for processing each pref.
+    error_fn: ErrorFn,   // Callback for parse errors.
+    has_errors: bool,    // Have we encountered errors?
 }
 
 // As described above, we use 0 to represent EOF.
 const EOF: u8 = b'\0';
 
 impl<'t> Parser<'t> {
-    fn new(path: &'t str, buf: &'t [u8], pref_fn: PrefFn, error_fn: ErrorFn) -> Parser<'t> {
+    fn new(path: &'t str, kind: PrefValueKind, buf: &'t [u8], pref_fn: PrefFn, error_fn: ErrorFn)
+        -> Parser<'t> {
         // Make sure these tables take up 1 byte per entry.
         assert!(std::mem::size_of_val(&CHAR_KINDS) == 256);
         assert!(std::mem::size_of_val(&SPECIAL_STRING_CHARS) == 256);
 
         Parser {
             path: path,
+            kind: kind,
             buf: buf,
             i: 0,
             line_num: 1,
             pref_fn: pref_fn,
             error_fn: error_fn,
             has_errors: false,
         }
     }
@@ -318,17 +330,17 @@ impl<'t> Parser<'t> {
         let mut none_str  = Vec::with_capacity(0);   // For tokens that shouldn't be strings.
 
         let mut token = self.get_token(&mut none_str);
 
         // At the top of the loop we already have a token. In a valid input
         // this will be either the first token of a new pref, or EOF.
         loop {
             // <pref-spec>
-            let (pref_value_kind, is_sticky) = match token {
+            let (pref_value_kind, mut is_sticky) = match token {
                 Token::Pref => (PrefValueKind::Default, false),
                 Token::StickyPref => (PrefValueKind::Default, true),
                 Token::UserPref => (PrefValueKind::User, false),
                 Token::SingleChar(EOF) => return !self.has_errors,
                 _ => {
                     token = self.error_and_recover(
                         token, "expected pref specifier at start of pref definition");
                     continue;
@@ -365,17 +377,16 @@ impl<'t> Parser<'t> {
                     (PrefType::Bool, PrefValue { bool_val: true })
                 }
                 Token::False => {
                     (PrefType::Bool, PrefValue { bool_val: false })
                 }
                 Token::String => {
                     (PrefType::String,
                      PrefValue { string_val: value_str.as_ptr() as *const c_char })
-
                 }
                 Token::Int(u) => {
                     // Accept u <= 2147483647; anything larger will overflow i32.
                     if u <= std::i32::MAX as u32 {
                         (PrefType::Int, PrefValue { int_val: u as i32 })
                     } else {
                         token = self.error_and_recover(
                             Token::Error("integer literal overflowed"), "");
@@ -420,32 +431,71 @@ impl<'t> Parser<'t> {
 
                 }
                 _ => {
                     token = self.error_and_recover(token, "expected pref value after ','");
                     continue;
                 }
             };
 
+            // ("," <pref-attr>)*   // default pref files only
+            let mut is_locked = false;
+            let mut has_attrs = false;
+            if self.kind == PrefValueKind::Default {
+                let ok = loop {
+                    // ","
+                    token = self.get_token(&mut none_str);
+                    if token != Token::SingleChar(b',') {
+                        break true;
+                    }
+
+                    // <pref-attr>
+                    token = self.get_token(&mut none_str);
+                    match token {
+                        Token::Sticky => is_sticky = true,
+                        Token::Locked => is_locked = true,
+                        _ => {
+                            token =
+                              self.error_and_recover(token, "expected pref attribute after ','");
+                            break false;
+                        }
+                    }
+                    has_attrs = true;
+                };
+                if !ok {
+                    continue;
+                }
+            } else {
+                token = self.get_token(&mut none_str);
+            }
+
             // ")"
-            token = self.get_token(&mut none_str);
             if token != Token::SingleChar(b')') {
-                token = self.error_and_recover(token, "expected ')' after pref value");
+                let expected_msg = if self.kind == PrefValueKind::Default {
+                    if has_attrs {
+                        "expected ',' or ')' after pref attribute"
+                    } else {
+                        "expected ',' or ')' after pref value"
+                    }
+                } else {
+                    "expected ')' after pref value"
+                };
+                token = self.error_and_recover(token, expected_msg);
                 continue;
             }
 
             // ";"
             token = self.get_token(&mut none_str);
             if token != Token::SingleChar(b';') {
                 token = self.error_and_recover(token, "expected ';' after ')'");
                 continue;
             }
 
             unsafe { (self.pref_fn)(pref_name.as_ptr() as *const c_char, pref_type, pref_value_kind,
-                                    pref_value, is_sticky) };
+                                    pref_value, is_sticky, is_locked) };
 
             token = self.get_token(&mut none_str);
         }
     }
 
     fn error_and_recover(&mut self, token: Token, msg: &str) -> Token {
         self.has_errors = true;
 
--- a/modules/libpref/test/gtest/Parser.cpp
+++ b/modules/libpref/test/gtest/Parser.cpp
@@ -1,71 +1,80 @@
 /* -*- 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(const char* aText, nsCString& aErrorMsg);
+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(text_, expectedErrorMsg_)                                            \
+#define P(kind_, text_, expectedErrorMsg_)                                     \
   do {                                                                         \
-    TestParseError(text_, actualErrorMsg);                                     \
+    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.
-  P(R"(
+  DEFAULT(R"(
 pref("bool", true);
 sticky_pref("int", 123);
 user_pref("string", "value");
     )",
     ""
   );
 
   // Totally empty input.
-  P("",
+  DEFAULT("",
     ""
   );
 
   // Whitespace-only input.
-  P(R"(   
+  DEFAULT(R"(   
 		
     )" "\v \t \v \f",
     ""
   );
 
   //-------------------------------------------------------------------------
   // 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.
-  P(R"(
+  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);
@@ -79,146 +88,146 @@ pref("int.overflow", 1234567890987654321
     "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.
-  P(R"(
+  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.
-  P(R"(
+  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.
-  P(R"(
+  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.
-  P(R"(
+  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.
-  P(R"(
+  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.)
-  P(R"(
+  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.
-  P(R"(
+  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.
-  P(R"(
+  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.
-  P(R"(
+  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.)
-  P(R"(
+  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 value.
   // (The string literal is broken in two so that MSVC doesn't complain about
   // an invalid universal-character-name.)
-  P(R"(
+  DEFAULT(R"(
 pref("string.bad-u-surrogate", "foo\)" R"(ud83c\u1234");
 pref("int.ok", 0);
     )",
     "test:2: prefs parse error: invalid low surrogate value after high surrogate\n"
   );
 
   // Unlike in JavaScript, \b, \f, \t, \v aren't allowed.
-  P(R"(
+  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.
-  P(R"(
+  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"
@@ -227,176 +236,202 @@ pref("string.bad-escape", "foo\)",
     "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.
-  P(R"(
+  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.
-  P(R"(
+  DEFAULT(R"(
 pref("string.unterminated-string", "foo);
 pref("int.ok", 0);
     )",
     "test:3: prefs parse error: unknown keyword\n"
   );
 
   // Mismatched quotes (1).
-  P(R"(
+  DEFAULT(R"(
 pref("string.unterminated-string", "foo');
     )",
     "test:3: prefs parse error: unterminated string literal\n"
   );
 
   // Mismatched quotes (2).
-  P(R"(
+  DEFAULT(R"(
 pref("string.unterminated-string", 'foo");
     )",
     "test:3: prefs parse error: unterminated string literal\n"
   );
 
   // Unknown keywords.
-  P(R"(
+  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.
-  P(R"(
+  DEFAULT(R"(
 /* comment
     )",
     "test:3: prefs parse error: unterminated /* comment\n"
   );
 
   // Malformed comment.
-  P(R"(
+  DEFAULT(R"(
 / comment
     )",
     "test:2: prefs parse error: expected '/' or '*' after '/'\n"
   );
 
   // C++-style comment ending in EOF (1).
-  P(R"(
+  DEFAULT(R"(
 // comment)",
     ""
   );
 
   // C++-style comment ending in EOF (2).
-  P(R"(
+  DEFAULT(R"(
 //)",
     ""
   );
 
   // Various unexpected characters.
-  P(R"(
+  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.
   //-------------------------------------------------------------------------
 
-  P(R"(
+  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 ')' after pref value\n"
-    "test:11: prefs parse error: expected ';' after ')'\n"
-    "test:12: prefs parse error: expected ';' 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;
+pref("int.ok", 1);
+    )",
+    "test:2: prefs parse error: expected ')' after pref value\n"
   );
 
   // Parse errors involving unexpected EOF.
 
-  P(R"(
+  DEFAULT(R"(
 pref)",
     "test:2: prefs parse error: expected '(' after pref specifier\n"
   );
 
-  P(R"(
+  DEFAULT(R"(
 pref()",
     "test:2: prefs parse error: expected pref name after '('\n"
   );
 
-  P(R"(
+  DEFAULT(R"(
 pref("parse.error")",
     "test:2: prefs parse error: expected ',' after pref name\n"
   );
 
-  P(R"(
+  DEFAULT(R"(
 pref("parse.error",)",
     "test:2: prefs parse error: expected pref value after ','\n"
   );
 
-  P(R"(
+  DEFAULT(R"(
 pref("parse.error", -)",
     "test:2: prefs parse error: expected integer literal after '-'\n"
   );
 
-  P(R"(
+  DEFAULT(R"(
 pref("parse.error", +)",
     "test:2: prefs parse error: expected integer literal after '+'\n"
   );
 
-  P(R"(
+  DEFAULT(R"(
+pref("parse.error", true)",
+    "test:2: prefs parse error: expected ',' or ')' after pref value\n"
+  );
+
+  USER(R"(
 pref("parse.error", true)",
     "test:2: prefs parse error: expected ')' after pref value\n"
   );
 
-  P(R"(
+  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.
-  P(R"(
+  DEFAULT(R"(
 pref("parse.error", true);;
-pref("parse.error", true);;;
-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"
@@ -406,31 +441,31 @@ pref("int.ok", 0);
   // 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.)
 
-  P("\n \r \r\n bad",
+  DEFAULT("\n \r \r\n bad",
     "test:4: prefs parse error: unknown keyword\n"
   );
 
-  P("#\n#\r#\r\n bad",
+  DEFAULT("#\n#\r#\r\n bad",
     "test:4: prefs parse error: unknown keyword\n"
   );
 
-  P("//\n//\r//\r\n bad",
+  DEFAULT("//\n//\r//\r\n bad",
     "test:4: prefs parse error: unknown keyword\n"
   );
 
-  P("/*\n \r \r\n*/ bad",
+  DEFAULT("/*\n \r \r\n*/ bad",
     "test:4: prefs parse error: unknown keyword\n"
   );
 
   // Note: the escape sequences do *not* affect the line number.
-  P("pref(\"foo\\n\n foo\\r\r foo\\r\\n\r\n foo\", bad);",
+  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
 }
--- a/modules/libpref/test/unit/data/testParser.js
+++ b/modules/libpref/test/unit/data/testParser.js
@@ -1,10 +1,11 @@
-// Note: this file tests only valid syntax. See
-// modules/libpref/test/gtest/Parser.cpp for tests if invalid syntax.
+// Note: this file tests only valid syntax (of user pref files, not default
+// pref files). See modules/libpref/test/gtest/Parser.cpp for tests if invalid
+// syntax.
 
 #
 # comment
     # comment £
 //
 // comment
         // comment £
 /**/
@@ -45,16 +46,20 @@ pref
                  ,
                    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);
--- a/modules/libpref/test/unit/test_parser.js
+++ b/modules/libpref/test/unit/test_parser.js
@@ -5,25 +5,30 @@ function run_test() {
   const PREF_NAME = "testPref";
 
   var ps = Cc["@mozilla.org/preferences-service;1"]
            .getService(Ci.nsIPrefService);
   var defaultPrefs = ps.getDefaultBranch(null);
   var prefs = ps.getBranch(null);
 
   ps.resetPrefs();
-  ps.readUserPrefsFromFile(do_get_file('data/testParser.js'));
+  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);
--- a/modules/libpref/test/unit/test_stickyprefs.js
+++ b/modules/libpref/test/unit/test_stickyprefs.js
@@ -1,22 +1,27 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/licenses/publicdomain/  */
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 const ps = Services.prefs;
 
-// A little helper to reset the service and load some pref files
-function resetAndLoad(filenames) {
+// A little helper to reset the service and load one pref file.
+function resetAndLoadDefaults() {
   ps.resetPrefs();
-  for (let filename of filenames) {
-    ps.readUserPrefsFromFile(do_get_file(filename));
-  }
+  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();
@@ -34,17 +39,17 @@ function saveAndReload() {
 }
 
 function run_test() {
   run_next_test();
 }
 
 // A sticky pref should not be written if the value is unchanged.
 add_test(function notWrittenWhenUnchanged() {
-  resetAndLoad(["data/testPrefSticky.js"]);
+  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");
@@ -56,58 +61,58 @@ add_test(function notWrittenWhenUnchange
 });
 
 // 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.
-  resetAndLoad(["data/testPrefSticky.js", "data/testPrefStickyUser.js"]);
+  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.
-  resetAndLoad(["data/testPrefSticky.js", "data/testPrefStickyUser.js"]);
+  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.
-  resetAndLoad(["data/testPrefSticky.js", "data/testPrefStickyUser.js"]);
+  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.
-  resetAndLoad(["data/testPrefSticky.js"]);
+  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");
 
@@ -116,28 +121,28 @@ add_test(function hasUserValue() {
             "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.
-  resetAndLoad(["data/testPrefSticky.js", "data/testPrefStickyUser.js"]);
+  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.
-  resetAndLoad(["data/testPrefSticky.js", "data/testPrefStickyUser.js"]);
+  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) {
@@ -148,17 +153,17 @@ add_test(function clearUserPref() {
 
 // 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.
-  resetAndLoad(["data/testPrefSticky.js"]);
+  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);
 
--- a/services/sync/tests/unit/test_prefs_store.js
+++ b/services/sync/tests/unit/test_prefs_store.js
@@ -20,17 +20,17 @@ function makePersona(id) {
     name: Math.random().toString(),
     headerURL: "http://localhost:1234/a"
   };
 }
 
 add_task(async function run_test() {
   _("Test fixtures.");
   // read our custom prefs file before doing anything.
-  Services.prefs.readUserPrefsFromFile(do_get_file("prefs_test_prefs_store.js"));
+  Services.prefs.readDefaultPrefsFromFile(do_get_file("prefs_test_prefs_store.js"));
 
   let engine = Service.engineManager.get("prefs");
   let store = engine._store;
   let prefs = new Preferences();
   try {
 
     _("The GUID corresponds to XUL App ID.");
     let allIDs = await store.getAllIDs();