/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "CacheFile.h"

#include <algorithm>
#include <utility>

#include "CacheFileChunk.h"
#include "CacheFileInputStream.h"
#include "CacheFileOutputStream.h"
#include "CacheFileUtils.h"
#include "CacheIndex.h"
#include "CacheLog.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/Telemetry.h"
#include "mozilla/TelemetryHistogramEnums.h"
#include "nsComponentManagerUtils.h"
#include "nsICacheEntry.h"
#include "nsProxyRelease.h"
#include "nsThreadUtils.h"

// When CACHE_CHUNKS is defined we always cache unused chunks in mCacheChunks.
// When it is not defined, we always release the chunks ASAP, i.e. we cache
// unused chunks only when:
//  - CacheFile is memory-only
//  - CacheFile is still waiting for the handle
//  - the chunk is preloaded

// #define CACHE_CHUNKS

namespace mozilla::net {

using CacheFileUtils::CacheFileLock;

class NotifyCacheFileListenerEvent : public Runnable {
 public:
  NotifyCacheFileListenerEvent(CacheFileListener* aCallback, nsresult aResult,
                               bool aIsNew)
      : Runnable("net::NotifyCacheFileListenerEvent"),
        mCallback(aCallback),
        mRV(aResult),
        mIsNew(aIsNew) {
    LOG(
        ("NotifyCacheFileListenerEvent::NotifyCacheFileListenerEvent() "
         "[this=%p]",
         this));
  }

 protected:
  ~NotifyCacheFileListenerEvent() {
    LOG(
        ("NotifyCacheFileListenerEvent::~NotifyCacheFileListenerEvent() "
         "[this=%p]",
         this));
  }

 public:
  NS_IMETHOD Run() override {
    LOG(("NotifyCacheFileListenerEvent::Run() [this=%p]", this));

    mCallback->OnFileReady(mRV, mIsNew);
    return NS_OK;
  }

 protected:
  nsCOMPtr<CacheFileListener> mCallback;
  nsresult mRV;
  bool mIsNew;
};

class NotifyChunkListenerEvent : public Runnable {
 public:
  NotifyChunkListenerEvent(CacheFileChunkListener* aCallback, nsresult aResult,
                           uint32_t aChunkIdx, CacheFileChunk* aChunk)
      : Runnable("net::NotifyChunkListenerEvent"),
        mCallback(aCallback),
        mRV(aResult),
        mChunkIdx(aChunkIdx),
        mChunk(aChunk) {
    LOG(("NotifyChunkListenerEvent::NotifyChunkListenerEvent() [this=%p]",
         this));
  }

 protected:
  ~NotifyChunkListenerEvent() {
    LOG(("NotifyChunkListenerEvent::~NotifyChunkListenerEvent() [this=%p]",
         this));
  }

 public:
  NS_IMETHOD Run() override {
    LOG(("NotifyChunkListenerEvent::Run() [this=%p]", this));

    mCallback->OnChunkAvailable(mRV, mChunkIdx, mChunk);
    return NS_OK;
  }

 protected:
  nsCOMPtr<CacheFileChunkListener> mCallback;
  nsresult mRV;
  uint32_t mChunkIdx;
  RefPtr<CacheFileChunk> mChunk;
};

class DoomFileHelper : public CacheFileIOListener {
 public:
  NS_DECL_THREADSAFE_ISUPPORTS

  explicit DoomFileHelper(CacheFileListener* aListener)
      : mListener(aListener) {}

  NS_IMETHOD OnFileOpened(CacheFileHandle* aHandle, nsresult aResult) override {
    MOZ_CRASH("DoomFileHelper::OnFileOpened should not be called!");
    return NS_ERROR_UNEXPECTED;
  }

  NS_IMETHOD OnDataWritten(CacheFileHandle* aHandle, const char* aBuf,
                           nsresult aResult) override {
    MOZ_CRASH("DoomFileHelper::OnDataWritten should not be called!");
    return NS_ERROR_UNEXPECTED;
  }

  NS_IMETHOD OnDataRead(CacheFileHandle* aHandle, char* aBuf,
                        nsresult aResult) override {
    MOZ_CRASH("DoomFileHelper::OnDataRead should not be called!");
    return NS_ERROR_UNEXPECTED;
  }

  NS_IMETHOD OnFileDoomed(CacheFileHandle* aHandle, nsresult aResult) override {
    if (mListener) mListener->OnFileDoomed(aResult);
    return NS_OK;
  }

  NS_IMETHOD OnEOFSet(CacheFileHandle* aHandle, nsresult aResult) override {
    MOZ_CRASH("DoomFileHelper::OnEOFSet should not be called!");
    return NS_ERROR_UNEXPECTED;
  }

  NS_IMETHOD OnFileRenamed(CacheFileHandle* aHandle,
                           nsresult aResult) override {
    MOZ_CRASH("DoomFileHelper::OnFileRenamed should not be called!");
    return NS_ERROR_UNEXPECTED;
  }

 private:
  virtual ~DoomFileHelper() = default;

  nsCOMPtr<CacheFileListener> mListener;
};

NS_IMPL_ISUPPORTS(DoomFileHelper, CacheFileIOListener)

NS_IMPL_ADDREF(CacheFile)
NS_IMPL_RELEASE(CacheFile)
NS_INTERFACE_MAP_BEGIN(CacheFile)
  NS_INTERFACE_MAP_ENTRY(mozilla::net::CacheFileChunkListener)
  NS_INTERFACE_MAP_ENTRY(mozilla::net::CacheFileIOListener)
  NS_INTERFACE_MAP_ENTRY(mozilla::net::CacheFileMetadataListener)
  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports,
                                   mozilla::net::CacheFileChunkListener)
NS_INTERFACE_MAP_END

CacheFile::CacheFile() : mLock(new CacheFileLock()) {
  LOG(("CacheFile::CacheFile() [this=%p]", this));
}

CacheFile::~CacheFile() {
  LOG(("CacheFile::~CacheFile() [this=%p]", this));

  MutexAutoLock lock(mLock->Lock());
  if (!mMemoryOnly && mReady && !mKill) {
    // mReady flag indicates we have metadata plus in a valid state.
    WriteMetadataIfNeededLocked(true);
  }
}

nsresult CacheFile::Init(const nsACString& aKey, bool aCreateNew,
                         bool aMemoryOnly, bool aSkipSizeCheck, bool aPriority,
                         bool aPinned, CacheFileListener* aCallback)
    MOZ_NO_THREAD_SAFETY_ANALYSIS {
  MOZ_ASSERT(!mListener);
  MOZ_ASSERT(!mHandle);

  MOZ_ASSERT(!(aMemoryOnly && aPinned));

  nsresult rv;

  mKey = aKey;
  mOpenAsMemoryOnly = mMemoryOnly = aMemoryOnly;
  mSkipSizeCheck = aSkipSizeCheck;
  mPriority = aPriority;
  mPinned = aPinned;

  // Some consumers (at least nsHTTPCompressConv) assume that Read() can read
  // such amount of data that was announced by Available().
  // CacheFileInputStream::Available() uses also preloaded chunks to compute
  // number of available bytes in the input stream, so we have to make sure the
  // preloadChunkCount won't change during CacheFile's lifetime since otherwise
  // we could potentially release some cached chunks that was used to calculate
  // available bytes but would not be available later during call to
  // CacheFileInputStream::Read().
  mPreloadChunkCount = CacheObserver::PreloadChunkCount();

  LOG(
      ("CacheFile::Init() [this=%p, key=%s, createNew=%d, memoryOnly=%d, "
       "priority=%d, listener=%p]",
       this, mKey.get(), aCreateNew, aMemoryOnly, aPriority, aCallback));

  if (mMemoryOnly) {
    MOZ_ASSERT(!aCallback);

    mMetadata = new CacheFileMetadata(mOpenAsMemoryOnly, false, mKey,
                                      WrapNotNull(mLock));
    mReady = true;
    mDataSize = mMetadata->Offset();
    return NS_OK;
  }
  uint32_t flags;
  if (aCreateNew) {
    MOZ_ASSERT(!aCallback);
    flags = CacheFileIOManager::CREATE_NEW;

    // make sure we can use this entry immediately
    mMetadata = new CacheFileMetadata(mOpenAsMemoryOnly, mPinned, mKey,
                                      WrapNotNull(mLock));
    mReady = true;
    mDataSize = mMetadata->Offset();
  } else {
    flags = CacheFileIOManager::CREATE;
  }

  if (mPriority) {
    flags |= CacheFileIOManager::PRIORITY;
  }

  if (mPinned) {
    flags |= CacheFileIOManager::PINNED;
  }

  mOpeningFile = true;
  mListener = aCallback;
  rv = CacheFileIOManager::OpenFile(mKey, flags, this);
  if (NS_FAILED(rv)) {
    mListener = nullptr;
    mOpeningFile = false;

    if (mPinned) {
      LOG(
          ("CacheFile::Init() - CacheFileIOManager::OpenFile() failed "
           "but we want to pin, fail the file opening. [this=%p]",
           this));
      return NS_ERROR_NOT_AVAILABLE;
    }

    if (aCreateNew) {
      NS_WARNING("Forcing memory-only entry since OpenFile failed");
      LOG(
          ("CacheFile::Init() - CacheFileIOManager::OpenFile() failed "
           "synchronously. We can continue in memory-only mode since "
           "aCreateNew == true. [this=%p]",
           this));

      mMemoryOnly = true;
    } else if (rv == NS_ERROR_NOT_INITIALIZED) {
      NS_WARNING(
          "Forcing memory-only entry since CacheIOManager isn't "
          "initialized.");
      LOG(
          ("CacheFile::Init() - CacheFileIOManager isn't initialized, "
           "initializing entry as memory-only. [this=%p]",
           this));

      mMemoryOnly = true;
      mMetadata = new CacheFileMetadata(mOpenAsMemoryOnly, mPinned, mKey,
                                        WrapNotNull(mLock));
      mReady = true;
      mDataSize = mMetadata->Offset();

      RefPtr<NotifyCacheFileListenerEvent> ev;
      ev = new NotifyCacheFileListenerEvent(aCallback, NS_OK, true);
      rv = NS_DispatchToCurrentThread(ev);
      NS_ENSURE_SUCCESS(rv, rv);
    } else {
      NS_ENSURE_SUCCESS(rv, rv);
    }
  }

  return NS_OK;
}

void CacheFile::Key(nsACString& aKey) {
  CacheFileAutoLock lock(this);
  aKey = mKey;
}

bool CacheFile::IsPinned() {
  CacheFileAutoLock lock(this);
  return mPinned;
}

