tor-browser

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

CacheFileUtils.cpp (18109B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 #include "CacheIndex.h"
      6 #include "CacheLog.h"
      7 #include "CacheFileUtils.h"
      8 #include "CacheObserver.h"
      9 #include "LoadContextInfo.h"
     10 #include "mozilla/glean/NetwerkProtocolHttpMetrics.h"
     11 #include "mozilla/Tokenizer.h"
     12 #include "mozilla/Telemetry.h"
     13 #include "nsCOMPtr.h"
     14 #include "nsString.h"
     15 #include <algorithm>
     16 
     17 namespace mozilla::net::CacheFileUtils {
     18 
     19 // This designates the format for the "alt-data" metadata.
     20 // When the format changes we need to update the version.
     21 static uint32_t const kAltDataVersion = 1;
     22 const char* kAltDataKey = "alt-data";
     23 
     24 namespace {
     25 
     26 /**
     27 * A simple recursive descent parser for the mapping key.
     28 */
     29 class KeyParser : protected Tokenizer {
     30 public:
     31  explicit KeyParser(nsACString const& aInput)
     32      : Tokenizer(aInput),
     33        isAnonymous(false)
     34        // Initialize the cache key to a zero length by default
     35        ,
     36        lastTag(0) {}
     37 
     38 private:
     39  // Results
     40  OriginAttributes originAttribs;
     41  bool isAnonymous;
     42  nsCString idEnhance;
     43  nsDependentCSubstring cacheKey;
     44 
     45  // Keeps the last tag name, used for alphabetical sort checking
     46  char lastTag;
     47 
     48  // Classifier for the 'tag' character valid range.
     49  // Explicitly using unsigned char as 127 is -1 when signed and it would only
     50  // produce a warning.
     51  static bool TagChar(const char aChar) {
     52    unsigned char c = static_cast<unsigned char>(aChar);
     53    return c >= ' ' && c <= '\x7f';
     54  }
     55 
     56  bool ParseTags() {
     57    // Expects to be at the tag name or at the end
     58    if (CheckEOF()) {
     59      return true;
     60    }
     61 
     62    char tag;
     63    if (!ReadChar(&TagChar, &tag)) {
     64      return false;
     65    }
     66 
     67    // Check the alphabetical order, hard-fail on disobedience
     68    if (!(lastTag < tag || tag == ':')) {
     69      return false;
     70    }
     71    lastTag = tag;
     72 
     73    switch (tag) {
     74      case ':':
     75        // last possible tag, when present there is the cacheKey following,
     76        // not terminated with ',' and no need to unescape.
     77        cacheKey.Rebind(mCursor, mEnd - mCursor);
     78        return true;
     79      case 'O': {
     80        nsAutoCString originSuffix;
     81        if (!ParseValue(&originSuffix) ||
     82            !originAttribs.PopulateFromSuffix(originSuffix)) {
     83          return false;
     84        }
     85        break;
     86      }
     87      case 'p':
     88        originAttribs.SyncAttributesWithPrivateBrowsing(true);
     89        break;
     90      case 'b':
     91        // Leaving to be able to read and understand oldformatted entries
     92        break;
     93      case 'a':
     94        isAnonymous = true;
     95        break;
     96      case 'i': {
     97        // Leaving to be able to read and understand oldformatted entries
     98        uint32_t deprecatedAppId = 0;
     99        if (!ReadInteger(&deprecatedAppId)) {
    100          return false;  // not a valid 32-bit integer
    101        }
    102        break;
    103      }
    104      case '~':
    105        if (!ParseValue(&idEnhance)) {
    106          return false;
    107        }
    108        break;
    109      default:
    110        if (!ParseValue()) {  // skip any tag values, optional
    111          return false;
    112        }
    113        break;
    114    }
    115 
    116    // We expect a comma after every tag
    117    if (!CheckChar(',')) {
    118      return false;
    119    }
    120 
    121    // Recurse to the next tag
    122    return ParseTags();
    123  }
    124 
    125  bool ParseValue(nsACString* result = nullptr) {
    126    // If at the end, fail since we expect a comma ; value may be empty tho
    127    if (CheckEOF()) {
    128      return false;
    129    }
    130 
    131    Token t;
    132    while (Next(t)) {
    133      if (!Token::Char(',').Equals(t)) {
    134        if (result) {
    135          result->Append(t.Fragment());
    136        }
    137        continue;
    138      }
    139 
    140      if (CheckChar(',')) {
    141        // Two commas in a row, escaping
    142        if (result) {
    143          result->Append(',');
    144        }
    145        continue;
    146      }
    147 
    148      // We must give the comma back since the upper calls expect it
    149      Rollback();
    150      return true;
    151    }
    152 
    153    return false;
    154  }
    155 
    156 public:
    157  already_AddRefed<LoadContextInfo> Parse() {
    158    RefPtr<LoadContextInfo> info;
    159    if (ParseTags()) {
    160      info = GetLoadContextInfo(isAnonymous, originAttribs);
    161    }
    162 
    163    return info.forget();
    164  }
    165 
    166  void URISpec(nsACString& result) { result.Assign(cacheKey); }
    167 
    168  void IdEnhance(nsACString& result) { result.Assign(idEnhance); }
    169 };
    170 
    171 }  // namespace
    172 
    173 already_AddRefed<nsILoadContextInfo> ParseKey(const nsACString& aKey,
    174                                              nsACString* aIdEnhance,
    175                                              nsACString* aURISpec) {
    176  KeyParser parser(aKey);
    177  RefPtr<LoadContextInfo> info = parser.Parse();
    178 
    179  if (info) {
    180    if (aIdEnhance) parser.IdEnhance(*aIdEnhance);
    181    if (aURISpec) parser.URISpec(*aURISpec);
    182  }
    183 
    184  return info.forget();
    185 }
    186 
    187 void AppendKeyPrefix(nsILoadContextInfo* aInfo, nsACString& _retval) {
    188  /**
    189   * This key is used to salt file hashes.  When form of the key is changed
    190   * cache entries will fail to find on disk.
    191   *
    192   * IMPORTANT NOTE:
    193   * Keep the attributes list sorted according their ASCII code.
    194   */
    195 
    196  if (!aInfo) {
    197    return;
    198  }
    199 
    200  OriginAttributes const* oa = aInfo->OriginAttributesPtr();
    201  nsAutoCString suffix;
    202  oa->CreateSuffix(suffix);
    203  if (!suffix.IsEmpty()) {
    204    AppendTagWithValue(_retval, 'O', suffix);
    205  }
    206 
    207  if (aInfo->IsAnonymous()) {
    208    _retval.AppendLiteral("a,");
    209  }
    210 
    211  if (aInfo->IsPrivate()) {
    212    _retval.AppendLiteral("p,");
    213  }
    214 }
    215 
    216 void AppendTagWithValue(nsACString& aTarget, char const aTag,
    217                        const nsACString& aValue) {
    218  aTarget.Append(aTag);
    219 
    220  // First check the value string to save some memory copying
    221  // for cases we don't need to escape at all (most likely).
    222  if (!aValue.IsEmpty()) {
    223    if (!aValue.Contains(',')) {
    224      // No need to escape
    225      aTarget.Append(aValue);
    226    } else {
    227      nsAutoCString escapedValue(aValue);
    228      escapedValue.ReplaceSubstring(","_ns, ",,"_ns);
    229      aTarget.Append(escapedValue);
    230    }
    231  }
    232 
    233  aTarget.Append(',');
    234 }
    235 
    236 nsresult KeyMatchesLoadContextInfo(const nsACString& aKey,
    237                                   nsILoadContextInfo* aInfo, bool* _retval) {
    238  nsCOMPtr<nsILoadContextInfo> info = ParseKey(aKey);
    239 
    240  if (!info) {
    241    return NS_ERROR_FAILURE;
    242  }
    243 
    244  *_retval = info->Equals(aInfo);
    245  return NS_OK;
    246 }
    247 
    248 ValidityPair::ValidityPair(uint32_t aOffset, uint32_t aLen)
    249    : mOffset(aOffset), mLen(aLen) {}
    250 
    251 bool ValidityPair::CanBeMerged(const ValidityPair& aOther) const {
    252  // The pairs can be merged into a single one if the start of one of the pairs
    253  // is placed anywhere in the validity interval of other pair or exactly after
    254  // its end.
    255  return IsInOrFollows(aOther.mOffset) || aOther.IsInOrFollows(mOffset);
    256 }
    257 
    258 bool ValidityPair::IsInOrFollows(uint32_t aOffset) const {
    259  return mOffset <= aOffset && mOffset + mLen >= aOffset;
    260 }
    261 
    262 bool ValidityPair::LessThan(const ValidityPair& aOther) const {
    263  if (mOffset < aOther.mOffset) {
    264    return true;
    265  }
    266 
    267  if (mOffset == aOther.mOffset && mLen < aOther.mLen) {
    268    return true;
    269  }
    270 
    271  return false;
    272 }
    273 
    274 void ValidityPair::Merge(const ValidityPair& aOther) {
    275  MOZ_ASSERT(CanBeMerged(aOther));
    276 
    277  uint32_t offset = std::min(mOffset, aOther.mOffset);
    278  uint32_t end = std::max(mOffset + mLen, aOther.mOffset + aOther.mLen);
    279 
    280  mOffset = offset;
    281  mLen = end - offset;
    282 }
    283 
    284 void ValidityMap::Log() const {
    285  LOG(("ValidityMap::Log() - number of pairs: %zu", mMap.Length()));
    286  for (uint32_t i = 0; i < mMap.Length(); i++) {
    287    LOG(("    (%u, %u)", mMap[i].Offset() + 0, mMap[i].Len() + 0));
    288  }
    289 }
    290 
    291 uint32_t ValidityMap::Length() const { return mMap.Length(); }
    292 
    293 void ValidityMap::AddPair(uint32_t aOffset, uint32_t aLen) {
    294  ValidityPair pair(aOffset, aLen);
    295 
    296  if (mMap.Length() == 0) {
    297    mMap.AppendElement(pair);
    298    return;
    299  }
    300 
    301  // Find out where to place this pair into the map, it can overlap only with
    302  // one preceding pair and all subsequent pairs.
    303  uint32_t pos = 0;
    304  for (pos = mMap.Length(); pos > 0;) {
    305    --pos;
    306 
    307    if (mMap[pos].LessThan(pair)) {
    308      // The new pair should be either inserted after pos or merged with it.
    309      if (mMap[pos].CanBeMerged(pair)) {
    310        // Merge with the preceding pair
    311        mMap[pos].Merge(pair);
    312      } else {
    313        // They don't overlap, element must be placed after pos element
    314        ++pos;
    315        if (pos == mMap.Length()) {
    316          mMap.AppendElement(pair);
    317        } else {
    318          mMap.InsertElementAt(pos, pair);
    319        }
    320      }
    321 
    322      break;
    323    }
    324 
    325    if (pos == 0) {
    326      // The new pair should be placed in front of all existing pairs.
    327      mMap.InsertElementAt(0, pair);
    328    }
    329  }
    330 
    331  // pos now points to merged or inserted pair, check whether it overlaps with
    332  // subsequent pairs.
    333  while (pos + 1 < mMap.Length()) {
    334    if (mMap[pos].CanBeMerged(mMap[pos + 1])) {
    335      mMap[pos].Merge(mMap[pos + 1]);
    336      mMap.RemoveElementAt(pos + 1);
    337    } else {
    338      break;
    339    }
    340  }
    341 }
    342 
    343 void ValidityMap::Clear() { mMap.Clear(); }
    344 
    345 size_t ValidityMap::SizeOfExcludingThis(
    346    mozilla::MallocSizeOf mallocSizeOf) const {
    347  return mMap.ShallowSizeOfExcludingThis(mallocSizeOf);
    348 }
    349 
    350 ValidityPair& ValidityMap::operator[](uint32_t aIdx) {
    351  return mMap.ElementAt(aIdx);
    352 }
    353 
    354 StaticMutex DetailedCacheHitTelemetry::sLock;
    355 uint32_t DetailedCacheHitTelemetry::sRecordCnt = 0;
    356 MOZ_RUNINIT DetailedCacheHitTelemetry::HitRate
    357    DetailedCacheHitTelemetry::sHRStats[kNumOfRanges];
    358 
    359 DetailedCacheHitTelemetry::HitRate::HitRate() { Reset(); }
    360 
    361 void DetailedCacheHitTelemetry::HitRate::AddRecord(ERecType aType) {
    362  if (aType == HIT) {
    363    ++mHitCnt;
    364  } else {
    365    ++mMissCnt;
    366  }
    367 }
    368 
    369 uint32_t DetailedCacheHitTelemetry::HitRate::GetHitRateBucket() const {
    370  if (mHitCnt + mMissCnt == 0) {
    371    return 0;
    372  }
    373  return (100 * mHitCnt) / (mHitCnt + mMissCnt);
    374 }
    375 
    376 uint32_t DetailedCacheHitTelemetry::HitRate::Count() {
    377  return mHitCnt + mMissCnt;
    378 }
    379 
    380 void DetailedCacheHitTelemetry::HitRate::Reset() {
    381  mHitCnt = 0;
    382  mMissCnt = 0;
    383 }
    384 
    385 // static
    386 void DetailedCacheHitTelemetry::AddRecord(ERecType aType,
    387                                          TimeStamp aLoadStart) {
    388  bool isUpToDate = false;
    389  CacheIndex::IsUpToDate(&isUpToDate);
    390  if (!isUpToDate) {
    391    // Ignore the record when the entry file count might be incorrect
    392    return;
    393  }
    394 
    395  uint32_t entryCount;
    396  nsresult rv = CacheIndex::GetEntryFileCount(&entryCount);
    397  if (NS_FAILED(rv)) {
    398    return;
    399  }
    400 
    401  uint32_t rangeIdx = entryCount / kRangeSize;
    402  if (rangeIdx >= kNumOfRanges) {  // The last range has no upper limit.
    403    rangeIdx = kNumOfRanges - 1;
    404  }
    405 
    406 #ifndef ANDROID
    407  nsAutoCString hitMissValue;
    408  if (aType == HIT) {
    409    hitMissValue.AppendLiteral("Hit ");
    410  } else {
    411    hitMissValue.AppendLiteral("Miss ");
    412  }
    413 
    414  uint32_t lowerBound = rangeIdx * kRangeSize;
    415  if (rangeIdx < kNumOfRanges - 1) {
    416    uint32_t upperBound = (rangeIdx + 1) * kRangeSize - 1;
    417    hitMissValue.AppendInt(lowerBound);
    418    hitMissValue.AppendLiteral("-");
    419    hitMissValue.AppendInt(upperBound);
    420  } else {
    421    // Since no upper limit
    422    hitMissValue.AppendInt(lowerBound);
    423    hitMissValue.AppendLiteral("+");
    424  }
    425 #endif
    426 
    427  StaticMutexAutoLock lock(sLock);
    428 
    429  if (aType == MISS) {
    430    mozilla::glean::network::cache_miss_time.AccumulateRawDuration(
    431        TimeStamp::Now() - aLoadStart);
    432  } else {
    433    mozilla::glean::network::cache_hit_time.AccumulateRawDuration(
    434        TimeStamp::Now() - aLoadStart);
    435  }
    436 #ifndef ANDROID
    437  mozilla::glean::network::cache_hit_miss_stat_per_cache_size.Get(hitMissValue)
    438      .Add(1);
    439 #endif
    440 
    441  sHRStats[rangeIdx].AddRecord(aType);
    442  ++sRecordCnt;
    443 
    444  if (sRecordCnt < kTotalSamplesReportLimit) {
    445    return;
    446  }
    447 
    448  sRecordCnt = 0;
    449 
    450  for (uint32_t i = 0; i < kNumOfRanges; ++i) {
    451    if (sHRStats[i].Count() >= kHitRateSamplesReportLimit) {
    452 #ifndef ANDROID
    453      nsAutoCString cacheSizeIdx;
    454      cacheSizeIdx.AppendInt(i);
    455      uint32_t hitRateBucket = sHRStats[i].GetHitRateBucket();
    456 
    457      mozilla::glean::network::cache_hit_rate_per_cache_size.Get(cacheSizeIdx)
    458          .AccumulateSingleSample(hitRateBucket);
    459 #endif
    460      sHRStats[i].Reset();
    461    }
    462  }
    463 }
    464 
    465 StaticMutex CachePerfStats::sLock;
    466 MOZ_RUNINIT CachePerfStats::PerfData
    467    CachePerfStats::sData[CachePerfStats::LAST];
    468 uint32_t CachePerfStats::sCacheSlowCnt = 0;
    469 uint32_t CachePerfStats::sCacheNotSlowCnt = 0;
    470 
    471 CachePerfStats::MMA::MMA(uint32_t aTotalWeight, bool aFilter)
    472    : mSum(0), mSumSq(0), mCnt(0), mWeight(aTotalWeight), mFilter(aFilter) {}
    473 
    474 void CachePerfStats::MMA::AddValue(uint32_t aValue) {
    475  if (mFilter) {
    476    // Filter high spikes
    477    uint32_t avg = GetAverage();
    478    uint32_t stddev = GetStdDev();
    479    uint32_t maxdiff = avg + (3 * stddev);
    480    if (avg && aValue > avg + maxdiff) {
    481      return;
    482    }
    483  }
    484 
    485  if (mCnt < mWeight) {
    486    // Compute arithmetic average until we have at least mWeight values
    487    CheckedInt<uint64_t> newSumSq = CheckedInt<uint64_t>(aValue) * aValue;
    488    newSumSq += mSumSq;
    489    if (!newSumSq.isValid()) {
    490      return;  // ignore this value
    491    }
    492    mSumSq = newSumSq.value();
    493    mSum += aValue;
    494    ++mCnt;
    495  } else {
    496    CheckedInt<uint64_t> newSumSq = mSumSq - mSumSq / mCnt;
    497    newSumSq += static_cast<uint64_t>(aValue) * aValue;
    498    if (!newSumSq.isValid()) {
    499      return;  // ignore this value
    500    }
    501    mSumSq = newSumSq.value();
    502 
    503    // Compute modified moving average for more values:
    504    // newAvg = ((weight - 1) * oldAvg + newValue) / weight
    505    mSum -= GetAverage();
    506    mSum += aValue;
    507  }
    508 }
    509 
    510 uint32_t CachePerfStats::MMA::GetAverage() {
    511  if (mCnt == 0) {
    512    return 0;
    513  }
    514 
    515  return mSum / mCnt;
    516 }
    517 
    518 uint32_t CachePerfStats::MMA::GetStdDev() {
    519  if (mCnt == 0) {
    520    return 0;
    521  }
    522 
    523  uint32_t avg = GetAverage();
    524  uint64_t avgSq = static_cast<uint64_t>(avg) * avg;
    525  uint64_t variance = mSumSq / mCnt;
    526  if (variance < avgSq) {
    527    // Due to rounding error when using integer data type, it can happen that
    528    // average of squares of the values is smaller than square of the average
    529    // of the values. In this case fix mSumSq.
    530    variance = avgSq;
    531    mSumSq = variance * mCnt;
    532  }
    533 
    534  variance -= avgSq;
    535  return sqrt(static_cast<double>(variance));
    536 }
    537 
    538 CachePerfStats::PerfData::PerfData()
    539    : mFilteredAvg(50, true), mShortAvg(3, false) {}
    540 
    541 void CachePerfStats::PerfData::AddValue(uint32_t aValue, bool aShortOnly) {
    542  if (!aShortOnly) {
    543    mFilteredAvg.AddValue(aValue);
    544  }
    545  mShortAvg.AddValue(aValue);
    546 }
    547 
    548 uint32_t CachePerfStats::PerfData::GetAverage(bool aFiltered) {
    549  return aFiltered ? mFilteredAvg.GetAverage() : mShortAvg.GetAverage();
    550 }
    551 
    552 uint32_t CachePerfStats::PerfData::GetStdDev(bool aFiltered) {
    553  return aFiltered ? mFilteredAvg.GetStdDev() : mShortAvg.GetStdDev();
    554 }
    555 
    556 // static
    557 void CachePerfStats::AddValue(EDataType aType, uint32_t aValue,
    558                              bool aShortOnly) {
    559  StaticMutexAutoLock lock(sLock);
    560  sData[aType].AddValue(aValue, aShortOnly);
    561 }
    562 
    563 // static
    564 uint32_t CachePerfStats::GetAverage(EDataType aType, bool aFiltered) {
    565  StaticMutexAutoLock lock(sLock);
    566  return sData[aType].GetAverage(aFiltered);
    567 }
    568 
    569 // static
    570 uint32_t CachePerfStats::GetStdDev(EDataType aType, bool aFiltered) {
    571  StaticMutexAutoLock lock(sLock);
    572  return sData[aType].GetStdDev(aFiltered);
    573 }
    574 
    575 // static
    576 bool CachePerfStats::IsCacheSlow() {
    577  StaticMutexAutoLock lock(sLock);
    578 
    579  // Compare mShortAvg with mFilteredAvg to find out whether cache is getting
    580  // slower. Use only data about single IO operations because ENTRY_OPEN can be
    581  // affected by more factors than a slow disk.
    582  for (uint32_t i = 0; i < ENTRY_OPEN; ++i) {
    583    if (i == IO_WRITE) {
    584      // Skip this data type. IsCacheSlow is used for determining cache slowness
    585      // when opening entries. Writes have low priority and it's normal that
    586      // they are delayed a lot, but this doesn't necessarily affect opening
    587      // cache entries.
    588      continue;
    589    }
    590 
    591    uint32_t avgLong = sData[i].GetAverage(true);
    592    if (avgLong == 0) {
    593      // We have no perf data yet, skip this data type.
    594      continue;
    595    }
    596    uint32_t avgShort = sData[i].GetAverage(false);
    597    uint32_t stddevLong = sData[i].GetStdDev(true);
    598    uint32_t maxdiff = avgLong + (3 * stddevLong);
    599 
    600    if (avgShort > avgLong + maxdiff) {
    601      LOG(
    602          ("CachePerfStats::IsCacheSlow() - result is slow based on perf "
    603           "type %u [avgShort=%u, avgLong=%u, stddevLong=%u]",
    604           i, avgShort, avgLong, stddevLong));
    605      ++sCacheSlowCnt;
    606      return true;
    607    }
    608  }
    609 
    610  ++sCacheNotSlowCnt;
    611  return false;
    612 }
    613 
    614 // static
    615 void CachePerfStats::GetSlowStats(uint32_t* aSlow, uint32_t* aNotSlow) {
    616  StaticMutexAutoLock lock(sLock);
    617  *aSlow = sCacheSlowCnt;
    618  *aNotSlow = sCacheNotSlowCnt;
    619 }
    620 
    621 void FreeBuffer(void* aBuf) {
    622 #ifndef NS_FREE_PERMANENT_DATA
    623  if (CacheObserver::ShuttingDown()) {
    624    return;
    625  }
    626 #endif
    627 
    628  free(aBuf);
    629 }
    630 
    631 nsresult ParseAlternativeDataInfo(const char* aInfo, int64_t* _offset,
    632                                  nsACString* _type) {
    633  // The format is: "1;12345,javascript/binary"
    634  //         <version>;<offset>,<type>
    635  mozilla::Tokenizer p(aInfo, nullptr, "/");
    636  uint32_t altDataVersion = 0;
    637  int64_t altDataOffset = -1;
    638 
    639  // The metadata format has a wrong version number.
    640  if (!p.ReadInteger(&altDataVersion) || altDataVersion != kAltDataVersion) {
    641    LOG(
    642        ("ParseAlternativeDataInfo() - altDataVersion=%u, "
    643         "expectedVersion=%u",
    644         altDataVersion, kAltDataVersion));
    645    return NS_ERROR_NOT_AVAILABLE;
    646  }
    647 
    648  if (!p.CheckChar(';') || !p.ReadInteger(&altDataOffset) ||
    649      !p.CheckChar(',')) {
    650    return NS_ERROR_NOT_AVAILABLE;
    651  }
    652 
    653  // The requested alt-data representation is not available
    654  if (altDataOffset < 0) {
    655    return NS_ERROR_NOT_AVAILABLE;
    656  }
    657 
    658  if (_offset) {
    659    *_offset = altDataOffset;
    660  }
    661 
    662  if (_type) {
    663    (void)p.ReadUntil(Tokenizer::Token::EndOfFile(), *_type);
    664  }
    665 
    666  return NS_OK;
    667 }
    668 
    669 void BuildAlternativeDataInfo(const char* aInfo, int64_t aOffset,
    670                              nsACString& _retval) {
    671  _retval.Truncate();
    672  _retval.AppendInt(kAltDataVersion);
    673  _retval.Append(';');
    674  _retval.AppendInt(aOffset);
    675  _retval.Append(',');
    676  _retval.Append(aInfo);
    677 }
    678 
    679 }  // namespace mozilla::net::CacheFileUtils