mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
bug 775370 - (part 1/2) introduce DataStorage r=froydnj r=mmc
This commit is contained in:
parent
746732f06e
commit
20f55eaa2e
793
security/manager/boot/src/DataStorage.cpp
Normal file
793
security/manager/boot/src/DataStorage.cpp
Normal file
@ -0,0 +1,793 @@
|
||||
/* -*- 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/Preferences.h"
|
||||
#include "mozilla/Services.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)
|
||||
|
||||
DataStorage::DataStorage(const nsString& aFilename)
|
||||
: mMutex("DataStorage::mMutex")
|
||||
, mPendingWrite(false)
|
||||
, mShuttingDown(false)
|
||||
, mReadyMonitor("DataStorage::mReadyMonitor")
|
||||
, mReady(false)
|
||||
, mFilename(aFilename)
|
||||
{
|
||||
}
|
||||
|
||||
DataStorage::~DataStorage()
|
||||
{
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
nsresult rv;
|
||||
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;
|
||||
}
|
||||
|
||||
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.
|
||||
os->AddObserver(this, "profile-before-change", 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 nsRunnable
|
||||
{
|
||||
public:
|
||||
explicit Reader(DataStorage* aDataStorage)
|
||||
: mDataStorage(aDataStorage)
|
||||
{
|
||||
}
|
||||
~Reader();
|
||||
|
||||
private:
|
||||
NS_DECL_NSIRUNNABLE
|
||||
|
||||
static nsresult ParseLine(nsDependentCSubstring& aLine, nsCString& aKeyOut,
|
||||
Entry& aEntryOut);
|
||||
|
||||
nsRefPtr<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 =
|
||||
NS_NewRunnableMethodWithArg<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);
|
||||
}
|
||||
|
||||
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*/)
|
||||
{
|
||||
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).
|
||||
nsRefPtr<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");
|
||||
}
|
||||
|
||||
// NB: The lock must be held when calling this function.
|
||||
/* static */
|
||||
PLDHashOperator
|
||||
DataStorage::EvictCallback(const nsACString& aKey, Entry aEntry, void* aArg)
|
||||
{
|
||||
KeyAndEntry* toEvict = (KeyAndEntry*)aArg;
|
||||
if (aEntry.mScore < toEvict->mEntry.mScore) {
|
||||
toEvict->mKey = aKey;
|
||||
toEvict->mEntry = aEntry;
|
||||
}
|
||||
return PLDHashOperator::PL_DHASH_NEXT;
|
||||
}
|
||||
|
||||
// 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;
|
||||
table.EnumerateRead(EvictCallback, (void*)&toEvict);
|
||||
table.Remove(toEvict.mKey);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
class DataStorage::Writer : public nsRunnable
|
||||
{
|
||||
public:
|
||||
Writer(nsCString& aData, DataStorage* aDataStorage)
|
||||
: mData(aData)
|
||||
, mDataStorage(aDataStorage)
|
||||
{
|
||||
}
|
||||
|
||||
private:
|
||||
NS_DECL_NSIRUNNABLE
|
||||
|
||||
nsCString mData;
|
||||
nsRefPtr<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 =
|
||||
NS_NewRunnableMethodWithArg<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;
|
||||
}
|
||||
|
||||
// NB: The lock must be held when calling this function.
|
||||
/* static */
|
||||
PLDHashOperator
|
||||
DataStorage::WriteDataCallback(const nsACString& aKey, Entry aEntry, void* aArg)
|
||||
{
|
||||
nsCString* output = (nsCString*)aArg;
|
||||
output->Append(aKey);
|
||||
output->Append('\t');
|
||||
output->AppendInt(aEntry.mScore);
|
||||
output->Append('\t');
|
||||
output->AppendInt(aEntry.mLastAccessed);
|
||||
output->Append('\t');
|
||||
output->Append(aEntry.mValue);
|
||||
output->Append('\n');
|
||||
return PLDHashOperator::PL_DHASH_NEXT;
|
||||
}
|
||||
|
||||
nsresult
|
||||
DataStorage::AsyncWriteData(const MutexAutoLock& /*aProofOfLock*/)
|
||||
{
|
||||
if (mShuttingDown || !mBackingFile) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsCString output;
|
||||
mPersistentDataTable.EnumerateRead(WriteDataCallback, (void*)&output);
|
||||
|
||||
nsRefPtr<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();
|
||||
|
||||
// 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;
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
/* static */
|
||||
void
|
||||
DataStorage::TimerCallback(nsITimer* aTimer, void* aClosure)
|
||||
{
|
||||
nsRefPtr<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) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
mPendingWrite = true;
|
||||
nsCOMPtr<nsIRunnable> job =
|
||||
NS_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());
|
||||
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*/)
|
||||
{
|
||||
nsCOMPtr<nsIRunnable> job =
|
||||
NS_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(!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) {
|
||||
{
|
||||
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;
|
||||
}
|
||||
} 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
|
185
security/manager/boot/src/DataStorage.h
Normal file
185
security/manager/boot/src/DataStorage.h
Normal file
@ -0,0 +1,185 @@
|
||||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#ifndef mozilla_DataStorage_h
|
||||
#define mozilla_DataStorage_h
|
||||
|
||||
#include "mozilla/Monitor.h"
|
||||
#include "mozilla/Mutex.h"
|
||||
#include "nsCOMPtr.h"
|
||||
#include "nsDataHashtable.h"
|
||||
#include "nsIObserver.h"
|
||||
#include "nsIThread.h"
|
||||
#include "nsITimer.h"
|
||||
#include "nsString.h"
|
||||
|
||||
namespace mozilla {
|
||||
|
||||
/**
|
||||
* DataStorage is a threadsafe, generic, narrow string-based hash map that
|
||||
* persists data on disk and additionally handles temporary and private data.
|
||||
* However, if used in a context where there is no profile directory, data
|
||||
* will not be persisted.
|
||||
*
|
||||
* Its lifecycle is as follows:
|
||||
* - Allocate with a filename (this is or will eventually be a file in the
|
||||
* profile directory, if the profile exists).
|
||||
* - Call Init() from the main thread. This spins off an asynchronous read
|
||||
* of the backing file.
|
||||
* - Eventually observers of the topic "data-storage-ready" will be notified
|
||||
* with the backing filename as the data in the notification when this
|
||||
* has completed.
|
||||
* - Should the profile directory not be available, (e.g. in xpcshell),
|
||||
* DataStorage will not initially read any persistent data. The
|
||||
* "data-storage-ready" event will still be emitted. This follows semantics
|
||||
* similar to the permission manager and allows tests that test
|
||||
* unrelated components to proceed without a profile.
|
||||
* - When any persistent data changes, a timer is initialized that will
|
||||
* eventually asynchronously write all persistent data to the backing file.
|
||||
* When this happens, observers will be notified with the topic
|
||||
* "data-storage-written" and the backing filename as the data.
|
||||
* It is possible to receive a "data-storage-written" event while there exist
|
||||
* pending persistent data changes. However, those changes will cause the
|
||||
* timer to be reinitialized and another "data-storage-written" event will
|
||||
* be sent.
|
||||
* - When DataStorage observes the topic "profile-before-change" in
|
||||
* anticipation of shutdown, all persistent data is synchronously written to
|
||||
* the backing file. The worker thread responsible for these writes is then
|
||||
* disabled to prevent further writes to that file (the delayed-write timer
|
||||
* is cancelled when this happens).
|
||||
* - For testing purposes, the preference "test.datastorage.write_timer_ms" can
|
||||
* be set to cause the asynchronous writing of data to happen more quickly.
|
||||
* - To prevent unbounded memory and disk use, the number of entries in each
|
||||
* table is limited to 1024. Evictions are handled in by a modified LRU scheme
|
||||
* (see implementation comments).
|
||||
* - NB: Instances of DataStorage have long lifetimes because they are strong
|
||||
* observers of events and won't go away until the observer service does.
|
||||
*
|
||||
* For each key/value:
|
||||
* - The key must be a non-empty string containing no instances of '\t' or '\n'
|
||||
* (this is a limitation of how the data is stored and will be addressed in
|
||||
* the future).
|
||||
* - The key 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 disk and memory usage)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Data that is DataStorage_Persistent is saved on disk. DataStorage_Temporary
|
||||
* and DataStorage_Private are not saved. DataStorage_Private is meant to
|
||||
* only be set and accessed from private contexts. It will be cleared upon
|
||||
* observing the event "last-pb-context-exited".
|
||||
*/
|
||||
enum DataStorageType {
|
||||
DataStorage_Persistent,
|
||||
DataStorage_Temporary,
|
||||
DataStorage_Private
|
||||
};
|
||||
|
||||
class DataStorage : public nsIObserver
|
||||
{
|
||||
public:
|
||||
NS_DECL_THREADSAFE_ISUPPORTS
|
||||
NS_DECL_NSIOBSERVER
|
||||
|
||||
// If there is a profile directory, there is or will eventually be a file
|
||||
// by the name specified by aFilename there.
|
||||
explicit DataStorage(const nsString& aFilename);
|
||||
|
||||
// Initializes the DataStorage. Must be called before using.
|
||||
// aDataWillPersist returns whether or not data can be persistently saved.
|
||||
nsresult Init(/*out*/bool& aDataWillPersist);
|
||||
// Given a key and a type of data, returns a value. Returns an empty string if
|
||||
// the key is not present for that type of data. If Get is called before the
|
||||
// "data-storage-ready" event is observed, it will block. NB: It is not
|
||||
// currently possible to differentiate between missing data and data that is
|
||||
// the empty string.
|
||||
nsCString Get(const nsCString& aKey, DataStorageType aType);
|
||||
// Give a key, value, and type of data, adds an entry as appropriate.
|
||||
// Updates existing entries.
|
||||
nsresult Put(const nsCString& aKey, const nsCString& aValue,
|
||||
DataStorageType aType);
|
||||
// Given a key and type of data, removes an entry if present.
|
||||
void Remove(const nsCString& aKey, DataStorageType aType);
|
||||
// Removes all entries of all types of data.
|
||||
nsresult Clear();
|
||||
|
||||
private:
|
||||
virtual ~DataStorage();
|
||||
|
||||
class Writer;
|
||||
class Reader;
|
||||
|
||||
class Entry
|
||||
{
|
||||
public:
|
||||
Entry();
|
||||
bool UpdateScore();
|
||||
|
||||
uint32_t mScore;
|
||||
int32_t mLastAccessed; // the last accessed time in days since the epoch
|
||||
nsCString mValue;
|
||||
};
|
||||
|
||||
// Utility class for scanning tables for an entry to evict.
|
||||
class KeyAndEntry
|
||||
{
|
||||
public:
|
||||
nsCString mKey;
|
||||
Entry mEntry;
|
||||
};
|
||||
|
||||
typedef nsDataHashtable<nsCStringHashKey, Entry> DataStorageTable;
|
||||
|
||||
void WaitForReady();
|
||||
nsresult AsyncWriteData(const MutexAutoLock& aProofOfLock);
|
||||
nsresult AsyncReadData(bool& aHaveProfileDir,
|
||||
const MutexAutoLock& aProofOfLock);
|
||||
nsresult AsyncSetTimer(const MutexAutoLock& aProofOfLock);
|
||||
nsresult DispatchShutdownTimer(const MutexAutoLock& aProofOfLock);
|
||||
|
||||
static nsresult ValidateKeyAndValue(const nsCString& aKey,
|
||||
const nsCString& aValue);
|
||||
static void TimerCallback(nsITimer* aTimer, void* aClosure);
|
||||
static PLDHashOperator WriteDataCallback(const nsACString& aKey, Entry aEntry,
|
||||
void* aArg);
|
||||
static PLDHashOperator EvictCallback(const nsACString& aKey, Entry aEntry,
|
||||
void* aArg);
|
||||
void SetTimer();
|
||||
void ShutdownTimer();
|
||||
void NotifyObservers(const char* aTopic);
|
||||
|
||||
bool GetInternal(const nsCString& aKey, Entry* aEntry, DataStorageType aType,
|
||||
const MutexAutoLock& aProofOfLock);
|
||||
nsresult PutInternal(const nsCString& aKey, Entry& aEntry,
|
||||
DataStorageType aType,
|
||||
const MutexAutoLock& aProofOfLock);
|
||||
void MaybeEvictOneEntry(DataStorageType aType,
|
||||
const MutexAutoLock& aProofOfLock);
|
||||
DataStorageTable& GetTableForType(DataStorageType aType,
|
||||
const MutexAutoLock& aProofOfLock);
|
||||
|
||||
Mutex mMutex; // This mutex protects access to the following members:
|
||||
DataStorageTable mPersistentDataTable;
|
||||
DataStorageTable mTemporaryDataTable;
|
||||
DataStorageTable mPrivateDataTable;
|
||||
nsCOMPtr<nsIThread> mWorkerThread;
|
||||
nsCOMPtr<nsIFile> mBackingFile;
|
||||
nsCOMPtr<nsITimer> mTimer; // All uses after init must be on the worker thread
|
||||
uint32_t mTimerDelay; // in milliseconds
|
||||
bool mPendingWrite; // true if a write is needed but hasn't been dispatched
|
||||
bool mShuttingDown;
|
||||
// (End list of members protected by mMutex)
|
||||
|
||||
Monitor mReadyMonitor; // Do not acquire this at the same time as mMutex.
|
||||
bool mReady; // Indicates that saved data has been read and Get can proceed.
|
||||
|
||||
const nsString mFilename;
|
||||
};
|
||||
|
||||
}; // namespace mozilla
|
||||
|
||||
#endif // mozilla_DataStorage_h
|
@ -4,7 +4,12 @@
|
||||
# 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/.
|
||||
|
||||
EXPORTS.mozilla += [
|
||||
'DataStorage.h',
|
||||
]
|
||||
|
||||
UNIFIED_SOURCES += [
|
||||
'DataStorage.cpp',
|
||||
'nsBOOTModule.cpp',
|
||||
'nsEntropyCollector.cpp',
|
||||
'nsSecurityHeaderParser.cpp',
|
||||
|
224
security/manager/ssl/tests/gtest/DataStorageTest.cpp
Normal file
224
security/manager/ssl/tests/gtest/DataStorageTest.cpp
Normal file
@ -0,0 +1,224 @@
|
||||
/* -*- 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/DataStorage.h"
|
||||
#include "nsAppDirectoryServiceDefs.h"
|
||||
#include "nsDirectoryServiceUtils.h"
|
||||
#include "nsNetUtil.h"
|
||||
#include "nsPrintfCString.h"
|
||||
#include "nsStreamUtils.h"
|
||||
|
||||
using namespace mozilla;
|
||||
|
||||
class DataStorageTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
virtual void SetUp()
|
||||
{
|
||||
const ::testing::TestInfo* const testInfo =
|
||||
::testing::UnitTest::GetInstance()->current_test_info();
|
||||
NS_ConvertUTF8toUTF16 testName(testInfo->name());
|
||||
storage = new DataStorage(testName);
|
||||
storage->Init(dataWillPersist);
|
||||
}
|
||||
|
||||
nsRefPtr<DataStorage> storage;
|
||||
bool dataWillPersist;
|
||||
};
|
||||
|
||||
NS_NAMED_LITERAL_CSTRING(testKey, "test");
|
||||
NS_NAMED_LITERAL_CSTRING(testValue, "value");
|
||||
NS_NAMED_LITERAL_CSTRING(privateTestValue, "private");
|
||||
|
||||
TEST_F(DataStorageTest, GetPutRemove)
|
||||
{
|
||||
EXPECT_TRUE(dataWillPersist);
|
||||
|
||||
// Test Put/Get on Persistent data
|
||||
EXPECT_EQ(NS_OK, storage->Put(testKey, testValue, DataStorage_Persistent));
|
||||
// Don't re-use testKey / testValue here, to make sure that this works as
|
||||
// expected with objects that have the same semantic value but are not
|
||||
// literally the same object.
|
||||
nsCString result = storage->Get(NS_LITERAL_CSTRING("test"),
|
||||
DataStorage_Persistent);
|
||||
EXPECT_STREQ("value", result.get());
|
||||
|
||||
// Get on Temporary/Private data with the same key should give nothing
|
||||
result = storage->Get(testKey, DataStorage_Temporary);
|
||||
EXPECT_TRUE(result.IsEmpty());
|
||||
result = storage->Get(testKey, DataStorage_Private);
|
||||
EXPECT_TRUE(result.IsEmpty());
|
||||
|
||||
// Put with Temporary/Private data shouldn't affect Persistent data
|
||||
NS_NAMED_LITERAL_CSTRING(temporaryTestValue, "temporary");
|
||||
EXPECT_EQ(NS_OK, storage->Put(testKey, temporaryTestValue,
|
||||
DataStorage_Temporary));
|
||||
EXPECT_EQ(NS_OK, storage->Put(testKey, privateTestValue,
|
||||
DataStorage_Private));
|
||||
result = storage->Get(testKey, DataStorage_Temporary);
|
||||
EXPECT_STREQ("temporary", result.get());
|
||||
result = storage->Get(testKey, DataStorage_Private);
|
||||
EXPECT_STREQ("private", result.get());
|
||||
result = storage->Get(testKey, DataStorage_Persistent);
|
||||
EXPECT_STREQ("value", result.get());
|
||||
|
||||
// Put of a previously-present key overwrites it (if of the same type)
|
||||
NS_NAMED_LITERAL_CSTRING(newValue, "new");
|
||||
EXPECT_EQ(NS_OK, storage->Put(testKey, newValue, DataStorage_Persistent));
|
||||
result = storage->Get(testKey, DataStorage_Persistent);
|
||||
EXPECT_STREQ("new", result.get());
|
||||
|
||||
// Removal should work
|
||||
storage->Remove(testKey, DataStorage_Temporary);
|
||||
result = storage->Get(testKey, DataStorage_Temporary);
|
||||
EXPECT_TRUE(result.IsEmpty());
|
||||
// But removing one type shouldn't affect the others
|
||||
result = storage->Get(testKey, DataStorage_Private);
|
||||
EXPECT_STREQ("private", result.get());
|
||||
result = storage->Get(testKey, DataStorage_Persistent);
|
||||
EXPECT_STREQ("new", result.get());
|
||||
// Test removing the other types as well
|
||||
storage->Remove(testKey, DataStorage_Private);
|
||||
result = storage->Get(testKey, DataStorage_Private);
|
||||
EXPECT_TRUE(result.IsEmpty());
|
||||
storage->Remove(testKey, DataStorage_Persistent);
|
||||
result = storage->Get(testKey, DataStorage_Persistent);
|
||||
EXPECT_TRUE(result.IsEmpty());
|
||||
}
|
||||
|
||||
TEST_F(DataStorageTest, InputValidation)
|
||||
{
|
||||
EXPECT_TRUE(dataWillPersist);
|
||||
|
||||
// Keys may not have tabs or newlines
|
||||
EXPECT_EQ(NS_ERROR_INVALID_ARG,
|
||||
storage->Put(NS_LITERAL_CSTRING("key\thas tab"), testValue,
|
||||
DataStorage_Persistent));
|
||||
nsCString result = storage->Get(NS_LITERAL_CSTRING("key\thas tab"),
|
||||
DataStorage_Persistent);
|
||||
EXPECT_TRUE(result.IsEmpty());
|
||||
EXPECT_EQ(NS_ERROR_INVALID_ARG,
|
||||
storage->Put(NS_LITERAL_CSTRING("key has\nnewline"), testValue,
|
||||
DataStorage_Persistent));
|
||||
result = storage->Get(NS_LITERAL_CSTRING("keyhas\nnewline"),
|
||||
DataStorage_Persistent);
|
||||
EXPECT_TRUE(result.IsEmpty());
|
||||
// Values may not have newlines
|
||||
EXPECT_EQ(NS_ERROR_INVALID_ARG,
|
||||
storage->Put(testKey, NS_LITERAL_CSTRING("value\nhas newline"),
|
||||
DataStorage_Persistent));
|
||||
result = storage->Get(testKey, DataStorage_Persistent);
|
||||
// Values may have tabs
|
||||
EXPECT_TRUE(result.IsEmpty());
|
||||
EXPECT_EQ(NS_OK, storage->Put(testKey,
|
||||
NS_LITERAL_CSTRING("val\thas tab; this is ok"),
|
||||
DataStorage_Persistent));
|
||||
result = storage->Get(testKey, DataStorage_Persistent);
|
||||
EXPECT_STREQ("val\thas tab; this is ok", result.get());
|
||||
|
||||
nsCString longKey("a");
|
||||
for (int i = 0; i < 8; i++) {
|
||||
longKey.Append(longKey);
|
||||
}
|
||||
// A key of length 256 will work
|
||||
EXPECT_EQ(NS_OK, storage->Put(longKey, testValue, DataStorage_Persistent));
|
||||
result = storage->Get(longKey, DataStorage_Persistent);
|
||||
EXPECT_STREQ("value", result.get());
|
||||
longKey.Append("a");
|
||||
// A key longer than that will not work
|
||||
EXPECT_EQ(NS_ERROR_INVALID_ARG,
|
||||
storage->Put(longKey, testValue, DataStorage_Persistent));
|
||||
result = storage->Get(longKey, DataStorage_Persistent);
|
||||
EXPECT_TRUE(result.IsEmpty());
|
||||
|
||||
nsCString longValue("a");
|
||||
for (int i = 0; i < 10; i++) {
|
||||
longValue.Append(longValue);
|
||||
}
|
||||
// A value of length 1024 will work
|
||||
EXPECT_EQ(NS_OK, storage->Put(testKey, longValue, DataStorage_Persistent));
|
||||
result = storage->Get(testKey, DataStorage_Persistent);
|
||||
EXPECT_STREQ(longValue.get(), result.get());
|
||||
longValue.Append("a");
|
||||
// A value longer than that will not work
|
||||
storage->Remove(testKey, DataStorage_Persistent);
|
||||
EXPECT_EQ(NS_ERROR_INVALID_ARG,
|
||||
storage->Put(testKey, longValue, DataStorage_Persistent));
|
||||
result = storage->Get(testKey, DataStorage_Persistent);
|
||||
EXPECT_TRUE(result.IsEmpty());
|
||||
}
|
||||
|
||||
TEST_F(DataStorageTest, Eviction)
|
||||
{
|
||||
EXPECT_TRUE(dataWillPersist);
|
||||
|
||||
// Eviction is on a per-table basis. Tables shouldn't affect each other.
|
||||
EXPECT_EQ(NS_OK, storage->Put(testKey, testValue, DataStorage_Persistent));
|
||||
for (int i = 0; i < 1025; i++) {
|
||||
EXPECT_EQ(NS_OK, storage->Put(nsPrintfCString("%d", i),
|
||||
nsPrintfCString("%d", i),
|
||||
DataStorage_Temporary));
|
||||
nsCString result = storage->Get(nsPrintfCString("%d", i),
|
||||
DataStorage_Temporary);
|
||||
EXPECT_STREQ(nsPrintfCString("%d", i).get(), result.get());
|
||||
}
|
||||
// We don't know which entry got evicted, but we can count them.
|
||||
int entries = 0;
|
||||
for (int i = 0; i < 1025; i++) {
|
||||
nsCString result = storage->Get(nsPrintfCString("%d", i),
|
||||
DataStorage_Temporary);
|
||||
if (!result.IsEmpty()) {
|
||||
entries++;
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(entries, 1024);
|
||||
nsCString result = storage->Get(testKey, DataStorage_Persistent);
|
||||
EXPECT_STREQ("value", result.get());
|
||||
}
|
||||
|
||||
TEST_F(DataStorageTest, ClearPrivateData)
|
||||
{
|
||||
EXPECT_TRUE(dataWillPersist);
|
||||
|
||||
EXPECT_EQ(NS_OK, storage->Put(testKey, privateTestValue,
|
||||
DataStorage_Private));
|
||||
nsCString result = storage->Get(testKey, DataStorage_Private);
|
||||
EXPECT_STREQ("private", result.get());
|
||||
storage->Observe(nullptr, "last-pb-context-exited", nullptr);
|
||||
result = storage->Get(testKey, DataStorage_Private);
|
||||
EXPECT_TRUE(result.IsEmpty());
|
||||
}
|
||||
|
||||
TEST_F(DataStorageTest, Shutdown)
|
||||
{
|
||||
EXPECT_TRUE(dataWillPersist);
|
||||
|
||||
EXPECT_EQ(NS_OK, storage->Put(testKey, testValue, DataStorage_Persistent));
|
||||
nsCString result = storage->Get(testKey, DataStorage_Persistent);
|
||||
EXPECT_STREQ("value", result.get());
|
||||
// Get "now" (in days) close to when the data was last touched, so we won't
|
||||
// get intermittent failures with the day not matching.
|
||||
int64_t microsecondsPerDay = 24 * 60 * 60 * int64_t(PR_USEC_PER_SEC);
|
||||
int32_t nowInDays = int32_t(PR_Now() / microsecondsPerDay);
|
||||
storage->Observe(nullptr, "profile-before-change", nullptr);
|
||||
nsCOMPtr<nsIFile> backingFile;
|
||||
EXPECT_EQ(NS_OK, NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
|
||||
getter_AddRefs(backingFile)));
|
||||
const ::testing::TestInfo* const testInfo =
|
||||
::testing::UnitTest::GetInstance()->current_test_info();
|
||||
NS_ConvertUTF8toUTF16 testName(testInfo->name());
|
||||
EXPECT_EQ(NS_OK, backingFile->Append(testName));
|
||||
nsCOMPtr<nsIInputStream> fileInputStream;
|
||||
EXPECT_EQ(NS_OK, NS_NewLocalFileInputStream(getter_AddRefs(fileInputStream),
|
||||
backingFile));
|
||||
nsCString data;
|
||||
EXPECT_EQ(NS_OK, NS_ConsumeStream(fileInputStream, UINT32_MAX, data));
|
||||
// The data will be of the form 'test\t0\t<days since the epoch>\tvalue'
|
||||
EXPECT_STREQ(nsPrintfCString("test\t0\t%d\tvalue\n", nowInDays).get(),
|
||||
data.get());
|
||||
}
|
@ -5,6 +5,7 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
SOURCES += [
|
||||
'DataStorageTest.cpp',
|
||||
'OCSPCacheTest.cpp',
|
||||
'TLSIntoleranceTest.cpp',
|
||||
]
|
||||
|
Loading…
Reference in New Issue
Block a user