nsresult CacheFile::OnChunkRead(nsresult aResult, CacheFileChunk* aChunk) {
  CacheFileAutoLock lock(this);

  nsresult rv;

  uint32_t index = aChunk->Index();

  LOG(("CacheFile::OnChunkRead() [this=%p, rv=0x%08" PRIx32
       ", chunk=%p, idx=%u]",
       this, static_cast<uint32_t>(aResult), aChunk, index));

  if (aChunk->mDiscardedChunk) {
    // We discard only unused chunks, so it must be still unused when reading
    // data finishes.
    MOZ_ASSERT(aChunk->mRefCnt == 2);
    aChunk->mActiveChunk = false;
    ReleaseOutsideLock(
        RefPtr<CacheFileChunkListener>(std::move(aChunk->mFile)));

    DebugOnly<bool> removed = mDiscardedChunks.RemoveElement(aChunk);
    MOZ_ASSERT(removed);
    return NS_OK;
  }

  if (NS_FAILED(aResult)) {
    SetError(aResult);
  }

  if (HaveChunkListeners(index)) {
    rv = NotifyChunkListeners(index, aResult, aChunk);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  return NS_OK;
}

nsresult CacheFile::OnChunkWritten(nsresult aResult, CacheFileChunk* aChunk) {
  // In case the chunk was reused, made dirty and released between calls to
  // CacheFileChunk::Write() and CacheFile::OnChunkWritten(), we must write
  // the chunk to the disk again. When the chunk is unused and is dirty simply
  // addref and release (outside the lock) the chunk which ensures that
  // CacheFile::DeactivateChunk() will be called again.
  RefPtr<CacheFileChunk> deactivateChunkAgain;

  CacheFileAutoLock lock(this);

  nsresult rv;

  LOG(("CacheFile::OnChunkWritten() [this=%p, rv=0x%08" PRIx32
       ", chunk=%p, idx=%u]",
       this, static_cast<uint32_t>(aResult), aChunk, aChunk->Index()));

  MOZ_ASSERT(!mMemoryOnly);
  MOZ_ASSERT(!mOpeningFile);
  MOZ_ASSERT(mHandle);

  if (aChunk->mDiscardedChunk) {
    // We discard only unused chunks, so it must be still unused when writing
    // data finishes.
    MOZ_ASSERT(aChunk->mRefCnt == 2);
    aChunk->mActiveChunk = false;
    ReleaseOutsideLock(
        RefPtr<CacheFileChunkListener>(std::move(aChunk->mFile)));

    DebugOnly<bool> removed = mDiscardedChunks.RemoveElement(aChunk);
    MOZ_ASSERT(removed);
    return NS_OK;
  }

  if (NS_FAILED(aResult)) {
    SetError(aResult);
  }

  if (NS_SUCCEEDED(aResult) && !aChunk->IsDirty()) {
    // update hash value in metadata
    mMetadata->SetHash(aChunk->Index(), aChunk->Hash());
  }

  // notify listeners if there is any
  if (HaveChunkListeners(aChunk->Index())) {
    // don't release the chunk since there are some listeners queued
    rv = NotifyChunkListeners(aChunk->Index(), aResult, aChunk);
    if (NS_SUCCEEDED(rv)) {
      MOZ_ASSERT(aChunk->mRefCnt != 2);
      return NS_OK;
    }
  }

  if (aChunk->mRefCnt != 2) {
    LOG(
        ("CacheFile::OnChunkWritten() - Chunk is still used [this=%p, chunk=%p,"
         " refcnt=%" PRIuPTR "]",
         this, aChunk, aChunk->mRefCnt.get()));

    return NS_OK;
  }

  if (aChunk->IsDirty()) {
    LOG(
        ("CacheFile::OnChunkWritten() - Unused chunk is dirty. We must go "
         "through deactivation again. [this=%p, chunk=%p]",
         this, aChunk));

    deactivateChunkAgain = aChunk;
    return NS_OK;
  }

  bool keepChunk = false;
  if (NS_SUCCEEDED(aResult)) {
    keepChunk = ShouldCacheChunk(aChunk->Index());
    LOG(("CacheFile::OnChunkWritten() - %s unused chunk [this=%p, chunk=%p]",
         keepChunk ? "Caching" : "Releasing", this, aChunk));
  } else {
    LOG(
        ("CacheFile::OnChunkWritten() - Releasing failed chunk [this=%p, "
         "chunk=%p]",
         this, aChunk));
  }

  RemoveChunkInternal(aChunk, keepChunk);

  WriteMetadataIfNeededLocked();

  return NS_OK;
}

nsresult CacheFile::OnChunkAvailable(nsresult aResult, uint32_t aChunkIdx,
                                     CacheFileChunk* aChunk) {
  MOZ_CRASH("CacheFile::OnChunkAvailable should not be called!");
  return NS_ERROR_UNEXPECTED;
}

nsresult CacheFile::OnChunkUpdated(CacheFileChunk* aChunk) {
  MOZ_CRASH("CacheFile::OnChunkUpdated should not be called!");
  return NS_ERROR_UNEXPECTED;
}

nsresult CacheFile::OnFileOpened(CacheFileHandle* aHandle, nsresult aResult) {
  // Using an 'auto' class to perform doom or fail the listener
  // outside the CacheFile's lock.
  class AutoFailDoomListener {
   public:
    explicit AutoFailDoomListener(CacheFileHandle* aHandle)
        : mHandle(aHandle), mAlreadyDoomed(false) {}
    ~AutoFailDoomListener() {
      if (!mListener) return;

      if (mHandle) {
        if (mAlreadyDoomed) {
          mListener->OnFileDoomed(mHandle, NS_OK);
        } else {
          CacheFileIOManager::DoomFile(mHandle, mListener);
        }
      } else {
        mListener->OnFileDoomed(nullptr, NS_ERROR_NOT_AVAILABLE);
      }
    }

    CacheFileHandle* mHandle;
    nsCOMPtr<CacheFileIOListener> mListener;
    bool mAlreadyDoomed;
  } autoDoom(aHandle);

  RefPtr<CacheFileMetadata> metadata;
  nsCOMPtr<CacheFileListener> listener;
  bool isNew = false;
  nsresult retval = NS_OK;

  {
    CacheFileAutoLock lock(this);

    MOZ_ASSERT(mOpeningFile);
    MOZ_ASSERT((NS_SUCCEEDED(aResult) && aHandle) ||
               (NS_FAILED(aResult) && !aHandle));
    MOZ_ASSERT((mListener && !mMetadata) ||  // !createNew
               (!mListener && mMetadata));   // createNew
    MOZ_ASSERT(!mMemoryOnly || mMetadata);   // memory-only was set on new entry

    LOG(("CacheFile::OnFileOpened() [this=%p, rv=0x%08" PRIx32 ", handle=%p]",
         this, static_cast<uint32_t>(aResult), aHandle));

    mOpeningFile = false;

    autoDoom.mListener.swap(mDoomAfterOpenListener);

    if (mMemoryOnly) {
      // We can be here only in case the entry was initilized as createNew and
      // SetMemoryOnly() was called.

      // Just don't store the handle into mHandle and exit
      autoDoom.mAlreadyDoomed = true;
      return NS_OK;
    }

    if (NS_FAILED(aResult)) {
      if (mMetadata) {
        // This entry was initialized as createNew, just switch to memory-only
        // mode.
        NS_WARNING("Forcing memory-only entry since OpenFile failed");
        LOG(
            ("CacheFile::OnFileOpened() - CacheFileIOManager::OpenFile() "
             "failed asynchronously. We can continue in memory-only mode since "
             "aCreateNew == true. [this=%p]",
             this));

        mMemoryOnly = true;
        return NS_OK;
      }

      if (aResult == NS_ERROR_FILE_INVALID_PATH) {
        // CacheFileIOManager doesn't have mCacheDirectory, switch to
        // memory-only mode.
        NS_WARNING(
            "Forcing memory-only entry since CacheFileIOManager doesn't "
            "have mCacheDirectory.");
        LOG(
            ("CacheFile::OnFileOpened() - CacheFileIOManager doesn't have "
             "mCacheDirectory, initializing entry as memory-only. [this=%p]",
             this));

        mMemoryOnly = true;
        mMetadata = new CacheFileMetadata(mOpenAsMemoryOnly, mPinned, mKey,
                                          WrapNotNull(mLock));
        mReady = true;
        mDataSize = mMetadata->Offset();

        isNew = true;
        retval = NS_OK;
      } else {
        // CacheFileIOManager::OpenFile() failed for another reason.
        isNew = false;
        retval = aResult;
      }

      mListener.swap(listener);
    } else {
      mHandle = aHandle;
      if (NS_FAILED(mStatus)) {
        CacheFileIOManager::DoomFile(mHandle, nullptr);
      }

      if (mMetadata) {
        InitIndexEntry();

        // The entry was initialized as createNew, don't try to read metadata.
        mMetadata->SetHandle(mHandle);

        // Write all cached chunks, otherwise they may stay unwritten.
        for (auto iter = mCachedChunks.Iter(); !iter.Done(); iter.Next()) {
          uint32_t idx = iter.Key();
          RefPtr<CacheFileChunk>& chunk = iter.Data();

          LOG(("CacheFile::OnFileOpened() - write [this=%p, idx=%u, chunk=%p]",
               this, idx, chunk.get()));

          mChunks.InsertOrUpdate(idx, RefPtr{chunk});
          chunk->mFile = this;
          chunk->mActiveChunk = true;

          MOZ_ASSERT(chunk->IsReady());

          // This would be cleaner if we had an nsRefPtr constructor that took
          // a RefPtr<Derived>.
          ReleaseOutsideLock(std::move(chunk));

          iter.Remove();
        }

        return NS_OK;
      }
    }
    if (listener) {
      lock.Unlock();
      listener->OnFileReady(retval, isNew);
      return NS_OK;
    }

    MOZ_ASSERT(NS_SUCCEEDED(aResult));
    MOZ_ASSERT(!mMetadata);
    MOZ_ASSERT(mListener);

    // mMetaData is protected by a lock, but ReadMetaData has to be called
    // without the lock.  Alternatively we could make a
    // "ReadMetaDataLocked", and temporarily unlock to call OnFileReady
    metadata = mMetadata =
        new CacheFileMetadata(mHandle, mKey, WrapNotNull(mLock));
  }
  metadata->ReadMetadata(this);
  return NS_OK;
}

nsresult CacheFile::OnDataWritten(CacheFileHandle* aHandle, const char* aBuf,
                                  nsresult aResult) {
  MOZ_CRASH("CacheFile::OnDataWritten should not be called!");
  return NS_ERROR_UNEXPECTED;
}

nsresult CacheFile::OnDataRead(CacheFileHandle* aHandle, char* aBuf,
                               nsresult aResult) {
  MOZ_CRASH("CacheFile::OnDataRead should not be called!");
  return NS_ERROR_UNEXPECTED;
}

nsresult CacheFile::OnMetadataRead(nsresult aResult) {
  nsCOMPtr<CacheFileListener> listener;
  bool isNew = false;
  {
    CacheFileAutoLock lock(this);
    MOZ_ASSERT(mListener);

    LOG(("CacheFile::OnMetadataRead() [this=%p, rv=0x%08" PRIx32 "]", this,
         static_cast<uint32_t>(aResult)));

    if (NS_SUCCEEDED(aResult)) {
      mPinned = mMetadata->Pinned();
      mReady = true;
      mDataSize = mMetadata->Offset();
      if (mDataSize == 0 && mMetadata->ElementsSize() == 0) {
        isNew = true;
        mMetadata->MarkDirty();
      } else {
        const char* altData =
            mMetadata->GetElement(CacheFileUtils::kAltDataKey);
        if (altData && (NS_FAILED(CacheFileUtils::ParseAlternativeDataInfo(
                            altData, &mAltDataOffset, &mAltDataType)) ||
                        (mAltDataOffset > mDataSize))) {
          // alt-metadata cannot be parsed or alt-data offset is invalid
          mMetadata->InitEmptyMetadata();
          isNew = true;
          mAltDataOffset = -1;
          mAltDataType.Truncate();
          mDataSize = 0;
        } else {
          PreloadChunks(0);
        }
      }

      InitIndexEntry();
    }

    mListener.swap(listener);
  }
  listener->OnFileReady(aResult, isNew);
  return NS_OK;
}

nsresult CacheFile::OnMetadataWritten(nsresult aResult) {
  CacheFileAutoLock lock(this);

  LOG(("CacheFile::OnMetadataWritten() [this=%p, rv=0x%08" PRIx32 "]", this,
       static_cast<uint32_t>(aResult)));

  MOZ_ASSERT(mWritingMetadata);
  mWritingMetadata = false;

  MOZ_ASSERT(!mMemoryOnly);
  MOZ_ASSERT(!mOpeningFile);

  if (NS_WARN_IF(NS_FAILED(aResult))) {
    // TODO close streams with an error ???
    SetError(aResult);
  }

  if (mOutput || mInputs.Length() || mChunks.Count()) return NS_OK;

  if (IsDirty()) WriteMetadataIfNeededLocked();

  if (!mWritingMetadata) {
    LOG(("CacheFile::OnMetadataWritten() - Releasing file handle [this=%p]",
         this));
    CacheFileIOManager::ReleaseNSPRHandle(mHandle);
  }

  return NS_OK;
}

nsresult CacheFile::OnFileDoomed(CacheFileHandle* aHandle, nsresult aResult) {
  nsCOMPtr<CacheFileListener> listener;

  {
    CacheFileAutoLock lock(this);

    MOZ_ASSERT(mListener);

    LOG(("CacheFile::OnFileDoomed() [this=%p, rv=0x%08" PRIx32 ", handle=%p]",
         this, static_cast<uint32_t>(aResult), aHandle));

    mListener.swap(listener);
  }

  listener->OnFileDoomed(aResult);
  return NS_OK;
}

nsresult CacheFile::OnEOFSet(CacheFileHandle* aHandle, nsresult aResult) {
  MOZ_CRASH("CacheFile::OnEOFSet should not be called!");
  return NS_ERROR_UNEXPECTED;
}

nsresult CacheFile::OnFileRenamed(CacheFileHandle* aHandle, nsresult aResult) {
  MOZ_CRASH("CacheFile::OnFileRenamed should not be called!");
  return NS_ERROR_UNEXPECTED;
}

bool CacheFile::IsKilled() {
  bool killed = mKill;
  if (killed) {
    LOG(("CacheFile is killed, this=%p", this));
  }

  return killed;
}

nsresult CacheFile::OpenInputStream(nsICacheEntry* aEntryHandle,
                                    nsIInputStream** _retval) {
  CacheFileAutoLock lock(this);

  MOZ_ASSERT(mHandle || mMemoryOnly || mOpeningFile);

  if (!mReady) {
    LOG(("CacheFile::OpenInputStream() - CacheFile is not ready [this=%p]",
         this));

    return NS_ERROR_NOT_AVAILABLE;
  }

  if (NS_FAILED(mStatus)) {
    LOG(
        ("CacheFile::OpenInputStream() - CacheFile is in a failure state "
         "[this=%p, status=0x%08" PRIx32 "]",
         this, static_cast<uint32_t>(mStatus)));

    // Don't allow opening the input stream when this CacheFile is in
    // a failed state.  This is the only way to protect consumers correctly
    // from reading a broken entry.  When the file is in the failed state,
    // it's also doomed, so reopening the entry won't make any difference -
    // data will still be inaccessible anymore.  Note that for just doomed
    // files, we must allow reading the data.
    return mStatus;
  }

  // Once we open input stream we no longer allow preloading of chunks without
  // input stream, i.e. we will no longer keep first few chunks preloaded when
  // the last input stream is closed.
  mPreloadWithoutInputStreams = false;

  CacheFileInputStream* input =
      new CacheFileInputStream(this, aEntryHandle, false);
  LOG(("CacheFile::OpenInputStream() - Creating new input stream %p [this=%p]",
       input, this));

  mInputs.AppendElement(input);
  NS_ADDREF(input);

  mDataAccessed = true;
  *_retval = do_AddRef(input).take();
  return NS_OK;
}

nsresult CacheFile::OpenAlternativeInputStream(nsICacheEntry* aEntryHandle,
                                               const char* aAltDataType,
                                               nsIInputStream** _retval) {
  CacheFileAutoLock lock(this);

  MOZ_ASSERT(mHandle || mMemoryOnly || mOpeningFile);

  if (NS_WARN_IF(!mReady)) {
    LOG(
        ("CacheFile::OpenAlternativeInputStream() - CacheFile is not ready "
         "[this=%p]",
         this));
    return NS_ERROR_NOT_AVAILABLE;
  }

  if (mAltDataOffset == -1) {
    LOG(
        ("CacheFile::OpenAlternativeInputStream() - Alternative data is not "
         "available [this=%p]",
         this));
    return NS_ERROR_NOT_AVAILABLE;
  }

  if (NS_FAILED(mStatus)) {
    LOG(
        ("CacheFile::OpenAlternativeInputStream() - CacheFile is in a failure "
         "state [this=%p, status=0x%08" PRIx32 "]",
         this, static_cast<uint32_t>(mStatus)));

    // Don't allow opening the input stream when this CacheFile is in
    // a failed state.  This is the only way to protect consumers correctly
    // from reading a broken entry.  When the file is in the failed state,
    // it's also doomed, so reopening the entry won't make any difference -
    // data will still be inaccessible anymore.  Note that for just doomed
    // files, we must allow reading the data.
    return mStatus;
  }

  if (mAltDataType != aAltDataType) {
    LOG(
        ("CacheFile::OpenAlternativeInputStream() - Alternative data is of a "
         "different type than requested [this=%p, availableType=%s, "
         "requestedType=%s]",
         this, mAltDataType.get(), aAltDataType));
    return NS_ERROR_NOT_AVAILABLE;
  }

  // Once we open input stream we no longer allow preloading of chunks without
  // input stream, i.e. we will no longer keep first few chunks preloaded when
  // the last input stream is closed.
  mPreloadWithoutInputStreams = false;

  CacheFileInputStream* input =
      new CacheFileInputStream(this, aEntryHandle, true);

  LOG(
      ("CacheFile::OpenAlternativeInputStream() - Creating new input stream %p "
       "[this=%p]",
       input, this));

  mInputs.AppendElement(input);
  NS_ADDREF(input);

  mDataAccessed = true;
  *_retval = do_AddRef(input).take();

  return NS_OK;
}

nsresult CacheFile::OpenOutputStream(CacheOutputCloseListener* aCloseListener,
                                     nsIOutputStream** _retval) {
  CacheFileAutoLock lock(this);

  MOZ_ASSERT(mHandle || mMemoryOnly || mOpeningFile);

  nsresult rv;

  if (!mReady) {
    LOG(("CacheFile::OpenOutputStream() - CacheFile is not ready [this=%p]",
         this));

    return NS_ERROR_NOT_AVAILABLE;
  }

  if (mOutput) {
    LOG(
        ("CacheFile::OpenOutputStream() - We already have output stream %p "
         "[this=%p]",
         mOutput, this));

    return NS_ERROR_NOT_AVAILABLE;
  }

  if (NS_FAILED(mStatus)) {
    LOG(
        ("CacheFile::OpenOutputStream() - CacheFile is in a failure state "
         "[this=%p, status=0x%08" PRIx32 "]",
         this, static_cast<uint32_t>(mStatus)));

    // The CacheFile is already doomed. It make no sense to allow to write any
    // data to such entry.
    return mStatus;
  }

  // Fail if there is any input stream opened for alternative data
  for (uint32_t i = 0; i < mInputs.Length(); ++i) {
    if (mInputs[i]->IsAlternativeData()) {
      return NS_ERROR_NOT_AVAILABLE;
    }
  }

  if (mAltDataOffset != -1) {
    // Remove alt-data
    rv = Truncate(mAltDataOffset);
    if (NS_FAILED(rv)) {
      LOG(
          ("CacheFile::OpenOutputStream() - Truncating alt-data failed "
           "[rv=0x%08" PRIx32 "]",
           static_cast<uint32_t>(rv)));
      return rv;
    }
    SetAltMetadata(nullptr);
    mAltDataOffset = -1;
    mAltDataType.Truncate();
  }

  // Once we open output stream we no longer allow preloading of chunks without
  // input stream. There is no reason to believe that some input stream will be
  // opened soon. Otherwise we would cache unused chunks of all newly created
  // entries until the CacheFile is destroyed.
  mPreloadWithoutInputStreams = false;

  mOutput = new CacheFileOutputStream(this, aCloseListener, false);

  LOG(
      ("CacheFile::OpenOutputStream() - Creating new output stream %p "
       "[this=%p]",
       mOutput, this));

  mDataAccessed = true;
  *_retval = do_AddRef(mOutput).take();
  return NS_OK;
}

nsresult CacheFile::OpenAlternativeOutputStream(
    CacheOutputCloseListener* aCloseListener, const char* aAltDataType,
    nsIAsyncOutputStream** _retval) {
  CacheFileAutoLock lock(this);

  MOZ_ASSERT(mHandle || mMemoryOnly || mOpeningFile);

  if (!mReady) {
    LOG(
        ("CacheFile::OpenAlternativeOutputStream() - CacheFile is not ready "
         "[this=%p]",
         this));

    return NS_ERROR_NOT_AVAILABLE;
  }

  if (mOutput) {
    LOG(
        ("CacheFile::OpenAlternativeOutputStream() - We already have output "
         "stream %p [this=%p]",
         mOutput, this));

    return NS_ERROR_NOT_AVAILABLE;
  }

  if (NS_FAILED(mStatus)) {
    LOG(
        ("CacheFile::OpenAlternativeOutputStream() - CacheFile is in a failure "
         "state [this=%p, status=0x%08" PRIx32 "]",
         this, static_cast<uint32_t>(mStatus)));

    // The CacheFile is already doomed. It make no sense to allow to write any
    // data to such entry.
    return mStatus;
  }

  // Fail if there is any input stream opened for alternative data
  for (uint32_t i = 0; i < mInputs.Length(); ++i) {
    if (mInputs[i]->IsAlternativeData()) {
      return NS_ERROR_NOT_AVAILABLE;
    }
  }

  nsresult rv;

  if (mAltDataOffset != -1) {
    // Truncate old alt-data
    rv = Truncate(mAltDataOffset);
    if (NS_FAILED(rv)) {
      LOG(
          ("CacheFile::OpenAlternativeOutputStream() - Truncating old alt-data "
           "failed [rv=0x%08" PRIx32 "]",
           static_cast<uint32_t>(rv)));
      return rv;
    }
  } else {
    mAltDataOffset = mDataSize;
  }

  nsAutoCString altMetadata;
  CacheFileUtils::BuildAlternativeDataInfo(aAltDataType, mAltDataOffset,
                                           altMetadata);
  rv = SetAltMetadata(altMetadata.get());
  if (NS_FAILED(rv)) {
    LOG(
        ("CacheFile::OpenAlternativeOutputStream() - Set Metadata for alt-data"
         "failed [rv=0x%08" PRIx32 "]",
         static_cast<uint32_t>(rv)));
    return rv;
  }

  // Once we open output stream we no longer allow preloading of chunks without
  // input stream. There is no reason to believe that some input stream will be
  // opened soon. Otherwise we would cache unused chunks of all newly created
  // entries until the CacheFile is destroyed.
  mPreloadWithoutInputStreams = false;

  mOutput = new CacheFileOutputStream(this, aCloseListener, true);

  LOG(
      ("CacheFile::OpenAlternativeOutputStream() - Creating new output stream "
       "%p [this=%p]",
       mOutput, this));

  mDataAccessed = true;
  mAltDataType = aAltDataType;
  *_retval = do_AddRef(mOutput).take();
  return NS_OK;
}

nsresult CacheFile::SetMemoryOnly() {
  CacheFileAutoLock lock(this);

  LOG(("CacheFile::SetMemoryOnly() mMemoryOnly=%d [this=%p]", mMemoryOnly,
       this));

  if (mMemoryOnly) return NS_OK;

  MOZ_ASSERT(mReady);

  if (!mReady) {
    LOG(("CacheFile::SetMemoryOnly() - CacheFile is not ready [this=%p]",
         this));

    return NS_ERROR_NOT_AVAILABLE;
  }

  if (mDataAccessed) {
    LOG(("CacheFile::SetMemoryOnly() - Data was already accessed [this=%p]",
         this));
    return NS_ERROR_NOT_AVAILABLE;
  }

  // TODO what to do when this isn't a new entry and has an existing metadata???
  mMemoryOnly = true;
  return NS_OK;
}

nsresult CacheFile::Doom(CacheFileListener* aCallback) {
  LOG(("CacheFile::Doom() [this=%p, listener=%p]", this, aCallback));

  CacheFileAutoLock lock(this);

  return DoomLocked(aCallback);
}

nsresult CacheFile::DoomLocked(CacheFileListener* aCallback) {
  AssertOwnsLock();
  MOZ_ASSERT(mHandle || mMemoryOnly || mOpeningFile);

  LOG(("CacheFile::DoomLocked() [this=%p, listener=%p]", this, aCallback));

  nsresult rv = NS_OK;

  if (mMemoryOnly) {
    return NS_ERROR_FILE_NOT_FOUND;
  }

  if (mHandle && mHandle->IsDoomed()) {
    return NS_ERROR_FILE_NOT_FOUND;
  }

  nsCOMPtr<CacheFileIOListener> listener;
  if (aCallback || !mHandle) {
    listener = new DoomFileHelper(aCallback);
  }
  if (mHandle) {
    rv = CacheFileIOManager::DoomFile(mHandle, listener);
  } else if (mOpeningFile) {
    mDoomAfterOpenListener = listener;
  }

  return rv;
}

nsresult CacheFile::ThrowMemoryCachedData() {
  CacheFileAutoLock lock(this);

  LOG(("CacheFile::ThrowMemoryCachedData() [this=%p]", this));

  if (mMemoryOnly) {
    // This method should not be called when the CacheFile was initialized as
    // memory-only, but it can be called when CacheFile end up as memory-only
    // due to e.g. IO failure since CacheEntry doesn't know it.
    LOG(
        ("CacheFile::ThrowMemoryCachedData() - Ignoring request because the "
         "entry is memory-only. [this=%p]",
         this));

    return NS_ERROR_NOT_AVAILABLE;
  }

  if (mOpeningFile) {
    // mayhemer, note: we shouldn't get here, since CacheEntry prevents loading
    // entries from being purged.

    LOG(
        ("CacheFile::ThrowMemoryCachedData() - Ignoring request because the "
         "entry is still opening the file [this=%p]",
         this));

    return NS_ERROR_ABORT;
  }

  // We cannot release all cached chunks since we need to keep preloaded chunks
  // in memory. See initialization of mPreloadChunkCount for explanation.
  CleanUpCachedChunks();

  return NS_OK;
}

nsresult CacheFile::GetElement(const char* aKey, char** _retval) {
  CacheFileAutoLock lock(this);
  MOZ_ASSERT(mMetadata);
  NS_ENSURE_TRUE(mMetadata, NS_ERROR_UNEXPECTED);

  const char* value;
  value = mMetadata->GetElement(aKey);
  if (!value) return NS_ERROR_NOT_AVAILABLE;

  *_retval = NS_xstrdup(value);
  return NS_OK;
}

nsresult CacheFile::SetElement(const char* aKey, const char* aValue) {
  CacheFileAutoLock lock(this);

  LOG(("CacheFile::SetElement() this=%p", this));

  MOZ_ASSERT(mMetadata);
  NS_ENSURE_TRUE(mMetadata, NS_ERROR_UNEXPECTED);

  if (!strcmp(aKey, CacheFileUtils::kAltDataKey)) {
    NS_ERROR(
        "alt-data element is reserved for internal use and must not be "
        "changed via CacheFile::SetElement()");
    return NS_ERROR_FAILURE;
  }

  PostWriteTimer();
  return mMetadata->SetElement(aKey, aValue);
}

nsresult CacheFile::VisitMetaData(nsICacheEntryMetaDataVisitor* aVisitor) {
  CacheFileAutoLock lock(this);
  MOZ_ASSERT(mMetadata);
  MOZ_ASSERT(mReady);
  NS_ENSURE_TRUE(mMetadata, NS_ERROR_UNEXPECTED);

  mMetadata->Visit(aVisitor);
  return NS_OK;
}

nsresult CacheFile::ElementsSize(uint32_t* _retval) {
  CacheFileAutoLock lock(this);

  if (!mMetadata) return NS_ERROR_NOT_AVAILABLE;

  *_retval = mMetadata->ElementsSize();
  return NS_OK;
}

nsresult CacheFile::SetExpirationTime(uint32_t aExpirationTime) {
  CacheFileAutoLock lock(this);

  LOG(("CacheFile::SetExpirationTime() this=%p, expiration=%u", this,
       aExpirationTime));

  MOZ_ASSERT(mMetadata);
  NS_ENSURE_TRUE(mMetadata, NS_ERROR_UNEXPECTED);

  PostWriteTimer();
  mMetadata->SetExpirationTime(aExpirationTime);
  return NS_OK;
}

nsresult CacheFile::GetExpirationTime(uint32_t* _retval) {
  CacheFileAutoLock lock(this);
  MOZ_ASSERT(mMetadata);
  NS_ENSURE_TRUE(mMetadata, NS_ERROR_UNEXPECTED);

  *_retval = mMetadata->GetExpirationTime();
  return NS_OK;
}

nsresult CacheFile::SetFrecency(uint32_t aFrecency) {
  CacheFileAutoLock lock(this);

  LOG(("CacheFile::SetFrecency() this=%p, frecency=%u", this, aFrecency));

  MOZ_ASSERT(mMetadata);
  NS_ENSURE_TRUE(mMetadata, NS_ERROR_UNEXPECTED);

  PostWriteTimer();

  if (mHandle && !mHandle->IsDoomed()) {
    CacheFileIOManager::UpdateIndexEntry(mHandle, &aFrecency, nullptr, nullptr,
                                         nullptr, nullptr);
  }

  mMetadata->SetFrecency(aFrecency);
  return NS_OK;
}

nsresult CacheFile::GetFrecency(uint32_t* _retval) {
  CacheFileAutoLock lock(this);
  MOZ_ASSERT(mMetadata);
  NS_ENSURE_TRUE(mMetadata, NS_ERROR_UNEXPECTED);
  *_retval = mMetadata->GetFrecency();
  return NS_OK;
}

nsresult CacheFile::SetNetworkTimes(uint64_t aOnStartTime,
                                    uint64_t aOnStopTime) {
  CacheFileAutoLock lock(this);

  LOG(("CacheFile::SetNetworkTimes() this=%p, aOnStartTime=%" PRIu64
       ", aOnStopTime=%" PRIu64 "",
       this, aOnStartTime, aOnStopTime));

  MOZ_ASSERT(mMetadata);
  NS_ENSURE_TRUE(mMetadata, NS_ERROR_UNEXPECTED);

  PostWriteTimer();

  nsAutoCString onStartTime;
  onStartTime.AppendInt(aOnStartTime);
  nsresult rv =
      mMetadata->SetElement("net-response-time-onstart", onStartTime.get());
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  nsAutoCString onStopTime;
  onStopTime.AppendInt(aOnStopTime);
  rv = mMetadata->SetElement("net-response-time-onstop", onStopTime.get());
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  uint16_t onStartTime16 = aOnStartTime <= kIndexTimeOutOfBound
                               ? aOnStartTime
                               : kIndexTimeOutOfBound;
  uint16_t onStopTime16 =
      aOnStopTime <= kIndexTimeOutOfBound ? aOnStopTime : kIndexTimeOutOfBound;

  if (mHandle && !mHandle->IsDoomed()) {
    CacheFileIOManager::UpdateIndexEntry(
        mHandle, nullptr, nullptr, &onStartTime16, &onStopTime16, nullptr);
  }
  return NS_OK;
}

nsresult CacheFile::GetOnStartTime(uint64_t* _retval) {
  CacheFileAutoLock lock(this);

  MOZ_ASSERT(mMetadata);
  const char* onStartTimeStr =
      mMetadata->GetElement("net-response-time-onstart");
  if (!onStartTimeStr) {
    return NS_ERROR_NOT_AVAILABLE;
  }
  nsresult rv;
  *_retval = nsDependentCString(onStartTimeStr).ToInteger64(&rv);
  MOZ_ASSERT(NS_SUCCEEDED(rv));
  return NS_OK;
}

nsresult CacheFile::GetOnStopTime(uint64_t* _retval) {
  CacheFileAutoLock lock(this);

  MOZ_ASSERT(mMetadata);
  const char* onStopTimeStr = mMetadata->GetElement("net-response-time-onstop");
  if (!onStopTimeStr) {
    return NS_ERROR_NOT_AVAILABLE;
  }
  nsresult rv;
  *_retval = nsDependentCString(onStopTimeStr).ToInteger64(&rv);
  MOZ_ASSERT(NS_SUCCEEDED(rv));
  return NS_OK;
}

nsresult CacheFile::SetContentType(uint8_t aContentType) {
  CacheFileAutoLock lock(this);

  LOG(("CacheFile::SetContentType() this=%p, contentType=%u", this,
       aContentType));

  MOZ_ASSERT(mMetadata);
  NS_ENSURE_TRUE(mMetadata, NS_ERROR_UNEXPECTED);

  PostWriteTimer();

  // Save the content type to metadata for case we need to rebuild the index.
  nsAutoCString contentType;
  contentType.AppendInt(aContentType);
  nsresult rv = mMetadata->SetElement("ctid", contentType.get());
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  if (mHandle && !mHandle->IsDoomed()) {
    CacheFileIOManager::UpdateIndexEntry(mHandle, nullptr, nullptr, nullptr,
                                         nullptr, &aContentType);
  }
  return NS_OK;
}

nsresult CacheFile::SetAltMetadata(const char* aAltMetadata) {
  AssertOwnsLock();
  LOG(("CacheFile::SetAltMetadata() this=%p, aAltMetadata=%s", this,
       aAltMetadata ? aAltMetadata : ""));

  MOZ_ASSERT(mMetadata);
  NS_ENSURE_TRUE(mMetadata, NS_ERROR_UNEXPECTED);

  PostWriteTimer();

  nsresult rv =
      mMetadata->SetElement(CacheFileUtils::kAltDataKey, aAltMetadata);

  bool hasAltData = !!aAltMetadata;

  if (NS_FAILED(rv)) {
    // Removing element shouldn't fail because it doesn't allocate memory.
    mMetadata->SetElement(CacheFileUtils::kAltDataKey, nullptr);

    mAltDataOffset = -1;
    mAltDataType.Truncate();
    hasAltData = false;
  }

  if (mHandle && !mHandle->IsDoomed()) {
    CacheFileIOManager::UpdateIndexEntry(mHandle, nullptr, &hasAltData, nullptr,
                                         nullptr, nullptr);
  }
  return rv;
}

nsresult CacheFile::GetLastModified(uint32_t* _retval) {
  CacheFileAutoLock lock(this);
  MOZ_ASSERT(mMetadata);
  NS_ENSURE_TRUE(mMetadata, NS_ERROR_UNEXPECTED);

  *_retval = mMetadata->GetLastModified();
  return NS_OK;
}

nsresult CacheFile::GetLastFetched(uint32_t* _retval) {
  CacheFileAutoLock lock(this);
  MOZ_ASSERT(mMetadata);
  NS_ENSURE_TRUE(mMetadata, NS_ERROR_UNEXPECTED);

  *_retval = mMetadata->GetLastFetched();
  return NS_OK;
}

nsresult CacheFile::GetFetchCount(uint32_t* _retval) {
  CacheFileAutoLock lock(this);
  MOZ_ASSERT(mMetadata);
  NS_ENSURE_TRUE(mMetadata, NS_ERROR_UNEXPECTED);
  *_retval = mMetadata->GetFetchCount();
  return NS_OK;
}

nsresult CacheFile::GetDiskStorageSizeInKB(uint32_t* aDiskStorageSize) {
  CacheFileAutoLock lock(this);
  if (!mHandle) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  *aDiskStorageSize = mHandle->FileSizeInK();
  return NS_OK;
}

nsresult CacheFile::OnFetched() {
  CacheFileAutoLock lock(this);

  LOG(("CacheFile::OnFetched() this=%p", this));

  MOZ_ASSERT(mMetadata);
  NS_ENSURE_TRUE(mMetadata, NS_ERROR_UNEXPECTED);

  PostWriteTimer();

  mMetadata->OnFetched();
  return NS_OK;
}

void CacheFile::ReleaseOutsideLock(RefPtr<nsISupports> aObject) {
  AssertOwnsLock();

  mObjsToRelease.AppendElement(std::move(aObject));
}

nsresult CacheFile::GetChunkLocked(uint32_t aIndex, ECallerType aCaller,
                                   CacheFileChunkListener* aCallback,
                                   CacheFileChunk** _retval) {
  AssertOwnsLock();

  LOG(("CacheFile::GetChunkLocked() [this=%p, idx=%u, caller=%d, listener=%p]",
       this, aIndex, aCaller, aCallback));

  MOZ_ASSERT(mReady);
  MOZ_ASSERT(mHandle || mMemoryOnly || mOpeningFile);
  MOZ_ASSERT((aCaller == READER && aCallback) ||
             (aCaller == WRITER && !aCallback) ||
             (aCaller == PRELOADER && !aCallback));

  // Preload chunks from disk when this is disk backed entry and the listener
  // is reader.
  bool preload = !mMemoryOnly && (aCaller == READER);

  nsresult rv;

  RefPtr<CacheFileChunk> chunk;
  if (mChunks.Get(aIndex, getter_AddRefs(chunk))) {
    LOG(("CacheFile::GetChunkLocked() - Found chunk %p in mChunks [this=%p]",
         chunk.get(), this));

    // Preloader calls this method to preload only non-loaded chunks.
    MOZ_ASSERT(aCaller != PRELOADER, "Unexpected!");

    // We might get failed chunk between releasing the lock in
    // CacheFileChunk::OnDataWritten/Read and CacheFile::OnChunkWritten/Read
    rv = chunk->GetStatus();
    if (NS_FAILED(rv)) {
      SetError(rv);
      LOG(
          ("CacheFile::GetChunkLocked() - Found failed chunk in mChunks "
           "[this=%p]",
           this));
      return rv;
    }

    if (chunk->IsReady() || aCaller == WRITER) {
      chunk.swap(*_retval);
    } else {
      QueueChunkListener(aIndex, aCallback);
    }

    if (preload) {
      PreloadChunks(aIndex + 1);
    }

    return NS_OK;
  }

  if (mCachedChunks.Get(aIndex, getter_AddRefs(chunk))) {
    LOG(("CacheFile::GetChunkLocked() - Reusing cached chunk %p [this=%p]",
         chunk.get(), this));

    // Preloader calls this method to preload only non-loaded chunks.
    MOZ_ASSERT(aCaller != PRELOADER, "Unexpected!");

    mChunks.InsertOrUpdate(aIndex, RefPtr{chunk});
    mCachedChunks.Remove(aIndex);
    chunk->mFile = this;
    chunk->mActiveChunk = true;

    MOZ_ASSERT(chunk->IsReady());

    chunk.swap(*_retval);

    if (preload) {
      PreloadChunks(aIndex + 1);
    }

    return NS_OK;
  }

  int64_t off = aIndex * static_cast<int64_t>(kChunkSize);

  if (off < mDataSize) {
    // We cannot be here if this is memory only entry since the chunk must exist
    MOZ_ASSERT(!mMemoryOnly);
    if (mMemoryOnly) {
      // If this ever really happen it is better to fail rather than crashing on
      // a null handle.
      LOG(
          ("CacheFile::GetChunkLocked() - Unexpected state! Offset < mDataSize "
           "for memory-only entry. [this=%p, off=%" PRId64
           ", mDataSize=%" PRId64 "]",
           this, off, mDataSize));

      return NS_ERROR_UNEXPECTED;
    }

    chunk = new CacheFileChunk(this, aIndex, aCaller == WRITER);
    mChunks.InsertOrUpdate(aIndex, RefPtr{chunk});
    chunk->mActiveChunk = true;

    LOG(
        ("CacheFile::GetChunkLocked() - Reading newly created chunk %p from "
         "the disk [this=%p]",
         chunk.get(), this));

    // Read the chunk from the disk
    rv = chunk->Read(mHandle,
                     std::min(static_cast<uint32_t>(mDataSize - off),
                              static_cast<uint32_t>(kChunkSize)),
                     mMetadata->GetHash(aIndex), this);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      RemoveChunkInternal(chunk, false);
      return rv;
    }

    if (aCaller == WRITER) {
      chunk.swap(*_retval);
    } else if (aCaller != PRELOADER) {
      QueueChunkListener(aIndex, aCallback);
    }

    if (preload) {
      PreloadChunks(aIndex + 1);
    }

    return NS_OK;
  }
  if (off == mDataSize) {
    if (aCaller == WRITER) {
      // this listener is going to write to the chunk
      chunk = new CacheFileChunk(this, aIndex, true);
      mChunks.InsertOrUpdate(aIndex, RefPtr{chunk});
      chunk->mActiveChunk = true;

      LOG(("CacheFile::GetChunkLocked() - Created new empty chunk %p [this=%p]",
           chunk.get(), this));

      chunk->InitNew();
      mMetadata->SetHash(aIndex, chunk->Hash());

      if (HaveChunkListeners(aIndex)) {
        rv = NotifyChunkListeners(aIndex, NS_OK, chunk);
        NS_ENSURE_SUCCESS(rv, rv);
      }

      chunk.swap(*_retval);
      return NS_OK;
    }
  } else {
    if (aCaller == WRITER) {
      // this chunk was requested by writer, but we need to fill the gap first

      // Fill with zero the last chunk if it is incomplete
      if (mDataSize % kChunkSize) {
        rv = PadChunkWithZeroes(mDataSize / kChunkSize);
        NS_ENSURE_SUCCESS(rv, rv);

        MOZ_ASSERT(!(mDataSize % kChunkSize));
      }

      uint32_t startChunk = mDataSize / kChunkSize;

      if (mMemoryOnly) {
        // We need to create all missing CacheFileChunks if this is memory-only
        // entry
        for (uint32_t i = startChunk; i < aIndex; i++) {
          rv = PadChunkWithZeroes(i);
          NS_ENSURE_SUCCESS(rv, rv);
        }
      } else {
        // We don't need to create CacheFileChunk for other empty chunks unless
        // there is some input stream waiting for this chunk.

        if (startChunk != aIndex) {
          // Make sure the file contains zeroes at the end of the file
          rv = CacheFileIOManager::TruncateSeekSetEOF(
              mHandle, startChunk * kChunkSize, aIndex * kChunkSize, nullptr);
          NS_ENSURE_SUCCESS(rv, rv);
        }

        for (uint32_t i = startChunk; i < aIndex; i++) {
          if (HaveChunkListeners(i)) {
            rv = PadChunkWithZeroes(i);
            NS_ENSURE_SUCCESS(rv, rv);
          } else {
            mMetadata->SetHash(i, kEmptyChunkHash);
            mDataSize = (i + 1) * kChunkSize;
          }
        }
      }

      MOZ_ASSERT(mDataSize == off);
      rv = GetChunkLocked(aIndex, WRITER, nullptr, getter_AddRefs(chunk));
      NS_ENSURE_SUCCESS(rv, rv);

      chunk.swap(*_retval);
      return NS_OK;
    }
  }

  // We can be here only if the caller is reader since writer always create a
  // new chunk above and preloader calls this method to preload only chunks that
  // are not loaded but that do exist.
  MOZ_ASSERT(aCaller == READER, "Unexpected!");

  if (mOutput) {
    // the chunk doesn't exist but mOutput may create it
    QueueChunkListener(aIndex, aCallback);
  } else {
    return NS_ERROR_NOT_AVAILABLE;
  }

  return NS_OK;
}

