Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

"use strict";
// A corrupt on-disk index must be detected when it is read and the cache must
// recover by rebuilding the index from the entry files, leaving all entries
// accessible. This matters more now that the index is the durable store for
// frecency across an unclean exit. Only recoverability is asserted: frecency may
// reset to the (possibly stale) per-entry metadata value after a rebuild.
//
// nsICacheTesting.shutdownCacheForTesting()/startupCacheForTesting() simulate a
// browser restart in-process, so the index is re-read from disk through the same
// parse / integrity-check / rebuild path as a real startup.
const COUNT = 5;
function entryURL(i) {
return "http://corrupt/" + i;
}
function openEntry(behavior, meta, data, url) {
return new Promise(resolve => {
asyncOpenCacheEntry(
url,
"disk",
Ci.nsICacheStorage.OPEN_NORMALLY,
null,
new OpenCallback(behavior, meta, data, resolve)
);
});
}
function flushCache() {
return new Promise(resolve => {
Services.cache2.QueryInterface(Ci.nsICacheTesting).flush({
QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
observe() {
resolve();
},
});
});
}
function indexFilePath() {
let f = getDiskCacheDirectory();
f.append("index");
return f.path;
}
async function waitForIndexFile() {
for (let i = 0; i < 100; i++) {
if (await IOUtils.exists(indexFilePath())) {
return true;
}
await new Promise(resolve => do_timeout(50, resolve));
}
return false;
}
add_task(async function test_corrupt_index_recovers() {
do_get_profile();
// Write the index eagerly so it is actually on disk to corrupt (the default
// thresholds would not flush for just a handful of entries), and start the
// post-startup index rebuild immediately rather than after the default delay.
Services.prefs.setIntPref(
"browser.cache.disk.index.min_unwritten_changes",
1
);
Services.prefs.setIntPref("browser.cache.disk.index.min_dump_interval_ms", 0);
Services.prefs.setIntPref(
"browser.cache.disk.index.update_start_delay_ms",
0
);
for (let i = 0; i < COUNT; i++) {
await openEntry(NEW | WAITFORWRITE, "meta" + i, "data" + i, entryURL(i));
}
await new Promise(wait_for_cache_index);
// Read hits dirty the index entries; with the eager prefs above this triggers
// a write so the index is actually on disk to corrupt.
for (let i = 0; i < COUNT; i++) {
await openEntry(NORMAL, "meta" + i, "data" + i, entryURL(i));
}
await flushCache();
Assert.ok(await waitForIndexFile(), "index written to disk");
// Shut the cache down (closes handles, writes a clean index), then corrupt the
// on-disk index so the next startup fails its integrity check and rebuilds
// from the entry files.
let testing = Services.cache2.QueryInterface(Ci.nsICacheTesting);
testing.shutdownCacheForTesting();
let original = await IOUtils.read(indexFilePath());
let garbage = new Uint8Array(Math.max(original.length, 128)).fill(0xab);
await IOUtils.write(indexFilePath(), garbage);
// Start back up and read the corrupt index; the cache must recover.
testing.startupCacheForTesting();
await new Promise(wait_for_cache_index);
// Recovery: wait_for_cache_index above only returns once the index has been
// rebuilt to the READY state, and every entry must still be readable with its
// original data. An open below would fail (NOTFOUND / wrong data) if recovery
// had lost the entry, so this is the core corruption-recovery guarantee.
let recovered = 0;
for (let i = 0; i < COUNT; i++) {
await openEntry(NORMAL, "meta" + i, "data" + i, entryURL(i));
recovered++;
}
Assert.equal(
recovered,
COUNT,
"all entries readable after recovering from a corrupt index"
);
});