1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /* vim:set ts=4 sw=4 sts=4 cin et: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include "nsCache.h"
8 #include "nsDiskCacheMap.h"
9 #include "nsDiskCacheBinding.h"
10 #include "nsDiskCacheEntry.h"
11 #include "nsDiskCacheDevice.h"
12 #include "nsCacheService.h"
13 
14 #include <string.h>
15 #include "nsPrintfCString.h"
16 
17 #include "nsISerializable.h"
18 #include "nsSerializationHelper.h"
19 
20 #include "mozilla/MemoryReporting.h"
21 #include "mozilla/Sprintf.h"
22 #include "mozilla/Telemetry.h"
23 #include <algorithm>
24 
25 using namespace mozilla;
26 
27 /******************************************************************************
28  *  nsDiskCacheMap
29  *****************************************************************************/
30 
31 /**
32  *  File operations
33  */
34 
Open(nsIFile * cacheDirectory,nsDiskCache::CorruptCacheInfo * corruptInfo)35 nsresult nsDiskCacheMap::Open(nsIFile *cacheDirectory,
36                               nsDiskCache::CorruptCacheInfo *corruptInfo) {
37   NS_ENSURE_ARG_POINTER(corruptInfo);
38 
39   // Assume we have an unexpected error until we find otherwise.
40   *corruptInfo = nsDiskCache::kUnexpectedError;
41   NS_ENSURE_ARG_POINTER(cacheDirectory);
42   if (mMapFD) return NS_ERROR_ALREADY_INITIALIZED;
43 
44   mCacheDirectory = cacheDirectory;  // save a reference for ourselves
45 
46   // create nsIFile for _CACHE_MAP_
47   nsresult rv;
48   nsCOMPtr<nsIFile> file;
49   rv = cacheDirectory->Clone(getter_AddRefs(file));
50   rv = file->AppendNative(NS_LITERAL_CSTRING("_CACHE_MAP_"));
51   NS_ENSURE_SUCCESS(rv, rv);
52 
53   // open the file - restricted to user, the data could be confidential
54   rv = file->OpenNSPRFileDesc(PR_RDWR | PR_CREATE_FILE, 00600, &mMapFD);
55   if (NS_FAILED(rv)) {
56     *corruptInfo = nsDiskCache::kOpenCacheMapError;
57     NS_WARNING("Could not open cache map file");
58     return NS_ERROR_FILE_CORRUPTED;
59   }
60 
61   bool cacheFilesExist = CacheFilesExist();
62   rv = NS_ERROR_FILE_CORRUPTED;  // presume the worst
63   uint32_t mapSize = PR_Available(mMapFD);
64 
65   if (NS_FAILED(InitCacheClean(cacheDirectory, corruptInfo))) {
66     // corruptInfo is set in the call to InitCacheClean
67     goto error_exit;
68   }
69 
70   // check size of map file
71   if (mapSize == 0) {  // creating a new _CACHE_MAP_
72 
73     // block files shouldn't exist if we're creating the _CACHE_MAP_
74     if (cacheFilesExist) {
75       *corruptInfo = nsDiskCache::kBlockFilesShouldNotExist;
76       goto error_exit;
77     }
78 
79     if (NS_FAILED(CreateCacheSubDirectories())) {
80       *corruptInfo = nsDiskCache::kCreateCacheSubdirectories;
81       goto error_exit;
82     }
83 
84     // create the file - initialize in memory
85     memset(&mHeader, 0, sizeof(nsDiskCacheHeader));
86     mHeader.mVersion = nsDiskCache::kCurrentVersion;
87     mHeader.mRecordCount = kMinRecordCount;
88     mRecordArray = (nsDiskCacheRecord *)calloc(mHeader.mRecordCount,
89                                                sizeof(nsDiskCacheRecord));
90     if (!mRecordArray) {
91       *corruptInfo = nsDiskCache::kOutOfMemory;
92       rv = NS_ERROR_OUT_OF_MEMORY;
93       goto error_exit;
94     }
95   } else if (mapSize >=
96              sizeof(nsDiskCacheHeader)) {  // read existing _CACHE_MAP_
97 
98     // if _CACHE_MAP_ exists, so should the block files
99     if (!cacheFilesExist) {
100       *corruptInfo = nsDiskCache::kBlockFilesShouldExist;
101       goto error_exit;
102     }
103 
104     CACHE_LOG_DEBUG(
105         ("CACHE: nsDiskCacheMap::Open [this=%p] reading map", this));
106 
107     // read the header
108     uint32_t bytesRead = PR_Read(mMapFD, &mHeader, sizeof(nsDiskCacheHeader));
109     if (sizeof(nsDiskCacheHeader) != bytesRead) {
110       *corruptInfo = nsDiskCache::kHeaderSizeNotRead;
111       goto error_exit;
112     }
113     mHeader.Unswap();
114 
115     if (mHeader.mIsDirty) {
116       *corruptInfo = nsDiskCache::kHeaderIsDirty;
117       goto error_exit;
118     }
119 
120     if (mHeader.mVersion != nsDiskCache::kCurrentVersion) {
121       *corruptInfo = nsDiskCache::kVersionMismatch;
122       goto error_exit;
123     }
124 
125     uint32_t recordArraySize = mHeader.mRecordCount * sizeof(nsDiskCacheRecord);
126     if (mapSize < recordArraySize + sizeof(nsDiskCacheHeader)) {
127       *corruptInfo = nsDiskCache::kRecordsIncomplete;
128       goto error_exit;
129     }
130 
131     // Get the space for the records
132     mRecordArray = (nsDiskCacheRecord *)malloc(recordArraySize);
133     if (!mRecordArray) {
134       *corruptInfo = nsDiskCache::kOutOfMemory;
135       rv = NS_ERROR_OUT_OF_MEMORY;
136       goto error_exit;
137     }
138 
139     // Read the records
140     bytesRead = PR_Read(mMapFD, mRecordArray, recordArraySize);
141     if (bytesRead < recordArraySize) {
142       *corruptInfo = nsDiskCache::kNotEnoughToRead;
143       goto error_exit;
144     }
145 
146     // Unswap each record
147     int32_t total = 0;
148     for (int32_t i = 0; i < mHeader.mRecordCount; ++i) {
149       if (mRecordArray[i].HashNumber()) {
150 #if defined(IS_LITTLE_ENDIAN)
151         mRecordArray[i].Unswap();
152 #endif
153         total++;
154       }
155     }
156 
157     // verify entry count
158     if (total != mHeader.mEntryCount) {
159       *corruptInfo = nsDiskCache::kEntryCountIncorrect;
160       goto error_exit;
161     }
162 
163   } else {
164     *corruptInfo = nsDiskCache::kHeaderIncomplete;
165     goto error_exit;
166   }
167 
168   rv = OpenBlockFiles(corruptInfo);
169   if (NS_FAILED(rv)) {
170     // corruptInfo is set in the call to OpenBlockFiles
171     goto error_exit;
172   }
173 
174   // set dirty bit and flush header
175   mHeader.mIsDirty = true;
176   rv = FlushHeader();
177   if (NS_FAILED(rv)) {
178     *corruptInfo = nsDiskCache::kFlushHeaderError;
179     goto error_exit;
180   }
181 
182   Telemetry::Accumulate(Telemetry::HTTP_DISK_CACHE_OVERHEAD,
183                         (uint32_t)SizeOfExcludingThis(moz_malloc_size_of));
184 
185   *corruptInfo = nsDiskCache::kNotCorrupt;
186   return NS_OK;
187 
188 error_exit:
189   (void)Close(false);
190 
191   return rv;
192 }
193 
Close(bool flush)194 nsresult nsDiskCacheMap::Close(bool flush) {
195   nsCacheService::AssertOwnsLock();
196   nsresult rv = NS_OK;
197 
198   // Cancel any pending cache validation event, the FlushRecords call below
199   // will validate the cache.
200   if (mCleanCacheTimer) {
201     mCleanCacheTimer->Cancel();
202   }
203 
204   // If cache map file and its block files are still open, close them
205   if (mMapFD) {
206     // close block files
207     rv = CloseBlockFiles(flush);
208     if (NS_SUCCEEDED(rv) && flush && mRecordArray) {
209       // write the map records
210       rv = FlushRecords(false);  // don't bother swapping buckets back
211       if (NS_SUCCEEDED(rv)) {
212         // clear dirty bit
213         mHeader.mIsDirty = false;
214         rv = FlushHeader();
215       }
216     }
217     if ((PR_Close(mMapFD) != PR_SUCCESS) && (NS_SUCCEEDED(rv)))
218       rv = NS_ERROR_UNEXPECTED;
219 
220     mMapFD = nullptr;
221   }
222 
223   if (mCleanFD) {
224     PR_Close(mCleanFD);
225     mCleanFD = nullptr;
226   }
227 
228   free(mRecordArray);
229   mRecordArray = nullptr;
230   free(mBuffer);
231   mBuffer = nullptr;
232   mBufferSize = 0;
233   return rv;
234 }
235 
Trim()236 nsresult nsDiskCacheMap::Trim() {
237   nsresult rv, rv2 = NS_OK;
238   for (int i = 0; i < kNumBlockFiles; ++i) {
239     rv = mBlockFile[i].Trim();
240     if (NS_FAILED(rv)) rv2 = rv;  // if one or more errors, report at least one
241   }
242   // Try to shrink the records array
243   rv = ShrinkRecords();
244   if (NS_FAILED(rv)) rv2 = rv;  // if one or more errors, report at least one
245   return rv2;
246 }
247 
FlushHeader()248 nsresult nsDiskCacheMap::FlushHeader() {
249   if (!mMapFD) return NS_ERROR_NOT_AVAILABLE;
250 
251   // seek to beginning of cache map
252   int32_t filePos = PR_Seek(mMapFD, 0, PR_SEEK_SET);
253   if (filePos != 0) return NS_ERROR_UNEXPECTED;
254 
255   // write the header
256   mHeader.Swap();
257   int32_t bytesWritten = PR_Write(mMapFD, &mHeader, sizeof(nsDiskCacheHeader));
258   mHeader.Unswap();
259   if (sizeof(nsDiskCacheHeader) != bytesWritten) {
260     return NS_ERROR_UNEXPECTED;
261   }
262 
263   PRStatus err = PR_Sync(mMapFD);
264   if (err != PR_SUCCESS) return NS_ERROR_UNEXPECTED;
265 
266   // If we have a clean header then revalidate the cache clean file
267   if (!mHeader.mIsDirty) {
268     RevalidateCache();
269   }
270 
271   return NS_OK;
272 }
273 
FlushRecords(bool unswap)274 nsresult nsDiskCacheMap::FlushRecords(bool unswap) {
275   if (!mMapFD) return NS_ERROR_NOT_AVAILABLE;
276 
277   // seek to beginning of buckets
278   int32_t filePos = PR_Seek(mMapFD, sizeof(nsDiskCacheHeader), PR_SEEK_SET);
279   if (filePos != sizeof(nsDiskCacheHeader)) return NS_ERROR_UNEXPECTED;
280 
281 #if defined(IS_LITTLE_ENDIAN)
282   // Swap each record
283   for (int32_t i = 0; i < mHeader.mRecordCount; ++i) {
284     if (mRecordArray[i].HashNumber()) mRecordArray[i].Swap();
285   }
286 #endif
287 
288   int32_t recordArraySize = sizeof(nsDiskCacheRecord) * mHeader.mRecordCount;
289 
290   int32_t bytesWritten = PR_Write(mMapFD, mRecordArray, recordArraySize);
291   if (bytesWritten != recordArraySize) return NS_ERROR_UNEXPECTED;
292 
293 #if defined(IS_LITTLE_ENDIAN)
294   if (unswap) {
295     // Unswap each record
296     for (int32_t i = 0; i < mHeader.mRecordCount; ++i) {
297       if (mRecordArray[i].HashNumber()) mRecordArray[i].Unswap();
298     }
299   }
300 #endif
301 
302   return NS_OK;
303 }
304 
305 /**
306  *  Record operations
307  */
308 
GetBucketRank(uint32_t bucketIndex,uint32_t targetRank)309 uint32_t nsDiskCacheMap::GetBucketRank(uint32_t bucketIndex,
310                                        uint32_t targetRank) {
311   nsDiskCacheRecord *records = GetFirstRecordInBucket(bucketIndex);
312   uint32_t rank = 0;
313 
314   for (int i = mHeader.mBucketUsage[bucketIndex] - 1; i >= 0; i--) {
315     if ((rank < records[i].EvictionRank()) &&
316         ((targetRank == 0) || (records[i].EvictionRank() < targetRank)))
317       rank = records[i].EvictionRank();
318   }
319   return rank;
320 }
321 
GrowRecords()322 nsresult nsDiskCacheMap::GrowRecords() {
323   if (mHeader.mRecordCount >= mMaxRecordCount) return NS_OK;
324   CACHE_LOG_DEBUG(("CACHE: GrowRecords\n"));
325 
326   // Resize the record array
327   int32_t newCount = mHeader.mRecordCount << 1;
328   if (newCount > mMaxRecordCount) newCount = mMaxRecordCount;
329   nsDiskCacheRecord *newArray = (nsDiskCacheRecord *)realloc(
330       mRecordArray, newCount * sizeof(nsDiskCacheRecord));
331   if (!newArray) return NS_ERROR_OUT_OF_MEMORY;
332 
333   // Space out the buckets
334   uint32_t oldRecordsPerBucket = GetRecordsPerBucket();
335   uint32_t newRecordsPerBucket = newCount / kBuckets;
336   // Work from back to space out each bucket to the new array
337   for (int bucketIndex = kBuckets - 1; bucketIndex >= 0; --bucketIndex) {
338     // Move bucket
339     nsDiskCacheRecord *newRecords =
340         newArray + bucketIndex * newRecordsPerBucket;
341     const uint32_t count = mHeader.mBucketUsage[bucketIndex];
342     memmove(newRecords, newArray + bucketIndex * oldRecordsPerBucket,
343             count * sizeof(nsDiskCacheRecord));
344     // clear unused records
345     memset(newRecords + count, 0,
346            (newRecordsPerBucket - count) * sizeof(nsDiskCacheRecord));
347   }
348 
349   // Set as the new record array
350   mRecordArray = newArray;
351   mHeader.mRecordCount = newCount;
352 
353   InvalidateCache();
354 
355   return NS_OK;
356 }
357 
ShrinkRecords()358 nsresult nsDiskCacheMap::ShrinkRecords() {
359   if (mHeader.mRecordCount <= kMinRecordCount) return NS_OK;
360   CACHE_LOG_DEBUG(("CACHE: ShrinkRecords\n"));
361 
362   // Verify if we can shrink the record array: all buckets must be less than
363   // 1/2 filled
364   uint32_t maxUsage = 0, bucketIndex;
365   for (bucketIndex = 0; bucketIndex < kBuckets; ++bucketIndex) {
366     if (maxUsage < mHeader.mBucketUsage[bucketIndex])
367       maxUsage = mHeader.mBucketUsage[bucketIndex];
368   }
369   // Determine new bucket size, halve size until maxUsage
370   uint32_t oldRecordsPerBucket = GetRecordsPerBucket();
371   uint32_t newRecordsPerBucket = oldRecordsPerBucket;
372   while (maxUsage < (newRecordsPerBucket >> 1)) newRecordsPerBucket >>= 1;
373   if (newRecordsPerBucket < (kMinRecordCount / kBuckets))
374     newRecordsPerBucket = (kMinRecordCount / kBuckets);
375   NS_ASSERTION(newRecordsPerBucket <= oldRecordsPerBucket,
376                "ShrinkRecords() can't grow records!");
377   if (newRecordsPerBucket == oldRecordsPerBucket) return NS_OK;
378   // Move the buckets close to each other
379   for (bucketIndex = 1; bucketIndex < kBuckets; ++bucketIndex) {
380     // Move bucket
381     memmove(mRecordArray + bucketIndex * newRecordsPerBucket,
382             mRecordArray + bucketIndex * oldRecordsPerBucket,
383             newRecordsPerBucket * sizeof(nsDiskCacheRecord));
384   }
385 
386   // Shrink the record array memory block itself
387   uint32_t newCount = newRecordsPerBucket * kBuckets;
388   nsDiskCacheRecord *newArray = (nsDiskCacheRecord *)realloc(
389       mRecordArray, newCount * sizeof(nsDiskCacheRecord));
390   if (!newArray) return NS_ERROR_OUT_OF_MEMORY;
391 
392   // Set as the new record array
393   mRecordArray = newArray;
394   mHeader.mRecordCount = newCount;
395 
396   InvalidateCache();
397 
398   return NS_OK;
399 }
400 
AddRecord(nsDiskCacheRecord * mapRecord,nsDiskCacheRecord * oldRecord)401 nsresult nsDiskCacheMap::AddRecord(nsDiskCacheRecord *mapRecord,
402                                    nsDiskCacheRecord *oldRecord) {
403   CACHE_LOG_DEBUG(("CACHE: AddRecord [%x]\n", mapRecord->HashNumber()));
404 
405   const uint32_t hashNumber = mapRecord->HashNumber();
406   const uint32_t bucketIndex = GetBucketIndex(hashNumber);
407   const uint32_t count = mHeader.mBucketUsage[bucketIndex];
408 
409   oldRecord->SetHashNumber(0);  // signify no record
410 
411   if (count == GetRecordsPerBucket()) {
412     // Ignore failure to grow the record space, we will then reuse old records
413     GrowRecords();
414   }
415 
416   nsDiskCacheRecord *records = GetFirstRecordInBucket(bucketIndex);
417   if (count < GetRecordsPerBucket()) {
418     // stick the new record at the end
419     records[count] = *mapRecord;
420     mHeader.mEntryCount++;
421     mHeader.mBucketUsage[bucketIndex]++;
422     if (mHeader.mEvictionRank[bucketIndex] < mapRecord->EvictionRank())
423       mHeader.mEvictionRank[bucketIndex] = mapRecord->EvictionRank();
424     InvalidateCache();
425   } else {
426     // Find the record with the highest eviction rank
427     nsDiskCacheRecord *mostEvictable = &records[0];
428     for (int i = count - 1; i > 0; i--) {
429       if (records[i].EvictionRank() > mostEvictable->EvictionRank())
430         mostEvictable = &records[i];
431     }
432     *oldRecord = *mostEvictable;  // i == GetRecordsPerBucket(), so
433                                   // evict the mostEvictable
434     *mostEvictable = *mapRecord;  // replace it with the new record
435     // check if we need to update mostEvictable entry in header
436     if (mHeader.mEvictionRank[bucketIndex] < mapRecord->EvictionRank())
437       mHeader.mEvictionRank[bucketIndex] = mapRecord->EvictionRank();
438     if (oldRecord->EvictionRank() >= mHeader.mEvictionRank[bucketIndex])
439       mHeader.mEvictionRank[bucketIndex] = GetBucketRank(bucketIndex, 0);
440     InvalidateCache();
441   }
442 
443   NS_ASSERTION(
444       mHeader.mEvictionRank[bucketIndex] == GetBucketRank(bucketIndex, 0),
445       "eviction rank out of sync");
446   return NS_OK;
447 }
448 
UpdateRecord(nsDiskCacheRecord * mapRecord)449 nsresult nsDiskCacheMap::UpdateRecord(nsDiskCacheRecord *mapRecord) {
450   CACHE_LOG_DEBUG(("CACHE: UpdateRecord [%x]\n", mapRecord->HashNumber()));
451 
452   const uint32_t hashNumber = mapRecord->HashNumber();
453   const uint32_t bucketIndex = GetBucketIndex(hashNumber);
454   nsDiskCacheRecord *records = GetFirstRecordInBucket(bucketIndex);
455 
456   for (int i = mHeader.mBucketUsage[bucketIndex] - 1; i >= 0; i--) {
457     if (records[i].HashNumber() == hashNumber) {
458       const uint32_t oldRank = records[i].EvictionRank();
459 
460       // stick the new record here
461       records[i] = *mapRecord;
462 
463       // update eviction rank in header if necessary
464       if (mHeader.mEvictionRank[bucketIndex] < mapRecord->EvictionRank())
465         mHeader.mEvictionRank[bucketIndex] = mapRecord->EvictionRank();
466       else if (mHeader.mEvictionRank[bucketIndex] == oldRank)
467         mHeader.mEvictionRank[bucketIndex] = GetBucketRank(bucketIndex, 0);
468 
469       InvalidateCache();
470 
471       NS_ASSERTION(
472           mHeader.mEvictionRank[bucketIndex] == GetBucketRank(bucketIndex, 0),
473           "eviction rank out of sync");
474       return NS_OK;
475     }
476   }
477   NS_NOTREACHED("record not found");
478   return NS_ERROR_UNEXPECTED;
479 }
480 
FindRecord(uint32_t hashNumber,nsDiskCacheRecord * result)481 nsresult nsDiskCacheMap::FindRecord(uint32_t hashNumber,
482                                     nsDiskCacheRecord *result) {
483   const uint32_t bucketIndex = GetBucketIndex(hashNumber);
484   nsDiskCacheRecord *records = GetFirstRecordInBucket(bucketIndex);
485 
486   for (int i = mHeader.mBucketUsage[bucketIndex] - 1; i >= 0; i--) {
487     if (records[i].HashNumber() == hashNumber) {
488       *result = records[i];  // copy the record
489       NS_ASSERTION(result->ValidRecord(), "bad cache map record");
490       return NS_OK;
491     }
492   }
493   return NS_ERROR_CACHE_KEY_NOT_FOUND;
494 }
495 
DeleteRecord(nsDiskCacheRecord * mapRecord)496 nsresult nsDiskCacheMap::DeleteRecord(nsDiskCacheRecord *mapRecord) {
497   CACHE_LOG_DEBUG(("CACHE: DeleteRecord [%x]\n", mapRecord->HashNumber()));
498 
499   const uint32_t hashNumber = mapRecord->HashNumber();
500   const uint32_t bucketIndex = GetBucketIndex(hashNumber);
501   nsDiskCacheRecord *records = GetFirstRecordInBucket(bucketIndex);
502   uint32_t last = mHeader.mBucketUsage[bucketIndex] - 1;
503 
504   for (int i = last; i >= 0; i--) {
505     if (records[i].HashNumber() == hashNumber) {
506       // found it, now delete it.
507       uint32_t evictionRank = records[i].EvictionRank();
508       NS_ASSERTION(evictionRank == mapRecord->EvictionRank(),
509                    "evictionRank out of sync");
510       // if not the last record, shift last record into opening
511       records[i] = records[last];
512       records[last].SetHashNumber(0);  // clear last record
513       mHeader.mBucketUsage[bucketIndex] = last;
514       mHeader.mEntryCount--;
515 
516       // update eviction rank
517       uint32_t bucketIndex = GetBucketIndex(mapRecord->HashNumber());
518       if (mHeader.mEvictionRank[bucketIndex] <= evictionRank) {
519         mHeader.mEvictionRank[bucketIndex] = GetBucketRank(bucketIndex, 0);
520       }
521 
522       InvalidateCache();
523 
524       NS_ASSERTION(
525           mHeader.mEvictionRank[bucketIndex] == GetBucketRank(bucketIndex, 0),
526           "eviction rank out of sync");
527       return NS_OK;
528     }
529   }
530   return NS_ERROR_UNEXPECTED;
531 }
532 
VisitEachRecord(uint32_t bucketIndex,nsDiskCacheRecordVisitor * visitor,uint32_t evictionRank)533 int32_t nsDiskCacheMap::VisitEachRecord(uint32_t bucketIndex,
534                                         nsDiskCacheRecordVisitor *visitor,
535                                         uint32_t evictionRank) {
536   int32_t rv = kVisitNextRecord;
537   uint32_t count = mHeader.mBucketUsage[bucketIndex];
538   nsDiskCacheRecord *records = GetFirstRecordInBucket(bucketIndex);
539 
540   // call visitor for each entry (matching any eviction rank)
541   for (int i = count - 1; i >= 0; i--) {
542     if (evictionRank > records[i].EvictionRank()) continue;
543 
544     rv = visitor->VisitRecord(&records[i]);
545     if (rv == kStopVisitingRecords) break;  // Stop visiting records
546 
547     if (rv == kDeleteRecordAndContinue) {
548       --count;
549       records[i] = records[count];
550       records[count].SetHashNumber(0);
551       InvalidateCache();
552     }
553   }
554 
555   if (mHeader.mBucketUsage[bucketIndex] - count != 0) {
556     mHeader.mEntryCount -= mHeader.mBucketUsage[bucketIndex] - count;
557     mHeader.mBucketUsage[bucketIndex] = count;
558     // recalc eviction rank
559     mHeader.mEvictionRank[bucketIndex] = GetBucketRank(bucketIndex, 0);
560   }
561   NS_ASSERTION(
562       mHeader.mEvictionRank[bucketIndex] == GetBucketRank(bucketIndex, 0),
563       "eviction rank out of sync");
564 
565   return rv;
566 }
567 
568 /**
569  *  VisitRecords
570  *
571  *  Visit every record in cache map in the most convenient order
572  */
VisitRecords(nsDiskCacheRecordVisitor * visitor)573 nsresult nsDiskCacheMap::VisitRecords(nsDiskCacheRecordVisitor *visitor) {
574   for (int bucketIndex = 0; bucketIndex < kBuckets; ++bucketIndex) {
575     if (VisitEachRecord(bucketIndex, visitor, 0) == kStopVisitingRecords) break;
576   }
577   return NS_OK;
578 }
579 
580 /**
581  *  EvictRecords
582  *
583  *  Just like VisitRecords, but visits the records in order of their eviction
584  *  rank
585  */
EvictRecords(nsDiskCacheRecordVisitor * visitor)586 nsresult nsDiskCacheMap::EvictRecords(nsDiskCacheRecordVisitor *visitor) {
587   uint32_t tempRank[kBuckets];
588   int bucketIndex = 0;
589 
590   // copy eviction rank array
591   for (bucketIndex = 0; bucketIndex < kBuckets; ++bucketIndex)
592     tempRank[bucketIndex] = mHeader.mEvictionRank[bucketIndex];
593 
594   // Maximum number of iterations determined by number of records
595   // as a safety limiter for the loop. Use a copy of mHeader.mEntryCount since
596   // the value could decrease if some entry is evicted.
597   int32_t entryCount = mHeader.mEntryCount;
598   for (int n = 0; n < entryCount; ++n) {
599     // find bucket with highest eviction rank
600     uint32_t rank = 0;
601     for (int i = 0; i < kBuckets; ++i) {
602       if (rank < tempRank[i]) {
603         rank = tempRank[i];
604         bucketIndex = i;
605       }
606     }
607 
608     if (rank == 0) break;  // we've examined all the records
609 
610     // visit records in bucket with eviction ranks >= target eviction rank
611     if (VisitEachRecord(bucketIndex, visitor, rank) == kStopVisitingRecords)
612       break;
613 
614     // find greatest rank less than 'rank'
615     tempRank[bucketIndex] = GetBucketRank(bucketIndex, rank);
616   }
617   return NS_OK;
618 }
619 
OpenBlockFiles(nsDiskCache::CorruptCacheInfo * corruptInfo)620 nsresult nsDiskCacheMap::OpenBlockFiles(
621     nsDiskCache::CorruptCacheInfo *corruptInfo) {
622   NS_ENSURE_ARG_POINTER(corruptInfo);
623 
624   // create nsIFile for block file
625   nsCOMPtr<nsIFile> blockFile;
626   nsresult rv = NS_OK;
627   *corruptInfo = nsDiskCache::kUnexpectedError;
628 
629   for (int i = 0; i < kNumBlockFiles; ++i) {
630     rv = GetBlockFileForIndex(i, getter_AddRefs(blockFile));
631     if (NS_FAILED(rv)) {
632       *corruptInfo = nsDiskCache::kCouldNotGetBlockFileForIndex;
633       break;
634     }
635 
636     uint32_t blockSize =
637         GetBlockSizeForIndex(i + 1);  // +1 to match file selectors 1,2,3
638     uint32_t bitMapSize = GetBitMapSizeForIndex(i + 1);
639     rv = mBlockFile[i].Open(blockFile, blockSize, bitMapSize, corruptInfo);
640     if (NS_FAILED(rv)) {
641       // corruptInfo was set inside the call to mBlockFile[i].Open
642       break;
643     }
644   }
645   // close all files in case of any error
646   if (NS_FAILED(rv))
647     (void)CloseBlockFiles(false);  // we already have an error to report
648 
649   return rv;
650 }
651 
CloseBlockFiles(bool flush)652 nsresult nsDiskCacheMap::CloseBlockFiles(bool flush) {
653   nsresult rv, rv2 = NS_OK;
654   for (int i = 0; i < kNumBlockFiles; ++i) {
655     rv = mBlockFile[i].Close(flush);
656     if (NS_FAILED(rv)) rv2 = rv;  // if one or more errors, report at least one
657   }
658   return rv2;
659 }
660 
CacheFilesExist()661 bool nsDiskCacheMap::CacheFilesExist() {
662   nsCOMPtr<nsIFile> blockFile;
663   nsresult rv;
664 
665   for (int i = 0; i < kNumBlockFiles; ++i) {
666     bool exists;
667     rv = GetBlockFileForIndex(i, getter_AddRefs(blockFile));
668     if (NS_FAILED(rv)) return false;
669 
670     rv = blockFile->Exists(&exists);
671     if (NS_FAILED(rv) || !exists) return false;
672   }
673 
674   return true;
675 }
676 
CreateCacheSubDirectories()677 nsresult nsDiskCacheMap::CreateCacheSubDirectories() {
678   if (!mCacheDirectory) return NS_ERROR_UNEXPECTED;
679 
680   for (int32_t index = 0; index < 16; index++) {
681     nsCOMPtr<nsIFile> file;
682     nsresult rv = mCacheDirectory->Clone(getter_AddRefs(file));
683     if (NS_FAILED(rv)) return rv;
684 
685     rv = file->AppendNative(nsPrintfCString("%X", index));
686     if (NS_FAILED(rv)) return rv;
687 
688     rv = file->Create(nsIFile::DIRECTORY_TYPE, 0700);
689     if (NS_FAILED(rv)) return rv;
690   }
691 
692   return NS_OK;
693 }
694 
ReadDiskCacheEntry(nsDiskCacheRecord * record)695 nsDiskCacheEntry *nsDiskCacheMap::ReadDiskCacheEntry(
696     nsDiskCacheRecord *record) {
697   CACHE_LOG_DEBUG(("CACHE: ReadDiskCacheEntry [%x]\n", record->HashNumber()));
698 
699   nsresult rv = NS_ERROR_UNEXPECTED;
700   nsDiskCacheEntry *diskEntry = nullptr;
701   uint32_t metaFile = record->MetaFile();
702   int32_t bytesRead = 0;
703 
704   if (!record->MetaLocationInitialized()) return nullptr;
705 
706   if (metaFile == 0) {  // entry/metadata stored in separate file
707     // open and read the file
708     nsCOMPtr<nsIFile> file;
709     rv = GetLocalFileForDiskCacheRecord(record, nsDiskCache::kMetaData, false,
710                                         getter_AddRefs(file));
711     NS_ENSURE_SUCCESS(rv, nullptr);
712 
713     CACHE_LOG_DEBUG(
714         ("CACHE: nsDiskCacheMap::ReadDiskCacheEntry"
715          "[this=%p] reading disk cache entry",
716          this));
717 
718     PRFileDesc *fd = nullptr;
719 
720     // open the file - restricted to user, the data could be confidential
721     rv = file->OpenNSPRFileDesc(PR_RDONLY, 00600, &fd);
722     NS_ENSURE_SUCCESS(rv, nullptr);
723 
724     int32_t fileSize = PR_Available(fd);
725     if (fileSize < 0) {
726       // an error occurred. We could call PR_GetError(), but how would that
727       // help?
728       rv = NS_ERROR_UNEXPECTED;
729     } else {
730       rv = EnsureBuffer(fileSize);
731       if (NS_SUCCEEDED(rv)) {
732         bytesRead = PR_Read(fd, mBuffer, fileSize);
733         if (bytesRead < fileSize) {
734           rv = NS_ERROR_UNEXPECTED;
735         }
736       }
737     }
738     PR_Close(fd);
739     NS_ENSURE_SUCCESS(rv, nullptr);
740 
741   } else if (metaFile < (kNumBlockFiles + 1)) {
742     // entry/metadata stored in cache block file
743 
744     // allocate buffer
745     uint32_t blockCount = record->MetaBlockCount();
746     bytesRead = blockCount * GetBlockSizeForIndex(metaFile);
747 
748     rv = EnsureBuffer(bytesRead);
749     NS_ENSURE_SUCCESS(rv, nullptr);
750 
751     // read diskEntry, note when the blocks are at the end of file,
752     // bytesRead may be less than blockSize*blockCount.
753     // But the bytesRead should at least agree with the real disk entry size.
754     rv = mBlockFile[metaFile - 1].ReadBlocks(mBuffer, record->MetaStartBlock(),
755                                              blockCount, &bytesRead);
756     NS_ENSURE_SUCCESS(rv, nullptr);
757   }
758   diskEntry = (nsDiskCacheEntry *)mBuffer;
759   diskEntry->Unswap();  // disk to memory
760   // Check if calculated size agrees with bytesRead
761   if (bytesRead < 0 || (uint32_t)bytesRead < diskEntry->Size()) return nullptr;
762 
763   // Return the buffer containing the diskEntry structure
764   return diskEntry;
765 }
766 
767 /**
768  *  CreateDiskCacheEntry(nsCacheEntry * entry)
769  *
770  *  Prepare an nsCacheEntry for writing to disk
771  */
CreateDiskCacheEntry(nsDiskCacheBinding * binding,uint32_t * aSize)772 nsDiskCacheEntry *nsDiskCacheMap::CreateDiskCacheEntry(
773     nsDiskCacheBinding *binding, uint32_t *aSize) {
774   nsCacheEntry *entry = binding->mCacheEntry;
775   if (!entry) return nullptr;
776 
777   // Store security info, if it is serializable
778   nsCOMPtr<nsISupports> infoObj = entry->SecurityInfo();
779   nsCOMPtr<nsISerializable> serializable = do_QueryInterface(infoObj);
780   if (infoObj && !serializable) return nullptr;
781   if (serializable) {
782     nsCString info;
783     nsresult rv = NS_SerializeToString(serializable, info);
784     if (NS_FAILED(rv)) return nullptr;
785     rv = entry->SetMetaDataElement("security-info", info.get());
786     if (NS_FAILED(rv)) return nullptr;
787   }
788 
789   uint32_t keySize = entry->Key()->Length() + 1;
790   uint32_t metaSize = entry->MetaDataSize();
791   uint32_t size = sizeof(nsDiskCacheEntry) + keySize + metaSize;
792 
793   if (aSize) *aSize = size;
794 
795   nsresult rv = EnsureBuffer(size);
796   if (NS_FAILED(rv)) return nullptr;
797 
798   nsDiskCacheEntry *diskEntry = (nsDiskCacheEntry *)mBuffer;
799   diskEntry->mHeaderVersion = nsDiskCache::kCurrentVersion;
800   diskEntry->mMetaLocation = binding->mRecord.MetaLocation();
801   diskEntry->mFetchCount = entry->FetchCount();
802   diskEntry->mLastFetched = entry->LastFetched();
803   diskEntry->mLastModified = entry->LastModified();
804   diskEntry->mExpirationTime = entry->ExpirationTime();
805   diskEntry->mDataSize = entry->DataSize();
806   diskEntry->mKeySize = keySize;
807   diskEntry->mMetaDataSize = metaSize;
808 
809   memcpy(diskEntry->Key(), entry->Key()->get(), keySize);
810 
811   rv = entry->FlattenMetaData(diskEntry->MetaData(), metaSize);
812   if (NS_FAILED(rv)) return nullptr;
813 
814   return diskEntry;
815 }
816 
WriteDiskCacheEntry(nsDiskCacheBinding * binding)817 nsresult nsDiskCacheMap::WriteDiskCacheEntry(nsDiskCacheBinding *binding) {
818   CACHE_LOG_DEBUG(
819       ("CACHE: WriteDiskCacheEntry [%x]\n", binding->mRecord.HashNumber()));
820 
821   nsresult rv = NS_OK;
822   uint32_t size;
823   nsDiskCacheEntry *diskEntry = CreateDiskCacheEntry(binding, &size);
824   if (!diskEntry) return NS_ERROR_UNEXPECTED;
825 
826   uint32_t fileIndex = CalculateFileIndex(size);
827 
828   // Deallocate old storage if necessary
829   if (binding->mRecord.MetaLocationInitialized()) {
830     // we have existing storage
831 
832     if ((binding->mRecord.MetaFile() == 0) &&
833         (fileIndex == 0)) {  // keeping the separate file
834       // just decrement total
835       DecrementTotalSize(binding->mRecord.MetaFileSize());
836       NS_ASSERTION(
837           binding->mRecord.MetaFileGeneration() == binding->mGeneration,
838           "generations out of sync");
839     } else {
840       rv = DeleteStorage(&binding->mRecord, nsDiskCache::kMetaData);
841       NS_ENSURE_SUCCESS(rv, rv);
842     }
843   }
844 
845   binding->mRecord.SetEvictionRank(ULONG_MAX - SecondsFromPRTime(PR_Now()));
846   // write entry data to disk cache block file
847   diskEntry->Swap();
848 
849   if (fileIndex != 0) {
850     while (1) {
851       uint32_t blockSize = GetBlockSizeForIndex(fileIndex);
852       uint32_t blocks = ((size - 1) / blockSize) + 1;
853 
854       int32_t startBlock;
855       rv = mBlockFile[fileIndex - 1].WriteBlocks(diskEntry, size, blocks,
856                                                  &startBlock);
857       if (NS_SUCCEEDED(rv)) {
858         // update binding and cache map record
859         binding->mRecord.SetMetaBlocks(fileIndex, startBlock, blocks);
860 
861         rv = UpdateRecord(&binding->mRecord);
862         NS_ENSURE_SUCCESS(rv, rv);
863 
864         // XXX we should probably write out bucket ourselves
865 
866         IncrementTotalSize(blocks, blockSize);
867         break;
868       }
869 
870       if (fileIndex == kNumBlockFiles) {
871         fileIndex = 0;  // write data to separate file
872         break;
873       }
874 
875       // try next block file
876       fileIndex++;
877     }
878   }
879 
880   if (fileIndex == 0) {
881     // Write entry data to separate file
882     uint32_t metaFileSizeK = ((size + 0x03FF) >> 10);  // round up to nearest 1k
883     if (metaFileSizeK > kMaxDataSizeK) metaFileSizeK = kMaxDataSizeK;
884 
885     binding->mRecord.SetMetaFileGeneration(binding->mGeneration);
886     binding->mRecord.SetMetaFileSize(metaFileSizeK);
887     rv = UpdateRecord(&binding->mRecord);
888     NS_ENSURE_SUCCESS(rv, rv);
889 
890     nsCOMPtr<nsIFile> localFile;
891     rv = GetLocalFileForDiskCacheRecord(&binding->mRecord,
892                                         nsDiskCache::kMetaData, true,
893                                         getter_AddRefs(localFile));
894     NS_ENSURE_SUCCESS(rv, rv);
895 
896     // open the file
897     PRFileDesc *fd;
898     // open the file - restricted to user, the data could be confidential
899     rv = localFile->OpenNSPRFileDesc(PR_RDWR | PR_TRUNCATE | PR_CREATE_FILE,
900                                      00600, &fd);
901     NS_ENSURE_SUCCESS(rv, rv);
902 
903     // write the file
904     int32_t bytesWritten = PR_Write(fd, diskEntry, size);
905 
906     PRStatus err = PR_Close(fd);
907     if ((bytesWritten != (int32_t)size) || (err != PR_SUCCESS)) {
908       return NS_ERROR_UNEXPECTED;
909     }
910 
911     IncrementTotalSize(metaFileSizeK);
912   }
913 
914   return rv;
915 }
916 
ReadDataCacheBlocks(nsDiskCacheBinding * binding,char * buffer,uint32_t size)917 nsresult nsDiskCacheMap::ReadDataCacheBlocks(nsDiskCacheBinding *binding,
918                                              char *buffer, uint32_t size) {
919   CACHE_LOG_DEBUG(("CACHE: ReadDataCacheBlocks [%x size=%u]\n",
920                    binding->mRecord.HashNumber(), size));
921 
922   uint32_t fileIndex = binding->mRecord.DataFile();
923   int32_t readSize = size;
924 
925   nsresult rv = mBlockFile[fileIndex - 1].ReadBlocks(
926       buffer, binding->mRecord.DataStartBlock(),
927       binding->mRecord.DataBlockCount(), &readSize);
928   NS_ENSURE_SUCCESS(rv, rv);
929   if (readSize < (int32_t)size) {
930     rv = NS_ERROR_UNEXPECTED;
931   }
932   return rv;
933 }
934 
WriteDataCacheBlocks(nsDiskCacheBinding * binding,char * buffer,uint32_t size)935 nsresult nsDiskCacheMap::WriteDataCacheBlocks(nsDiskCacheBinding *binding,
936                                               char *buffer, uint32_t size) {
937   CACHE_LOG_DEBUG(("CACHE: WriteDataCacheBlocks [%x size=%u]\n",
938                    binding->mRecord.HashNumber(), size));
939 
940   nsresult rv = NS_OK;
941 
942   // determine block file & number of blocks
943   uint32_t fileIndex = CalculateFileIndex(size);
944   uint32_t blockCount = 0;
945   int32_t startBlock = 0;
946 
947   if (size > 0) {
948     // if fileIndex is 0, bad things happen below, which makes gcc 4.7
949     // complain, but it's not supposed to happen. See bug 854105.
950     MOZ_ASSERT(fileIndex);
951     while (fileIndex) {
952       uint32_t blockSize = GetBlockSizeForIndex(fileIndex);
953       blockCount = ((size - 1) / blockSize) + 1;
954 
955       rv = mBlockFile[fileIndex - 1].WriteBlocks(buffer, size, blockCount,
956                                                  &startBlock);
957       if (NS_SUCCEEDED(rv)) {
958         IncrementTotalSize(blockCount, blockSize);
959         break;
960       }
961 
962       if (fileIndex == kNumBlockFiles) return rv;
963 
964       fileIndex++;
965     }
966   }
967 
968   // update binding and cache map record
969   binding->mRecord.SetDataBlocks(fileIndex, startBlock, blockCount);
970   if (!binding->mDoomed) {
971     rv = UpdateRecord(&binding->mRecord);
972   }
973   return rv;
974 }
975 
DeleteStorage(nsDiskCacheRecord * record)976 nsresult nsDiskCacheMap::DeleteStorage(nsDiskCacheRecord *record) {
977   nsresult rv1 = DeleteStorage(record, nsDiskCache::kData);
978   nsresult rv2 = DeleteStorage(record, nsDiskCache::kMetaData);
979   return NS_FAILED(rv1) ? rv1 : rv2;
980 }
981 
DeleteStorage(nsDiskCacheRecord * record,bool metaData)982 nsresult nsDiskCacheMap::DeleteStorage(nsDiskCacheRecord *record,
983                                        bool metaData) {
984   CACHE_LOG_DEBUG(
985       ("CACHE: DeleteStorage [%x %u]\n", record->HashNumber(), metaData));
986 
987   nsresult rv = NS_ERROR_UNEXPECTED;
988   uint32_t fileIndex = metaData ? record->MetaFile() : record->DataFile();
989   nsCOMPtr<nsIFile> file;
990 
991   if (fileIndex == 0) {
992     // delete the file
993     uint32_t sizeK = metaData ? record->MetaFileSize() : record->DataFileSize();
994     // XXX if sizeK == USHRT_MAX, stat file for actual size
995 
996     rv = GetFileForDiskCacheRecord(record, metaData, false,
997                                    getter_AddRefs(file));
998     if (NS_SUCCEEDED(rv)) {
999       rv = file->Remove(false);  // false == non-recursive
1000     }
1001     DecrementTotalSize(sizeK);
1002 
1003   } else if (fileIndex < (kNumBlockFiles + 1)) {
1004     // deallocate blocks
1005     uint32_t startBlock =
1006         metaData ? record->MetaStartBlock() : record->DataStartBlock();
1007     uint32_t blockCount =
1008         metaData ? record->MetaBlockCount() : record->DataBlockCount();
1009 
1010     rv = mBlockFile[fileIndex - 1].DeallocateBlocks(startBlock, blockCount);
1011     DecrementTotalSize(blockCount, GetBlockSizeForIndex(fileIndex));
1012   }
1013   if (metaData)
1014     record->ClearMetaLocation();
1015   else
1016     record->ClearDataLocation();
1017 
1018   return rv;
1019 }
1020 
GetFileForDiskCacheRecord(nsDiskCacheRecord * record,bool meta,bool createPath,nsIFile ** result)1021 nsresult nsDiskCacheMap::GetFileForDiskCacheRecord(nsDiskCacheRecord *record,
1022                                                    bool meta, bool createPath,
1023                                                    nsIFile **result) {
1024   if (!mCacheDirectory) return NS_ERROR_NOT_AVAILABLE;
1025 
1026   nsCOMPtr<nsIFile> file;
1027   nsresult rv = mCacheDirectory->Clone(getter_AddRefs(file));
1028   if (NS_FAILED(rv)) return rv;
1029 
1030   uint32_t hash = record->HashNumber();
1031 
1032   // The file is stored under subdirectories according to the hash number:
1033   // 0x01234567 -> 0/12/
1034   rv = file->AppendNative(nsPrintfCString("%X", hash >> 28));
1035   if (NS_FAILED(rv)) return rv;
1036   rv = file->AppendNative(nsPrintfCString("%02X", (hash >> 20) & 0xFF));
1037   if (NS_FAILED(rv)) return rv;
1038 
1039   bool exists;
1040   if (createPath && (NS_FAILED(file->Exists(&exists)) || !exists)) {
1041     rv = file->Create(nsIFile::DIRECTORY_TYPE, 0700);
1042     if (NS_FAILED(rv)) return rv;
1043   }
1044 
1045   int16_t generation = record->Generation();
1046   char name[32];
1047   // Cut the beginning of the hash that was used in the path
1048   ::SprintfLiteral(name, "%05X%c%02X", hash & 0xFFFFF, (meta ? 'm' : 'd'),
1049                    generation);
1050   rv = file->AppendNative(nsDependentCString(name));
1051   if (NS_FAILED(rv)) return rv;
1052 
1053   NS_IF_ADDREF(*result = file);
1054   return rv;
1055 }
1056 
GetLocalFileForDiskCacheRecord(nsDiskCacheRecord * record,bool meta,bool createPath,nsIFile ** result)1057 nsresult nsDiskCacheMap::GetLocalFileForDiskCacheRecord(
1058     nsDiskCacheRecord *record, bool meta, bool createPath, nsIFile **result) {
1059   nsCOMPtr<nsIFile> file;
1060   nsresult rv =
1061       GetFileForDiskCacheRecord(record, meta, createPath, getter_AddRefs(file));
1062   if (NS_FAILED(rv)) return rv;
1063 
1064   NS_IF_ADDREF(*result = file);
1065   return rv;
1066 }
1067 
GetBlockFileForIndex(uint32_t index,nsIFile ** result)1068 nsresult nsDiskCacheMap::GetBlockFileForIndex(uint32_t index,
1069                                               nsIFile **result) {
1070   if (!mCacheDirectory) return NS_ERROR_NOT_AVAILABLE;
1071 
1072   nsCOMPtr<nsIFile> file;
1073   nsresult rv = mCacheDirectory->Clone(getter_AddRefs(file));
1074   if (NS_FAILED(rv)) return rv;
1075 
1076   char name[32];
1077   ::SprintfLiteral(name, "_CACHE_%03d_", index + 1);
1078   rv = file->AppendNative(nsDependentCString(name));
1079   if (NS_FAILED(rv)) return rv;
1080 
1081   NS_IF_ADDREF(*result = file);
1082 
1083   return rv;
1084 }
1085 
CalculateFileIndex(uint32_t size)1086 uint32_t nsDiskCacheMap::CalculateFileIndex(uint32_t size) {
1087   // We prefer to use block file with larger block if the wasted space would
1088   // be the same. E.g. store entry with size of 3073 bytes in 1 4K-block
1089   // instead of in 4 1K-blocks.
1090 
1091   if (size <= 3 * BLOCK_SIZE_FOR_INDEX(1)) return 1;
1092   if (size <= 3 * BLOCK_SIZE_FOR_INDEX(2)) return 2;
1093   if (size <= 4 * BLOCK_SIZE_FOR_INDEX(3)) return 3;
1094   return 0;
1095 }
1096 
EnsureBuffer(uint32_t bufSize)1097 nsresult nsDiskCacheMap::EnsureBuffer(uint32_t bufSize) {
1098   if (mBufferSize < bufSize) {
1099     char *buf = (char *)realloc(mBuffer, bufSize);
1100     if (!buf) {
1101       mBufferSize = 0;
1102       return NS_ERROR_OUT_OF_MEMORY;
1103     }
1104     mBuffer = buf;
1105     mBufferSize = bufSize;
1106   }
1107   return NS_OK;
1108 }
1109 
NotifyCapacityChange(uint32_t capacity)1110 void nsDiskCacheMap::NotifyCapacityChange(uint32_t capacity) {
1111   // Heuristic 1. average cache entry size is probably around 1KB
1112   // Heuristic 2. we don't want more than 32MB reserved to store the record
1113   //              map in memory.
1114   const int32_t RECORD_COUNT_LIMIT =
1115       32 * 1024 * 1024 / sizeof(nsDiskCacheRecord);
1116   int32_t maxRecordCount = std::min(int32_t(capacity), RECORD_COUNT_LIMIT);
1117   if (mMaxRecordCount < maxRecordCount) {
1118     // We can only grow
1119     mMaxRecordCount = maxRecordCount;
1120   }
1121 }
1122 
SizeOfExcludingThis(MallocSizeOf aMallocSizeOf)1123 size_t nsDiskCacheMap::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) {
1124   size_t usage = aMallocSizeOf(mRecordArray);
1125 
1126   usage += aMallocSizeOf(mBuffer);
1127   usage += aMallocSizeOf(mMapFD);
1128   usage += aMallocSizeOf(mCleanFD);
1129   usage += aMallocSizeOf(mCacheDirectory);
1130   usage += aMallocSizeOf(mCleanCacheTimer);
1131 
1132   for (int i = 0; i < kNumBlockFiles; i++) {
1133     usage += mBlockFile[i].SizeOfExcludingThis(aMallocSizeOf);
1134   }
1135 
1136   return usage;
1137 }
1138 
InitCacheClean(nsIFile * cacheDirectory,nsDiskCache::CorruptCacheInfo * corruptInfo)1139 nsresult nsDiskCacheMap::InitCacheClean(
1140     nsIFile *cacheDirectory, nsDiskCache::CorruptCacheInfo *corruptInfo) {
1141   // The _CACHE_CLEAN_ file will be used in the future to determine
1142   // if the cache is clean or not.
1143   bool cacheCleanFileExists = false;
1144   nsCOMPtr<nsIFile> cacheCleanFile;
1145   nsresult rv = cacheDirectory->GetParent(getter_AddRefs(cacheCleanFile));
1146   if (NS_SUCCEEDED(rv)) {
1147     rv = cacheCleanFile->AppendNative(NS_LITERAL_CSTRING("_CACHE_CLEAN_"));
1148     if (NS_SUCCEEDED(rv)) {
1149       // Check if the file already exists, if it does, we will later read the
1150       // value and report it to telemetry.
1151       cacheCleanFile->Exists(&cacheCleanFileExists);
1152     }
1153   }
1154   if (NS_FAILED(rv)) {
1155     NS_WARNING("Could not build cache clean file path");
1156     *corruptInfo = nsDiskCache::kCacheCleanFilePathError;
1157     return rv;
1158   }
1159 
1160   // Make sure the _CACHE_CLEAN_ file exists
1161   rv = cacheCleanFile->OpenNSPRFileDesc(PR_RDWR | PR_CREATE_FILE, 00600,
1162                                         &mCleanFD);
1163   if (NS_FAILED(rv)) {
1164     NS_WARNING("Could not open cache clean file");
1165     *corruptInfo = nsDiskCache::kCacheCleanOpenFileError;
1166     return rv;
1167   }
1168 
1169   if (cacheCleanFileExists) {
1170     char clean = '0';
1171     int32_t bytesRead = PR_Read(mCleanFD, &clean, 1);
1172     if (bytesRead != 1) {
1173       NS_WARNING("Could not read _CACHE_CLEAN_ file contents");
1174     }
1175   }
1176 
1177   // Create a timer that will be used to validate the cache
1178   // as long as an activity threshold was met
1179   mCleanCacheTimer =
1180       NS_NewTimer(nsCacheService::GlobalInstance()->mCacheIOThread);
1181   rv = mCleanCacheTimer ? ResetCacheTimer() : NS_ERROR_OUT_OF_MEMORY;
1182 
1183   if (NS_FAILED(rv)) {
1184     NS_WARNING("Could not create cache clean timer");
1185     mCleanCacheTimer = nullptr;
1186     *corruptInfo = nsDiskCache::kCacheCleanTimerError;
1187     return rv;
1188   }
1189 
1190   return NS_OK;
1191 }
1192 
WriteCacheClean(bool clean)1193 nsresult nsDiskCacheMap::WriteCacheClean(bool clean) {
1194   nsCacheService::AssertOwnsLock();
1195   if (!mCleanFD) {
1196     NS_WARNING("Cache clean file is not open!");
1197     return NS_ERROR_FAILURE;
1198   }
1199 
1200   CACHE_LOG_DEBUG(("CACHE: WriteCacheClean: %d\n", clean ? 1 : 0));
1201   // I'm using a simple '1' or '0' to denote cache clean
1202   // since it can be edited easily by any text editor for testing.
1203   char data = clean ? '1' : '0';
1204   int32_t filePos = PR_Seek(mCleanFD, 0, PR_SEEK_SET);
1205   if (filePos != 0) {
1206     NS_WARNING("Could not seek in cache clean file!");
1207     return NS_ERROR_FAILURE;
1208   }
1209   int32_t bytesWritten = PR_Write(mCleanFD, &data, 1);
1210   if (bytesWritten != 1) {
1211     NS_WARNING("Could not write cache clean file!");
1212     return NS_ERROR_FAILURE;
1213   }
1214   PRStatus err = PR_Sync(mCleanFD);
1215   if (err != PR_SUCCESS) {
1216     NS_WARNING("Could not flush cache clean file!");
1217   }
1218 
1219   return NS_OK;
1220 }
1221 
InvalidateCache()1222 nsresult nsDiskCacheMap::InvalidateCache() {
1223   nsCacheService::AssertOwnsLock();
1224   CACHE_LOG_DEBUG(("CACHE: InvalidateCache\n"));
1225   nsresult rv;
1226 
1227   if (!mIsDirtyCacheFlushed) {
1228     rv = WriteCacheClean(false);
1229     if (NS_FAILED(rv)) {
1230       return rv;
1231     }
1232 
1233     mIsDirtyCacheFlushed = true;
1234   }
1235 
1236   rv = ResetCacheTimer();
1237   NS_ENSURE_SUCCESS(rv, rv);
1238 
1239   return NS_OK;
1240 }
1241 
ResetCacheTimer(int32_t timeout)1242 nsresult nsDiskCacheMap::ResetCacheTimer(int32_t timeout) {
1243   mCleanCacheTimer->Cancel();
1244   nsresult rv = mCleanCacheTimer->InitWithNamedFuncCallback(
1245       RevalidateTimerCallback, nullptr, timeout, nsITimer::TYPE_ONE_SHOT,
1246       "nsDiskCacheMap::ResetCacheTimer");
1247   NS_ENSURE_SUCCESS(rv, rv);
1248   mLastInvalidateTime = PR_IntervalNow();
1249 
1250   return rv;
1251 }
1252 
RevalidateTimerCallback(nsITimer * aTimer,void * arg)1253 void nsDiskCacheMap::RevalidateTimerCallback(nsITimer *aTimer, void *arg) {
1254   nsCacheServiceAutoLock lock;
1255   if (!nsCacheService::gService->mDiskDevice ||
1256       !nsCacheService::gService->mDiskDevice->Initialized()) {
1257     return;
1258   }
1259 
1260   nsDiskCacheMap *diskCacheMap =
1261       &nsCacheService::gService->mDiskDevice->mCacheMap;
1262 
1263   // If we have less than kRevalidateCacheTimeout since the last timer was
1264   // issued then another thread called InvalidateCache.  This won't catch
1265   // all cases where we wanted to cancel the timer, but under the lock it
1266   // is always OK to revalidate as long as IsCacheInSafeState() returns
1267   // true.  We just want to avoid revalidating when we can to reduce IO
1268   // and this check will do that.
1269   uint32_t delta = PR_IntervalToMilliseconds(
1270                        PR_IntervalNow() - diskCacheMap->mLastInvalidateTime) +
1271                    kRevalidateCacheTimeoutTolerance;
1272   if (delta < kRevalidateCacheTimeout) {
1273     diskCacheMap->ResetCacheTimer();
1274     return;
1275   }
1276 
1277   nsresult rv = diskCacheMap->RevalidateCache();
1278   if (NS_FAILED(rv)) {
1279     diskCacheMap->ResetCacheTimer(kRevalidateCacheErrorTimeout);
1280   }
1281 }
1282 
IsCacheInSafeState()1283 bool nsDiskCacheMap::IsCacheInSafeState() {
1284   return nsCacheService::GlobalInstance()->IsDoomListEmpty();
1285 }
1286 
RevalidateCache()1287 nsresult nsDiskCacheMap::RevalidateCache() {
1288   CACHE_LOG_DEBUG(("CACHE: RevalidateCache\n"));
1289   nsresult rv;
1290 
1291   if (!IsCacheInSafeState()) {
1292     CACHE_LOG_DEBUG(
1293         ("CACHE: Revalidation should not performed because "
1294          "cache not in a safe state\n"));
1295     // Normally we would return an error here, but there is a bug where
1296     // the doom list sometimes gets an entry 'stuck' and doens't clear it
1297     // until browser shutdown.  So we allow revalidation for the time being
1298     // to get proper telemetry data of how much the cache corruption plan
1299     // would help.
1300   }
1301 
1302   // If telemetry data shows it is worth it, we'll be flushing headers and
1303   // records before flushing the clean cache file.
1304 
1305   // Write out the _CACHE_CLEAN_ file with '1'
1306   rv = WriteCacheClean(true);
1307   if (NS_FAILED(rv)) {
1308     return rv;
1309   }
1310 
1311   mIsDirtyCacheFlushed = false;
1312 
1313   return NS_OK;
1314 }
1315