tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

head_cache2.js (12777B)


      1 /* import-globals-from head_cache.js */
      2 /* import-globals-from head_channels.js */
      3 
      4 "use strict";
      5 
      6 var callbacks = [];
      7 
      8 // Expect an existing entry
      9 const NORMAL = 0;
     10 // Expect a new entry
     11 const NEW = 1 << 0;
     12 // Return early from onCacheEntryCheck and set the callback to state it expects onCacheEntryCheck to happen
     13 const NOTVALID = 1 << 1;
     14 // Throw from onCacheEntryAvailable
     15 const THROWAVAIL = 1 << 2;
     16 // Open entry for reading-only
     17 const READONLY = 1 << 3;
     18 // Expect the entry to not be found
     19 const NOTFOUND = 1 << 4;
     20 // Return ENTRY_NEEDS_REVALIDATION from onCacheEntryCheck
     21 const REVAL = 1 << 5;
     22 // Return ENTRY_PARTIAL from onCacheEntryCheck, in combo with NEW or RECREATE bypasses check for emptiness of the entry
     23 const PARTIAL = 1 << 6;
     24 // Expect the entry is doomed, i.e. the output stream should not be possible to open
     25 const DOOMED = 1 << 7;
     26 // Don't trigger the go-on callback until the entry is written
     27 const WAITFORWRITE = 1 << 8;
     28 // Don't write data (i.e. don't open output stream)
     29 const METAONLY = 1 << 9;
     30 // Do recreation of an existing cache entry
     31 const RECREATE = 1 << 10;
     32 // Do not give me the entry
     33 const NOTWANTED = 1 << 11;
     34 // Tell the cache to wait for the entry to be completely written first
     35 const COMPLETE = 1 << 12;
     36 // Don't write meta/data and don't set valid in the callback, consumer will do it manually
     37 const DONTFILL = 1 << 13;
     38 // Used in combination with METAONLY, don't call setValid() on the entry after metadata has been set
     39 const DONTSETVALID = 1 << 14;
     40 // Notify before checking the data, useful for proper callback ordering checks
     41 const NOTIFYBEFOREREAD = 1 << 15;
     42 // It's allowed to not get an existing entry (result of opening is undetermined)
     43 const MAYBE_NEW = 1 << 16;
     44 
     45 var log_c2 = true;
     46 function LOG_C2(o, m) {
     47  if (!log_c2) {
     48    return;
     49  }
     50  if (!m) {
     51    dump("TEST-INFO | CACHE2: " + o + "\n");
     52  } else {
     53    dump(
     54      "TEST-INFO | CACHE2: callback #" +
     55        o.order +
     56        "(" +
     57        (o.workingData ? o.workingData.substr(0, 10) : "---") +
     58        ") " +
     59        m +
     60        "\n"
     61    );
     62  }
     63 }
     64 
     65 function pumpReadStream(inputStream, goon) {
     66  if (inputStream.isNonBlocking()) {
     67    // non-blocking stream, must read via pump
     68    var pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance(
     69      Ci.nsIInputStreamPump
     70    );
     71    pump.init(inputStream, 0, 0, true);
     72    let data = "";
     73    pump.asyncRead({
     74      onStartRequest() {},
     75      onDataAvailable(aRequest, aInputStream) {
     76        var wrapper = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
     77          Ci.nsIScriptableInputStream
     78        );
     79        wrapper.init(aInputStream);
     80        var str = wrapper.read(wrapper.available());
     81        LOG_C2("reading data '" + str.substring(0, 5) + "'");
     82        data += str;
     83      },
     84      onStopRequest(aRequest, aStatusCode) {
     85        LOG_C2("done reading data: " + aStatusCode);
     86        Assert.equal(aStatusCode, Cr.NS_OK);
     87        goon(data);
     88      },
     89    });
     90  } else {
     91    // blocking stream
     92    let data = read_stream(inputStream, inputStream.available());
     93    goon(data);
     94  }
     95 }
     96 
     97 OpenCallback.prototype = {
     98  QueryInterface: ChromeUtils.generateQI(["nsICacheEntryOpenCallback"]),
     99  onCacheEntryCheck(entry) {
    100    LOG_C2(this, "onCacheEntryCheck");
    101    Assert.ok(!this.onCheckPassed);
    102    this.onCheckPassed = true;
    103 
    104    if (this.behavior & NOTVALID) {
    105      LOG_C2(this, "onCacheEntryCheck DONE, return ENTRY_WANTED");
    106      return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
    107    }
    108 
    109    if (this.behavior & NOTWANTED) {
    110      LOG_C2(this, "onCacheEntryCheck DONE, return ENTRY_NOT_WANTED");
    111      return Ci.nsICacheEntryOpenCallback.ENTRY_NOT_WANTED;
    112    }
    113 
    114    Assert.equal(entry.getMetaDataElement("meto"), this.workingMetadata);
    115 
    116    // check for sane flag combination
    117    Assert.notEqual(this.behavior & (REVAL | PARTIAL), REVAL | PARTIAL);
    118 
    119    if (this.behavior & (REVAL | PARTIAL)) {
    120      LOG_C2(this, "onCacheEntryCheck DONE, return ENTRY_NEEDS_REVALIDATION");
    121      return Ci.nsICacheEntryOpenCallback.ENTRY_NEEDS_REVALIDATION;
    122    }
    123 
    124    if (this.behavior & COMPLETE) {
    125      LOG_C2(
    126        this,
    127        "onCacheEntryCheck DONE, return RECHECK_AFTER_WRITE_FINISHED"
    128      );
    129      // Specific to the new backend because of concurrent read/write:
    130      // when a consumer returns RECHECK_AFTER_WRITE_FINISHED from onCacheEntryCheck
    131      // the cache calls this callback again after the entry write has finished.
    132      // This gives the consumer a chance to recheck completeness of the entry
    133      // again.
    134      // Thus, we reset state as onCheck would have never been called.
    135      this.onCheckPassed = false;
    136      // Don't return RECHECK_AFTER_WRITE_FINISHED on second call of onCacheEntryCheck.
    137      this.behavior &= ~COMPLETE;
    138      return Ci.nsICacheEntryOpenCallback.RECHECK_AFTER_WRITE_FINISHED;
    139    }
    140 
    141    LOG_C2(this, "onCacheEntryCheck DONE, return ENTRY_WANTED");
    142    return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
    143  },
    144  onCacheEntryAvailable(entry, isnew, status) {
    145    if (this.behavior & MAYBE_NEW && isnew) {
    146      this.behavior |= NEW;
    147    }
    148 
    149    LOG_C2(this, "onCacheEntryAvailable, " + this.behavior);
    150    Assert.ok(!this.onAvailPassed);
    151    this.onAvailPassed = true;
    152 
    153    Assert.equal(isnew, !!(this.behavior & NEW));
    154 
    155    if (this.behavior & (NOTFOUND | NOTWANTED)) {
    156      Assert.equal(status, Cr.NS_ERROR_CACHE_KEY_NOT_FOUND);
    157      Assert.ok(!entry);
    158      if (this.behavior & THROWAVAIL) {
    159        this.throwAndNotify(entry);
    160      }
    161      this.goon(entry);
    162    } else if (this.behavior & (NEW | RECREATE)) {
    163      Assert.ok(!!entry);
    164 
    165      if (this.behavior & RECREATE) {
    166        entry = entry.recreate();
    167        Assert.ok(!!entry);
    168      }
    169 
    170      if (this.behavior & THROWAVAIL) {
    171        this.throwAndNotify(entry);
    172      }
    173 
    174      if (!(this.behavior & WAITFORWRITE)) {
    175        this.goon(entry);
    176      }
    177 
    178      if (!(this.behavior & PARTIAL)) {
    179        try {
    180          entry.getMetaDataElement("meto");
    181          Assert.ok(false);
    182        } catch (ex) {}
    183      }
    184 
    185      if (this.behavior & DONTFILL) {
    186        Assert.equal(false, this.behavior & WAITFORWRITE);
    187        return;
    188      }
    189 
    190      let self = this;
    191      executeSoon(function () {
    192        // emulate network latency
    193        entry.setMetaDataElement("meto", self.workingMetadata);
    194        entry.metaDataReady();
    195        if (self.behavior & METAONLY) {
    196          // Since forcing GC/CC doesn't trigger OnWriterClosed, we have to set the entry valid manually :(
    197          if (!(self.behavior & DONTSETVALID)) {
    198            entry.setValid();
    199          }
    200 
    201          if (self.behavior & WAITFORWRITE) {
    202            self.goon(entry);
    203          }
    204 
    205          return;
    206        }
    207        executeSoon(function () {
    208          // emulate more network latency
    209          if (self.behavior & DOOMED) {
    210            LOG_C2(self, "checking doom state");
    211            try {
    212              let os = entry.openOutputStream(0, -1);
    213              // Unfortunately, in the undetermined state we cannot even check whether the entry
    214              // is actually doomed or not.
    215              os.close();
    216              Assert.ok(!!(self.behavior & MAYBE_NEW));
    217            } catch (ex) {
    218              Assert.ok(true);
    219            }
    220            if (self.behavior & WAITFORWRITE) {
    221              self.goon(entry);
    222            }
    223            return;
    224          }
    225 
    226          var offset = self.behavior & PARTIAL ? entry.dataSize : 0;
    227          LOG_C2(self, "openOutputStream @ " + offset);
    228          let os = entry.openOutputStream(offset, -1);
    229          LOG_C2(self, "writing data");
    230          var wrt = os.write(self.workingData, self.workingData.length);
    231          Assert.equal(wrt, self.workingData.length);
    232          os.close();
    233          if (self.behavior & WAITFORWRITE) {
    234            self.goon(entry);
    235          }
    236        });
    237      });
    238    } else {
    239      // NORMAL
    240      Assert.ok(!!entry);
    241      Assert.equal(entry.getMetaDataElement("meto"), this.workingMetadata);
    242      if (this.behavior & THROWAVAIL) {
    243        this.throwAndNotify(entry);
    244      }
    245      if (this.behavior & NOTIFYBEFOREREAD) {
    246        this.goon(entry, true);
    247      }
    248 
    249      let self = this;
    250      pumpReadStream(entry.openInputStream(0), function (data) {
    251        Assert.equal(data, self.workingData);
    252        self.onDataCheckPassed = true;
    253        LOG_C2(self, "entry read done");
    254        self.goon(entry);
    255      });
    256    }
    257  },
    258  selfCheck() {
    259    LOG_C2(this, "selfCheck");
    260 
    261    Assert.ok(this.onCheckPassed || this.behavior & MAYBE_NEW);
    262    Assert.ok(this.onAvailPassed);
    263    Assert.ok(this.onDataCheckPassed || this.behavior & MAYBE_NEW);
    264  },
    265  throwAndNotify(entry) {
    266    LOG_C2(this, "Throwing");
    267    var self = this;
    268    executeSoon(function () {
    269      LOG_C2(self, "Notifying");
    270      self.goon(entry);
    271    });
    272    throw Components.Exception("", Cr.NS_ERROR_FAILURE);
    273  },
    274 };
    275 
    276 function OpenCallback(behavior, workingMetadata, workingData, goon) {
    277  this.behavior = behavior;
    278  this.workingMetadata = workingMetadata;
    279  this.workingData = workingData;
    280  this.goon = goon;
    281  this.onCheckPassed =
    282    (!!(behavior & (NEW | RECREATE)) || !workingMetadata) &&
    283    !(behavior & NOTVALID);
    284  this.onAvailPassed = false;
    285  this.onDataCheckPassed =
    286    !!(behavior & (NEW | RECREATE | NOTWANTED)) || !workingMetadata;
    287  callbacks.push(this);
    288  this.order = callbacks.length;
    289 }
    290 
    291 VisitCallback.prototype = {
    292  QueryInterface: ChromeUtils.generateQI(["nsICacheStorageVisitor"]),
    293  onCacheStorageInfo(num, consumption) {
    294    LOG_C2(this, "onCacheStorageInfo: num=" + num + ", size=" + consumption);
    295    Assert.equal(this.num, num);
    296    Assert.equal(this.consumption, consumption);
    297    if (!this.entries) {
    298      this.notify();
    299    }
    300  },
    301  onCacheEntryInfo(
    302    aURI,
    303    aIdEnhance,
    304    aDataSize,
    305    aAltDataSize,
    306    aFetchCount,
    307    aLastModifiedTime,
    308    aExpirationTime,
    309    aPinned,
    310    aInfo
    311  ) {
    312    var key = (aIdEnhance ? aIdEnhance + ":" : "") + aURI.asciiSpec;
    313    LOG_C2(this, "onCacheEntryInfo: key=" + key);
    314 
    315    function findCacheIndex(element) {
    316      if (typeof element === "string") {
    317        return element === key;
    318      } else if (typeof element === "object") {
    319        return (
    320          element.uri === key &&
    321          element.lci.isAnonymous === aInfo.isAnonymous &&
    322          ChromeUtils.isOriginAttributesEqual(
    323            element.lci.originAttributes,
    324            aInfo.originAttributes
    325          )
    326        );
    327      }
    328 
    329      return false;
    330    }
    331 
    332    Assert.ok(
    333      !!this.entries,
    334      "Ensure that the fact that we found cache entries matches expectations."
    335    );
    336 
    337    var index = this.entries.findIndex(findCacheIndex);
    338    Assert.greater(index, -1, "Cache entry should exist");
    339 
    340    this.entries.splice(index, 1);
    341  },
    342  onCacheEntryVisitCompleted() {
    343    LOG_C2(this, "onCacheEntryVisitCompleted");
    344    if (this.entries) {
    345      Assert.equal(
    346        this.entries.length,
    347        0,
    348        "Visited all expected cache entries."
    349      );
    350    }
    351    this.notify();
    352  },
    353  notify() {
    354    Assert.ok(!!this.goon, "goon should not be null");
    355    var goon = this.goon;
    356    this.goon = null;
    357    executeSoon(goon);
    358  },
    359  selfCheck() {
    360    Assert.ok(!this.entries || !this.entries.length, "entries should be empty");
    361  },
    362 };
    363 
    364 function VisitCallback(num, consumption, entries, goon) {
    365  this.num = num;
    366  this.consumption = consumption;
    367  this.entries = entries;
    368  this.goon = goon;
    369  callbacks.push(this);
    370  this.order = callbacks.length;
    371 }
    372 
    373 EvictionCallback.prototype = {
    374  QueryInterface: ChromeUtils.generateQI(["nsICacheEntryDoomCallback"]),
    375  onCacheEntryDoomed(result) {
    376    Assert.equal(this.expectedSuccess, result == Cr.NS_OK);
    377    this.goon();
    378  },
    379  selfCheck() {},
    380 };
    381 
    382 function EvictionCallback(success, goon) {
    383  this.expectedSuccess = success;
    384  this.goon = goon;
    385  callbacks.push(this);
    386  this.order = callbacks.length;
    387 }
    388 
    389 MultipleCallbacks.prototype = {
    390  fired() {
    391    if (--this.pending == 0) {
    392      var self = this;
    393      if (this.delayed) {
    394        executeSoon(function () {
    395          self.goon();
    396        });
    397      } else {
    398        this.goon();
    399      }
    400    }
    401  },
    402  add() {
    403    ++this.pending;
    404  },
    405 };
    406 
    407 function MultipleCallbacks(number, goon, delayed) {
    408  this.pending = number;
    409  this.goon = goon;
    410  this.delayed = delayed;
    411 }
    412 
    413 function wait_for_cache_index(continue_func) {
    414  // This callback will not fire before the index is in the ready state.  nsICacheStorage.exists() will
    415  // no longer throw after this point.
    416  Services.cache2.asyncGetDiskConsumption({
    417    onNetworkCacheDiskConsumption() {
    418      continue_func();
    419    },
    420    // eslint-disable-next-line mozilla/use-chromeutils-generateqi
    421    QueryInterface() {
    422      return this;
    423    },
    424  });
    425 }
    426 
    427 function finish_cache2_test() {
    428  callbacks.forEach(function (callback) {
    429    callback.selfCheck();
    430  });
    431  do_test_finished();
    432 }