summaryrefslogtreecommitdiffstats
path: root/netwerk/cache/nsDiskCacheDevice.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'netwerk/cache/nsDiskCacheDevice.cpp')
-rw-r--r--netwerk/cache/nsDiskCacheDevice.cpp1149
1 files changed, 1149 insertions, 0 deletions
diff --git a/netwerk/cache/nsDiskCacheDevice.cpp b/netwerk/cache/nsDiskCacheDevice.cpp
new file mode 100644
index 000000000..ac91534ff
--- /dev/null
+++ b/netwerk/cache/nsDiskCacheDevice.cpp
@@ -0,0 +1,1149 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * 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 <limits.h>
+
+#include "mozilla/DebugOnly.h"
+
+#include "nsCache.h"
+#include "nsIMemoryReporter.h"
+
+// include files for ftruncate (or equivalent)
+#if defined(XP_UNIX)
+#include <unistd.h>
+#elif defined(XP_WIN)
+#include <windows.h>
+#else
+// XXX add necessary include file for ftruncate (or equivalent)
+#endif
+
+#include "prthread.h"
+
+#include "private/pprio.h"
+
+#include "nsDiskCacheDevice.h"
+#include "nsDiskCacheEntry.h"
+#include "nsDiskCacheMap.h"
+#include "nsDiskCacheStreams.h"
+
+#include "nsDiskCache.h"
+
+#include "nsCacheService.h"
+
+#include "nsDeleteDir.h"
+
+#include "nsICacheVisitor.h"
+#include "nsReadableUtils.h"
+#include "nsIInputStream.h"
+#include "nsIOutputStream.h"
+#include "nsCRT.h"
+#include "nsCOMArray.h"
+#include "nsISimpleEnumerator.h"
+
+#include "nsThreadUtils.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/Telemetry.h"
+
+static const char DISK_CACHE_DEVICE_ID[] = { "disk" };
+using namespace mozilla;
+
+class nsDiskCacheDeviceDeactivateEntryEvent : public Runnable {
+public:
+ nsDiskCacheDeviceDeactivateEntryEvent(nsDiskCacheDevice *device,
+ nsCacheEntry * entry,
+ nsDiskCacheBinding * binding)
+ : mCanceled(false),
+ mEntry(entry),
+ mDevice(device),
+ mBinding(binding)
+ {
+ }
+
+ NS_IMETHOD Run() override
+ {
+ nsCacheServiceAutoLock lock;
+ CACHE_LOG_DEBUG(("nsDiskCacheDeviceDeactivateEntryEvent[%p]\n", this));
+ if (!mCanceled) {
+ (void) mDevice->DeactivateEntry_Private(mEntry, mBinding);
+ }
+ return NS_OK;
+ }
+
+ void CancelEvent() { mCanceled = true; }
+private:
+ bool mCanceled;
+ nsCacheEntry *mEntry;
+ nsDiskCacheDevice *mDevice;
+ nsDiskCacheBinding *mBinding;
+};
+
+class nsEvictDiskCacheEntriesEvent : public Runnable {
+public:
+ explicit nsEvictDiskCacheEntriesEvent(nsDiskCacheDevice *device)
+ : mDevice(device) {}
+
+ NS_IMETHOD Run() override
+ {
+ nsCacheServiceAutoLock lock;
+ mDevice->EvictDiskCacheEntries(mDevice->mCacheCapacity);
+ return NS_OK;
+ }
+
+private:
+ nsDiskCacheDevice *mDevice;
+};
+
+/******************************************************************************
+ * nsDiskCacheEvictor
+ *
+ * Helper class for nsDiskCacheDevice.
+ *
+ *****************************************************************************/
+
+class nsDiskCacheEvictor : public nsDiskCacheRecordVisitor
+{
+public:
+ nsDiskCacheEvictor( nsDiskCacheMap * cacheMap,
+ nsDiskCacheBindery * cacheBindery,
+ uint32_t targetSize,
+ const char * clientID)
+ : mCacheMap(cacheMap)
+ , mBindery(cacheBindery)
+ , mTargetSize(targetSize)
+ , mClientID(clientID)
+ {
+ mClientIDSize = clientID ? strlen(clientID) : 0;
+ }
+
+ virtual int32_t VisitRecord(nsDiskCacheRecord * mapRecord);
+
+private:
+ nsDiskCacheMap * mCacheMap;
+ nsDiskCacheBindery * mBindery;
+ uint32_t mTargetSize;
+ const char * mClientID;
+ uint32_t mClientIDSize;
+};
+
+
+int32_t
+nsDiskCacheEvictor::VisitRecord(nsDiskCacheRecord * mapRecord)
+{
+ if (mCacheMap->TotalSize() < mTargetSize)
+ return kStopVisitingRecords;
+
+ if (mClientID) {
+ // we're just evicting records for a specific client
+ nsDiskCacheEntry * diskEntry = mCacheMap->ReadDiskCacheEntry(mapRecord);
+ if (!diskEntry)
+ return kVisitNextRecord; // XXX or delete record?
+
+ // Compare clientID's without malloc
+ if ((diskEntry->mKeySize <= mClientIDSize) ||
+ (diskEntry->Key()[mClientIDSize] != ':') ||
+ (memcmp(diskEntry->Key(), mClientID, mClientIDSize) != 0)) {
+ return kVisitNextRecord; // clientID doesn't match, skip it
+ }
+ }
+
+ nsDiskCacheBinding * binding = mBindery->FindActiveBinding(mapRecord->HashNumber());
+ if (binding) {
+ // If the entry is pending deactivation, cancel deactivation and doom
+ // the entry
+ if (binding->mDeactivateEvent) {
+ binding->mDeactivateEvent->CancelEvent();
+ binding->mDeactivateEvent = nullptr;
+ }
+ // We are currently using this entry, so all we can do is doom it.
+ // Since we're enumerating the records, we don't want to call
+ // DeleteRecord when nsCacheService::DoomEntry() calls us back.
+ binding->mDoomed = true; // mark binding record as 'deleted'
+ nsCacheService::DoomEntry(binding->mCacheEntry);
+ } else {
+ // entry not in use, just delete storage because we're enumerating the records
+ (void) mCacheMap->DeleteStorage(mapRecord);
+ }
+
+ return kDeleteRecordAndContinue; // this will REALLY delete the record
+}
+
+
+/******************************************************************************
+ * nsDiskCacheDeviceInfo
+ *****************************************************************************/
+
+class nsDiskCacheDeviceInfo : public nsICacheDeviceInfo {
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSICACHEDEVICEINFO
+
+ explicit nsDiskCacheDeviceInfo(nsDiskCacheDevice* device)
+ : mDevice(device)
+ {
+ }
+
+private:
+ virtual ~nsDiskCacheDeviceInfo() {}
+
+ nsDiskCacheDevice* mDevice;
+};
+
+NS_IMPL_ISUPPORTS(nsDiskCacheDeviceInfo, nsICacheDeviceInfo)
+
+NS_IMETHODIMP nsDiskCacheDeviceInfo::GetDescription(char ** aDescription)
+{
+ NS_ENSURE_ARG_POINTER(aDescription);
+ *aDescription = NS_strdup("Disk cache device");
+ return *aDescription ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
+}
+
+NS_IMETHODIMP nsDiskCacheDeviceInfo::GetUsageReport(char ** usageReport)
+{
+ NS_ENSURE_ARG_POINTER(usageReport);
+ nsCString buffer;
+
+ buffer.AssignLiteral(" <tr>\n"
+ " <th>Cache Directory:</th>\n"
+ " <td>");
+ nsCOMPtr<nsIFile> cacheDir;
+ nsAutoString path;
+ mDevice->getCacheDirectory(getter_AddRefs(cacheDir));
+ nsresult rv = cacheDir->GetPath(path);
+ if (NS_SUCCEEDED(rv)) {
+ AppendUTF16toUTF8(path, buffer);
+ } else {
+ buffer.AppendLiteral("directory unavailable");
+ }
+ buffer.AppendLiteral("</td>\n"
+ " </tr>\n");
+
+ *usageReport = ToNewCString(buffer);
+ if (!*usageReport) return NS_ERROR_OUT_OF_MEMORY;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDiskCacheDeviceInfo::GetEntryCount(uint32_t *aEntryCount)
+{
+ NS_ENSURE_ARG_POINTER(aEntryCount);
+ *aEntryCount = mDevice->getEntryCount();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDiskCacheDeviceInfo::GetTotalSize(uint32_t *aTotalSize)
+{
+ NS_ENSURE_ARG_POINTER(aTotalSize);
+ // Returned unit's are in bytes
+ *aTotalSize = mDevice->getCacheSize() * 1024;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsDiskCacheDeviceInfo::GetMaximumSize(uint32_t *aMaximumSize)
+{
+ NS_ENSURE_ARG_POINTER(aMaximumSize);
+ // Returned unit's are in bytes
+ *aMaximumSize = mDevice->getCacheCapacity() * 1024;
+ return NS_OK;
+}
+
+
+/******************************************************************************
+ * nsDiskCache
+ *****************************************************************************/
+
+/**
+ * nsDiskCache::Hash(const char * key, PLDHashNumber initval)
+ *
+ * See http://burtleburtle.net/bob/hash/evahash.html for more information
+ * about this hash function.
+ *
+ * This algorithm of this method implies nsDiskCacheRecords will be stored
+ * in a certain order on disk. If the algorithm changes, existing cache
+ * map files may become invalid, and therefore the kCurrentVersion needs
+ * to be revised.
+ */
+
+static inline void hashmix(uint32_t& a, uint32_t& b, uint32_t& c)
+{
+ a -= b; a -= c; a ^= (c>>13);
+ b -= c; b -= a; b ^= (a<<8);
+ c -= a; c -= b; c ^= (b>>13);
+ a -= b; a -= c; a ^= (c>>12);
+ b -= c; b -= a; b ^= (a<<16);
+ c -= a; c -= b; c ^= (b>>5);
+ a -= b; a -= c; a ^= (c>>3);
+ b -= c; b -= a; b ^= (a<<10);
+ c -= a; c -= b; c ^= (b>>15);
+}
+
+PLDHashNumber
+nsDiskCache::Hash(const char * key, PLDHashNumber initval)
+{
+ const uint8_t *k = reinterpret_cast<const uint8_t*>(key);
+ uint32_t a, b, c, len, length;
+
+ length = strlen(key);
+ /* Set up the internal state */
+ len = length;
+ a = b = 0x9e3779b9; /* the golden ratio; an arbitrary value */
+ c = initval; /* variable initialization of internal state */
+
+ /*---------------------------------------- handle most of the key */
+ while (len >= 12)
+ {
+ a += k[0] + (uint32_t(k[1])<<8) + (uint32_t(k[2])<<16) + (uint32_t(k[3])<<24);
+ b += k[4] + (uint32_t(k[5])<<8) + (uint32_t(k[6])<<16) + (uint32_t(k[7])<<24);
+ c += k[8] + (uint32_t(k[9])<<8) + (uint32_t(k[10])<<16) + (uint32_t(k[11])<<24);
+ hashmix(a, b, c);
+ k += 12; len -= 12;
+ }
+
+ /*------------------------------------- handle the last 11 bytes */
+ c += length;
+ switch(len) { /* all the case statements fall through */
+ case 11: c += (uint32_t(k[10])<<24); MOZ_FALLTHROUGH;
+ case 10: c += (uint32_t(k[9])<<16); MOZ_FALLTHROUGH;
+ case 9 : c += (uint32_t(k[8])<<8); MOZ_FALLTHROUGH;
+ /* the low-order byte of c is reserved for the length */
+ case 8 : b += (uint32_t(k[7])<<24); MOZ_FALLTHROUGH;
+ case 7 : b += (uint32_t(k[6])<<16); MOZ_FALLTHROUGH;
+ case 6 : b += (uint32_t(k[5])<<8); MOZ_FALLTHROUGH;
+ case 5 : b += k[4]; MOZ_FALLTHROUGH;
+ case 4 : a += (uint32_t(k[3])<<24); MOZ_FALLTHROUGH;
+ case 3 : a += (uint32_t(k[2])<<16); MOZ_FALLTHROUGH;
+ case 2 : a += (uint32_t(k[1])<<8); MOZ_FALLTHROUGH;
+ case 1 : a += k[0];
+ /* case 0: nothing left to add */
+ }
+ hashmix(a, b, c);
+
+ return c;
+}
+
+nsresult
+nsDiskCache::Truncate(PRFileDesc * fd, uint32_t newEOF)
+{
+ // use modified SetEOF from nsFileStreams::SetEOF()
+
+#if defined(XP_UNIX)
+ if (ftruncate(PR_FileDesc2NativeHandle(fd), newEOF) != 0) {
+ NS_ERROR("ftruncate failed");
+ return NS_ERROR_FAILURE;
+ }
+
+#elif defined(XP_WIN)
+ int32_t cnt = PR_Seek(fd, newEOF, PR_SEEK_SET);
+ if (cnt == -1) return NS_ERROR_FAILURE;
+ if (!SetEndOfFile((HANDLE) PR_FileDesc2NativeHandle(fd))) {
+ NS_ERROR("SetEndOfFile failed");
+ return NS_ERROR_FAILURE;
+ }
+
+#else
+ // add implementations for other platforms here
+#endif
+ return NS_OK;
+}
+
+
+/******************************************************************************
+ * nsDiskCacheDevice
+ *****************************************************************************/
+
+nsDiskCacheDevice::nsDiskCacheDevice()
+ : mCacheCapacity(0)
+ , mMaxEntrySize(-1) // -1 means "no limit"
+ , mInitialized(false)
+ , mClearingDiskCache(false)
+{
+}
+
+nsDiskCacheDevice::~nsDiskCacheDevice()
+{
+ Shutdown();
+}
+
+
+/**
+ * methods of nsCacheDevice
+ */
+nsresult
+nsDiskCacheDevice::Init()
+{
+ nsresult rv;
+
+ if (Initialized()) {
+ NS_ERROR("Disk cache already initialized!");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (!mCacheDirectory)
+ return NS_ERROR_FAILURE;
+
+ mBindery.Init();
+
+ // Open Disk Cache
+ rv = OpenDiskCache();
+ if (NS_FAILED(rv)) {
+ (void) mCacheMap.Close(false);
+ return rv;
+ }
+
+ mInitialized = true;
+ return NS_OK;
+}
+
+
+/**
+ * NOTE: called while holding the cache service lock
+ */
+nsresult
+nsDiskCacheDevice::Shutdown()
+{
+ nsCacheService::AssertOwnsLock();
+
+ nsresult rv = Shutdown_Private(true);
+ if (NS_FAILED(rv))
+ return rv;
+
+ return NS_OK;
+}
+
+
+nsresult
+nsDiskCacheDevice::Shutdown_Private(bool flush)
+{
+ CACHE_LOG_DEBUG(("CACHE: disk Shutdown_Private [%u]\n", flush));
+
+ if (Initialized()) {
+ // check cache limits in case we need to evict.
+ EvictDiskCacheEntries(mCacheCapacity);
+
+ // At this point there may be a number of pending cache-requests on the
+ // cache-io thread. Wait for all these to run before we wipe out our
+ // datastructures (see bug #620660)
+ (void) nsCacheService::SyncWithCacheIOThread();
+
+ // write out persistent information about the cache.
+ (void) mCacheMap.Close(flush);
+
+ mBindery.Reset();
+
+ mInitialized = false;
+ }
+
+ return NS_OK;
+}
+
+
+const char *
+nsDiskCacheDevice::GetDeviceID()
+{
+ return DISK_CACHE_DEVICE_ID;
+}
+
+/**
+ * FindEntry -
+ *
+ * cases: key not in disk cache, hash number free
+ * key not in disk cache, hash number used
+ * key in disk cache
+ *
+ * NOTE: called while holding the cache service lock
+ */
+nsCacheEntry *
+nsDiskCacheDevice::FindEntry(nsCString * key, bool *collision)
+{
+ Telemetry::AutoTimer<Telemetry::CACHE_DISK_SEARCH_2> timer;
+ if (!Initialized()) return nullptr; // NS_ERROR_NOT_INITIALIZED
+ if (mClearingDiskCache) return nullptr;
+ nsDiskCacheRecord record;
+ nsDiskCacheBinding * binding = nullptr;
+ PLDHashNumber hashNumber = nsDiskCache::Hash(key->get());
+
+ *collision = false;
+
+ binding = mBindery.FindActiveBinding(hashNumber);
+ if (binding && !binding->mCacheEntry->Key()->Equals(*key)) {
+ *collision = true;
+ return nullptr;
+ } else if (binding && binding->mDeactivateEvent) {
+ binding->mDeactivateEvent->CancelEvent();
+ binding->mDeactivateEvent = nullptr;
+ CACHE_LOG_DEBUG(("CACHE: reusing deactivated entry %p " \
+ "req-key=%s entry-key=%s\n",
+ binding->mCacheEntry, key, binding->mCacheEntry->Key()));
+
+ return binding->mCacheEntry; // just return this one, observing that
+ // FindActiveBinding() does not return
+ // bindings to doomed entries
+ }
+ binding = nullptr;
+
+ // lookup hash number in cache map
+ nsresult rv = mCacheMap.FindRecord(hashNumber, &record);
+ if (NS_FAILED(rv)) return nullptr; // XXX log error?
+
+ nsDiskCacheEntry * diskEntry = mCacheMap.ReadDiskCacheEntry(&record);
+ if (!diskEntry) return nullptr;
+
+ // compare key to be sure
+ if (!key->Equals(diskEntry->Key())) {
+ *collision = true;
+ return nullptr;
+ }
+
+ nsCacheEntry * entry = diskEntry->CreateCacheEntry(this);
+ if (entry) {
+ binding = mBindery.CreateBinding(entry, &record);
+ if (!binding) {
+ delete entry;
+ entry = nullptr;
+ }
+ }
+
+ if (!entry) {
+ (void) mCacheMap.DeleteStorage(&record);
+ (void) mCacheMap.DeleteRecord(&record);
+ }
+
+ return entry;
+}
+
+
+/**
+ * NOTE: called while holding the cache service lock
+ */
+nsresult
+nsDiskCacheDevice::DeactivateEntry(nsCacheEntry * entry)
+{
+ nsDiskCacheBinding * binding = GetCacheEntryBinding(entry);
+ if (!IsValidBinding(binding))
+ return NS_ERROR_UNEXPECTED;
+
+ CACHE_LOG_DEBUG(("CACHE: disk DeactivateEntry [%p %x]\n",
+ entry, binding->mRecord.HashNumber()));
+
+ nsDiskCacheDeviceDeactivateEntryEvent *event =
+ new nsDiskCacheDeviceDeactivateEntryEvent(this, entry, binding);
+
+ // ensure we can cancel the event via the binding later if necessary
+ binding->mDeactivateEvent = event;
+
+ DebugOnly<nsresult> rv = nsCacheService::DispatchToCacheIOThread(event);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "DeactivateEntry: Failed dispatching "
+ "deactivation event");
+ return NS_OK;
+}
+
+/**
+ * NOTE: called while holding the cache service lock
+ */
+nsresult
+nsDiskCacheDevice::DeactivateEntry_Private(nsCacheEntry * entry,
+ nsDiskCacheBinding * binding)
+{
+ nsresult rv = NS_OK;
+ if (entry->IsDoomed()) {
+ // delete data, entry, record from disk for entry
+ rv = mCacheMap.DeleteStorage(&binding->mRecord);
+
+ } else {
+ // save stuff to disk for entry
+ rv = mCacheMap.WriteDiskCacheEntry(binding);
+ if (NS_FAILED(rv)) {
+ // clean up as best we can
+ (void) mCacheMap.DeleteStorage(&binding->mRecord);
+ (void) mCacheMap.DeleteRecord(&binding->mRecord);
+ binding->mDoomed = true; // record is no longer in cache map
+ }
+ }
+
+ mBindery.RemoveBinding(binding); // extract binding from collision detection stuff
+ delete entry; // which will release binding
+ return rv;
+}
+
+
+/**
+ * BindEntry()
+ * no hash number collision -> no problem
+ * collision
+ * record not active -> evict, no problem
+ * record is active
+ * record is already doomed -> record shouldn't have been in map, no problem
+ * record is not doomed -> doom, and replace record in map
+ *
+ * walk matching hashnumber list to find lowest generation number
+ * take generation number from other (data/meta) location,
+ * or walk active list
+ *
+ * NOTE: called while holding the cache service lock
+ */
+nsresult
+nsDiskCacheDevice::BindEntry(nsCacheEntry * entry)
+{
+ if (!Initialized()) return NS_ERROR_NOT_INITIALIZED;
+ if (mClearingDiskCache) return NS_ERROR_NOT_AVAILABLE;
+ nsresult rv = NS_OK;
+ nsDiskCacheRecord record, oldRecord;
+ nsDiskCacheBinding *binding;
+ PLDHashNumber hashNumber = nsDiskCache::Hash(entry->Key()->get());
+
+ // Find out if there is already an active binding for this hash. If yes it
+ // should have another key since BindEntry() shouldn't be called twice for
+ // the same entry. Doom the old entry, the new one will get another
+ // generation number so files won't collide.
+ binding = mBindery.FindActiveBinding(hashNumber);
+ if (binding) {
+ NS_ASSERTION(!binding->mCacheEntry->Key()->Equals(*entry->Key()),
+ "BindEntry called for already bound entry!");
+ // If the entry is pending deactivation, cancel deactivation
+ if (binding->mDeactivateEvent) {
+ binding->mDeactivateEvent->CancelEvent();
+ binding->mDeactivateEvent = nullptr;
+ }
+ nsCacheService::DoomEntry(binding->mCacheEntry);
+ binding = nullptr;
+ }
+
+ // Lookup hash number in cache map. There can be a colliding inactive entry.
+ // See bug #321361 comment 21 for the scenario. If there is such entry,
+ // delete it.
+ rv = mCacheMap.FindRecord(hashNumber, &record);
+ if (NS_SUCCEEDED(rv)) {
+ nsDiskCacheEntry * diskEntry = mCacheMap.ReadDiskCacheEntry(&record);
+ if (diskEntry) {
+ // compare key to be sure
+ if (!entry->Key()->Equals(diskEntry->Key())) {
+ mCacheMap.DeleteStorage(&record);
+ rv = mCacheMap.DeleteRecord(&record);
+ if (NS_FAILED(rv)) return rv;
+ }
+ }
+ record = nsDiskCacheRecord();
+ }
+
+ // create a new record for this entry
+ record.SetHashNumber(nsDiskCache::Hash(entry->Key()->get()));
+ record.SetEvictionRank(ULONG_MAX - SecondsFromPRTime(PR_Now()));
+
+ CACHE_LOG_DEBUG(("CACHE: disk BindEntry [%p %x]\n",
+ entry, record.HashNumber()));
+
+ if (!entry->IsDoomed()) {
+ // if entry isn't doomed, add it to the cache map
+ rv = mCacheMap.AddRecord(&record, &oldRecord); // deletes old record, if any
+ if (NS_FAILED(rv)) return rv;
+
+ uint32_t oldHashNumber = oldRecord.HashNumber();
+ if (oldHashNumber) {
+ // gotta evict this one first
+ nsDiskCacheBinding * oldBinding = mBindery.FindActiveBinding(oldHashNumber);
+ if (oldBinding) {
+ // XXX if debug : compare keys for hashNumber collision
+
+ if (!oldBinding->mCacheEntry->IsDoomed()) {
+ // If the old entry is pending deactivation, cancel deactivation
+ if (oldBinding->mDeactivateEvent) {
+ oldBinding->mDeactivateEvent->CancelEvent();
+ oldBinding->mDeactivateEvent = nullptr;
+ }
+ // we've got a live one!
+ nsCacheService::DoomEntry(oldBinding->mCacheEntry);
+ // storage will be delete when oldBinding->mCacheEntry is Deactivated
+ }
+ } else {
+ // delete storage
+ // XXX if debug : compare keys for hashNumber collision
+ rv = mCacheMap.DeleteStorage(&oldRecord);
+ if (NS_FAILED(rv)) return rv; // XXX delete record we just added?
+ }
+ }
+ }
+
+ // Make sure this entry has its associated nsDiskCacheBinding attached.
+ binding = mBindery.CreateBinding(entry, &record);
+ NS_ASSERTION(binding, "nsDiskCacheDevice::BindEntry");
+ if (!binding) return NS_ERROR_OUT_OF_MEMORY;
+ NS_ASSERTION(binding->mRecord.ValidRecord(), "bad cache map record");
+
+ return NS_OK;
+}
+
+
+/**
+ * NOTE: called while holding the cache service lock
+ */
+void
+nsDiskCacheDevice::DoomEntry(nsCacheEntry * entry)
+{
+ CACHE_LOG_DEBUG(("CACHE: disk DoomEntry [%p]\n", entry));
+
+ nsDiskCacheBinding * binding = GetCacheEntryBinding(entry);
+ NS_ASSERTION(binding, "DoomEntry: binding == nullptr");
+ if (!binding)
+ return;
+
+ if (!binding->mDoomed) {
+ // so it can't be seen by FindEntry() ever again.
+#ifdef DEBUG
+ nsresult rv =
+#endif
+ mCacheMap.DeleteRecord(&binding->mRecord);
+ NS_ASSERTION(NS_SUCCEEDED(rv),"DeleteRecord failed.");
+ binding->mDoomed = true; // record in no longer in cache map
+ }
+}
+
+
+/**
+ * NOTE: called while holding the cache service lock
+ */
+nsresult
+nsDiskCacheDevice::OpenInputStreamForEntry(nsCacheEntry * entry,
+ nsCacheAccessMode mode,
+ uint32_t offset,
+ nsIInputStream ** result)
+{
+ CACHE_LOG_DEBUG(("CACHE: disk OpenInputStreamForEntry [%p %x %u]\n",
+ entry, mode, offset));
+
+ NS_ENSURE_ARG_POINTER(entry);
+ NS_ENSURE_ARG_POINTER(result);
+
+ nsresult rv;
+ nsDiskCacheBinding * binding = GetCacheEntryBinding(entry);
+ if (!IsValidBinding(binding))
+ return NS_ERROR_UNEXPECTED;
+
+ NS_ASSERTION(binding->mCacheEntry == entry, "binding & entry don't point to each other");
+
+ rv = binding->EnsureStreamIO();
+ if (NS_FAILED(rv)) return rv;
+
+ return binding->mStreamIO->GetInputStream(offset, result);
+}
+
+
+/**
+ * NOTE: called while holding the cache service lock
+ */
+nsresult
+nsDiskCacheDevice::OpenOutputStreamForEntry(nsCacheEntry * entry,
+ nsCacheAccessMode mode,
+ uint32_t offset,
+ nsIOutputStream ** result)
+{
+ CACHE_LOG_DEBUG(("CACHE: disk OpenOutputStreamForEntry [%p %x %u]\n",
+ entry, mode, offset));
+
+ NS_ENSURE_ARG_POINTER(entry);
+ NS_ENSURE_ARG_POINTER(result);
+
+ nsresult rv;
+ nsDiskCacheBinding * binding = GetCacheEntryBinding(entry);
+ if (!IsValidBinding(binding))
+ return NS_ERROR_UNEXPECTED;
+
+ NS_ASSERTION(binding->mCacheEntry == entry, "binding & entry don't point to each other");
+
+ rv = binding->EnsureStreamIO();
+ if (NS_FAILED(rv)) return rv;
+
+ return binding->mStreamIO->GetOutputStream(offset, result);
+}
+
+
+/**
+ * NOTE: called while holding the cache service lock
+ */
+nsresult
+nsDiskCacheDevice::GetFileForEntry(nsCacheEntry * entry,
+ nsIFile ** result)
+{
+ NS_ENSURE_ARG_POINTER(result);
+ *result = nullptr;
+
+ nsresult rv;
+
+ nsDiskCacheBinding * binding = GetCacheEntryBinding(entry);
+ if (!IsValidBinding(binding))
+ return NS_ERROR_UNEXPECTED;
+
+ // check/set binding->mRecord for separate file, sync w/mCacheMap
+ if (binding->mRecord.DataLocationInitialized()) {
+ if (binding->mRecord.DataFile() != 0)
+ return NS_ERROR_NOT_AVAILABLE; // data not stored as separate file
+
+ NS_ASSERTION(binding->mRecord.DataFileGeneration() == binding->mGeneration, "error generations out of sync");
+ } else {
+ binding->mRecord.SetDataFileGeneration(binding->mGeneration);
+ binding->mRecord.SetDataFileSize(0); // 1k minimum
+ if (!binding->mDoomed) {
+ // record stored in cache map, so update it
+ rv = mCacheMap.UpdateRecord(&binding->mRecord);
+ if (NS_FAILED(rv)) return rv;
+ }
+ }
+
+ nsCOMPtr<nsIFile> file;
+ rv = mCacheMap.GetFileForDiskCacheRecord(&binding->mRecord,
+ nsDiskCache::kData,
+ false,
+ getter_AddRefs(file));
+ if (NS_FAILED(rv)) return rv;
+
+ NS_IF_ADDREF(*result = file);
+ return NS_OK;
+}
+
+
+/**
+ * This routine will get called every time an open descriptor is written to.
+ *
+ * NOTE: called while holding the cache service lock
+ */
+nsresult
+nsDiskCacheDevice::OnDataSizeChange(nsCacheEntry * entry, int32_t deltaSize)
+{
+ CACHE_LOG_DEBUG(("CACHE: disk OnDataSizeChange [%p %d]\n",
+ entry, deltaSize));
+
+ // If passed a negative value, then there's nothing to do.
+ if (deltaSize < 0)
+ return NS_OK;
+
+ nsDiskCacheBinding * binding = GetCacheEntryBinding(entry);
+ if (!IsValidBinding(binding))
+ return NS_ERROR_UNEXPECTED;
+
+ NS_ASSERTION(binding->mRecord.ValidRecord(), "bad record");
+
+ uint32_t newSize = entry->DataSize() + deltaSize;
+ uint32_t newSizeK = ((newSize + 0x3FF) >> 10);
+
+ // If the new size is larger than max. file size or larger than
+ // 1/8 the cache capacity (which is in KiB's), doom the entry and abort.
+ if (EntryIsTooBig(newSize)) {
+#ifdef DEBUG
+ nsresult rv =
+#endif
+ nsCacheService::DoomEntry(entry);
+ NS_ASSERTION(NS_SUCCEEDED(rv),"DoomEntry() failed.");
+ return NS_ERROR_ABORT;
+ }
+
+ uint32_t sizeK = ((entry->DataSize() + 0x03FF) >> 10); // round up to next 1k
+
+ // In total count we ignore anything over kMaxDataSizeK (bug #651100), so
+ // the target capacity should be calculated the same way.
+ if (sizeK > kMaxDataSizeK) sizeK = kMaxDataSizeK;
+ if (newSizeK > kMaxDataSizeK) newSizeK = kMaxDataSizeK;
+
+ // pre-evict entries to make space for new data
+ uint32_t targetCapacity = mCacheCapacity > (newSizeK - sizeK)
+ ? mCacheCapacity - (newSizeK - sizeK)
+ : 0;
+ EvictDiskCacheEntries(targetCapacity);
+
+ return NS_OK;
+}
+
+
+/******************************************************************************
+ * EntryInfoVisitor
+ *****************************************************************************/
+class EntryInfoVisitor : public nsDiskCacheRecordVisitor
+{
+public:
+ EntryInfoVisitor(nsDiskCacheMap * cacheMap,
+ nsICacheVisitor * visitor)
+ : mCacheMap(cacheMap)
+ , mVisitor(visitor)
+ {}
+
+ virtual int32_t VisitRecord(nsDiskCacheRecord * mapRecord)
+ {
+ // XXX optimization: do we have this record in memory?
+
+ // read in the entry (metadata)
+ nsDiskCacheEntry * diskEntry = mCacheMap->ReadDiskCacheEntry(mapRecord);
+ if (!diskEntry) {
+ return kVisitNextRecord;
+ }
+
+ // create nsICacheEntryInfo
+ nsDiskCacheEntryInfo * entryInfo = new nsDiskCacheEntryInfo(DISK_CACHE_DEVICE_ID, diskEntry);
+ if (!entryInfo) {
+ return kStopVisitingRecords;
+ }
+ nsCOMPtr<nsICacheEntryInfo> ref(entryInfo);
+
+ bool keepGoing;
+ (void)mVisitor->VisitEntry(DISK_CACHE_DEVICE_ID, entryInfo, &keepGoing);
+ return keepGoing ? kVisitNextRecord : kStopVisitingRecords;
+ }
+
+private:
+ nsDiskCacheMap * mCacheMap;
+ nsICacheVisitor * mVisitor;
+};
+
+
+nsresult
+nsDiskCacheDevice::Visit(nsICacheVisitor * visitor)
+{
+ if (!Initialized()) return NS_ERROR_NOT_INITIALIZED;
+ nsDiskCacheDeviceInfo* deviceInfo = new nsDiskCacheDeviceInfo(this);
+ nsCOMPtr<nsICacheDeviceInfo> ref(deviceInfo);
+
+ bool keepGoing;
+ nsresult rv = visitor->VisitDevice(DISK_CACHE_DEVICE_ID, deviceInfo, &keepGoing);
+ if (NS_FAILED(rv)) return rv;
+
+ if (keepGoing) {
+ EntryInfoVisitor infoVisitor(&mCacheMap, visitor);
+ return mCacheMap.VisitRecords(&infoVisitor);
+ }
+
+ return NS_OK;
+}
+
+// Max allowed size for an entry is currently MIN(mMaxEntrySize, 1/8 CacheCapacity)
+bool
+nsDiskCacheDevice::EntryIsTooBig(int64_t entrySize)
+{
+ if (mMaxEntrySize == -1) // no limit
+ return entrySize > (static_cast<int64_t>(mCacheCapacity) * 1024 / 8);
+ else
+ return entrySize > mMaxEntrySize ||
+ entrySize > (static_cast<int64_t>(mCacheCapacity) * 1024 / 8);
+}
+
+nsresult
+nsDiskCacheDevice::EvictEntries(const char * clientID)
+{
+ CACHE_LOG_DEBUG(("CACHE: disk EvictEntries [%s]\n", clientID));
+
+ if (!Initialized()) return NS_ERROR_NOT_INITIALIZED;
+ nsresult rv;
+
+ if (clientID == nullptr) {
+ // we're clearing the entire disk cache
+ rv = ClearDiskCache();
+ if (rv != NS_ERROR_CACHE_IN_USE)
+ return rv;
+ }
+
+ nsDiskCacheEvictor evictor(&mCacheMap, &mBindery, 0, clientID);
+ rv = mCacheMap.VisitRecords(&evictor);
+
+ if (clientID == nullptr) // we tried to clear the entire cache
+ rv = mCacheMap.Trim(); // so trim cache block files (if possible)
+ return rv;
+}
+
+
+/**
+ * private methods
+ */
+
+nsresult
+nsDiskCacheDevice::OpenDiskCache()
+{
+ Telemetry::AutoTimer<Telemetry::NETWORK_DISK_CACHE_OPEN> timer;
+ // if we don't have a cache directory, create one and open it
+ bool exists;
+ nsresult rv = mCacheDirectory->Exists(&exists);
+ if (NS_FAILED(rv))
+ return rv;
+
+ if (exists) {
+ // Try opening cache map file.
+ nsDiskCache::CorruptCacheInfo corruptInfo;
+ rv = mCacheMap.Open(mCacheDirectory, &corruptInfo);
+
+ if (rv == NS_ERROR_ALREADY_INITIALIZED) {
+ NS_WARNING("nsDiskCacheDevice::OpenDiskCache: already open!");
+ } else if (NS_FAILED(rv)) {
+ // Consider cache corrupt: delete it
+ // delay delete by 1 minute to avoid IO thrash at startup
+ rv = nsDeleteDir::DeleteDir(mCacheDirectory, true, 60000);
+ if (NS_FAILED(rv))
+ return rv;
+ exists = false;
+ }
+ }
+
+ // if we don't have a cache directory, create one and open it
+ if (!exists) {
+ nsCacheService::MarkStartingFresh();
+ rv = mCacheDirectory->Create(nsIFile::DIRECTORY_TYPE, 0777);
+ CACHE_LOG_PATH(LogLevel::Info, "\ncreate cache directory: %s\n", mCacheDirectory);
+ CACHE_LOG_INFO(("mCacheDirectory->Create() = %x\n", rv));
+ if (NS_FAILED(rv))
+ return rv;
+
+ // reopen the cache map
+ nsDiskCache::CorruptCacheInfo corruptInfo;
+ rv = mCacheMap.Open(mCacheDirectory, &corruptInfo);
+ if (NS_FAILED(rv))
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+
+nsresult
+nsDiskCacheDevice::ClearDiskCache()
+{
+ if (mBindery.ActiveBindings())
+ return NS_ERROR_CACHE_IN_USE;
+
+ mClearingDiskCache = true;
+
+ nsresult rv = Shutdown_Private(false); // false: don't bother flushing
+ if (NS_FAILED(rv))
+ return rv;
+
+ mClearingDiskCache = false;
+
+ // If the disk cache directory is already gone, then it's not an error if
+ // we fail to delete it ;-)
+ rv = nsDeleteDir::DeleteDir(mCacheDirectory, true);
+ if (NS_FAILED(rv) && rv != NS_ERROR_FILE_TARGET_DOES_NOT_EXIST)
+ return rv;
+
+ return Init();
+}
+
+
+nsresult
+nsDiskCacheDevice::EvictDiskCacheEntries(uint32_t targetCapacity)
+{
+ CACHE_LOG_DEBUG(("CACHE: disk EvictDiskCacheEntries [%u]\n",
+ targetCapacity));
+
+ NS_ASSERTION(targetCapacity > 0, "oops");
+
+ if (mCacheMap.TotalSize() < targetCapacity)
+ return NS_OK;
+
+ // targetCapacity is in KiB's
+ nsDiskCacheEvictor evictor(&mCacheMap, &mBindery, targetCapacity, nullptr);
+ return mCacheMap.EvictRecords(&evictor);
+}
+
+
+/**
+ * methods for prefs
+ */
+
+void
+nsDiskCacheDevice::SetCacheParentDirectory(nsIFile * parentDir)
+{
+ nsresult rv;
+ bool exists;
+
+ if (Initialized()) {
+ NS_ASSERTION(false, "Cannot switch cache directory when initialized");
+ return;
+ }
+
+ if (!parentDir) {
+ mCacheDirectory = nullptr;
+ return;
+ }
+
+ // ensure parent directory exists
+ rv = parentDir->Exists(&exists);
+ if (NS_SUCCEEDED(rv) && !exists)
+ rv = parentDir->Create(nsIFile::DIRECTORY_TYPE, 0700);
+ if (NS_FAILED(rv)) return;
+
+ // ensure cache directory exists
+ nsCOMPtr<nsIFile> directory;
+
+ rv = parentDir->Clone(getter_AddRefs(directory));
+ if (NS_FAILED(rv)) return;
+ rv = directory->AppendNative(NS_LITERAL_CSTRING("Cache"));
+ if (NS_FAILED(rv)) return;
+
+ mCacheDirectory = do_QueryInterface(directory);
+}
+
+
+void
+nsDiskCacheDevice::getCacheDirectory(nsIFile ** result)
+{
+ *result = mCacheDirectory;
+ NS_IF_ADDREF(*result);
+}
+
+
+/**
+ * NOTE: called while holding the cache service lock
+ */
+void
+nsDiskCacheDevice::SetCapacity(uint32_t capacity)
+{
+ // Units are KiB's
+ mCacheCapacity = capacity;
+ if (Initialized()) {
+ if (NS_IsMainThread()) {
+ // Do not evict entries on the main thread
+ nsCacheService::DispatchToCacheIOThread(
+ new nsEvictDiskCacheEntriesEvent(this));
+ } else {
+ // start evicting entries if the new size is smaller!
+ EvictDiskCacheEntries(mCacheCapacity);
+ }
+ }
+ // Let cache map know of the new capacity
+ mCacheMap.NotifyCapacityChange(capacity);
+}
+
+
+uint32_t nsDiskCacheDevice::getCacheCapacity()
+{
+ return mCacheCapacity;
+}
+
+
+uint32_t nsDiskCacheDevice::getCacheSize()
+{
+ return mCacheMap.TotalSize();
+}
+
+
+uint32_t nsDiskCacheDevice::getEntryCount()
+{
+ return mCacheMap.EntryCount();
+}
+
+void
+nsDiskCacheDevice::SetMaxEntrySize(int32_t maxSizeInKilobytes)
+{
+ // Internal units are bytes. Changing this only takes effect *after* the
+ // change and has no consequences for existing cache-entries
+ if (maxSizeInKilobytes >= 0)
+ mMaxEntrySize = maxSizeInKilobytes * 1024;
+ else
+ mMaxEntrySize = -1;
+}
+
+size_t
+nsDiskCacheDevice::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf)
+{
+ size_t usage = aMallocSizeOf(this);
+
+ usage += mCacheMap.SizeOfExcludingThis(aMallocSizeOf);
+ usage += mBindery.SizeOfExcludingThis(aMallocSizeOf);
+
+ return usage;
+}