void CacheFile::PreloadChunks(uint32_t aIndex) {
  AssertOwnsLock();

  uint32_t limit = aIndex + mPreloadChunkCount;

  for (uint32_t i = aIndex; i < limit; ++i) {
    int64_t off = i * static_cast<int64_t>(kChunkSize);

    if (off >= mDataSize) {
      // This chunk is beyond EOF.
      return;
    }

    if (mChunks.GetWeak(i) || mCachedChunks.GetWeak(i)) {
      // This chunk is already in memory or is being read right now.
      continue;
    }

    LOG(("CacheFile::PreloadChunks() - Preloading chunk [this=%p, idx=%u]",
         this, i));

    RefPtr<CacheFileChunk> chunk;
    GetChunkLocked(i, PRELOADER, nullptr, getter_AddRefs(chunk));
    // We've checked that we don't have this chunk, so no chunk must be
    // returned.
    MOZ_ASSERT(!chunk);
  }
}

bool CacheFile::ShouldCacheChunk(uint32_t aIndex) {
  AssertOwnsLock();

#ifdef CACHE_CHUNKS
  // We cache all chunks.
  return true;
#else

  if (mPreloadChunkCount != 0 && mInputs.Length() == 0 &&
      mPreloadWithoutInputStreams && aIndex < mPreloadChunkCount) {
    // We don't have any input stream yet, but it is likely that some will be
    // opened soon. Keep first mPreloadChunkCount chunks in memory. The
    // condition is here instead of in MustKeepCachedChunk() since these
    // chunks should be preloaded and can be kept in memory as an optimization,
    // but they can be released at any time until they are considered as
    // preloaded chunks for any input stream.
    return true;
  }

  // Cache only chunks that we really need to keep.
  return MustKeepCachedChunk(aIndex);
#endif
}

