summaryrefslogtreecommitdiffstats
path: root/netwerk/cache/nsDiskCacheMap.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'netwerk/cache/nsDiskCacheMap.cpp')
-rw-r--r--netwerk/cache/nsDiskCacheMap.cpp1430
1 files changed, 1430 insertions, 0 deletions
diff --git a/netwerk/cache/nsDiskCacheMap.cpp b/netwerk/cache/nsDiskCacheMap.cpp
new file mode 100644
index 000000000..d7ce13a35
--- /dev/null
+++ b/netwerk/cache/nsDiskCacheMap.cpp
@@ -0,0 +1,1430 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim:set ts=4 sw=4 sts=4 cin et: */
+/* 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 "nsCache.h"
+#include "nsDiskCacheMap.h"
+#include "nsDiskCacheBinding.h"
+#include "nsDiskCacheEntry.h"
+#include "nsDiskCacheDevice.h"
+#include "nsCacheService.h"
+
+#include <string.h>
+#include "nsPrintfCString.h"
+
+#include "nsISerializable.h"
+#include "nsSerializationHelper.h"
+
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/Sprintf.h"
+#include "mozilla/Telemetry.h"
+#include <algorithm>
+
+using namespace mozilla;
+
+/******************************************************************************
+ * nsDiskCacheMap
+ *****************************************************************************/
+
+/**
+ * File operations
+ */
+
+nsresult
+nsDiskCacheMap::Open(nsIFile * cacheDirectory,
+ nsDiskCache::CorruptCacheInfo * corruptInfo)
+{
+ NS_ENSURE_ARG_POINTER(corruptInfo);
+
+ // Assume we have an unexpected error until we find otherwise.
+ *corruptInfo = nsDiskCache::kUnexpectedError;
+ NS_ENSURE_ARG_POINTER(cacheDirectory);
+ if (mMapFD) return NS_ERROR_ALREADY_INITIALIZED;
+
+ mCacheDirectory = cacheDirectory; // save a reference for ourselves
+
+ // create nsIFile for _CACHE_MAP_
+ nsresult rv;
+ nsCOMPtr<nsIFile> file;
+ rv = cacheDirectory->Clone(getter_AddRefs(file));
+ rv = file->AppendNative(NS_LITERAL_CSTRING("_CACHE_MAP_"));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // open the file - restricted to user, the data could be confidential
+ rv = file->OpenNSPRFileDesc(PR_RDWR | PR_CREATE_FILE, 00600, &mMapFD);
+ if (NS_FAILED(rv)) {
+ *corruptInfo = nsDiskCache::kOpenCacheMapError;
+ NS_WARNING("Could not open cache map file");
+ return NS_ERROR_FILE_CORRUPTED;
+ }
+
+ bool cacheFilesExist = CacheFilesExist();
+ rv = NS_ERROR_FILE_CORRUPTED; // presume the worst
+ uint32_t mapSize = PR_Available(mMapFD);
+
+ if (NS_FAILED(InitCacheClean(cacheDirectory,
+ corruptInfo))) {
+ // corruptInfo is set in the call to InitCacheClean
+ goto error_exit;
+ }
+
+ // check size of map file
+ if (mapSize == 0) { // creating a new _CACHE_MAP_
+
+ // block files shouldn't exist if we're creating the _CACHE_MAP_
+ if (cacheFilesExist) {
+ *corruptInfo = nsDiskCache::kBlockFilesShouldNotExist;
+ goto error_exit;
+ }
+
+ if (NS_FAILED(CreateCacheSubDirectories())) {
+ *corruptInfo = nsDiskCache::kCreateCacheSubdirectories;
+ goto error_exit;
+ }
+
+ // create the file - initialize in memory
+ memset(&mHeader, 0, sizeof(nsDiskCacheHeader));
+ mHeader.mVersion = nsDiskCache::kCurrentVersion;
+ mHeader.mRecordCount = kMinRecordCount;
+ mRecordArray = (nsDiskCacheRecord *)
+ PR_CALLOC(mHeader.mRecordCount * sizeof(nsDiskCacheRecord));
+ if (!mRecordArray) {
+ *corruptInfo = nsDiskCache::kOutOfMemory;
+ rv = NS_ERROR_OUT_OF_MEMORY;
+ goto error_exit;
+ }
+ } else if (mapSize >= sizeof(nsDiskCacheHeader)) { // read existing _CACHE_MAP_
+
+ // if _CACHE_MAP_ exists, so should the block files
+ if (!cacheFilesExist) {
+ *corruptInfo = nsDiskCache::kBlockFilesShouldExist;
+ goto error_exit;
+ }
+
+ CACHE_LOG_DEBUG(("CACHE: nsDiskCacheMap::Open [this=%p] reading map", this));
+
+ // read the header
+ uint32_t bytesRead = PR_Read(mMapFD, &mHeader, sizeof(nsDiskCacheHeader));
+ if (sizeof(nsDiskCacheHeader) != bytesRead) {
+ *corruptInfo = nsDiskCache::kHeaderSizeNotRead;
+ goto error_exit;
+ }
+ mHeader.Unswap();
+
+ if (mHeader.mIsDirty) {
+ *corruptInfo = nsDiskCache::kHeaderIsDirty;
+ goto error_exit;
+ }
+
+ if (mHeader.mVersion != nsDiskCache::kCurrentVersion) {
+ *corruptInfo = nsDiskCache::kVersionMismatch;
+ goto error_exit;
+ }
+
+ uint32_t recordArraySize =
+ mHeader.mRecordCount * sizeof(nsDiskCacheRecord);
+ if (mapSize < recordArraySize + sizeof(nsDiskCacheHeader)) {
+ *corruptInfo = nsDiskCache::kRecordsIncomplete;
+ goto error_exit;
+ }
+
+ // Get the space for the records
+ mRecordArray = (nsDiskCacheRecord *) PR_MALLOC(recordArraySize);
+ if (!mRecordArray) {
+ *corruptInfo = nsDiskCache::kOutOfMemory;
+ rv = NS_ERROR_OUT_OF_MEMORY;
+ goto error_exit;
+ }
+
+ // Read the records
+ bytesRead = PR_Read(mMapFD, mRecordArray, recordArraySize);
+ if (bytesRead < recordArraySize) {
+ *corruptInfo = nsDiskCache::kNotEnoughToRead;
+ goto error_exit;
+ }
+
+ // Unswap each record
+ int32_t total = 0;
+ for (int32_t i = 0; i < mHeader.mRecordCount; ++i) {
+ if (mRecordArray[i].HashNumber()) {
+#if defined(IS_LITTLE_ENDIAN)
+ mRecordArray[i].Unswap();
+#endif
+ total ++;
+ }
+ }
+
+ // verify entry count
+ if (total != mHeader.mEntryCount) {
+ *corruptInfo = nsDiskCache::kEntryCountIncorrect;
+ goto error_exit;
+ }
+
+ } else {
+ *corruptInfo = nsDiskCache::kHeaderIncomplete;
+ goto error_exit;
+ }
+
+ rv = OpenBlockFiles(corruptInfo);
+ if (NS_FAILED(rv)) {
+ // corruptInfo is set in the call to OpenBlockFiles
+ goto error_exit;
+ }
+
+ // set dirty bit and flush header
+ mHeader.mIsDirty = true;
+ rv = FlushHeader();
+ if (NS_FAILED(rv)) {
+ *corruptInfo = nsDiskCache::kFlushHeaderError;
+ goto error_exit;
+ }
+
+ Telemetry::Accumulate(Telemetry::HTTP_DISK_CACHE_OVERHEAD,
+ (uint32_t)SizeOfExcludingThis(moz_malloc_size_of));
+
+ *corruptInfo = nsDiskCache::kNotCorrupt;
+ return NS_OK;
+
+error_exit:
+ (void) Close(false);
+
+ return rv;
+}
+
+
+nsresult
+nsDiskCacheMap::Close(bool flush)
+{
+ nsCacheService::AssertOwnsLock();
+ nsresult rv = NS_OK;
+
+ // Cancel any pending cache validation event, the FlushRecords call below
+ // will validate the cache.
+ if (mCleanCacheTimer) {
+ mCleanCacheTimer->Cancel();
+ }
+
+ // If cache map file and its block files are still open, close them
+ if (mMapFD) {
+ // close block files
+ rv = CloseBlockFiles(flush);
+ if (NS_SUCCEEDED(rv) && flush && mRecordArray) {
+ // write the map records
+ rv = FlushRecords(false); // don't bother swapping buckets back
+ if (NS_SUCCEEDED(rv)) {
+ // clear dirty bit
+ mHeader.mIsDirty = false;
+ rv = FlushHeader();
+ }
+ }
+ if ((PR_Close(mMapFD) != PR_SUCCESS) && (NS_SUCCEEDED(rv)))
+ rv = NS_ERROR_UNEXPECTED;
+
+ mMapFD = nullptr;
+ }
+
+ if (mCleanFD) {
+ PR_Close(mCleanFD);
+ mCleanFD = nullptr;
+ }
+
+ PR_FREEIF(mRecordArray);
+ PR_FREEIF(mBuffer);
+ mBufferSize = 0;
+ return rv;
+}
+
+
+nsresult
+nsDiskCacheMap::Trim()
+{
+ nsresult rv, rv2 = NS_OK;
+ for (int i=0; i < kNumBlockFiles; ++i) {
+ rv = mBlockFile[i].Trim();
+ if (NS_FAILED(rv)) rv2 = rv; // if one or more errors, report at least one
+ }
+ // Try to shrink the records array
+ rv = ShrinkRecords();
+ if (NS_FAILED(rv)) rv2 = rv; // if one or more errors, report at least one
+ return rv2;
+}
+
+
+nsresult
+nsDiskCacheMap::FlushHeader()
+{
+ if (!mMapFD) return NS_ERROR_NOT_AVAILABLE;
+
+ // seek to beginning of cache map
+ int32_t filePos = PR_Seek(mMapFD, 0, PR_SEEK_SET);
+ if (filePos != 0) return NS_ERROR_UNEXPECTED;
+
+ // write the header
+ mHeader.Swap();
+ int32_t bytesWritten = PR_Write(mMapFD, &mHeader, sizeof(nsDiskCacheHeader));
+ mHeader.Unswap();
+ if (sizeof(nsDiskCacheHeader) != bytesWritten) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ PRStatus err = PR_Sync(mMapFD);
+ if (err != PR_SUCCESS) return NS_ERROR_UNEXPECTED;
+
+ // If we have a clean header then revalidate the cache clean file
+ if (!mHeader.mIsDirty) {
+ RevalidateCache();
+ }
+
+ return NS_OK;
+}
+
+
+nsresult
+nsDiskCacheMap::FlushRecords(bool unswap)
+{
+ if (!mMapFD) return NS_ERROR_NOT_AVAILABLE;
+
+ // seek to beginning of buckets
+ int32_t filePos = PR_Seek(mMapFD, sizeof(nsDiskCacheHeader), PR_SEEK_SET);
+ if (filePos != sizeof(nsDiskCacheHeader))
+ return NS_ERROR_UNEXPECTED;
+
+#if defined(IS_LITTLE_ENDIAN)
+ // Swap each record
+ for (int32_t i = 0; i < mHeader.mRecordCount; ++i) {
+ if (mRecordArray[i].HashNumber())
+ mRecordArray[i].Swap();
+ }
+#endif
+
+ int32_t recordArraySize = sizeof(nsDiskCacheRecord) * mHeader.mRecordCount;
+
+ int32_t bytesWritten = PR_Write(mMapFD, mRecordArray, recordArraySize);
+ if (bytesWritten != recordArraySize)
+ return NS_ERROR_UNEXPECTED;
+
+#if defined(IS_LITTLE_ENDIAN)
+ if (unswap) {
+ // Unswap each record
+ for (int32_t i = 0; i < mHeader.mRecordCount; ++i) {
+ if (mRecordArray[i].HashNumber())
+ mRecordArray[i].Unswap();
+ }
+ }
+#endif
+
+ return NS_OK;
+}
+
+
+/**
+ * Record operations
+ */
+
+uint32_t
+nsDiskCacheMap::GetBucketRank(uint32_t bucketIndex, uint32_t targetRank)
+{
+ nsDiskCacheRecord * records = GetFirstRecordInBucket(bucketIndex);
+ uint32_t rank = 0;
+
+ for (int i = mHeader.mBucketUsage[bucketIndex]-1; i >= 0; i--) {
+ if ((rank < records[i].EvictionRank()) &&
+ ((targetRank == 0) || (records[i].EvictionRank() < targetRank)))
+ rank = records[i].EvictionRank();
+ }
+ return rank;
+}
+
+nsresult
+nsDiskCacheMap::GrowRecords()
+{
+ if (mHeader.mRecordCount >= mMaxRecordCount)
+ return NS_OK;
+ CACHE_LOG_DEBUG(("CACHE: GrowRecords\n"));
+
+ // Resize the record array
+ int32_t newCount = mHeader.mRecordCount << 1;
+ if (newCount > mMaxRecordCount)
+ newCount = mMaxRecordCount;
+ nsDiskCacheRecord *newArray = (nsDiskCacheRecord *)
+ PR_REALLOC(mRecordArray, newCount * sizeof(nsDiskCacheRecord));
+ if (!newArray)
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ // Space out the buckets
+ uint32_t oldRecordsPerBucket = GetRecordsPerBucket();
+ uint32_t newRecordsPerBucket = newCount / kBuckets;
+ // Work from back to space out each bucket to the new array
+ for (int bucketIndex = kBuckets - 1; bucketIndex >= 0; --bucketIndex) {
+ // Move bucket
+ nsDiskCacheRecord *newRecords = newArray + bucketIndex * newRecordsPerBucket;
+ const uint32_t count = mHeader.mBucketUsage[bucketIndex];
+ memmove(newRecords,
+ newArray + bucketIndex * oldRecordsPerBucket,
+ count * sizeof(nsDiskCacheRecord));
+ // clear unused records
+ memset(newRecords + count, 0,
+ (newRecordsPerBucket - count) * sizeof(nsDiskCacheRecord));
+ }
+
+ // Set as the new record array
+ mRecordArray = newArray;
+ mHeader.mRecordCount = newCount;
+
+ InvalidateCache();
+
+ return NS_OK;
+}
+
+nsresult
+nsDiskCacheMap::ShrinkRecords()
+{
+ if (mHeader.mRecordCount <= kMinRecordCount)
+ return NS_OK;
+ CACHE_LOG_DEBUG(("CACHE: ShrinkRecords\n"));
+
+ // Verify if we can shrink the record array: all buckets must be less than
+ // 1/2 filled
+ uint32_t maxUsage = 0, bucketIndex;
+ for (bucketIndex = 0; bucketIndex < kBuckets; ++bucketIndex) {
+ if (maxUsage < mHeader.mBucketUsage[bucketIndex])
+ maxUsage = mHeader.mBucketUsage[bucketIndex];
+ }
+ // Determine new bucket size, halve size until maxUsage
+ uint32_t oldRecordsPerBucket = GetRecordsPerBucket();
+ uint32_t newRecordsPerBucket = oldRecordsPerBucket;
+ while (maxUsage < (newRecordsPerBucket >> 1))
+ newRecordsPerBucket >>= 1;
+ if (newRecordsPerBucket < (kMinRecordCount / kBuckets))
+ newRecordsPerBucket = (kMinRecordCount / kBuckets);
+ NS_ASSERTION(newRecordsPerBucket <= oldRecordsPerBucket,
+ "ShrinkRecords() can't grow records!");
+ if (newRecordsPerBucket == oldRecordsPerBucket)
+ return NS_OK;
+ // Move the buckets close to each other
+ for (bucketIndex = 1; bucketIndex < kBuckets; ++bucketIndex) {
+ // Move bucket
+ memmove(mRecordArray + bucketIndex * newRecordsPerBucket,
+ mRecordArray + bucketIndex * oldRecordsPerBucket,
+ newRecordsPerBucket * sizeof(nsDiskCacheRecord));
+ }
+
+ // Shrink the record array memory block itself
+ uint32_t newCount = newRecordsPerBucket * kBuckets;
+ nsDiskCacheRecord* newArray = (nsDiskCacheRecord *)
+ PR_REALLOC(mRecordArray, newCount * sizeof(nsDiskCacheRecord));
+ if (!newArray)
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ // Set as the new record array
+ mRecordArray = newArray;
+ mHeader.mRecordCount = newCount;
+
+ InvalidateCache();
+
+ return NS_OK;
+}
+
+nsresult
+nsDiskCacheMap::AddRecord( nsDiskCacheRecord * mapRecord,
+ nsDiskCacheRecord * oldRecord)
+{
+ CACHE_LOG_DEBUG(("CACHE: AddRecord [%x]\n", mapRecord->HashNumber()));
+
+ const uint32_t hashNumber = mapRecord->HashNumber();
+ const uint32_t bucketIndex = GetBucketIndex(hashNumber);
+ const uint32_t count = mHeader.mBucketUsage[bucketIndex];
+
+ oldRecord->SetHashNumber(0); // signify no record
+
+ if (count == GetRecordsPerBucket()) {
+ // Ignore failure to grow the record space, we will then reuse old records
+ GrowRecords();
+ }
+
+ nsDiskCacheRecord * records = GetFirstRecordInBucket(bucketIndex);
+ if (count < GetRecordsPerBucket()) {
+ // stick the new record at the end
+ records[count] = *mapRecord;
+ mHeader.mEntryCount++;
+ mHeader.mBucketUsage[bucketIndex]++;
+ if (mHeader.mEvictionRank[bucketIndex] < mapRecord->EvictionRank())
+ mHeader.mEvictionRank[bucketIndex] = mapRecord->EvictionRank();
+ InvalidateCache();
+ } else {
+ // Find the record with the highest eviction rank
+ nsDiskCacheRecord * mostEvictable = &records[0];
+ for (int i = count-1; i > 0; i--) {
+ if (records[i].EvictionRank() > mostEvictable->EvictionRank())
+ mostEvictable = &records[i];
+ }
+ *oldRecord = *mostEvictable; // i == GetRecordsPerBucket(), so
+ // evict the mostEvictable
+ *mostEvictable = *mapRecord; // replace it with the new record
+ // check if we need to update mostEvictable entry in header
+ if (mHeader.mEvictionRank[bucketIndex] < mapRecord->EvictionRank())
+ mHeader.mEvictionRank[bucketIndex] = mapRecord->EvictionRank();
+ if (oldRecord->EvictionRank() >= mHeader.mEvictionRank[bucketIndex])
+ mHeader.mEvictionRank[bucketIndex] = GetBucketRank(bucketIndex, 0);
+ InvalidateCache();
+ }
+
+ NS_ASSERTION(mHeader.mEvictionRank[bucketIndex] == GetBucketRank(bucketIndex, 0),
+ "eviction rank out of sync");
+ return NS_OK;
+}
+
+
+nsresult
+nsDiskCacheMap::UpdateRecord( nsDiskCacheRecord * mapRecord)
+{
+ CACHE_LOG_DEBUG(("CACHE: UpdateRecord [%x]\n", mapRecord->HashNumber()));
+
+ const uint32_t hashNumber = mapRecord->HashNumber();
+ const uint32_t bucketIndex = GetBucketIndex(hashNumber);
+ nsDiskCacheRecord * records = GetFirstRecordInBucket(bucketIndex);
+
+ for (int i = mHeader.mBucketUsage[bucketIndex]-1; i >= 0; i--) {
+ if (records[i].HashNumber() == hashNumber) {
+ const uint32_t oldRank = records[i].EvictionRank();
+
+ // stick the new record here
+ records[i] = *mapRecord;
+
+ // update eviction rank in header if necessary
+ if (mHeader.mEvictionRank[bucketIndex] < mapRecord->EvictionRank())
+ mHeader.mEvictionRank[bucketIndex] = mapRecord->EvictionRank();
+ else if (mHeader.mEvictionRank[bucketIndex] == oldRank)
+ mHeader.mEvictionRank[bucketIndex] = GetBucketRank(bucketIndex, 0);
+
+ InvalidateCache();
+
+NS_ASSERTION(mHeader.mEvictionRank[bucketIndex] == GetBucketRank(bucketIndex, 0),
+ "eviction rank out of sync");
+ return NS_OK;
+ }
+ }
+ NS_NOTREACHED("record not found");
+ return NS_ERROR_UNEXPECTED;
+}
+
+
+nsresult
+nsDiskCacheMap::FindRecord( uint32_t hashNumber, nsDiskCacheRecord * result)
+{
+ const uint32_t bucketIndex = GetBucketIndex(hashNumber);
+ nsDiskCacheRecord * records = GetFirstRecordInBucket(bucketIndex);
+
+ for (int i = mHeader.mBucketUsage[bucketIndex]-1; i >= 0; i--) {
+ if (records[i].HashNumber() == hashNumber) {
+ *result = records[i]; // copy the record
+ NS_ASSERTION(result->ValidRecord(), "bad cache map record");
+ return NS_OK;
+ }
+ }
+ return NS_ERROR_CACHE_KEY_NOT_FOUND;
+}
+
+
+nsresult
+nsDiskCacheMap::DeleteRecord( nsDiskCacheRecord * mapRecord)
+{
+ CACHE_LOG_DEBUG(("CACHE: DeleteRecord [%x]\n", mapRecord->HashNumber()));
+
+ const uint32_t hashNumber = mapRecord->HashNumber();
+ const uint32_t bucketIndex = GetBucketIndex(hashNumber);
+ nsDiskCacheRecord * records = GetFirstRecordInBucket(bucketIndex);
+ uint32_t last = mHeader.mBucketUsage[bucketIndex]-1;
+
+ for (int i = last; i >= 0; i--) {
+ if (records[i].HashNumber() == hashNumber) {
+ // found it, now delete it.
+ uint32_t evictionRank = records[i].EvictionRank();
+ NS_ASSERTION(evictionRank == mapRecord->EvictionRank(),
+ "evictionRank out of sync");
+ // if not the last record, shift last record into opening
+ records[i] = records[last];
+ records[last].SetHashNumber(0); // clear last record
+ mHeader.mBucketUsage[bucketIndex] = last;
+ mHeader.mEntryCount--;
+
+ // update eviction rank
+ uint32_t bucketIndex = GetBucketIndex(mapRecord->HashNumber());
+ if (mHeader.mEvictionRank[bucketIndex] <= evictionRank) {
+ mHeader.mEvictionRank[bucketIndex] = GetBucketRank(bucketIndex, 0);
+ }
+
+ InvalidateCache();
+
+ NS_ASSERTION(mHeader.mEvictionRank[bucketIndex] ==
+ GetBucketRank(bucketIndex, 0), "eviction rank out of sync");
+ return NS_OK;
+ }
+ }
+ return NS_ERROR_UNEXPECTED;
+}
+
+
+int32_t
+nsDiskCacheMap::VisitEachRecord(uint32_t bucketIndex,
+ nsDiskCacheRecordVisitor * visitor,
+ uint32_t evictionRank)
+{
+ int32_t rv = kVisitNextRecord;
+ uint32_t count = mHeader.mBucketUsage[bucketIndex];
+ nsDiskCacheRecord * records = GetFirstRecordInBucket(bucketIndex);
+
+ // call visitor for each entry (matching any eviction rank)
+ for (int i = count-1; i >= 0; i--) {
+ if (evictionRank > records[i].EvictionRank()) continue;
+
+ rv = visitor->VisitRecord(&records[i]);
+ if (rv == kStopVisitingRecords)
+ break; // Stop visiting records
+
+ if (rv == kDeleteRecordAndContinue) {
+ --count;
+ records[i] = records[count];
+ records[count].SetHashNumber(0);
+ InvalidateCache();
+ }
+ }
+
+ if (mHeader.mBucketUsage[bucketIndex] - count != 0) {
+ mHeader.mEntryCount -= mHeader.mBucketUsage[bucketIndex] - count;
+ mHeader.mBucketUsage[bucketIndex] = count;
+ // recalc eviction rank
+ mHeader.mEvictionRank[bucketIndex] = GetBucketRank(bucketIndex, 0);
+ }
+ NS_ASSERTION(mHeader.mEvictionRank[bucketIndex] ==
+ GetBucketRank(bucketIndex, 0), "eviction rank out of sync");
+
+ return rv;
+}
+
+
+/**
+ * VisitRecords
+ *
+ * Visit every record in cache map in the most convenient order
+ */
+nsresult
+nsDiskCacheMap::VisitRecords( nsDiskCacheRecordVisitor * visitor)
+{
+ for (int bucketIndex = 0; bucketIndex < kBuckets; ++bucketIndex) {
+ if (VisitEachRecord(bucketIndex, visitor, 0) == kStopVisitingRecords)
+ break;
+ }
+ return NS_OK;
+}
+
+
+/**
+ * EvictRecords
+ *
+ * Just like VisitRecords, but visits the records in order of their eviction rank
+ */
+nsresult
+nsDiskCacheMap::EvictRecords( nsDiskCacheRecordVisitor * visitor)
+{
+ uint32_t tempRank[kBuckets];
+ int bucketIndex = 0;
+
+ // copy eviction rank array
+ for (bucketIndex = 0; bucketIndex < kBuckets; ++bucketIndex)
+ tempRank[bucketIndex] = mHeader.mEvictionRank[bucketIndex];
+
+ // Maximum number of iterations determined by number of records
+ // as a safety limiter for the loop. Use a copy of mHeader.mEntryCount since
+ // the value could decrease if some entry is evicted.
+ int32_t entryCount = mHeader.mEntryCount;
+ for (int n = 0; n < entryCount; ++n) {
+
+ // find bucket with highest eviction rank
+ uint32_t rank = 0;
+ for (int i = 0; i < kBuckets; ++i) {
+ if (rank < tempRank[i]) {
+ rank = tempRank[i];
+ bucketIndex = i;
+ }
+ }
+
+ if (rank == 0) break; // we've examined all the records
+
+ // visit records in bucket with eviction ranks >= target eviction rank
+ if (VisitEachRecord(bucketIndex, visitor, rank) == kStopVisitingRecords)
+ break;
+
+ // find greatest rank less than 'rank'
+ tempRank[bucketIndex] = GetBucketRank(bucketIndex, rank);
+ }
+ return NS_OK;
+}
+
+
+
+nsresult
+nsDiskCacheMap::OpenBlockFiles(nsDiskCache::CorruptCacheInfo * corruptInfo)
+{
+ NS_ENSURE_ARG_POINTER(corruptInfo);
+
+ // create nsIFile for block file
+ nsCOMPtr<nsIFile> blockFile;
+ nsresult rv = NS_OK;
+ *corruptInfo = nsDiskCache::kUnexpectedError;
+
+ for (int i = 0; i < kNumBlockFiles; ++i) {
+ rv = GetBlockFileForIndex(i, getter_AddRefs(blockFile));
+ if (NS_FAILED(rv)) {
+ *corruptInfo = nsDiskCache::kCouldNotGetBlockFileForIndex;
+ break;
+ }
+
+ uint32_t blockSize = GetBlockSizeForIndex(i+1); // +1 to match file selectors 1,2,3
+ uint32_t bitMapSize = GetBitMapSizeForIndex(i+1);
+ rv = mBlockFile[i].Open(blockFile, blockSize, bitMapSize, corruptInfo);
+ if (NS_FAILED(rv)) {
+ // corruptInfo was set inside the call to mBlockFile[i].Open
+ break;
+ }
+ }
+ // close all files in case of any error
+ if (NS_FAILED(rv))
+ (void)CloseBlockFiles(false); // we already have an error to report
+
+ return rv;
+}
+
+
+nsresult
+nsDiskCacheMap::CloseBlockFiles(bool flush)
+{
+ nsresult rv, rv2 = NS_OK;
+ for (int i=0; i < kNumBlockFiles; ++i) {
+ rv = mBlockFile[i].Close(flush);
+ if (NS_FAILED(rv)) rv2 = rv; // if one or more errors, report at least one
+ }
+ return rv2;
+}
+
+
+bool
+nsDiskCacheMap::CacheFilesExist()
+{
+ nsCOMPtr<nsIFile> blockFile;
+ nsresult rv;
+
+ for (int i = 0; i < kNumBlockFiles; ++i) {
+ bool exists;
+ rv = GetBlockFileForIndex(i, getter_AddRefs(blockFile));
+ if (NS_FAILED(rv)) return false;
+
+ rv = blockFile->Exists(&exists);
+ if (NS_FAILED(rv) || !exists) return false;
+ }
+
+ return true;
+}
+
+
+nsresult
+nsDiskCacheMap::CreateCacheSubDirectories()
+{
+ if (!mCacheDirectory)
+ return NS_ERROR_UNEXPECTED;
+
+ for (int32_t index = 0 ; index < 16 ; index++) {
+ nsCOMPtr<nsIFile> file;
+ nsresult rv = mCacheDirectory->Clone(getter_AddRefs(file));
+ if (NS_FAILED(rv))
+ return rv;
+
+ rv = file->AppendNative(nsPrintfCString("%X", index));
+ if (NS_FAILED(rv))
+ return rv;
+
+ rv = file->Create(nsIFile::DIRECTORY_TYPE, 0700);
+ if (NS_FAILED(rv))
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+
+nsDiskCacheEntry *
+nsDiskCacheMap::ReadDiskCacheEntry(nsDiskCacheRecord * record)
+{
+ CACHE_LOG_DEBUG(("CACHE: ReadDiskCacheEntry [%x]\n", record->HashNumber()));
+
+ nsresult rv = NS_ERROR_UNEXPECTED;
+ nsDiskCacheEntry * diskEntry = nullptr;
+ uint32_t metaFile = record->MetaFile();
+ int32_t bytesRead = 0;
+
+ if (!record->MetaLocationInitialized()) return nullptr;
+
+ if (metaFile == 0) { // entry/metadata stored in separate file
+ // open and read the file
+ nsCOMPtr<nsIFile> file;
+ rv = GetLocalFileForDiskCacheRecord(record,
+ nsDiskCache::kMetaData,
+ false,
+ getter_AddRefs(file));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ CACHE_LOG_DEBUG(("CACHE: nsDiskCacheMap::ReadDiskCacheEntry"
+ "[this=%p] reading disk cache entry", this));
+
+ PRFileDesc * fd = nullptr;
+
+ // open the file - restricted to user, the data could be confidential
+ rv = file->OpenNSPRFileDesc(PR_RDONLY, 00600, &fd);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ int32_t fileSize = PR_Available(fd);
+ if (fileSize < 0) {
+ // an error occurred. We could call PR_GetError(), but how would that help?
+ rv = NS_ERROR_UNEXPECTED;
+ } else {
+ rv = EnsureBuffer(fileSize);
+ if (NS_SUCCEEDED(rv)) {
+ bytesRead = PR_Read(fd, mBuffer, fileSize);
+ if (bytesRead < fileSize) {
+ rv = NS_ERROR_UNEXPECTED;
+ }
+ }
+ }
+ PR_Close(fd);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ } else if (metaFile < (kNumBlockFiles + 1)) {
+ // entry/metadata stored in cache block file
+
+ // allocate buffer
+ uint32_t blockCount = record->MetaBlockCount();
+ bytesRead = blockCount * GetBlockSizeForIndex(metaFile);
+
+ rv = EnsureBuffer(bytesRead);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ // read diskEntry, note when the blocks are at the end of file,
+ // bytesRead may be less than blockSize*blockCount.
+ // But the bytesRead should at least agree with the real disk entry size.
+ rv = mBlockFile[metaFile - 1].ReadBlocks(mBuffer,
+ record->MetaStartBlock(),
+ blockCount,
+ &bytesRead);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ }
+ diskEntry = (nsDiskCacheEntry *)mBuffer;
+ diskEntry->Unswap(); // disk to memory
+ // Check if calculated size agrees with bytesRead
+ if (bytesRead < 0 || (uint32_t)bytesRead < diskEntry->Size())
+ return nullptr;
+
+ // Return the buffer containing the diskEntry structure
+ return diskEntry;
+}
+
+
+/**
+ * CreateDiskCacheEntry(nsCacheEntry * entry)
+ *
+ * Prepare an nsCacheEntry for writing to disk
+ */
+nsDiskCacheEntry *
+nsDiskCacheMap::CreateDiskCacheEntry(nsDiskCacheBinding * binding,
+ uint32_t * aSize)
+{
+ nsCacheEntry * entry = binding->mCacheEntry;
+ if (!entry) return nullptr;
+
+ // Store security info, if it is serializable
+ nsCOMPtr<nsISupports> infoObj = entry->SecurityInfo();
+ nsCOMPtr<nsISerializable> serializable = do_QueryInterface(infoObj);
+ if (infoObj && !serializable) return nullptr;
+ if (serializable) {
+ nsCString info;
+ nsresult rv = NS_SerializeToString(serializable, info);
+ if (NS_FAILED(rv)) return nullptr;
+ rv = entry->SetMetaDataElement("security-info", info.get());
+ if (NS_FAILED(rv)) return nullptr;
+ }
+
+ uint32_t keySize = entry->Key()->Length() + 1;
+ uint32_t metaSize = entry->MetaDataSize();
+ uint32_t size = sizeof(nsDiskCacheEntry) + keySize + metaSize;
+
+ if (aSize) *aSize = size;
+
+ nsresult rv = EnsureBuffer(size);
+ if (NS_FAILED(rv)) return nullptr;
+
+ nsDiskCacheEntry *diskEntry = (nsDiskCacheEntry *)mBuffer;
+ diskEntry->mHeaderVersion = nsDiskCache::kCurrentVersion;
+ diskEntry->mMetaLocation = binding->mRecord.MetaLocation();
+ diskEntry->mFetchCount = entry->FetchCount();
+ diskEntry->mLastFetched = entry->LastFetched();
+ diskEntry->mLastModified = entry->LastModified();
+ diskEntry->mExpirationTime = entry->ExpirationTime();
+ diskEntry->mDataSize = entry->DataSize();
+ diskEntry->mKeySize = keySize;
+ diskEntry->mMetaDataSize = metaSize;
+
+ memcpy(diskEntry->Key(), entry->Key()->get(), keySize);
+
+ rv = entry->FlattenMetaData(diskEntry->MetaData(), metaSize);
+ if (NS_FAILED(rv)) return nullptr;
+
+ return diskEntry;
+}
+
+
+nsresult
+nsDiskCacheMap::WriteDiskCacheEntry(nsDiskCacheBinding * binding)
+{
+ CACHE_LOG_DEBUG(("CACHE: WriteDiskCacheEntry [%x]\n",
+ binding->mRecord.HashNumber()));
+
+ nsresult rv = NS_OK;
+ uint32_t size;
+ nsDiskCacheEntry * diskEntry = CreateDiskCacheEntry(binding, &size);
+ if (!diskEntry) return NS_ERROR_UNEXPECTED;
+
+ uint32_t fileIndex = CalculateFileIndex(size);
+
+ // Deallocate old storage if necessary
+ if (binding->mRecord.MetaLocationInitialized()) {
+ // we have existing storage
+
+ if ((binding->mRecord.MetaFile() == 0) &&
+ (fileIndex == 0)) { // keeping the separate file
+ // just decrement total
+ DecrementTotalSize(binding->mRecord.MetaFileSize());
+ NS_ASSERTION(binding->mRecord.MetaFileGeneration() == binding->mGeneration,
+ "generations out of sync");
+ } else {
+ rv = DeleteStorage(&binding->mRecord, nsDiskCache::kMetaData);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ binding->mRecord.SetEvictionRank(ULONG_MAX - SecondsFromPRTime(PR_Now()));
+ // write entry data to disk cache block file
+ diskEntry->Swap();
+
+ if (fileIndex != 0) {
+ while (1) {
+ uint32_t blockSize = GetBlockSizeForIndex(fileIndex);
+ uint32_t blocks = ((size - 1) / blockSize) + 1;
+
+ int32_t startBlock;
+ rv = mBlockFile[fileIndex - 1].WriteBlocks(diskEntry, size, blocks,
+ &startBlock);
+ if (NS_SUCCEEDED(rv)) {
+ // update binding and cache map record
+ binding->mRecord.SetMetaBlocks(fileIndex, startBlock, blocks);
+
+ rv = UpdateRecord(&binding->mRecord);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // XXX we should probably write out bucket ourselves
+
+ IncrementTotalSize(blocks, blockSize);
+ break;
+ }
+
+ if (fileIndex == kNumBlockFiles) {
+ fileIndex = 0; // write data to separate file
+ break;
+ }
+
+ // try next block file
+ fileIndex++;
+ }
+ }
+
+ if (fileIndex == 0) {
+ // Write entry data to separate file
+ uint32_t metaFileSizeK = ((size + 0x03FF) >> 10); // round up to nearest 1k
+ if (metaFileSizeK > kMaxDataSizeK)
+ metaFileSizeK = kMaxDataSizeK;
+
+ binding->mRecord.SetMetaFileGeneration(binding->mGeneration);
+ binding->mRecord.SetMetaFileSize(metaFileSizeK);
+ rv = UpdateRecord(&binding->mRecord);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> localFile;
+ rv = GetLocalFileForDiskCacheRecord(&binding->mRecord,
+ nsDiskCache::kMetaData,
+ true,
+ getter_AddRefs(localFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // open the file
+ PRFileDesc * fd;
+ // open the file - restricted to user, the data could be confidential
+ rv = localFile->OpenNSPRFileDesc(PR_RDWR | PR_TRUNCATE | PR_CREATE_FILE, 00600, &fd);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // write the file
+ int32_t bytesWritten = PR_Write(fd, diskEntry, size);
+
+ PRStatus err = PR_Close(fd);
+ if ((bytesWritten != (int32_t)size) || (err != PR_SUCCESS)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ IncrementTotalSize(metaFileSizeK);
+ }
+
+ return rv;
+}
+
+
+nsresult
+nsDiskCacheMap::ReadDataCacheBlocks(nsDiskCacheBinding * binding, char * buffer, uint32_t size)
+{
+ CACHE_LOG_DEBUG(("CACHE: ReadDataCacheBlocks [%x size=%u]\n",
+ binding->mRecord.HashNumber(), size));
+
+ uint32_t fileIndex = binding->mRecord.DataFile();
+ int32_t readSize = size;
+
+ nsresult rv = mBlockFile[fileIndex - 1].ReadBlocks(buffer,
+ binding->mRecord.DataStartBlock(),
+ binding->mRecord.DataBlockCount(),
+ &readSize);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (readSize < (int32_t)size) {
+ rv = NS_ERROR_UNEXPECTED;
+ }
+ return rv;
+}
+
+
+nsresult
+nsDiskCacheMap::WriteDataCacheBlocks(nsDiskCacheBinding * binding, char * buffer, uint32_t size)
+{
+ CACHE_LOG_DEBUG(("CACHE: WriteDataCacheBlocks [%x size=%u]\n",
+ binding->mRecord.HashNumber(), size));
+
+ nsresult rv = NS_OK;
+
+ // determine block file & number of blocks
+ uint32_t fileIndex = CalculateFileIndex(size);
+ uint32_t blockCount = 0;
+ int32_t startBlock = 0;
+
+ if (size > 0) {
+ // if fileIndex is 0, bad things happen below, which makes gcc 4.7
+ // complain, but it's not supposed to happen. See bug 854105.
+ MOZ_ASSERT(fileIndex);
+ while (fileIndex) {
+ uint32_t blockSize = GetBlockSizeForIndex(fileIndex);
+ blockCount = ((size - 1) / blockSize) + 1;
+
+ rv = mBlockFile[fileIndex - 1].WriteBlocks(buffer, size, blockCount,
+ &startBlock);
+ if (NS_SUCCEEDED(rv)) {
+ IncrementTotalSize(blockCount, blockSize);
+ break;
+ }
+
+ if (fileIndex == kNumBlockFiles)
+ return rv;
+
+ fileIndex++;
+ }
+ }
+
+ // update binding and cache map record
+ binding->mRecord.SetDataBlocks(fileIndex, startBlock, blockCount);
+ if (!binding->mDoomed) {
+ rv = UpdateRecord(&binding->mRecord);
+ }
+ return rv;
+}
+
+
+nsresult
+nsDiskCacheMap::DeleteStorage(nsDiskCacheRecord * record)
+{
+ nsresult rv1 = DeleteStorage(record, nsDiskCache::kData);
+ nsresult rv2 = DeleteStorage(record, nsDiskCache::kMetaData);
+ return NS_FAILED(rv1) ? rv1 : rv2;
+}
+
+
+nsresult
+nsDiskCacheMap::DeleteStorage(nsDiskCacheRecord * record, bool metaData)
+{
+ CACHE_LOG_DEBUG(("CACHE: DeleteStorage [%x %u]\n", record->HashNumber(),
+ metaData));
+
+ nsresult rv = NS_ERROR_UNEXPECTED;
+ uint32_t fileIndex = metaData ? record->MetaFile() : record->DataFile();
+ nsCOMPtr<nsIFile> file;
+
+ if (fileIndex == 0) {
+ // delete the file
+ uint32_t sizeK = metaData ? record->MetaFileSize() : record->DataFileSize();
+ // XXX if sizeK == USHRT_MAX, stat file for actual size
+
+ rv = GetFileForDiskCacheRecord(record, metaData, false, getter_AddRefs(file));
+ if (NS_SUCCEEDED(rv)) {
+ rv = file->Remove(false); // false == non-recursive
+ }
+ DecrementTotalSize(sizeK);
+
+ } else if (fileIndex < (kNumBlockFiles + 1)) {
+ // deallocate blocks
+ uint32_t startBlock = metaData ? record->MetaStartBlock() : record->DataStartBlock();
+ uint32_t blockCount = metaData ? record->MetaBlockCount() : record->DataBlockCount();
+
+ rv = mBlockFile[fileIndex - 1].DeallocateBlocks(startBlock, blockCount);
+ DecrementTotalSize(blockCount, GetBlockSizeForIndex(fileIndex));
+ }
+ if (metaData) record->ClearMetaLocation();
+ else record->ClearDataLocation();
+
+ return rv;
+}
+
+
+nsresult
+nsDiskCacheMap::GetFileForDiskCacheRecord(nsDiskCacheRecord * record,
+ bool meta,
+ bool createPath,
+ nsIFile ** result)
+{
+ if (!mCacheDirectory) return NS_ERROR_NOT_AVAILABLE;
+
+ nsCOMPtr<nsIFile> file;
+ nsresult rv = mCacheDirectory->Clone(getter_AddRefs(file));
+ if (NS_FAILED(rv)) return rv;
+
+ uint32_t hash = record->HashNumber();
+
+ // The file is stored under subdirectories according to the hash number:
+ // 0x01234567 -> 0/12/
+ rv = file->AppendNative(nsPrintfCString("%X", hash >> 28));
+ if (NS_FAILED(rv)) return rv;
+ rv = file->AppendNative(nsPrintfCString("%02X", (hash >> 20) & 0xFF));
+ if (NS_FAILED(rv)) return rv;
+
+ bool exists;
+ if (createPath && (NS_FAILED(file->Exists(&exists)) || !exists)) {
+ rv = file->Create(nsIFile::DIRECTORY_TYPE, 0700);
+ if (NS_FAILED(rv)) return rv;
+ }
+
+ int16_t generation = record->Generation();
+ char name[32];
+ // Cut the beginning of the hash that was used in the path
+ ::SprintfLiteral(name, "%05X%c%02X", hash & 0xFFFFF, (meta ? 'm' : 'd'),
+ generation);
+ rv = file->AppendNative(nsDependentCString(name));
+ if (NS_FAILED(rv)) return rv;
+
+ NS_IF_ADDREF(*result = file);
+ return rv;
+}
+
+
+nsresult
+nsDiskCacheMap::GetLocalFileForDiskCacheRecord(nsDiskCacheRecord * record,
+ bool meta,
+ bool createPath,
+ nsIFile ** result)
+{
+ nsCOMPtr<nsIFile> file;
+ nsresult rv = GetFileForDiskCacheRecord(record,
+ meta,
+ createPath,
+ getter_AddRefs(file));
+ if (NS_FAILED(rv)) return rv;
+
+ NS_IF_ADDREF(*result = file);
+ return rv;
+}
+
+
+nsresult
+nsDiskCacheMap::GetBlockFileForIndex(uint32_t index, nsIFile ** result)
+{
+ if (!mCacheDirectory) return NS_ERROR_NOT_AVAILABLE;
+
+ nsCOMPtr<nsIFile> file;
+ nsresult rv = mCacheDirectory->Clone(getter_AddRefs(file));
+ if (NS_FAILED(rv)) return rv;
+
+ char name[32];
+ ::SprintfLiteral(name, "_CACHE_%03d_", index + 1);
+ rv = file->AppendNative(nsDependentCString(name));
+ if (NS_FAILED(rv)) return rv;
+
+ NS_IF_ADDREF(*result = file);
+
+ return rv;
+}
+
+
+uint32_t
+nsDiskCacheMap::CalculateFileIndex(uint32_t size)
+{
+ // We prefer to use block file with larger block if the wasted space would
+ // be the same. E.g. store entry with size of 3073 bytes in 1 4K-block
+ // instead of in 4 1K-blocks.
+
+ if (size <= 3 * BLOCK_SIZE_FOR_INDEX(1)) return 1;
+ if (size <= 3 * BLOCK_SIZE_FOR_INDEX(2)) return 2;
+ if (size <= 4 * BLOCK_SIZE_FOR_INDEX(3)) return 3;
+ return 0;
+}
+
+nsresult
+nsDiskCacheMap::EnsureBuffer(uint32_t bufSize)
+{
+ if (mBufferSize < bufSize) {
+ char * buf = (char *)PR_REALLOC(mBuffer, bufSize);
+ if (!buf) {
+ mBufferSize = 0;
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ mBuffer = buf;
+ mBufferSize = bufSize;
+ }
+ return NS_OK;
+}
+
+void
+nsDiskCacheMap::NotifyCapacityChange(uint32_t capacity)
+{
+ // Heuristic 1. average cache entry size is probably around 1KB
+ // Heuristic 2. we don't want more than 32MB reserved to store the record
+ // map in memory.
+ const int32_t RECORD_COUNT_LIMIT = 32 * 1024 * 1024 / sizeof(nsDiskCacheRecord);
+ int32_t maxRecordCount = std::min(int32_t(capacity), RECORD_COUNT_LIMIT);
+ if (mMaxRecordCount < maxRecordCount) {
+ // We can only grow
+ mMaxRecordCount = maxRecordCount;
+ }
+}
+
+size_t
+nsDiskCacheMap::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf)
+{
+ size_t usage = aMallocSizeOf(mRecordArray);
+
+ usage += aMallocSizeOf(mBuffer);
+ usage += aMallocSizeOf(mMapFD);
+ usage += aMallocSizeOf(mCleanFD);
+ usage += aMallocSizeOf(mCacheDirectory);
+ usage += aMallocSizeOf(mCleanCacheTimer);
+
+ for (int i = 0; i < kNumBlockFiles; i++) {
+ usage += mBlockFile[i].SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ return usage;
+}
+
+nsresult
+nsDiskCacheMap::InitCacheClean(nsIFile * cacheDirectory,
+ nsDiskCache::CorruptCacheInfo * corruptInfo)
+{
+ // The _CACHE_CLEAN_ file will be used in the future to determine
+ // if the cache is clean or not.
+ bool cacheCleanFileExists = false;
+ nsCOMPtr<nsIFile> cacheCleanFile;
+ nsresult rv = cacheDirectory->GetParent(getter_AddRefs(cacheCleanFile));
+ if (NS_SUCCEEDED(rv)) {
+ rv = cacheCleanFile->AppendNative(
+ NS_LITERAL_CSTRING("_CACHE_CLEAN_"));
+ if (NS_SUCCEEDED(rv)) {
+ // Check if the file already exists, if it does, we will later read the
+ // value and report it to telemetry.
+ cacheCleanFile->Exists(&cacheCleanFileExists);
+ }
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Could not build cache clean file path");
+ *corruptInfo = nsDiskCache::kCacheCleanFilePathError;
+ return rv;
+ }
+
+ // Make sure the _CACHE_CLEAN_ file exists
+ rv = cacheCleanFile->OpenNSPRFileDesc(PR_RDWR | PR_CREATE_FILE,
+ 00600, &mCleanFD);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Could not open cache clean file");
+ *corruptInfo = nsDiskCache::kCacheCleanOpenFileError;
+ return rv;
+ }
+
+ if (cacheCleanFileExists) {
+ char clean = '0';
+ int32_t bytesRead = PR_Read(mCleanFD, &clean, 1);
+ if (bytesRead != 1) {
+ NS_WARNING("Could not read _CACHE_CLEAN_ file contents");
+ }
+ }
+
+ // Create a timer that will be used to validate the cache
+ // as long as an activity threshold was met
+ mCleanCacheTimer = do_CreateInstance("@mozilla.org/timer;1", &rv);
+ if (NS_SUCCEEDED(rv)) {
+ mCleanCacheTimer->SetTarget(nsCacheService::GlobalInstance()->mCacheIOThread);
+ rv = ResetCacheTimer();
+ }
+
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Could not create cache clean timer");
+ mCleanCacheTimer = nullptr;
+ *corruptInfo = nsDiskCache::kCacheCleanTimerError;
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsDiskCacheMap::WriteCacheClean(bool clean)
+{
+ nsCacheService::AssertOwnsLock();
+ if (!mCleanFD) {
+ NS_WARNING("Cache clean file is not open!");
+ return NS_ERROR_FAILURE;
+ }
+
+ CACHE_LOG_DEBUG(("CACHE: WriteCacheClean: %d\n", clean? 1 : 0));
+ // I'm using a simple '1' or '0' to denote cache clean
+ // since it can be edited easily by any text editor for testing.
+ char data = clean? '1' : '0';
+ int32_t filePos = PR_Seek(mCleanFD, 0, PR_SEEK_SET);
+ if (filePos != 0) {
+ NS_WARNING("Could not seek in cache clean file!");
+ return NS_ERROR_FAILURE;
+ }
+ int32_t bytesWritten = PR_Write(mCleanFD, &data, 1);
+ if (bytesWritten != 1) {
+ NS_WARNING("Could not write cache clean file!");
+ return NS_ERROR_FAILURE;
+ }
+ PRStatus err = PR_Sync(mCleanFD);
+ if (err != PR_SUCCESS) {
+ NS_WARNING("Could not flush cache clean file!");
+ }
+
+ return NS_OK;
+}
+
+nsresult
+nsDiskCacheMap::InvalidateCache()
+{
+ nsCacheService::AssertOwnsLock();
+ CACHE_LOG_DEBUG(("CACHE: InvalidateCache\n"));
+ nsresult rv;
+
+ if (!mIsDirtyCacheFlushed) {
+ rv = WriteCacheClean(false);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ mIsDirtyCacheFlushed = true;
+ }
+
+ rv = ResetCacheTimer();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+nsresult
+nsDiskCacheMap::ResetCacheTimer(int32_t timeout)
+{
+ mCleanCacheTimer->Cancel();
+ nsresult rv =
+ mCleanCacheTimer->InitWithFuncCallback(RevalidateTimerCallback,
+ nullptr, timeout,
+ nsITimer::TYPE_ONE_SHOT);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mLastInvalidateTime = PR_IntervalNow();
+
+ return rv;
+}
+
+void
+nsDiskCacheMap::RevalidateTimerCallback(nsITimer *aTimer, void *arg)
+{
+ nsCacheServiceAutoLock lock;
+ if (!nsCacheService::gService->mDiskDevice ||
+ !nsCacheService::gService->mDiskDevice->Initialized()) {
+ return;
+ }
+
+ nsDiskCacheMap *diskCacheMap =
+ &nsCacheService::gService->mDiskDevice->mCacheMap;
+
+ // If we have less than kRevalidateCacheTimeout since the last timer was
+ // issued then another thread called InvalidateCache. This won't catch
+ // all cases where we wanted to cancel the timer, but under the lock it
+ // is always OK to revalidate as long as IsCacheInSafeState() returns
+ // true. We just want to avoid revalidating when we can to reduce IO
+ // and this check will do that.
+ uint32_t delta =
+ PR_IntervalToMilliseconds(PR_IntervalNow() -
+ diskCacheMap->mLastInvalidateTime) +
+ kRevalidateCacheTimeoutTolerance;
+ if (delta < kRevalidateCacheTimeout) {
+ diskCacheMap->ResetCacheTimer();
+ return;
+ }
+
+ nsresult rv = diskCacheMap->RevalidateCache();
+ if (NS_FAILED(rv)) {
+ diskCacheMap->ResetCacheTimer(kRevalidateCacheErrorTimeout);
+ }
+}
+
+bool
+nsDiskCacheMap::IsCacheInSafeState()
+{
+ return nsCacheService::GlobalInstance()->IsDoomListEmpty();
+}
+
+nsresult
+nsDiskCacheMap::RevalidateCache()
+{
+ CACHE_LOG_DEBUG(("CACHE: RevalidateCache\n"));
+ nsresult rv;
+
+ if (!IsCacheInSafeState()) {
+ CACHE_LOG_DEBUG(("CACHE: Revalidation should not performed because "
+ "cache not in a safe state\n"));
+ // Normally we would return an error here, but there is a bug where
+ // the doom list sometimes gets an entry 'stuck' and doens't clear it
+ // until browser shutdown. So we allow revalidation for the time being
+ // to get proper telemetry data of how much the cache corruption plan
+ // would help.
+ }
+
+ // If telemetry data shows it is worth it, we'll be flushing headers and
+ // records before flushing the clean cache file.
+
+ // Write out the _CACHE_CLEAN_ file with '1'
+ rv = WriteCacheClean(true);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ mIsDirtyCacheFlushed = false;
+
+ return NS_OK;
+}