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