bool CacheFile::MustKeepCachedChunk(uint32_t aIndex) {
  AssertOwnsLock();

  // We must keep the chunk when this is memory only entry or we don't have
  // a handle yet.
  if (mMemoryOnly || mOpeningFile) {
    return true;
  }

  if (mPreloadChunkCount == 0) {
    // Preloading of chunks is disabled
    return false;
  }

  // Check whether this chunk should be considered as preloaded chunk for any
  // existing input stream.

  // maxPos is the position of the last byte in the given chunk
  int64_t maxPos = static_cast<int64_t>(aIndex + 1) * kChunkSize - 1;

  // minPos is the position of the first byte in a chunk that precedes the given
  // chunk by mPreloadChunkCount chunks
  int64_t minPos;
  if (mPreloadChunkCount >= aIndex) {
    minPos = 0;
  } else {
    minPos = static_cast<int64_t>(aIndex - mPreloadChunkCount) * kChunkSize;
  }

  for (uint32_t i = 0; i < mInputs.Length(); ++i) {
    int64_t inputPos = mInputs[i]->GetPosition();
    if (inputPos >= minPos && inputPos <= maxPos) {
      return true;
    }
  }

  return false;
}

nsresult CacheFile::DeactivateChunk(CacheFileChunk* aChunk) {
  nsresult rv;

  // Avoid lock reentrancy by increasing the RefCnt
  RefPtr<CacheFileChunk> chunk = aChunk;

  {
    CacheFileAutoLock lock(this);

    LOG(("CacheFile::DeactivateChunk() [this=%p, chunk=%p, idx=%u]", this,
         aChunk, aChunk->Index()));

    MOZ_ASSERT(mReady);
    MOZ_ASSERT((mHandle && !mMemoryOnly && !mOpeningFile) ||
               (!mHandle && mMemoryOnly && !mOpeningFile) ||
               (!mHandle && !mMemoryOnly && mOpeningFile));

    if (aChunk->mRefCnt != 2) {
      LOG(
          ("CacheFile::DeactivateChunk() - Chunk is still used [this=%p, "
           "chunk=%p, refcnt=%" PRIuPTR "]",
           this, aChunk, aChunk->mRefCnt.get()));

      // somebody got the reference before the lock was acquired
      return NS_OK;
    }

    if (aChunk->mDiscardedChunk) {
      aChunk->mActiveChunk = false;
      ReleaseOutsideLock(
          RefPtr<CacheFileChunkListener>(std::move(aChunk->mFile)));

      DebugOnly<bool> removed = mDiscardedChunks.RemoveElement(aChunk);
      MOZ_ASSERT(removed);
      return NS_OK;
    }

#ifdef DEBUG
    {
      // We can be here iff the chunk is in the hash table
      RefPtr<CacheFileChunk> chunkCheck;
      mChunks.Get(chunk->Index(), getter_AddRefs(chunkCheck));
      MOZ_ASSERT(chunkCheck == chunk);

      // We also shouldn't have any queued listener for this chunk
      ChunkListeners* listeners;
      mChunkListeners.Get(chunk->Index(), &listeners);
      MOZ_ASSERT(!listeners);
    }
#endif

    if (NS_FAILED(chunk->GetStatus())) {
      SetError(chunk->GetStatus());
    }

    if (NS_FAILED(mStatus)) {
      // Don't write any chunk to disk since this entry will be doomed
      LOG(
          ("CacheFile::DeactivateChunk() - Releasing chunk because of status "
           "[this=%p, chunk=%p, mStatus=0x%08" PRIx32 "]",
           this, chunk.get(), static_cast<uint32_t>(mStatus)));

      RemoveChunkInternal(chunk, false);
      return mStatus;
    }

    if (chunk->IsDirty() && !mMemoryOnly && !mOpeningFile) {
      LOG(
          ("CacheFile::DeactivateChunk() - Writing dirty chunk to the disk "
           "[this=%p]",
           this));

      mDataIsDirty = true;

      rv = chunk->Write(mHandle, this);
      if (NS_FAILED(rv)) {
        LOG(
            ("CacheFile::DeactivateChunk() - CacheFileChunk::Write() failed "
             "synchronously. Removing it. [this=%p, chunk=%p, rv=0x%08" PRIx32
             "]",
             this, chunk.get(), static_cast<uint32_t>(rv)));

        RemoveChunkInternal(chunk, false);

        SetError(rv);
        return rv;
      }

      // Chunk will be removed in OnChunkWritten if it is still unused

      // chunk needs to be released under the lock to be able to rely on
      // CacheFileChunk::mRefCnt in CacheFile::OnChunkWritten()
      chunk = nullptr;
      return NS_OK;
    }

    bool keepChunk = ShouldCacheChunk(aChunk->Index());
    LOG(("CacheFile::DeactivateChunk() - %s unused chunk [this=%p, chunk=%p]",
         keepChunk ? "Caching" : "Releasing", this, chunk.get()));

    RemoveChunkInternal(chunk, keepChunk);

    if (!mMemoryOnly) WriteMetadataIfNeededLocked();
  }

  return NS_OK;
}

