mirror of
https://github.com/roytam1/palemoon27.git
synced 2026-05-26 14:30:27 +00:00
b564bd67d3
- Use CompositorWidgetProxy for dispatching vsync to the compositor. (bug 1269037 part 1, r=mchang) (c8b7a4240c) - Remove unused null widget checks. (bug 1269037 part 2, r=mchang) (4f4cc9952b) - Bug 1269422: Wrap Gonk widget in |CompositorWidgetProxyWrapper|. r=dvander (4513035cbf) - Remove nsBaseWidget::NewCompositorBridgeParent. (bug 1272472 part 1, r=kats) (ca813c1f2b) - Use IPDL to schedule composites on GTK. (bug 1272472 part 2, r=nical) (7402cf834e) - Use IPC to schedule composites on Windows. (bug 1272472 part 3, r=jimm) (e5d7281dd7) - Don't use nsIWidget to check APZ in LayerManagerComposite. (bug 1269653 part 1, r=kats) (79a1644111) - Restrict SetDispAcquireFence's nsIWidget access to Gonk. (bug 1269653 part 2, r=kats) (72110b7bc7) - Bug 1264764 - Move PTexture under PCompositorBridge r=nical,dvander (17e6ec7fc1) - Move CompositorThreadHolder into its own file. (bug 1273017 part 1, r=mattwoodrow) (464ede8be1) - Move CompositorBridgeParent::CompositorLoop to CompositorThreadHolder. (bug 1273017 part 2, r=mattwoodrow) (85708f3cde) - Bug 1268313: Part 1 - Be explicit about which NewRunnableMethod callers want to be able to cancel. r=froydnj (faa07aa139) - Bug 1268313: Part 2 - Replace some NewRunnableMethods with NS_NewNonOwningRunnableMethod. r=froydnj (010c43d000) - Bug 1268313: Part 3 - Replace some NewCancelableRunnableMethod with NS_NewNonOwningCancelableRunnableMethod. r=froydnj (55018ef234) - Bug 1268313: Part 4 - Replace NewCancelableRunnableMethod with NS_NewCancelableRunnableMethod. r=froydnj (c22711b35d) - Bug 1268313: Part 5 - Make NS_NewRunnableMethod able to call const functions. r=froydnj (b0f60963a3) - Bug 1268313: Part 6 - Replace NewRunnableMethod with NS_NewRunnableMethod. r=froydnj (18d40def2c) - Bug 1260950 - Set mInitialSizeFound to true when the initial size id found. r=jesup, r=pehrsons a=kwierso (5d6abe57e0) - Bug 1237176 - Notify synth start if we get a finished event without a blocking-changed event. r=roc (c1aebe903a) - Bug 911546, use a runnable so that popups don't rollup during a grab, r=karlt (a06bd44e6c) - Bug 1237617 - Call nsWindow::ForcePresent during going active r=bas.schouten (066cad8f89) - Bug 1268313: Part 7 - Move NS_NewRunnableMethod and friends to mozilla::NewRunnableMethod. r=froydnj (8b4bf34961) - Bug 1266595: Followup to fix IPDL tests. r=billm (216f2dcff5) - Bug 1268313: Fix up IPDL tests. r=billm (228348d642) - Add WinCompositorWidgetProxy. (bug 1265975 part 1, r=jimm) (bfafe7a8e2) - Implement WinCompositorWidgetProxy::GetClientSize. (bug 1265975 part 2, r=jimm) (a8710a3259) - Move the WM_SETTEXT present lock to CompositorWidgetProxy. (bug 1265975 part 3, r=jimm) (297ce28c8a) - Move transparency handling to WinCompositorWigetProxy. (bug 1265975 part 4, r=jimm) (46ba0c6d01) - Remove Windows-specific compositor calls to nsIWidget. (bug 1265975 part 5, r=jimm) (3ef157c160) - Remove plugin-related CompositorBridgeParent use of nsIWidget. (bug 1265975 part 6, r=jimm) (6d80cdd6fd) - Hide top-level CompositorBridgeParents behind a new API. (bug 1272472 part 4, r=mattwoodrow,kats,gwagner) (228c0efdb7) - Bug 1253424 - part 1 - add a already_AddRefed nsTransactionStack::Push overload; r=erahm (7059e20914) - Bug 1253424 - part 2 - add nsTransactionStack::IsEmpty; r=erahm (7e9764a146) - Bug 1254618 - modify nsTransactionStack to use nsDeque rather than std::deque; r=ehsan (5e47ea431e) - Bug 1136857 - Make DOMStorageCache::mLoaded flag atomic to prevent potential races, r=nfroyd (39aaea1de3) - Bug 1265408 - Add webidl for IIRFilterNode; r=smaug (040ce9aa43) - Bug 1265408 - Implement IIRFilterNode; r=padenot (6bf569a412) - Bug 1265408 - Import IIRFilter from blink; r=padenot (71b28c0ad2) - Bug 1265408 - Use IIRFilter from blink; r=padenot (5d058d8568) - Bug 1265408 - Add buffersAreZero to IIRFilter; r=karlt (45edba3e13) - Bug 1265408 - Avoid subnormals in IIRFilter; r=karlt (0e1ae93f0b) - Bug 1265408 - Add LogToDeveloperConsole to WebAudioUtils; r=padenot (88d5f0222a) - Bug 1268984 - Store GMPStorage on GMPServiceParent so that it persists inside the same PB session. r=gerald (17d4d0abaf) - Bug 1267905 - Replace uses of ScopedCERTCertList with UniqueCERTCertList. r=keeler (783bf11b2a) - Bug 1270005 - Replace uses of ScopedPK11SlotInfo with UniquePK11SlotInfo in PSM. r=keeler (ea9a4011aa) - Bug 1271501 - Remove unnecessary uses of reinterpret_cast in PSM. r=keeler (6be40f0a85) - Bug 1271501 - Downgrade unnecessarily strong reinterpret_casts in PSM. r=keeler (95245f00ce) - Bug 1082346 - 01. Convert PKCS12 password endian using copyAndSwapToBigEndian. r=keeler (9cc58fc550) - Bug 1082346 - 02. Test case. r=keeler r=Cykesiopka (7fb0e8abc4) - Bug 160122 - Stop using PR_smprintf in PSM. r=keeler (1e5b68819c) - Bug 1271501 - Use mozilla::BitwiseCast instead of reinterpret_cast in PSM. r=keeler (894966a2ef) - Bug 1273855: TraceLogger - Include PID in the log names in order to support browser with e10s, r=bbouvier (8cf2233db3) - Bug 1274189. Part 1 - rename some functions to be consistent with other MediaDataDecoder sub-classes. r=jya. (4511b3d3f7) - Bug 1274189. Part 2 - remove use of FlushableTaskQueue::Flush(). r=jya. (77e745fdd1) - Bug 1274189. Part 3 - remove use of FlushableTaskQueue. r=jya (aac61dcd02) - Bug 1269963. Part 1 - Add a SyncRunnable::DispatchToThread() overload for AbstractThread. r=bobbyholley. (839752aff4) - Bug 1269672 - part1 : revert sampling rate changing of the bug1235612. (9015782e13) - Bug 1270698 - check if we need to enter buffering periodically to ensure we start buffering when running out of decoded audio/video data. r=cpearce. (16734549b7) - Bug 1271581 - use newCurrentTime, instead of GetMediaTime() to decide the nextState; r=jwwang (9c5075eada) - Bug 1224973 - Part 1: Remove MediaDecoderOwner->IsHidden(). r=cpearce,jwwang (4fde3ede5a) - Bug 1224973 - Part 2: Set MediaDecoder visibility via NotifyOwnerActivityChanged. r=cpearce,jwwang (be917202eb) - Bug 1224973 - Part 3: Plumb element visibility into MDSM. r=jya,jwwang (9ec83fa243) - Bug 1224973 - Part 4: Pref media.suspend-bkgnd-video.enabled. r=cpearce,jwwang (43413a025f) - Bug 1269408: P1. Retry InternalSeek if previous attempt failed once more data is available. r=gerald (05db58dc7c) - crude fix (0097068989) - Bug 1269408: P2. Update mochitest. r=gerald (464b4c0724) - Bug 1269408: P3. Ensure a new seek request will cancel the previous internal seek. r=gerald (6ed4b8dc95) - Bug 1269408: P4. Ensure the decoders are flushed prior performing an internal seek. r=gerald (074234067b) - Bug 1269408: P5. Only drop the seek target if it's exactly the seek target. r=gerald (88701eb05a) - Bug 1269408: P6. Add debugging information, useful when a mochitest timeout. r=gerald (ef0270ab0d) - Bug 1269408: P7. Start skip to next keyframe logic when resume point is behind current time. r=gerald (bd40ebf3bc) - Bug 1269408: P8. Add debugging log. r=gerald (e6dbd1f0a6) - Bug 1269408: P9. Move handling logic of skip to next keyframe to its own function. r=gerald (3c8039e417) - Bug 1269408: P10. Reject promise early if in error state. r=me (8af54c574e) - Bug 1224973 - Part 5: Implement suspend decoding for background video. r=cpearce,jwwang,jya (22081521e3) - Bug 1242874 - part1 : create suspened types. r=baku (d3ac9548e5) - Bug 1242874 - part2 : window's suspend attribute. r=baku, r=ehsan (1fd9dc2647) - remove allowscirpted (39ab523036) - Bug 1242874 - part3 : implement different suspended methods. r=baku, r=jwwang (25d1f27a03) - Bug 1242874 - part4 : wrap the volume/mute/suspend for notifyStartedPlaying. r=baku (b8ba3238c2) - bug 1242874 - part5 : add test. r=baku, r=ehsan (f840139b5a) - Bug 1235612 - Part 1: Implement notify media-playback. r=baku (b5ec29da20) - Bug 1235612 - Part 2: Notify audible state in NotifyStartedPlaying. r=baku (dc38583a62) - Bug 1235612 - Part 3: Implement the logic of audible state notification for agent owners. r=baku (f65b3952fa) - Bug 1235612 - Part 4: Modify check audible method. r=jwwang (73457e39eb) - Bug 1269672 - part2 : move audible data checking from MDSM to DecodedAudioDataSink. (d2c3b6874c) - Bug 1269936 - Introduce and call a runtime-wide servo initialization hook. r=heycam (d4d505d4c2) - Bug 1263778 - Rename a bunch of low-level [[Prototype]] access methods to make their interactions with statically-known and dynamically-computed [[Prototype]]s clearer. r=efaust (66bbe8e7db) - Bug 888969 - Permit a cyclic [[Prototype]] chain to be created through a Location object. r=bz, r=efaust (3e3b9cbb16) - re-apply Bug 1054906 - Implement ES6 Symbol.hasInstance 2/2; r=jandem (8d5c7573ff) - Bug 1054906 - Implement ES6 Symbol.hasInstance 1/2; r=evilpie,bz (a836904e5d) - fix misspatch (54a5f2d708) - Bug 1270349 part 1. Add IDL parser support for [LegacyUnenumerableNamedProperties]. r=peterv (8c836bc74a) - Bug 1270349 part 2. Add [LegacyUnenumerableNamedProperties] to the interfaces that specify it in DOM and HTML. r=peterv (25d3cc1377) - Bug 1270349 part 3. Add a way to ask an interface descriptor for a proxy whether its named props should be enumerable. r=peterv (0a9f804867) - Bug 1270349 part 4. Use LegacyUnenumerableNamedProperties instead of NameIsEnumerable() calls to determine whether named props on DOM proxies should be enumerable. r=peterv (82f5158963) - Bug 1270349 part 5. Use LegacyUnenumerableNamedProperties instead of passing flags to GetSupportedNames to determine whether named props on DOM proxies should be reflected in ownPropertyKeys. r=peterv (3984176834) - Bug 1270349 followup to address a review comment. r=peterv (b49f4c5335)
937 lines
26 KiB
C++
937 lines
26 KiB
C++
/* -*- 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 "DataStorage.h"
|
|
|
|
#include "mozilla/ClearOnShutdown.h"
|
|
#include "mozilla/dom/PContent.h"
|
|
#include "mozilla/dom/ContentChild.h"
|
|
#include "mozilla/dom/ContentParent.h"
|
|
#include "mozilla/Preferences.h"
|
|
#include "mozilla/Services.h"
|
|
#include "mozilla/Telemetry.h"
|
|
#include "mozilla/unused.h"
|
|
#include "nsAppDirectoryServiceDefs.h"
|
|
#include "nsDirectoryServiceUtils.h"
|
|
#include "nsIObserverService.h"
|
|
#include "nsITimer.h"
|
|
#include "nsNetUtil.h"
|
|
#include "nsStreamUtils.h"
|
|
#include "nsThreadUtils.h"
|
|
#include "prprf.h"
|
|
|
|
// NB: Read DataStorage.h first.
|
|
|
|
// The default time between data changing and a write, in milliseconds.
|
|
static const uint32_t sDataStorageDefaultTimerDelay = 5u * 60u * 1000u;
|
|
// The maximum score an entry can have (prevents overflow)
|
|
static const uint32_t sMaxScore = UINT32_MAX;
|
|
// The maximum number of entries per type of data (limits resource use)
|
|
static const uint32_t sMaxDataEntries = 1024;
|
|
static const int64_t sOneDayInMicroseconds = int64_t(24 * 60 * 60) *
|
|
PR_USEC_PER_SEC;
|
|
|
|
namespace mozilla {
|
|
|
|
NS_IMPL_ISUPPORTS(DataStorage,
|
|
nsIObserver)
|
|
|
|
StaticAutoPtr<DataStorage::DataStorages> DataStorage::sDataStorages;
|
|
|
|
DataStorage::DataStorage(const nsString& aFilename)
|
|
: mMutex("DataStorage::mMutex")
|
|
, mPendingWrite(false)
|
|
, mShuttingDown(false)
|
|
, mInitCalled(false)
|
|
, mReadyMonitor("DataStorage::mReadyMonitor")
|
|
, mReady(false)
|
|
, mFilename(aFilename)
|
|
{
|
|
}
|
|
|
|
DataStorage::~DataStorage()
|
|
{
|
|
}
|
|
|
|
// static
|
|
already_AddRefed<DataStorage>
|
|
DataStorage::Get(const nsString& aFilename)
|
|
{
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
if (!sDataStorages) {
|
|
sDataStorages = new DataStorages();
|
|
ClearOnShutdown(&sDataStorages);
|
|
}
|
|
RefPtr<DataStorage> storage;
|
|
if (!sDataStorages->Get(aFilename, getter_AddRefs(storage))) {
|
|
storage = new DataStorage(aFilename);
|
|
sDataStorages->Put(aFilename, storage);
|
|
}
|
|
return storage.forget();
|
|
}
|
|
|
|
// static
|
|
already_AddRefed<DataStorage>
|
|
DataStorage::GetIfExists(const nsString& aFilename)
|
|
{
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
if (!sDataStorages) {
|
|
sDataStorages = new DataStorages();
|
|
}
|
|
RefPtr<DataStorage> storage;
|
|
sDataStorages->Get(aFilename, getter_AddRefs(storage));
|
|
return storage.forget();
|
|
}
|
|
|
|
nsresult
|
|
DataStorage::Init(bool& aDataWillPersist)
|
|
{
|
|
// Don't access the observer service or preferences off the main thread.
|
|
if (!NS_IsMainThread()) {
|
|
NS_NOTREACHED("DataStorage::Init called off main thread");
|
|
return NS_ERROR_NOT_SAME_THREAD;
|
|
}
|
|
|
|
MutexAutoLock lock(mMutex);
|
|
|
|
// Ignore attempts to initialize several times.
|
|
if (mInitCalled) {
|
|
return NS_OK;
|
|
}
|
|
|
|
mInitCalled = true;
|
|
|
|
nsresult rv;
|
|
if (XRE_IsParentProcess()) {
|
|
rv = NS_NewThread(getter_AddRefs(mWorkerThread));
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
|
|
rv = AsyncReadData(aDataWillPersist, lock);
|
|
if (NS_FAILED(rv)) {
|
|
return rv;
|
|
}
|
|
} else {
|
|
// In the child process, we ask the parent process for the data.
|
|
MOZ_ASSERT(XRE_IsContentProcess());
|
|
aDataWillPersist = false;
|
|
InfallibleTArray<DataStorageItem> items;
|
|
dom::ContentChild::GetSingleton()->
|
|
SendReadDataStorageArray(mFilename, &items);
|
|
for (auto& item : items) {
|
|
Entry entry;
|
|
entry.mValue = item.value();
|
|
rv = PutInternal(item.key(), entry, item.type(), lock);
|
|
if (NS_FAILED(rv)) {
|
|
return rv;
|
|
}
|
|
}
|
|
mReady = true;
|
|
NotifyObservers("data-storage-ready");
|
|
}
|
|
|
|
nsCOMPtr<nsIObserverService> os = services::GetObserverService();
|
|
if (NS_WARN_IF(!os)) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
// Clear private data as appropriate.
|
|
os->AddObserver(this, "last-pb-context-exited", false);
|
|
// Observe shutdown; save data and prevent any further writes.
|
|
// In the parent process, we need to write to the profile directory, so
|
|
// we should listen for profile-before-change so that we can safely
|
|
// write to the profile. In the content process however we don't have
|
|
// access to the profile directory and profile notifications are not
|
|
// dispatched, so we need to clean up on xpcom-shutdown.
|
|
if (XRE_IsParentProcess()) {
|
|
os->AddObserver(this, "profile-before-change", false);
|
|
} else {
|
|
os->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
|
|
}
|
|
|
|
// For test purposes, we can set the write timer to be very fast.
|
|
mTimerDelay = Preferences::GetInt("test.datastorage.write_timer_ms",
|
|
sDataStorageDefaultTimerDelay);
|
|
rv = Preferences::AddStrongObserver(this, "test.datastorage.write_timer_ms");
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
class DataStorage::Reader : public Runnable
|
|
{
|
|
public:
|
|
explicit Reader(DataStorage* aDataStorage)
|
|
: mDataStorage(aDataStorage)
|
|
{
|
|
}
|
|
~Reader();
|
|
|
|
private:
|
|
NS_DECL_NSIRUNNABLE
|
|
|
|
static nsresult ParseLine(nsDependentCSubstring& aLine, nsCString& aKeyOut,
|
|
Entry& aEntryOut);
|
|
|
|
RefPtr<DataStorage> mDataStorage;
|
|
};
|
|
|
|
DataStorage::Reader::~Reader()
|
|
{
|
|
// Notify that calls to Get can proceed.
|
|
{
|
|
MonitorAutoLock readyLock(mDataStorage->mReadyMonitor);
|
|
mDataStorage->mReady = true;
|
|
nsresult rv = mDataStorage->mReadyMonitor.NotifyAll();
|
|
Unused << NS_WARN_IF(NS_FAILED(rv));
|
|
}
|
|
|
|
// This is for tests.
|
|
nsCOMPtr<nsIRunnable> job =
|
|
NewRunnableMethod<const char*>(mDataStorage,
|
|
&DataStorage::NotifyObservers,
|
|
"data-storage-ready");
|
|
nsresult rv = NS_DispatchToMainThread(job, NS_DISPATCH_NORMAL);
|
|
Unused << NS_WARN_IF(NS_FAILED(rv));
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
DataStorage::Reader::Run()
|
|
{
|
|
nsresult rv;
|
|
// Concurrent operations on nsIFile objects are not guaranteed to be safe,
|
|
// so we clone the file while holding the lock and then release the lock.
|
|
// At that point, we can safely operate on the clone.
|
|
nsCOMPtr<nsIFile> file;
|
|
{
|
|
MutexAutoLock lock(mDataStorage->mMutex);
|
|
// If we don't have a profile, bail.
|
|
if (!mDataStorage->mBackingFile) {
|
|
return NS_OK;
|
|
}
|
|
rv = mDataStorage->mBackingFile->Clone(getter_AddRefs(file));
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
}
|
|
nsCOMPtr<nsIInputStream> fileInputStream;
|
|
rv = NS_NewLocalFileInputStream(getter_AddRefs(fileInputStream), file);
|
|
// If we failed for some reason other than the file doesn't exist, bail.
|
|
if (NS_WARN_IF(NS_FAILED(rv) &&
|
|
rv != NS_ERROR_FILE_TARGET_DOES_NOT_EXIST && // on Unix
|
|
rv != NS_ERROR_FILE_NOT_FOUND)) { // on Windows
|
|
return rv;
|
|
}
|
|
|
|
// If there is a file with data in it, read it. If there isn't,
|
|
// we'll essentially fall through to notifying that we're good to go.
|
|
nsCString data;
|
|
if (fileInputStream) {
|
|
// Limit to 2MB of data, but only store sMaxDataEntries entries.
|
|
rv = NS_ConsumeStream(fileInputStream, 1u << 21, data);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
}
|
|
|
|
// Atomically parse the data and insert the entries read.
|
|
// Don't clear existing entries - they may have been inserted between when
|
|
// this read was kicked-off and when it was run.
|
|
{
|
|
MutexAutoLock lock(mDataStorage->mMutex);
|
|
// The backing file consists of a list of
|
|
// <key>\t<score>\t<last accessed time>\t<value>\n
|
|
// The final \n is not optional; if it is not present the line is assumed
|
|
// to be corrupt.
|
|
int32_t currentIndex = 0;
|
|
int32_t newlineIndex = 0;
|
|
do {
|
|
newlineIndex = data.FindChar('\n', currentIndex);
|
|
// If there are no more newlines or the data table has too many
|
|
// entries, we are done.
|
|
if (newlineIndex < 0 ||
|
|
mDataStorage->mPersistentDataTable.Count() >= sMaxDataEntries) {
|
|
break;
|
|
}
|
|
|
|
nsDependentCSubstring line(data, currentIndex,
|
|
newlineIndex - currentIndex);
|
|
currentIndex = newlineIndex + 1;
|
|
nsCString key;
|
|
Entry entry;
|
|
nsresult parseRV = ParseLine(line, key, entry);
|
|
if (NS_SUCCEEDED(parseRV)) {
|
|
// It could be the case that a newer entry was added before
|
|
// we got around to reading the file. Don't overwrite new entries.
|
|
Entry newerEntry;
|
|
bool present = mDataStorage->mPersistentDataTable.Get(key, &newerEntry);
|
|
if (!present) {
|
|
mDataStorage->mPersistentDataTable.Put(key, entry);
|
|
}
|
|
}
|
|
} while (true);
|
|
|
|
Telemetry::Accumulate(Telemetry::DATA_STORAGE_ENTRIES,
|
|
mDataStorage->mPersistentDataTable.Count());
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
// The key must be a non-empty string containing no instances of '\t' or '\n',
|
|
// and must have a length no more than 256.
|
|
// The value must not contain '\n' and must have a length no more than 1024.
|
|
// The length limits are to prevent unbounded memory and disk usage.
|
|
/* static */
|
|
nsresult
|
|
DataStorage::ValidateKeyAndValue(const nsCString& aKey, const nsCString& aValue)
|
|
{
|
|
if (aKey.IsEmpty()) {
|
|
return NS_ERROR_INVALID_ARG;
|
|
}
|
|
if (aKey.Length() > 256) {
|
|
return NS_ERROR_INVALID_ARG;
|
|
}
|
|
int32_t delimiterIndex = aKey.FindChar('\t', 0);
|
|
if (delimiterIndex >= 0) {
|
|
return NS_ERROR_INVALID_ARG;
|
|
}
|
|
delimiterIndex = aKey.FindChar('\n', 0);
|
|
if (delimiterIndex >= 0) {
|
|
return NS_ERROR_INVALID_ARG;
|
|
}
|
|
delimiterIndex = aValue.FindChar('\n', 0);
|
|
if (delimiterIndex >= 0) {
|
|
return NS_ERROR_INVALID_ARG;
|
|
}
|
|
if (aValue.Length() > 1024) {
|
|
return NS_ERROR_INVALID_ARG;
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
// Each line is: <key>\t<score>\t<last accessed time>\t<value>
|
|
// Where <score> is a uint32_t as a string, <last accessed time> is a
|
|
// int32_t as a string, and the rest are strings.
|
|
// <value> can contain anything but a newline.
|
|
// Returns a successful status if the line can be decoded into a key and entry.
|
|
// Otherwise, an error status is returned and the values assigned to the
|
|
// output parameters are in an undefined state.
|
|
/* static */
|
|
nsresult
|
|
DataStorage::Reader::ParseLine(nsDependentCSubstring& aLine, nsCString& aKeyOut,
|
|
Entry& aEntryOut)
|
|
{
|
|
// First find the indices to each part of the line.
|
|
int32_t scoreIndex;
|
|
scoreIndex = aLine.FindChar('\t', 0) + 1;
|
|
if (scoreIndex <= 0) {
|
|
return NS_ERROR_UNEXPECTED;
|
|
}
|
|
int32_t accessedIndex = aLine.FindChar('\t', scoreIndex) + 1;
|
|
if (accessedIndex <= 0) {
|
|
return NS_ERROR_UNEXPECTED;
|
|
}
|
|
int32_t valueIndex = aLine.FindChar('\t', accessedIndex) + 1;
|
|
if (valueIndex <= 0) {
|
|
return NS_ERROR_UNEXPECTED;
|
|
}
|
|
|
|
// Now make substrings based on where each part is.
|
|
nsDependentCSubstring keyPart(aLine, 0, scoreIndex - 1);
|
|
nsDependentCSubstring scorePart(aLine, scoreIndex,
|
|
accessedIndex - scoreIndex - 1);
|
|
nsDependentCSubstring accessedPart(aLine, accessedIndex,
|
|
valueIndex - accessedIndex - 1);
|
|
nsDependentCSubstring valuePart(aLine, valueIndex);
|
|
|
|
nsresult rv;
|
|
rv = DataStorage::ValidateKeyAndValue(nsCString(keyPart),
|
|
nsCString(valuePart));
|
|
if (NS_FAILED(rv)) {
|
|
return NS_ERROR_UNEXPECTED;
|
|
}
|
|
|
|
// Now attempt to decode the score part as a uint32_t.
|
|
// XXX nsDependentCSubstring doesn't support ToInteger
|
|
int32_t integer = nsCString(scorePart).ToInteger(&rv);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
if (integer < 0) {
|
|
return NS_ERROR_UNEXPECTED;
|
|
}
|
|
aEntryOut.mScore = (uint32_t)integer;
|
|
|
|
integer = nsCString(accessedPart).ToInteger(&rv);
|
|
if (NS_FAILED(rv)) {
|
|
return rv;
|
|
}
|
|
if (integer < 0) {
|
|
return NS_ERROR_UNEXPECTED;
|
|
}
|
|
aEntryOut.mLastAccessed = integer;
|
|
|
|
// Now set the key and value.
|
|
aKeyOut.Assign(keyPart);
|
|
aEntryOut.mValue.Assign(valuePart);
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult
|
|
DataStorage::AsyncReadData(bool& aHaveProfileDir,
|
|
const MutexAutoLock& /*aProofOfLock*/)
|
|
{
|
|
MOZ_ASSERT(XRE_IsParentProcess());
|
|
aHaveProfileDir = false;
|
|
// Allocate a Reader so that even if it isn't dispatched,
|
|
// the data-storage-ready notification will be fired and Get
|
|
// will be able to proceed (this happens in its destructor).
|
|
RefPtr<Reader> job(new Reader(this));
|
|
nsresult rv;
|
|
// If we don't have a profile directory, this will fail.
|
|
// That's okay - it just means there is no persistent state.
|
|
rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
|
|
getter_AddRefs(mBackingFile));
|
|
if (NS_FAILED(rv)) {
|
|
mBackingFile = nullptr;
|
|
return NS_OK;
|
|
}
|
|
|
|
rv = mBackingFile->Append(mFilename);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
|
|
rv = mWorkerThread->Dispatch(job, NS_DISPATCH_NORMAL);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
|
|
aHaveProfileDir = true;
|
|
return NS_OK;
|
|
}
|
|
|
|
void
|
|
DataStorage::WaitForReady()
|
|
{
|
|
MonitorAutoLock readyLock(mReadyMonitor);
|
|
while (!mReady) {
|
|
nsresult rv = readyLock.Wait();
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
break;
|
|
}
|
|
}
|
|
MOZ_ASSERT(mReady);
|
|
}
|
|
|
|
nsCString
|
|
DataStorage::Get(const nsCString& aKey, DataStorageType aType)
|
|
{
|
|
WaitForReady();
|
|
MutexAutoLock lock(mMutex);
|
|
|
|
Entry entry;
|
|
bool foundValue = GetInternal(aKey, &entry, aType, lock);
|
|
if (!foundValue) {
|
|
return EmptyCString();
|
|
}
|
|
|
|
// If we're here, we found a value. Maybe update its score.
|
|
if (entry.UpdateScore()) {
|
|
PutInternal(aKey, entry, aType, lock);
|
|
}
|
|
|
|
return entry.mValue;
|
|
}
|
|
|
|
bool
|
|
DataStorage::GetInternal(const nsCString& aKey, Entry* aEntry,
|
|
DataStorageType aType,
|
|
const MutexAutoLock& aProofOfLock)
|
|
{
|
|
DataStorageTable& table = GetTableForType(aType, aProofOfLock);
|
|
bool foundValue = table.Get(aKey, aEntry);
|
|
return foundValue;
|
|
}
|
|
|
|
DataStorage::DataStorageTable&
|
|
DataStorage::GetTableForType(DataStorageType aType,
|
|
const MutexAutoLock& /*aProofOfLock*/)
|
|
{
|
|
switch (aType) {
|
|
case DataStorage_Persistent:
|
|
return mPersistentDataTable;
|
|
case DataStorage_Temporary:
|
|
return mTemporaryDataTable;
|
|
case DataStorage_Private:
|
|
return mPrivateDataTable;
|
|
}
|
|
|
|
MOZ_CRASH("given bad DataStorage storage type");
|
|
}
|
|
|
|
void
|
|
DataStorage::ReadAllFromTable(DataStorageType aType,
|
|
InfallibleTArray<dom::DataStorageItem>* aItems,
|
|
const MutexAutoLock& aProofOfLock)
|
|
{
|
|
for (auto iter = GetTableForType(aType, aProofOfLock).Iter();
|
|
!iter.Done(); iter.Next()) {
|
|
DataStorageItem* item = aItems->AppendElement();
|
|
item->key() = iter.Key();
|
|
item->value() = iter.Data().mValue;
|
|
item->type() = aType;
|
|
}
|
|
}
|
|
|
|
void
|
|
DataStorage::GetAll(InfallibleTArray<dom::DataStorageItem>* aItems)
|
|
{
|
|
WaitForReady();
|
|
MutexAutoLock lock(mMutex);
|
|
|
|
aItems->SetCapacity(mPersistentDataTable.Count() +
|
|
mTemporaryDataTable.Count() +
|
|
mPrivateDataTable.Count());
|
|
ReadAllFromTable(DataStorage_Persistent, aItems, lock);
|
|
ReadAllFromTable(DataStorage_Temporary, aItems, lock);
|
|
ReadAllFromTable(DataStorage_Private, aItems, lock);
|
|
}
|
|
|
|
// Limit the number of entries per table. This is to prevent unbounded
|
|
// resource use. The eviction strategy is as follows:
|
|
// - An entry's score is incremented once for every day it is accessed.
|
|
// - Evict an entry with score no more than any other entry in the table
|
|
// (this is the same as saying evict the entry with the lowest score,
|
|
// except for when there are multiple entries with the lowest score,
|
|
// in which case one of them is evicted - which one is not specified).
|
|
void
|
|
DataStorage::MaybeEvictOneEntry(DataStorageType aType,
|
|
const MutexAutoLock& aProofOfLock)
|
|
{
|
|
DataStorageTable& table = GetTableForType(aType, aProofOfLock);
|
|
if (table.Count() >= sMaxDataEntries) {
|
|
KeyAndEntry toEvict;
|
|
// If all entries have score sMaxScore, this won't actually remove
|
|
// anything. This will never happen, however, because having that high
|
|
// a score either means someone tampered with the backing file or every
|
|
// entry has been accessed once a day for ~4 billion days.
|
|
// The worst that will happen is there will be 1025 entries in the
|
|
// persistent data table, with the 1025th entry being replaced every time
|
|
// data with a new key is inserted into the table. This is bad but
|
|
// ultimately not that concerning, considering that if an attacker can
|
|
// modify data in the profile, they can cause much worse harm.
|
|
toEvict.mEntry.mScore = sMaxScore;
|
|
|
|
for (auto iter = table.Iter(); !iter.Done(); iter.Next()) {
|
|
Entry entry = iter.UserData();
|
|
if (entry.mScore < toEvict.mEntry.mScore) {
|
|
toEvict.mKey = iter.Key();
|
|
toEvict.mEntry = entry;
|
|
}
|
|
}
|
|
|
|
table.Remove(toEvict.mKey);
|
|
}
|
|
}
|
|
|
|
template <class Functor>
|
|
static
|
|
void
|
|
RunOnAllContentParents(Functor func)
|
|
{
|
|
if (!XRE_IsParentProcess()) {
|
|
return;
|
|
}
|
|
using dom::ContentParent;
|
|
nsTArray<ContentParent*> parents;
|
|
ContentParent::GetAll(parents);
|
|
for (auto& parent: parents) {
|
|
func(parent);
|
|
}
|
|
}
|
|
|
|
nsresult
|
|
DataStorage::Put(const nsCString& aKey, const nsCString& aValue,
|
|
DataStorageType aType)
|
|
{
|
|
WaitForReady();
|
|
MutexAutoLock lock(mMutex);
|
|
|
|
nsresult rv;
|
|
rv = ValidateKeyAndValue(aKey, aValue);
|
|
if (NS_FAILED(rv)) {
|
|
return rv;
|
|
}
|
|
|
|
Entry entry;
|
|
bool exists = GetInternal(aKey, &entry, aType, lock);
|
|
if (exists) {
|
|
entry.UpdateScore();
|
|
} else {
|
|
MaybeEvictOneEntry(aType, lock);
|
|
}
|
|
entry.mValue = aValue;
|
|
rv = PutInternal(aKey, entry, aType, lock);
|
|
if (NS_FAILED(rv)) {
|
|
return rv;
|
|
}
|
|
|
|
RunOnAllContentParents([&](dom::ContentParent* aParent) {
|
|
DataStorageItem item;
|
|
item.key() = aKey;
|
|
item.value() = aValue;
|
|
item.type() = aType;
|
|
Unused << aParent->SendDataStoragePut(mFilename, item);
|
|
});
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult
|
|
DataStorage::PutInternal(const nsCString& aKey, Entry& aEntry,
|
|
DataStorageType aType,
|
|
const MutexAutoLock& aProofOfLock)
|
|
{
|
|
DataStorageTable& table = GetTableForType(aType, aProofOfLock);
|
|
table.Put(aKey, aEntry);
|
|
|
|
if (aType == DataStorage_Persistent && !mPendingWrite) {
|
|
return AsyncSetTimer(aProofOfLock);
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
void
|
|
DataStorage::Remove(const nsCString& aKey, DataStorageType aType)
|
|
{
|
|
WaitForReady();
|
|
MutexAutoLock lock(mMutex);
|
|
|
|
DataStorageTable& table = GetTableForType(aType, lock);
|
|
table.Remove(aKey);
|
|
|
|
if (aType == DataStorage_Persistent && !mPendingWrite) {
|
|
Unused << AsyncSetTimer(lock);
|
|
}
|
|
|
|
RunOnAllContentParents([&](dom::ContentParent* aParent) {
|
|
Unused << aParent->SendDataStorageRemove(mFilename, aKey, aType);
|
|
});
|
|
}
|
|
|
|
class DataStorage::Writer : public Runnable
|
|
{
|
|
public:
|
|
Writer(nsCString& aData, DataStorage* aDataStorage)
|
|
: mData(aData)
|
|
, mDataStorage(aDataStorage)
|
|
{
|
|
}
|
|
|
|
private:
|
|
NS_DECL_NSIRUNNABLE
|
|
|
|
nsCString mData;
|
|
RefPtr<DataStorage> mDataStorage;
|
|
};
|
|
|
|
NS_IMETHODIMP
|
|
DataStorage::Writer::Run()
|
|
{
|
|
nsresult rv;
|
|
// Concurrent operations on nsIFile objects are not guaranteed to be safe,
|
|
// so we clone the file while holding the lock and then release the lock.
|
|
// At that point, we can safely operate on the clone.
|
|
nsCOMPtr<nsIFile> file;
|
|
{
|
|
MutexAutoLock lock(mDataStorage->mMutex);
|
|
// If we don't have a profile, bail.
|
|
if (!mDataStorage->mBackingFile) {
|
|
return NS_OK;
|
|
}
|
|
rv = mDataStorage->mBackingFile->Clone(getter_AddRefs(file));
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
}
|
|
|
|
nsCOMPtr<nsIOutputStream> outputStream;
|
|
rv = NS_NewLocalFileOutputStream(getter_AddRefs(outputStream), file,
|
|
PR_CREATE_FILE | PR_TRUNCATE | PR_WRONLY);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
|
|
const char* ptr = mData.get();
|
|
int32_t remaining = mData.Length();
|
|
uint32_t written = 0;
|
|
while (remaining > 0) {
|
|
rv = outputStream->Write(ptr, remaining, &written);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
remaining -= written;
|
|
ptr += written;
|
|
}
|
|
|
|
// Observed by tests.
|
|
nsCOMPtr<nsIRunnable> job =
|
|
NewRunnableMethod<const char*>(mDataStorage,
|
|
&DataStorage::NotifyObservers,
|
|
"data-storage-written");
|
|
rv = NS_DispatchToMainThread(job, NS_DISPATCH_NORMAL);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult
|
|
DataStorage::AsyncWriteData(const MutexAutoLock& /*aProofOfLock*/)
|
|
{
|
|
MOZ_ASSERT(XRE_IsParentProcess());
|
|
|
|
if (mShuttingDown || !mBackingFile) {
|
|
return NS_OK;
|
|
}
|
|
|
|
nsCString output;
|
|
for (auto iter = mPersistentDataTable.Iter(); !iter.Done(); iter.Next()) {
|
|
Entry entry = iter.UserData();
|
|
output.Append(iter.Key());
|
|
output.Append('\t');
|
|
output.AppendInt(entry.mScore);
|
|
output.Append('\t');
|
|
output.AppendInt(entry.mLastAccessed);
|
|
output.Append('\t');
|
|
output.Append(entry.mValue);
|
|
output.Append('\n');
|
|
}
|
|
|
|
RefPtr<Writer> job(new Writer(output, this));
|
|
nsresult rv = mWorkerThread->Dispatch(job, NS_DISPATCH_NORMAL);
|
|
mPendingWrite = false;
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult
|
|
DataStorage::Clear()
|
|
{
|
|
WaitForReady();
|
|
MutexAutoLock lock(mMutex);
|
|
mPersistentDataTable.Clear();
|
|
mTemporaryDataTable.Clear();
|
|
mPrivateDataTable.Clear();
|
|
|
|
if (XRE_IsParentProcess()) {
|
|
// Asynchronously clear the file. This is similar to the permission manager
|
|
// in that it doesn't wait to synchronously remove the data from its backing
|
|
// storage either.
|
|
nsresult rv = AsyncWriteData(lock);
|
|
if (NS_FAILED(rv)) {
|
|
return rv;
|
|
}
|
|
}
|
|
|
|
RunOnAllContentParents([&](dom::ContentParent* aParent) {
|
|
Unused << aParent->SendDataStorageClear(mFilename);
|
|
});
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
/* static */
|
|
void
|
|
DataStorage::TimerCallback(nsITimer* aTimer, void* aClosure)
|
|
{
|
|
MOZ_ASSERT(XRE_IsParentProcess());
|
|
|
|
RefPtr<DataStorage> aDataStorage = (DataStorage*)aClosure;
|
|
MutexAutoLock lock(aDataStorage->mMutex);
|
|
Unused << aDataStorage->AsyncWriteData(lock);
|
|
}
|
|
|
|
// We only initialize the timer on the worker thread because it's not safe
|
|
// to mix what threads are operating on the timer.
|
|
nsresult
|
|
DataStorage::AsyncSetTimer(const MutexAutoLock& /*aProofOfLock*/)
|
|
{
|
|
if (mShuttingDown || !XRE_IsParentProcess()) {
|
|
return NS_OK;
|
|
}
|
|
|
|
mPendingWrite = true;
|
|
nsCOMPtr<nsIRunnable> job =
|
|
NewRunnableMethod(this, &DataStorage::SetTimer);
|
|
nsresult rv = mWorkerThread->Dispatch(job, NS_DISPATCH_NORMAL);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
void
|
|
DataStorage::SetTimer()
|
|
{
|
|
MOZ_ASSERT(!NS_IsMainThread());
|
|
MOZ_ASSERT(XRE_IsParentProcess());
|
|
|
|
MutexAutoLock lock(mMutex);
|
|
|
|
nsresult rv;
|
|
if (!mTimer) {
|
|
mTimer = do_CreateInstance("@mozilla.org/timer;1", &rv);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
rv = mTimer->InitWithFuncCallback(TimerCallback, this,
|
|
mTimerDelay, nsITimer::TYPE_ONE_SHOT);
|
|
Unused << NS_WARN_IF(NS_FAILED(rv));
|
|
}
|
|
|
|
void
|
|
DataStorage::NotifyObservers(const char* aTopic)
|
|
{
|
|
// Don't access the observer service off the main thread.
|
|
if (!NS_IsMainThread()) {
|
|
NS_NOTREACHED("DataStorage::NotifyObservers called off main thread");
|
|
return;
|
|
}
|
|
|
|
nsCOMPtr<nsIObserverService> os = services::GetObserverService();
|
|
if (os) {
|
|
os->NotifyObservers(nullptr, aTopic, mFilename.get());
|
|
}
|
|
}
|
|
|
|
nsresult
|
|
DataStorage::DispatchShutdownTimer(const MutexAutoLock& /*aProofOfLock*/)
|
|
{
|
|
MOZ_ASSERT(XRE_IsParentProcess());
|
|
|
|
nsCOMPtr<nsIRunnable> job =
|
|
NewRunnableMethod(this, &DataStorage::ShutdownTimer);
|
|
nsresult rv = mWorkerThread->Dispatch(job, NS_DISPATCH_NORMAL);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
void
|
|
DataStorage::ShutdownTimer()
|
|
{
|
|
MOZ_ASSERT(XRE_IsParentProcess());
|
|
MOZ_ASSERT(!NS_IsMainThread());
|
|
MutexAutoLock lock(mMutex);
|
|
nsresult rv = mTimer->Cancel();
|
|
Unused << NS_WARN_IF(NS_FAILED(rv));
|
|
mTimer = nullptr;
|
|
}
|
|
|
|
//------------------------------------------------------------
|
|
// DataStorage::nsIObserver
|
|
//------------------------------------------------------------
|
|
|
|
NS_IMETHODIMP
|
|
DataStorage::Observe(nsISupports* aSubject, const char* aTopic,
|
|
const char16_t* aData)
|
|
{
|
|
// Don't access preferences off the main thread.
|
|
if (!NS_IsMainThread()) {
|
|
NS_NOTREACHED("DataStorage::Observe called off main thread");
|
|
return NS_ERROR_NOT_SAME_THREAD;
|
|
}
|
|
|
|
nsresult rv;
|
|
if (strcmp(aTopic, "last-pb-context-exited") == 0) {
|
|
MutexAutoLock lock(mMutex);
|
|
mPrivateDataTable.Clear();
|
|
} else if (strcmp(aTopic, "profile-before-change") == 0) {
|
|
MOZ_ASSERT(XRE_IsParentProcess());
|
|
{
|
|
MutexAutoLock lock(mMutex);
|
|
rv = AsyncWriteData(lock);
|
|
mShuttingDown = true;
|
|
Unused << NS_WARN_IF(NS_FAILED(rv));
|
|
if (mTimer) {
|
|
rv = DispatchShutdownTimer(lock);
|
|
Unused << NS_WARN_IF(NS_FAILED(rv));
|
|
}
|
|
}
|
|
// Run the thread to completion and prevent any further events
|
|
// being scheduled to it. The thread may need the lock, so we can't
|
|
// hold it here.
|
|
rv = mWorkerThread->Shutdown();
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
|
|
sDataStorages->Clear();
|
|
} else if (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) {
|
|
MOZ_ASSERT(!XRE_IsParentProcess());
|
|
sDataStorages->Clear();
|
|
} else if (strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID) == 0) {
|
|
MutexAutoLock lock(mMutex);
|
|
mTimerDelay = Preferences::GetInt("test.datastorage.write_timer_ms",
|
|
sDataStorageDefaultTimerDelay);
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
DataStorage::Entry::Entry()
|
|
: mScore(0)
|
|
, mLastAccessed((int32_t)(PR_Now() / sOneDayInMicroseconds))
|
|
{
|
|
}
|
|
|
|
// Updates this entry's score. Returns true if the score has actually changed.
|
|
// If it's been less than a day since this entry has been accessed, the score
|
|
// does not change. Otherwise, the score increases by 1.
|
|
// The default score is 0. The maximum score is the maximum value that can
|
|
// be represented by an unsigned 32 bit integer.
|
|
// This is to handle evictions from our tables, which in turn is to prevent
|
|
// unbounded resource use.
|
|
bool
|
|
DataStorage::Entry::UpdateScore()
|
|
{
|
|
|
|
int32_t nowInDays = (int32_t)(PR_Now() / sOneDayInMicroseconds);
|
|
int32_t daysSinceAccessed = (nowInDays - mLastAccessed);
|
|
|
|
// Update the last accessed time.
|
|
mLastAccessed = nowInDays;
|
|
|
|
// If it's been less than a day since we've been accessed,
|
|
// the score isn't updated.
|
|
if (daysSinceAccessed < 1) {
|
|
return false;
|
|
}
|
|
|
|
// Otherwise, increment the score (but don't overflow).
|
|
if (mScore < sMaxScore) {
|
|
mScore++;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
} // namespace mozilla
|