void CacheFile::RemoveChunkInternal(CacheFileChunk* aChunk, bool aCacheChunk) {
  AssertOwnsLock();

  aChunk->mActiveChunk = false;
  ReleaseOutsideLock(RefPtr<CacheFileChunkListener>(std::move(aChunk->mFile)));

  if (aCacheChunk) {
    mCachedChunks.InsertOrUpdate(aChunk->Index(), RefPtr{aChunk});
  }

  mChunks.Remove(aChunk->Index());
}

bool CacheFile::OutputStreamExists(bool aAlternativeData) {
  AssertOwnsLock();

  if (!mOutput) {
    return false;
  }

  return mOutput->IsAlternativeData() == aAlternativeData;
}

int64_t CacheFile::BytesFromChunk(uint32_t aIndex, bool aAlternativeData) {
  AssertOwnsLock();

  int64_t dataSize;

  if (mAltDataOffset != -1) {
    if (aAlternativeData) {
      dataSize = mDataSize;
    } else {
      dataSize = mAltDataOffset;
    }
  } else {
    MOZ_ASSERT(!aAlternativeData);
    dataSize = mDataSize;
  }

  if (!dataSize) {
    return 0;
  }

  // Index of the last existing chunk.
  uint32_t lastChunk = (dataSize - 1) / kChunkSize;
  if (aIndex > lastChunk) {
    return 0;
  }

  // We can use only preloaded chunks for the given stream to calculate
  // available bytes if this is an entry stored on disk, since only those
  // chunks are guaranteed not to be released.
  uint32_t maxPreloadedChunk;
  if (mMemoryOnly) {
    maxPreloadedChunk = lastChunk;
  } else {
    maxPreloadedChunk = std::min(aIndex + mPreloadChunkCount, lastChunk);
  }

  uint32_t i;
  for (i = aIndex; i <= maxPreloadedChunk; ++i) {
    CacheFileChunk* chunk;

    chunk = mChunks.GetWeak(i);
    if (chunk) {
      MOZ_ASSERT(i == lastChunk || chunk->DataSize() == kChunkSize);
      if (chunk->IsReady()) {
        continue;
      }

      // don't search this chunk in cached
      break;
    }

    chunk = mCachedChunks.GetWeak(i);
    if (chunk) {
      MOZ_ASSERT(i == lastChunk || chunk->DataSize() == kChunkSize);
      continue;
    }

    break;
  }

  // theoretic bytes in advance
  int64_t advance = int64_t(i - aIndex) * kChunkSize;
  // real bytes till the end of the file
  int64_t tail = dataSize - (aIndex * kChunkSize);

  return std::min(advance, tail);
}

nsresult CacheFile::Truncate(int64_t aOffset) {
  AssertOwnsLock();

  LOG(("CacheFile::Truncate() [this=%p, offset=%" PRId64 "]", this, aOffset));

  nsresult rv;

  // If we ever need to truncate on non alt-data boundary, we need to handle
  // existing input streams.
  MOZ_ASSERT(aOffset == mAltDataOffset,
             "Truncating normal data not implemented");
  MOZ_ASSERT(mReady);
  MOZ_ASSERT(!mOutput);

  uint32_t lastChunk = 0;
  if (mDataSize > 0) {
    lastChunk = (mDataSize - 1) / kChunkSize;
  }

  uint32_t newLastChunk = 0;
  if (aOffset > 0) {
    newLastChunk = (aOffset - 1) / kChunkSize;
  }

  uint32_t bytesInNewLastChunk = aOffset - newLastChunk * kChunkSize;

  LOG(
      ("CacheFileTruncate() - lastChunk=%u, newLastChunk=%u, "
       "bytesInNewLastChunk=%u",
       lastChunk, newLastChunk, bytesInNewLastChunk));

  // Remove all truncated chunks from mCachedChunks
  for (auto iter = mCachedChunks.Iter(); !iter.Done(); iter.Next()) {
    uint32_t idx = iter.Key();

    if (idx > newLastChunk) {
      // This is unused chunk, simply remove it.
      LOG(("CacheFile::Truncate() - removing cached chunk [idx=%u]", idx));
      iter.Remove();
    }
  }

  // We need to make sure no input stream holds a reference to a chunk we're
  // going to discard. In theory, if alt-data begins at chunk boundary, input
  // stream for normal data can get the chunk containing only alt-data via
  // EnsureCorrectChunk() call. The input stream won't read the data from such
  // chunk, but it will keep the reference until the stream is closed and we
  // cannot simply discard this chunk.
  int64_t maxInputChunk = -1;
  for (uint32_t i = 0; i < mInputs.Length(); ++i) {
    int64_t inputChunk = mInputs[i]->GetChunkIdx();

    if (maxInputChunk < inputChunk) {
      maxInputChunk = inputChunk;
    }

    MOZ_RELEASE_ASSERT(mInputs[i]->GetPosition() <= aOffset);
  }

  MOZ_RELEASE_ASSERT(maxInputChunk <= newLastChunk + 1);
  if (maxInputChunk == newLastChunk + 1) {
    // Truncating must be done at chunk boundary
    MOZ_RELEASE_ASSERT(bytesInNewLastChunk == kChunkSize);
    newLastChunk++;
    bytesInNewLastChunk = 0;
    LOG(
        ("CacheFile::Truncate() - chunk %p is still in use, using "
         "newLastChunk=%u and bytesInNewLastChunk=%u",
         mChunks.GetWeak(newLastChunk), newLastChunk, bytesInNewLastChunk));
  }

  // Discard all truncated chunks in mChunks
  for (auto iter = mChunks.Iter(); !iter.Done(); iter.Next()) {
    uint32_t idx = iter.Key();

    if (idx > newLastChunk) {
      RefPtr<CacheFileChunk>& chunk = iter.Data();
      LOG(("CacheFile::Truncate() - discarding chunk [idx=%u, chunk=%p]", idx,
           chunk.get()));

      if (HaveChunkListeners(idx)) {
        NotifyChunkListeners(idx, NS_ERROR_NOT_AVAILABLE, chunk);
      }

      chunk->mDiscardedChunk = true;
      mDiscardedChunks.AppendElement(chunk);
      iter.Remove();
    }
  }

  // Remove hashes of all removed chunks from the metadata
  for (uint32_t i = lastChunk; i > newLastChunk; --i) {
    mMetadata->RemoveHash(i);
  }

  // Truncate new last chunk
  if (bytesInNewLastChunk == kChunkSize) {
    LOG(("CacheFile::Truncate() - not truncating last chunk."));
  } else {
    RefPtr<CacheFileChunk> chunk;
    if (mChunks.Get(newLastChunk, getter_AddRefs(chunk))) {
      LOG(("CacheFile::Truncate() - New last chunk %p got from mChunks.",
           chunk.get()));
    } else if (mCachedChunks.Get(newLastChunk, getter_AddRefs(chunk))) {
      LOG(("CacheFile::Truncate() - New last chunk %p got from mCachedChunks.",
           chunk.get()));
    } else {
      // New last chunk isn't loaded but we need to update the hash.
      MOZ_ASSERT(!mMemoryOnly);
      MOZ_ASSERT(mHandle);

      rv = GetChunkLocked(newLastChunk, PRELOADER, nullptr,
                          getter_AddRefs(chunk));
      if (NS_FAILED(rv)) {
        return rv;
      }
      // We've checked that we don't have this chunk, so no chunk must be
      // returned.
      MOZ_ASSERT(!chunk);

      if (!mChunks.Get(newLastChunk, getter_AddRefs(chunk))) {
        return NS_ERROR_UNEXPECTED;
      }

      LOG(("CacheFile::Truncate() - New last chunk %p got from preloader.",
           chunk.get()));
    }

    rv = chunk->GetStatus();
    if (NS_FAILED(rv)) {
      LOG(
          ("CacheFile::Truncate() - New last chunk is failed "
           "[status=0x%08" PRIx32 "]",
           static_cast<uint32_t>(rv)));
      return rv;
    }

    chunk->Truncate(bytesInNewLastChunk);

    // If the chunk is ready set the new hash now. If it's still being loaded
    // CacheChunk::Truncate() made the chunk dirty and the hash will be updated
    // in OnChunkWritten().
    if (chunk->IsReady()) {
      mMetadata->SetHash(newLastChunk, chunk->Hash());
    }
  }

  if (mHandle) {
    rv = CacheFileIOManager::TruncateSeekSetEOF(mHandle, aOffset, aOffset,
                                                nullptr);
    if (NS_FAILED(rv)) {
      return rv;
    }
  }

  mDataSize = aOffset;

  return NS_OK;
}

static uint32_t StatusToTelemetryEnum(nsresult aStatus) {
  if (NS_SUCCEEDED(aStatus)) {
    return 0;
  }

  switch (aStatus) {
    case NS_BASE_STREAM_CLOSED:
      return 0;  // Log this as a success
    case NS_ERROR_OUT_OF_MEMORY:
      return 2;
    case NS_ERROR_FILE_NO_DEVICE_SPACE:
      return 3;
    case NS_ERROR_FILE_CORRUPTED:
      return 4;
    case NS_ERROR_FILE_NOT_FOUND:
      return 5;
    case NS_BINDING_ABORTED:
      return 6;
    default:
      return 1;  // other error
  }

  MOZ_ASSERT_UNREACHABLE("We should never get here");
}

void CacheFile::RemoveInput(CacheFileInputStream* aInput, nsresult aStatus) {
  AssertOwnsLock();

  LOG(("CacheFile::RemoveInput() [this=%p, input=%p, status=0x%08" PRIx32 "]",
       this, aInput, static_cast<uint32_t>(aStatus)));

  DebugOnly<bool> found{};
  found = mInputs.RemoveElement(aInput);
  MOZ_ASSERT(found);

  ReleaseOutsideLock(
      already_AddRefed<nsIInputStream>(static_cast<nsIInputStream*>(aInput)));

  if (!mMemoryOnly) WriteMetadataIfNeededLocked();

  // If the input didn't read all data, there might be left some preloaded
  // chunks that won't be used anymore.
  CleanUpCachedChunks();

  Telemetry::Accumulate(Telemetry::NETWORK_CACHE_V2_INPUT_STREAM_STATUS,
                        StatusToTelemetryEnum(aStatus));
}

void CacheFile::RemoveOutput(CacheFileOutputStream* aOutput, nsresult aStatus) {
  AssertOwnsLock();

  nsresult rv;

  LOG(("CacheFile::RemoveOutput() [this=%p, output=%p, status=0x%08" PRIx32 "]",
       this, aOutput, static_cast<uint32_t>(aStatus)));

  if (mOutput != aOutput) {
    LOG(
        ("CacheFile::RemoveOutput() - This output was already removed, ignoring"
         " call [this=%p]",
         this));
    return;
  }

  mOutput = nullptr;

  // Cancel all queued chunk and update listeners that cannot be satisfied
  NotifyListenersAboutOutputRemoval();

  if (!mMemoryOnly) WriteMetadataIfNeededLocked();

  // Make sure the CacheFile status is set to a failure when the output stream
  // is closed with a fatal error.  This way we propagate correctly and w/o any
  // windows the failure state of this entry to end consumers.
  if (NS_SUCCEEDED(mStatus) && NS_FAILED(aStatus) &&
      aStatus != NS_BASE_STREAM_CLOSED) {
    if (aOutput->IsAlternativeData()) {
      MOZ_ASSERT(mAltDataOffset != -1);
      // If there is no alt-data input stream truncate only alt-data, otherwise
      // doom the entry.
      bool altDataInputExists = false;
      for (uint32_t i = 0; i < mInputs.Length(); ++i) {
        if (mInputs[i]->IsAlternativeData()) {
          altDataInputExists = true;
          break;
        }
      }
      if (altDataInputExists) {
        SetError(aStatus);
      } else {
        rv = Truncate(mAltDataOffset);
        if (NS_FAILED(rv)) {
          LOG(
              ("CacheFile::RemoveOutput() - Truncating alt-data failed "
               "[rv=0x%08" PRIx32 "]",
               static_cast<uint32_t>(rv)));
          SetError(aStatus);
        } else {
          SetAltMetadata(nullptr);
          mAltDataOffset = -1;
          mAltDataType.Truncate();
        }
      }
    } else {
      SetError(aStatus);
    }
  }

  // Notify close listener as the last action
  aOutput->NotifyCloseListener();

  Telemetry::Accumulate(Telemetry::NETWORK_CACHE_V2_OUTPUT_STREAM_STATUS,
                        StatusToTelemetryEnum(aStatus));
}

nsresult CacheFile::NotifyChunkListener(CacheFileChunkListener* aCallback,
                                        nsIEventTarget* aTarget,
                                        nsresult aResult, uint32_t aChunkIdx,
                                        CacheFileChunk* aChunk) {
  LOG(
      ("CacheFile::NotifyChunkListener() [this=%p, listener=%p, target=%p, "
       "rv=0x%08" PRIx32 ", idx=%u, chunk=%p]",
       this, aCallback, aTarget, static_cast<uint32_t>(aResult), aChunkIdx,
       aChunk));

  RefPtr<NotifyChunkListenerEvent> ev;
  ev = new NotifyChunkListenerEvent(aCallback, aResult, aChunkIdx, aChunk);
  if (aTarget) {
    return aTarget->Dispatch(ev, NS_DISPATCH_NORMAL);
  }
  return NS_DispatchToCurrentThread(ev);
}

void CacheFile::QueueChunkListener(uint32_t aIndex,
                                   CacheFileChunkListener* aCallback) {
  LOG(("CacheFile::QueueChunkListener() [this=%p, idx=%u, listener=%p]", this,
       aIndex, aCallback));

  AssertOwnsLock();

  MOZ_ASSERT(aCallback);

  ChunkListenerItem* item = new ChunkListenerItem();
  item->mTarget = CacheFileIOManager::IOTarget();
  if (!item->mTarget) {
    LOG(
        ("CacheFile::QueueChunkListener() - Cannot get Cache I/O thread! Using "
         "main thread for callback."));
    item->mTarget = GetMainThreadSerialEventTarget();
  }
  item->mCallback = aCallback;

  mChunkListeners.GetOrInsertNew(aIndex)->mItems.AppendElement(item);
}

nsresult CacheFile::NotifyChunkListeners(uint32_t aIndex, nsresult aResult,
                                         CacheFileChunk* aChunk) {
  LOG(("CacheFile::NotifyChunkListeners() [this=%p, idx=%u, rv=0x%08" PRIx32
       ", "
       "chunk=%p]",
       this, aIndex, static_cast<uint32_t>(aResult), aChunk));

  AssertOwnsLock();

  nsresult rv, rv2;

  ChunkListeners* listeners;
  mChunkListeners.Get(aIndex, &listeners);
  MOZ_ASSERT(listeners);

  rv = NS_OK;
  for (uint32_t i = 0; i < listeners->mItems.Length(); i++) {
    ChunkListenerItem* item = listeners->mItems[i];
    rv2 = NotifyChunkListener(item->mCallback, item->mTarget, aResult, aIndex,
                              aChunk);
    if (NS_FAILED(rv2) && NS_SUCCEEDED(rv)) rv = rv2;
    delete item;
  }

  mChunkListeners.Remove(aIndex);

  return rv;
}

bool CacheFile::HaveChunkListeners(uint32_t aIndex) {
  AssertOwnsLock();
  ChunkListeners* listeners;
  mChunkListeners.Get(aIndex, &listeners);
  return !!listeners;
}

void CacheFile::NotifyListenersAboutOutputRemoval() {
  LOG(("CacheFile::NotifyListenersAboutOutputRemoval() [this=%p]", this));

  AssertOwnsLock();

  // First fail all chunk listeners that wait for non-existent chunk
  for (auto iter = mChunkListeners.Iter(); !iter.Done(); iter.Next()) {
    uint32_t idx = iter.Key();
    auto* listeners = iter.UserData();

    LOG(
        ("CacheFile::NotifyListenersAboutOutputRemoval() - fail "
         "[this=%p, idx=%u]",
         this, idx));

    RefPtr<CacheFileChunk> chunk;
    mChunks.Get(idx, getter_AddRefs(chunk));
    if (chunk) {
      // Skip these listeners because the chunk is being read. We don't have
      // assertion here to check its state because it might be already in READY
      // state while CacheFile::OnChunkRead() is waiting on Cache I/O thread for
      // a lock so the listeners hasn't been notified yet. In any case, the
      // listeners will be notified from CacheFile::OnChunkRead().
      continue;
    }

    for (uint32_t i = 0; i < listeners->mItems.Length(); i++) {
      ChunkListenerItem* item = listeners->mItems[i];
      NotifyChunkListener(item->mCallback, item->mTarget,
                          NS_ERROR_NOT_AVAILABLE, idx, nullptr);
      delete item;
    }

    iter.Remove();
  }

  // Fail all update listeners
  for (const auto& entry : mChunks) {
    const RefPtr<CacheFileChunk>& chunk = entry.GetData();
    LOG(
        ("CacheFile::NotifyListenersAboutOutputRemoval() - fail2 "
         "[this=%p, idx=%u]",
         this, entry.GetKey()));

    if (chunk->IsReady()) {
      chunk->NotifyUpdateListeners();
    }
  }
}

bool CacheFile::DataSize(int64_t* aSize) {
  CacheFileAutoLock lock(this);

  if (OutputStreamExists(false)) {
    return false;
  }

  if (mAltDataOffset == -1) {
    *aSize = mDataSize;
  } else {
    *aSize = mAltDataOffset;
  }

  return true;
}

nsresult CacheFile::GetAltDataSize(int64_t* aSize) {
  CacheFileAutoLock lock(this);
  if (mOutput) {
    return NS_ERROR_IN_PROGRESS;
  }

  if (mAltDataOffset == -1) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  *aSize = mDataSize - mAltDataOffset;
  return NS_OK;
}

nsresult CacheFile::GetAltDataType(nsACString& aType) {
  CacheFileAutoLock lock(this);

  if (mAltDataOffset == -1) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  aType = mAltDataType;
  return NS_OK;
}

bool CacheFile::IsDoomed() {
  CacheFileAutoLock lock(this);

  if (!mHandle) return false;

  return mHandle->IsDoomed();
}

bool CacheFile::IsWriteInProgress() {
  CacheFileAutoLock lock(this);

  bool result = false;

  if (!mMemoryOnly) {
    result =
        mDataIsDirty || (mMetadata && mMetadata->IsDirty()) || mWritingMetadata;
  }

  result = result || mOpeningFile || mOutput || mChunks.Count();

  return result;
}

bool CacheFile::EntryWouldExceedLimit(int64_t aOffset, int64_t aSize,
                                      bool aIsAltData) {
  CacheFileAutoLock lock(this);

  if (mSkipSizeCheck || aSize < 0) {
    return false;
  }

  int64_t totalSize = aOffset + aSize;
  if (aIsAltData) {
    totalSize += (mAltDataOffset == -1) ? mDataSize : mAltDataOffset;
  }

  return CacheObserver::EntryIsTooBig(totalSize, !mMemoryOnly);
}

bool CacheFile::IsDirty() { return mDataIsDirty || mMetadata->IsDirty(); }

void CacheFile::WriteMetadataIfNeeded() {
  LOG(("CacheFile::WriteMetadataIfNeeded() [this=%p]", this));

  CacheFileAutoLock lock(this);

  if (!mMemoryOnly) WriteMetadataIfNeededLocked();
}

void CacheFile::WriteMetadataIfNeededLocked(bool aFireAndForget) {
  // When aFireAndForget is set to true, we are called from dtor.
  // |this| must not be referenced after this method returns!

  LOG(("CacheFile::WriteMetadataIfNeededLocked() [this=%p]", this));

  nsresult rv;

  AssertOwnsLock();
  MOZ_ASSERT(!mMemoryOnly);

  if (!mMetadata) {
    MOZ_CRASH("Must have metadata here");
    return;
  }

  if (NS_FAILED(mStatus)) return;

  if (!IsDirty() || mOutput || mInputs.Length() || mChunks.Count() ||
      mWritingMetadata || mOpeningFile || mKill) {
    return;
  }

  if (!aFireAndForget) {
    // if aFireAndForget is set, we are called from dtor. Write
    // scheduler hard-refers CacheFile otherwise, so we cannot be here.
    CacheFileIOManager::UnscheduleMetadataWrite(this);
  }

  LOG(("CacheFile::WriteMetadataIfNeededLocked() - Writing metadata [this=%p]",
       this));

  rv = mMetadata->WriteMetadata(mDataSize, aFireAndForget ? nullptr : this);
  if (NS_SUCCEEDED(rv)) {
    mWritingMetadata = true;
    mDataIsDirty = false;
  } else {
    LOG(
        ("CacheFile::WriteMetadataIfNeededLocked() - Writing synchronously "
         "failed [this=%p]",
         this));
    // TODO: close streams with error
    SetError(rv);
  }
}

void CacheFile::PostWriteTimer() {
  if (mMemoryOnly) return;
  LOG(("CacheFile::PostWriteTimer() [this=%p]", this));

  CacheFileIOManager::ScheduleMetadataWrite(this);
}

void CacheFile::CleanUpCachedChunks() {
  for (auto iter = mCachedChunks.Iter(); !iter.Done(); iter.Next()) {
    uint32_t idx = iter.Key();
    const RefPtr<CacheFileChunk>& chunk = iter.Data();

    LOG(("CacheFile::CleanUpCachedChunks() [this=%p, idx=%u, chunk=%p]", this,
         idx, chunk.get()));

    if (MustKeepCachedChunk(idx)) {
      LOG(("CacheFile::CleanUpCachedChunks() - Keeping chunk"));
      continue;
    }

    LOG(("CacheFile::CleanUpCachedChunks() - Removing chunk"));
    iter.Remove();
  }
}

nsresult CacheFile::PadChunkWithZeroes(uint32_t aChunkIdx) {
  AssertOwnsLock();

  // This method is used to pad last incomplete chunk with zeroes or create
  // a new chunk full of zeroes
  MOZ_ASSERT(mDataSize / kChunkSize == aChunkIdx);

  nsresult rv;
  RefPtr<CacheFileChunk> chunk;
  rv = GetChunkLocked(aChunkIdx, WRITER, nullptr, getter_AddRefs(chunk));
  NS_ENSURE_SUCCESS(rv, rv);

  LOG(
      ("CacheFile::PadChunkWithZeroes() - Zeroing hole in chunk %d, range %d-%d"
       " [this=%p]",
       aChunkIdx, chunk->DataSize(), kChunkSize - 1, this));

  CacheFileChunkWriteHandle hnd = chunk->GetWriteHandle(kChunkSize);
  if (!hnd.Buf()) {
    ReleaseOutsideLock(std::move(chunk));
    SetError(NS_ERROR_OUT_OF_MEMORY);
    return NS_ERROR_OUT_OF_MEMORY;
  }

  uint32_t offset = hnd.DataSize();
  memset(hnd.Buf() + offset, 0, kChunkSize - offset);
  hnd.UpdateDataSize(offset, kChunkSize - offset);

  ReleaseOutsideLock(std::move(chunk));

  return NS_OK;
}

void CacheFile::SetError(nsresult aStatus) {
  AssertOwnsLock();

  if (NS_SUCCEEDED(mStatus)) {
    mStatus = aStatus;
    if (mHandle) {
      CacheFileIOManager::DoomFile(mHandle, nullptr);
    }
  }
}

nsresult CacheFile::InitIndexEntry() {
  AssertOwnsLock();
  MOZ_ASSERT(mHandle);

  if (mHandle->IsDoomed()) return NS_OK;

  nsresult rv;

  rv = CacheFileIOManager::InitIndexEntry(
      mHandle, GetOriginAttrsHash(mMetadata->OriginAttributes()),
      mMetadata->IsAnonymous(), mPinned);
  NS_ENSURE_SUCCESS(rv, rv);

  uint32_t frecency = mMetadata->GetFrecency();

  bool hasAltData =
      mMetadata->GetElement(CacheFileUtils::kAltDataKey) != nullptr;

  static auto toUint16 = [](const char* s) -> uint16_t {
    if (s) {
      nsresult rv;
      uint64_t n64 = nsDependentCString(s).ToInteger64(&rv);
      MOZ_ASSERT(NS_SUCCEEDED(rv));
      return n64 <= kIndexTimeOutOfBound ? n64 : kIndexTimeOutOfBound;
    }
    return kIndexTimeNotAvailable;
  };

  const char* onStartTimeStr =
      mMetadata->GetElement("net-response-time-onstart");
  uint16_t onStartTime = toUint16(onStartTimeStr);

  const char* onStopTimeStr = mMetadata->GetElement("net-response-time-onstop");
  uint16_t onStopTime = toUint16(onStopTimeStr);

  const char* contentTypeStr = mMetadata->GetElement("ctid");
  uint8_t contentType = nsICacheEntry::CONTENT_TYPE_UNKNOWN;
  if (contentTypeStr) {
    int64_t n64 = nsDependentCString(contentTypeStr).ToInteger64(&rv);
    if (NS_FAILED(rv) || n64 < nsICacheEntry::CONTENT_TYPE_UNKNOWN ||
        n64 >= nsICacheEntry::CONTENT_TYPE_LAST) {
      n64 = nsICacheEntry::CONTENT_TYPE_UNKNOWN;
    }
    contentType = n64;
  }

  rv = CacheFileIOManager::UpdateIndexEntry(
      mHandle, &frecency, &hasAltData, &onStartTime, &onStopTime, &contentType);
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

size_t CacheFile::SizeOfExcludingThis(
    mozilla::MallocSizeOf mallocSizeOf) const {
  CacheFileAutoLock lock(const_cast<CacheFile*>(this));

  size_t n = 0;
  n += mKey.SizeOfExcludingThisIfUnshared(mallocSizeOf);
  n += mChunks.ShallowSizeOfExcludingThis(mallocSizeOf);
  for (const auto& chunk : mChunks.Values()) {
    n += chunk->SizeOfIncludingThis(mallocSizeOf);
  }
  n += mCachedChunks.ShallowSizeOfExcludingThis(mallocSizeOf);
  for (const auto& chunk : mCachedChunks.Values()) {
    n += chunk->SizeOfIncludingThis(mallocSizeOf);
  }
  // Ignore metadata if it's still being read. It's not safe to access buffers
  // in CacheFileMetadata because they might be reallocated on another thread
  // outside CacheFile's lock.
  if (mMetadata && mReady) {
    n += mMetadata->SizeOfIncludingThis(mallocSizeOf);
  }

  // Input streams are not elsewhere reported.
  n += mInputs.ShallowSizeOfExcludingThis(mallocSizeOf);
  for (uint32_t i = 0; i < mInputs.Length(); ++i) {
    n += mInputs[i]->SizeOfIncludingThis(mallocSizeOf);
  }

  // Output streams are not elsewhere reported.
  if (mOutput) {
    n += mOutput->SizeOfIncludingThis(mallocSizeOf);
  }

  // The listeners are usually classes reported just above.
  n += mChunkListeners.ShallowSizeOfExcludingThis(mallocSizeOf);
  n += mObjsToRelease.ShallowSizeOfExcludingThis(mallocSizeOf);

  // mHandle reported directly from CacheFileIOManager.

  return n;
}

size_t CacheFile::SizeOfIncludingThis(
    mozilla::MallocSizeOf mallocSizeOf) const {
  return mallocSizeOf(this) + SizeOfExcludingThis(mallocSizeOf);
}

}  // namespace mozilla::net