/* 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 "SSLTokensCache.h"

#include "CertVerifier.h"
#include "CommonSocketControl.h"
#include "TransportSecurityInfo.h"
#include "brotli/decode.h"
#include "brotli/encode.h"
#include "mozIStorageConnection.h"
#include "mozIStorageService.h"
#include "mozIStorageStatement.h"
#include "mozStorageCID.h"
#include "mozStorageHelper.h"
#include "mozilla/ArrayAlgorithm.h"
#include "mozilla/Components.h"
#include "mozilla/EndianUtils.h"
#include "mozilla/Logging.h"
#include "mozilla/Maybe.h"
#include "mozilla/OriginAttributes.h"
#include "mozilla/Preferences.h"
#include "mozilla/Services.h"
#include "mozilla/Span.h"
#include "mozilla/StaticPrefs_privacy.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/glean/NetwerkMetrics.h"
#include "mozilla/net/SocketProcessChild.h"
#include "mozilla/net/SocketProcessParent.h"
#include "nsAppDirectoryServiceDefs.h"
#include "nsDirectoryServiceUtils.h"
#include "nsIEventTarget.h"
#include "nsIGlobalObject.h"
#include "nsIOService.h"
#include "nsIObserverService.h"
#include "nsThreadUtils.h"
#include "nss.h"
#include "prtime.h"
#include "ssl.h"
#include "sslexp.h"
#include "xpcpublic.h"

#define SSL_TOKENS_CACHE_DB_FILE "ssl_tokens_cache.sqlite"

constexpr int32_t kSSLTokensCacheSchemaVersion = 1;

namespace mozilla {
namespace net {

static LazyLogModule gSSLTokensCacheLog("SSLTokensCache");

// Main thread only: <profile>/ssl_tokens_cache.sqlite, or null if no profile.
static already_AddRefed<nsIFile> ProfileDBFile() {
  MOZ_ASSERT(NS_IsMainThread());
  nsCOMPtr<nsIFile> file;
  if (NS_FAILED(NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
                                       getter_AddRefs(file)))) {
    return nullptr;
  }
  file->AppendNative(nsLiteralCString(SSL_TOKENS_CACHE_DB_FILE));
  return file.forget();
}

#undef LOG
#define LOG(args) MOZ_LOG(gSSLTokensCacheLog, mozilla::LogLevel::Debug, args)
#undef LOG5_ENABLED
#define LOG5_ENABLED() \
  MOZ_LOG_TEST(mozilla::net::gSSLTokensCacheLog, mozilla::LogLevel::Verbose)

class ExpirationComparator {
 public:
  bool Equals(SSLTokensCache::TokenCacheRecord* a,
              SSLTokensCache::TokenCacheRecord* b) const {
    return a->mExpirationTime == b->mExpirationTime;
  }
  bool LessThan(SSLTokensCache::TokenCacheRecord* a,
                SSLTokensCache::TokenCacheRecord* b) const {
    return a->mExpirationTime < b->mExpirationTime;
  }
};

static nsTArray<nsTArray<uint8_t>> CloneCertChain(
    const nsTArray<nsTArray<uint8_t>>& aSrc) {
  return TransformIntoNewArray(aSrc, [](const auto& c) { return c.Clone(); });
}

SessionCacheInfo SessionCacheInfo::Clone() const {
  SessionCacheInfo result;
  result.mEVStatus = mEVStatus;
  result.mCertificateTransparencyStatus = mCertificateTransparencyStatus;
  result.mServerCertBytes = mServerCertBytes.Clone();
  result.mSucceededCertChainBytes =
      mSucceededCertChainBytes.map(CloneCertChain);
  result.mIsBuiltCertChainRootBuiltInRoot = mIsBuiltCertChainRootBuiltInRoot;
  result.mOverridableErrorCategory = mOverridableErrorCategory;
  result.mHandshakeCertificatesBytes =
      mHandshakeCertificatesBytes.map(CloneCertChain);
  return result;
}

// Serialized payload layout (all integers little-endian):
//   [4: token_len][token bytes]
//   [1: ev_status]
//   [2: ct_status]
//   [1: overridable_error]
//   [1: builtin_root] (0=Nothing, 1=Some(false), 2=Some(true))
//   [4: server_cert_len][server_cert bytes]
//   [1: has_succeeded_chain] if 1: [1: count] then for each [4: len][bytes]
//   [1: has_handshake_certs] if 1: [1: count] then for each [4: len][bytes]
//
// Stored compressed with an LE original-size prefix.
static constexpr size_t kOriginalLenSize = sizeof(uint32_t);

// Sanity bounds on the length fields above.
static constexpr uint32_t kMaxPayloadSize = 256 * 1024;
static constexpr uint32_t kMaxTokenSize = 256 * 1024;
static constexpr uint32_t kMaxCertSize = 0xffff;

template <typename T>
static void AppendLE(nsTArray<uint8_t>& aBuf, T aVal) {
  T le = mozilla::NativeEndian::swapToLittleEndian(aVal);
  aBuf.AppendElements(reinterpret_cast<const uint8_t*>(&le), sizeof(T));
}

static nsTArray<uint8_t> SerializeRecord(Span<const uint8_t> aToken,
                                         const SessionCacheInfo& aInfo) {
  nsTArray<uint8_t> buf;
  AppendLE(buf, AssertedCast<uint32_t>(aToken.Length()));
  buf.AppendElements(aToken.Elements(), aToken.Length());
  buf.AppendElement(aInfo.mEVStatus == psm::EVStatus::EV ? 1 : 0);
  AppendLE(buf, aInfo.mCertificateTransparencyStatus);
  buf.AppendElement(static_cast<uint8_t>(aInfo.mOverridableErrorCategory));
  if (aInfo.mIsBuiltCertChainRootBuiltInRoot.isNothing()) {
    buf.AppendElement(0);
  } else {
    buf.AppendElement(*aInfo.mIsBuiltCertChainRootBuiltInRoot ? 2 : 1);
  }
  AppendLE(buf, AssertedCast<uint32_t>(aInfo.mServerCertBytes.Length()));
  buf.AppendElements(aInfo.mServerCertBytes.Elements(),
                     aInfo.mServerCertBytes.Length());
  auto appendChain = [&](const Maybe<nsTArray<nsTArray<uint8_t>>>& aChain) {
    if (aChain.isNothing()) {
      buf.AppendElement(0);
      return;
    }
    buf.AppendElement(1);
    MOZ_RELEASE_ASSERT(aChain->Length() <= 0xFF);
    buf.AppendElement(static_cast<uint8_t>(aChain->Length()));
    for (const auto& cert : *aChain) {
      AppendLE(buf, AssertedCast<uint32_t>(cert.Length()));
      buf.AppendElements(cert.Elements(), cert.Length());
    }
  };
  appendChain(aInfo.mSucceededCertChainBytes);
  appendChain(aInfo.mHandshakeCertificatesBytes);
  return buf;
}

// Records are compressed on the socket thread during the handshake. Quality 1
// encodes ~3.5x faster than 5 for ~5% larger output, and the cache is bounded
// in bytes, so that costs cached sessions rather than memory. Quality 2 to 4
// compress this data worse than 1 for no speed gain.
static constexpr int kRecordCompressionQuality = 1;

// Returns empty array on failure.
static nsTArray<uint8_t> CompressRecord(Span<const uint8_t> aPayload) {
  nsTArray<uint8_t> encoded;
  if (!encoded.SetLength(BrotliEncoderMaxCompressedSize(aPayload.Length()),
                         fallible)) {
    return {};
  }
  size_t encodedSize = encoded.Length();
  if (!BrotliEncoderCompress(kRecordCompressionQuality, BROTLI_DEFAULT_WINDOW,
                             BROTLI_MODE_GENERIC, aPayload.Length(),
                             aPayload.Elements(), &encodedSize,
                             encoded.Elements())) {
    return {};
  }

  nsTArray<uint8_t> result;
  if (!result.SetCapacity(kOriginalLenSize + encodedSize, fallible)) {
    return {};
  }
  AppendLE(result, AssertedCast<uint32_t>(aPayload.Length()));
  result.AppendElements(encoded.Elements(), encodedSize);
  return result;
}

// Returns empty array on failure.
static nsTArray<uint8_t> DecompressRecord(Span<const uint8_t> aCompressed) {
  if (aCompressed.Length() < kOriginalLenSize) {
    return {};
  }
  uint32_t originalLen = LittleEndian::readUint32(aCompressed.Elements());
  if (originalLen > kMaxPayloadSize) {
    LOG(("SSLTokensCache: implausible payload originalLen %" PRIu32,
         originalLen));
    return {};
  }
  nsTArray<uint8_t> result;
  if (!result.SetLength(originalLen, fallible)) {
    return {};
  }
  auto encoded = aCompressed.From(kOriginalLenSize);
  size_t decodedSize = originalLen;
  BrotliDecoderResult r = BrotliDecoderDecompress(
      encoded.Length(), encoded.Elements(), &decodedSize, result.Elements());
  if (r != BROTLI_DECODER_RESULT_SUCCESS || decodedSize != originalLen) {
    return {};
  }
  return result;
}

struct PayloadReader {
  Span<const uint8_t> buf;
  size_t pos = 0;

  template <typename T>
  bool Read(T& out) {
    if (buf.Length() - pos < sizeof(T)) return false;
    if constexpr (sizeof(T) == 1) {
      out = static_cast<T>(buf[pos]);
    } else {
      T le;
      memcpy(&le, buf.Elements() + pos, sizeof(T));
      out = mozilla::NativeEndian::swapFromLittleEndian(le);
    }
    pos += sizeof(T);
    return true;
  }
  bool Bytes(nsTArray<uint8_t>& out, uint32_t len) {
    if (buf.Length() - pos < len) return false;
    if (!out.SetLength(len, fallible)) return false;
    memcpy(out.Elements(), buf.Elements() + pos, len);
    pos += len;
    return true;
  }
  bool AtEnd() const { return pos == buf.Length(); }
};

static bool DeserializeRecord(Span<const uint8_t> aBuf,
                              nsTArray<uint8_t>& aToken,
                              SessionCacheInfo& aInfo) {
  PayloadReader r{aBuf};

  uint32_t tokenLen;
  if (!r.Read(tokenLen)) return false;
  if (tokenLen > kMaxTokenSize) return false;
  if (!r.Bytes(aToken, tokenLen)) return false;

  uint8_t evStatus;
  if (!r.Read(evStatus)) return false;
  aInfo.mEVStatus = evStatus ? psm::EVStatus::EV : psm::EVStatus::NotEV;

  uint16_t ctStatus;
  if (!r.Read(ctStatus)) return false;
  aInfo.mCertificateTransparencyStatus = ctStatus;

  uint8_t overridableError;
  if (!r.Read(overridableError)) return false;
  aInfo.mOverridableErrorCategory =
      static_cast<nsITransportSecurityInfo::OverridableErrorCategory>(
          overridableError);

  uint8_t builtinRoot;
  if (!r.Read(builtinRoot)) return false;
  if (builtinRoot == 0) {
    aInfo.mIsBuiltCertChainRootBuiltInRoot = Nothing();
  } else if (builtinRoot == 1) {
    aInfo.mIsBuiltCertChainRootBuiltInRoot = Some(false);
  } else if (builtinRoot == 2) {
    aInfo.mIsBuiltCertChainRootBuiltInRoot = Some(true);
  } else {
    return false;
  }

  uint32_t serverCertLen;
  if (!r.Read(serverCertLen)) return false;
  if (serverCertLen > kMaxCertSize) return false;
  if (!r.Bytes(aInfo.mServerCertBytes, serverCertLen)) return false;

  auto readChain = [&](Maybe<nsTArray<nsTArray<uint8_t>>>& aChain) -> bool {
    uint8_t present;
    if (!r.Read(present)) return false;
    if (!present) {
      aChain = Nothing();
      return true;
    }
    uint8_t count;
    if (!r.Read(count)) return false;
    nsTArray<nsTArray<uint8_t>> chain;
    for (uint8_t i = 0; i < count; i++) {
      uint32_t certLen;
      if (!r.Read(certLen)) return false;
      if (certLen > kMaxCertSize) return false;
      nsTArray<uint8_t> cert;
      if (!r.Bytes(cert, certLen)) return false;
      chain.AppendElement(std::move(cert));
    }
    aChain = Some(std::move(chain));
    return true;
  };

  if (!readChain(aInfo.mSucceededCertChainBytes)) return false;
  if (!readChain(aInfo.mHandshakeCertificatesBytes)) return false;
  return r.AtEnd();
}

StaticRefPtr<SSLTokensCache> SSLTokensCache::gInstance;
StaticMutex SSLTokensCache::sLock;
uint64_t SSLTokensCache::sRecordId = 0;

SSLTokensCache::TokenCacheRecord::~TokenCacheRecord() {
  if (!gInstance) {
    return;
  }
  gInstance->OnRecordDestroyed(this);
}

uint32_t SSLTokensCache::TokenCacheRecord::Size() const {
  return mKey.Length() + mCompressedPayload.Length();
}

uint32_t SSLTokensCache::TokenCacheEntry::Size() const {
  uint32_t size = 0;
  for (const auto& rec : mRecords) {
    size += rec->Size();
  }
  return size;
}

void SSLTokensCache::TokenCacheEntry::AddRecord(
    UniquePtr<SSLTokensCache::TokenCacheRecord>&& aRecord,
    nsTArray<TokenCacheRecord*>& aExpirationArray) {
  if (mRecords.Length() ==
      StaticPrefs::network_ssl_tokens_cache_records_per_entry()) {
    aExpirationArray.RemoveElement(mRecords[0].get());
    mRecords.RemoveElementAt(0);
  }

  aExpirationArray.AppendElement(aRecord.get());
  for (int32_t i = mRecords.Length() - 1; i >= 0; --i) {
    if (aRecord->mExpirationTime > mRecords[i]->mExpirationTime) {
      mRecords.InsertElementAt(i + 1, std::move(aRecord));
      return;
    }
  }
  mRecords.InsertElementAt(0, std::move(aRecord));
}

UniquePtr<SSLTokensCache::TokenCacheRecord>
SSLTokensCache::TokenCacheEntry::RemoveWithId(uint64_t aId) {
  for (int32_t i = mRecords.Length() - 1; i >= 0; --i) {
    if (mRecords[i]->mId == aId) {
      UniquePtr<TokenCacheRecord> record = std::move(mRecords[i]);
      mRecords.RemoveElementAt(i);
      return record;
    }
  }
  return nullptr;
}

const UniquePtr<SSLTokensCache::TokenCacheRecord>&
SSLTokensCache::TokenCacheEntry::Get() {
  return mRecords[0];
}

NS_IMPL_ISUPPORTS(SSLTokensCache, nsIMemoryReporter, nsIObserver,
                  nsIAsyncShutdownBlocker)

template <typename Pred>
void SSLTokensCache::RemoveMatchingLocked(Pred&& aPredicate) {
  sLock.AssertCurrentThreadOwns();
  AutoTArray<nsCString, 4> keysToRemove;
  for (const auto& entry : mTokenCacheRecords) {
    if (aPredicate(entry.GetKey())) {
      keysToRemove.AppendElement(entry.GetKey());
    }
  }
  for (const auto& key : keysToRemove) {
    (void)RemoveAllLocked(key);
  }
}

void SSLTokensCache::ClearCacheLocked() {
  sLock.AssertCurrentThreadOwns();
  mLoadGeneration++;
  mExpirationArray.Clear();
  mTokenCacheRecords.Clear();
  mCacheSize = 0;
}

// static
nsTArray<SSLTokensCacheRecordInfo> SSLTokensCache::SerializeForIPC() {
  StaticMutexAutoLock lock(sLock);
  nsTArray<SSLTokensCacheRecordInfo> records;
  if (gInstance) {
    gInstance->CollectRecordInfosLocked(records,
                                        /* aFilterForPersistence */ true);
  }
  return records;
}

void SSLTokensCache::CollectRecordInfosLocked(
    nsTArray<SSLTokensCacheRecordInfo>& aOut,
    bool aFilterForPersistence) const {
  sLock.AssertCurrentThreadOwns();
  for (const auto& entry : mTokenCacheRecords.Values()) {
    for (const auto& rec : entry->Records()) {
      if (aFilterForPersistence &&
          !ShouldPersistKey(rec->mKey, rec->mOverridableError)) {
        continue;
      }
      auto info = aOut.AppendElement();
      info->key = rec->mKey;
      info->expirationTime = static_cast<int64_t>(rec->mExpirationTime);
      info->overridableError = rec->mOverridableError;
      info->restored = rec->mRestored;
      info->id = rec->mId;
      info->compressedPayload = rec->mCompressedPayload.Clone();
    }
  }
}

// static
void SSLTokensCache::GetAllRecords(nsTArray<SSLTokensCacheRecordInfo>& aOut) {
  StaticMutexAutoLock lock(sLock);
  if (!gInstance) {
    return;
  }
  gInstance->CollectRecordInfosLocked(aOut, /* aFilterForPersistence */ false);
}

// static
void SSLTokensCache::ReplaceAllRecords(
    nsTArray<SSLTokensCacheRecordInfo>&& aRecords) {
  StaticMutexAutoLock lock(sLock);
  if (!gInstance) {
    return;
  }
  gInstance->ClearCacheLocked();
  for (auto& info : aRecords) {
    auto rec = MakeRecord(info.key, static_cast<PRTime>(info.expirationTime),
                          info.overridableError, info.restored,
                          std::move(info.compressedPayload));
    gInstance->InsertRecordLocked(std::move(rec));
  }
}

// static
bool SSLTokensCache::DecodeCompressedPayload(Span<const uint8_t> aCompressed,
                                             nsTArray<uint8_t>& aToken,
                                             SessionCacheInfo& aInfo,
                                             uint32_t* aDecompressedLength) {
  nsTArray<uint8_t> payload = DecompressRecord(aCompressed);
  if (payload.IsEmpty()) {
    return false;
  }
  if (aDecompressedLength) {
    *aDecompressedLength = payload.Length();
  }
  return DeserializeRecord(payload, aToken, aInfo);
}

// static
void SSLTokensCache::DeserializeFromIPC(
    nsTArray<SSLTokensCacheRecordInfo>&& aRecords, bool aRestored) {
  if (aRecords.IsEmpty()) {
    return;
  }
  uint32_t loadGen = 0;
  {
    StaticMutexAutoLock lock(sLock);
    if (!gInstance) {
      return;
    }
    gInstance->ClearCacheLocked();
    loadGen = gInstance->mLoadGeneration;
  }
  PRTime now = PR_Now();
  for (const auto& rec : aRecords) {
    if (rec.expirationTime <= now) {
      continue;
    }
    PutFromPersisted(rec, loadGen, aRestored);
  }
}

// static
void SSLTokensCache::DeserializeFromIPCAsync(
    nsTArray<SSLTokensCacheRecordInfo>&& aRecords, bool aRestored) {
  if (aRecords.IsEmpty()) {
    return;
  }
  NS_DispatchBackgroundTask(NS_NewRunnableFunction(
      "SSLTokensCache::DeserializeFromIPCAsync",
      [records = std::move(aRecords), aRestored]() mutable {
        DeserializeFromIPC(std::move(records), aRestored);
      }));
}

// static
nsDependentCSubstring SSLTokensCache::BasePartFromKey(const nsACString& aKey) {
  int32_t caretPos = aKey.FindChar('^');
  return nsDependentCSubstring(
      aKey, 0, caretPos == kNotFound ? aKey.Length() : caretPos);
}

// static
// Extracts the host from a key's base part ("host:port" or "prefix:host:port").
// Returns an empty string if no colon is found.
nsDependentCSubstring SSLTokensCache::HostFromBasePart(
    const nsDependentCSubstring& aBasePart) {
  int32_t lastColon = aBasePart.RFindChar(':');
  if (lastColon == kNotFound) {
    return nsDependentCSubstring();
  }
  return nsDependentCSubstring(aBasePart, 0, lastColon);
}

// static
OriginAttributes SSLTokensCache::OAFromPeerId(const nsACString& aPeerId) {
  OriginAttributes oa;
  int32_t caretPos = aPeerId.FindChar('^');
  if (caretPos != kNotFound) {
    // PopulateFromSuffix expects the full OA suffix including the leading '^'.
    nsAutoCString suffix(Substring(aPeerId, caretPos));
    (void)oa.PopulateFromSuffix(suffix);
  }
  return oa;
}

// static
bool SSLTokensCache::SetupPersistenceLocked(uint32_t& aLoadGen,
                                            bool aStorageServiceAvailable) {
  sLock.AssertCurrentThreadOwns();
  MOZ_ASSERT(gInstance);
  MOZ_ASSERT(!gInstance->mDBActive);

  if (!aStorageServiceAvailable) {
    return false;
  }

  if (!gInstance->mDBFile) {
    nsCOMPtr<nsIFile> dbFile = ProfileDBFile();
    if (!dbFile) {
      return false;
    }
    gInstance->mDBFile = dbFile;
  }

  if (!gInstance->mDBQueue) {
    nsCOMPtr<nsISerialEventTarget> queue;
    NS_CreateBackgroundTaskQueue("SSLTokensDB", getter_AddRefs(queue));
    if (!queue) {
      return false;
    }
    gInstance->mDBQueue = queue;
  }

  gInstance->mDBActive = true;
  gInstance->mLoadStartTime = TimeStamp::Now();
  aLoadGen = gInstance->mLoadGeneration;
  return true;
}

// Best-effort removal of the pre-mozStorage backing files.
static void RemoveLegacyBackingFilesSync(nsIFile* aDbFile) {
  nsCOMPtr<nsIFile> legacy;
  if (NS_FAILED(aDbFile->Clone(getter_AddRefs(legacy)))) {
    return;
  }
  legacy->SetLeafName(u"ssl_tokens_cache.bin"_ns);
  (void)legacy->Remove(false);
  legacy->SetLeafName(u"ssl_tokens_cache.tmp"_ns);
  (void)legacy->Remove(false);
}

// static
nsresult SSLTokensCache::Init() {
  MOZ_ASSERT(NS_IsMainThread());

  if (XRE_IsParentProcess()) {
    if (nsCOMPtr<nsIFile> dbFile = ProfileDBFile()) {
      NS_DispatchBackgroundTask(NS_NewRunnableFunction(
          "SSLTokensCache::RemoveLegacyBackingFiles",
          [dbFile]() { RemoveLegacyBackingFilesSync(dbFile); }));
    }
  }

  StaticMutexAutoLock lock(sLock);

  // The socket process holds the live token cache, the parent process the
  // persistence layer. Some xpcshell tests use sockets in the parent, too.
  if (!(XRE_IsSocketProcess() || XRE_IsParentProcess())) {
    return NS_OK;
  }

  MOZ_ASSERT(!gInstance);

  gInstance = new SSLTokensCache();

  RegisterWeakMemoryReporter(gInstance);

  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
  if (obs && XRE_IsParentProcess()) {
    obs->AddObserver(gInstance, "profile-after-change", false);
    obs->AddObserver(gInstance, "last-pb-context-exited", false);
  }

  if (!StaticPrefs::network_ssl_tokens_cache_persistence()) {
    return NS_OK;
  }

  if (obs) {
    obs->AddObserver(gInstance, "application-background", false);
    obs->AddObserver(gInstance, "idle-daily", false);
    gInstance->mWriteObserversRegistered = true;
  }

  return NS_OK;
}

// static
nsresult SSLTokensCache::Shutdown() {
  MOZ_ASSERT(NS_IsMainThread());
  RefPtr<SSLTokensCache> instance;
  bool blockerRegistered = false;
  {
    StaticMutexAutoLock lock(sLock);

    if (!gInstance) {
      return NS_ERROR_UNEXPECTED;
    }

    instance = gInstance;
    blockerRegistered = gInstance->mShutdownBarrier != nullptr;
  }

  // With no blocker, BlockShutdown will never fire: write synchronously
  // (test environments only), unregister, and clear gInstance now.
  if (!blockerRegistered) {
#ifdef ENABLE_TESTS
    instance->DoWrite(true);
#endif
    UnregisterFromServices(instance);
    StaticMutexAutoLock lock(sLock);
    gInstance = nullptr;
  }
  return NS_OK;
}

// static
void SSLTokensCache::UnregisterFromServices(SSLTokensCache* aInstance) {
  MOZ_ASSERT(NS_IsMainThread());
  UnregisterWeakMemoryReporter(aInstance);

  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
  if (!obs) {
    return;
  }

  bool hadWriteObservers;
  {
    StaticMutexAutoLock lock(sLock);
    hadWriteObservers = aInstance->mWriteObserversRegistered;
  }
  if (hadWriteObservers) {
    obs->RemoveObserver(aInstance, "application-background");
    obs->RemoveObserver(aInstance, "idle-daily");
  }
  if (XRE_IsParentProcess()) {
    Preferences::UnregisterCallback(&SSLTokensCache::ReconcilePersistence,
                                    "network.ssl_tokens_cache_persistence");
    obs->RemoveObserver(aInstance, "profile-after-change");
    obs->RemoveObserver(aInstance, "last-pb-context-exited");
  }
}

SSLTokensCache::SSLTokensCache() { LOG(("SSLTokensCache::SSLTokensCache")); }

SSLTokensCache::~SSLTokensCache() { LOG(("SSLTokensCache::~SSLTokensCache")); }

// static
nsresult SSLTokensCache::CreateSchemaOn(mozIStorageConnection* aConn) {
  nsresult rv = aConn->SetSchemaVersion(kSSLTokensCacheSchemaVersion);
  NS_ENSURE_SUCCESS(rv, rv);
  return aConn->ExecuteSimpleSQL(
      nsLiteralCString("CREATE TABLE ssl_tokens ("
                       "key TEXT NOT NULL,"
                       "expiration_time INTEGER NOT NULL,"
                       "overridable_error INTEGER NOT NULL,"
                       "payload BLOB NOT NULL)"));
}

// static
nsresult SSLTokensCache::EnsureSchema(mozIStorageConnection* aConn) {
  bool tableExists = false;
  nsresult rv = aConn->TableExists("ssl_tokens"_ns, &tableExists);
  NS_ENSURE_SUCCESS(rv, rv);
  if (!tableExists) {
    return CreateSchemaOn(aConn);
  }

  // An unreadable version is as good as a wrong one: recreate.
  int32_t dbSchemaVersion = 0;
  rv = aConn->GetSchemaVersion(&dbSchemaVersion);
  if (NS_SUCCEEDED(rv) && dbSchemaVersion == kSSLTokensCacheSchemaVersion) {
    return NS_OK;
  }

  mozStorageTransaction transaction(aConn, false);
  rv = transaction.Start();
  NS_ENSURE_SUCCESS(rv, rv);
  rv = aConn->ExecuteSimpleSQL("DROP TABLE ssl_tokens"_ns);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = CreateSchemaOn(aConn);
  NS_ENSURE_SUCCESS(rv, rv);
  return transaction.Commit();
}

// static
nsresult SSLTokensCache::PrepareInsertStatement(
    mozIStorageConnection* aConn, nsCOMPtr<mozIStorageStatement>& aStmtInsert) {
  return aConn->CreateStatement(
      nsLiteralCString("INSERT INTO ssl_tokens (key, expiration_time, "
                       "overridable_error, payload) VALUES (:key, "
                       ":expiration_time, :overridable_error, :payload)"),
      getter_AddRefs(aStmtInsert));
}

// static
nsresult SSLTokensCache::WriteSnapshotTo(
    mozIStorageConnection* aConn, mozIStorageStatement* aStmtInsert,
    const nsTArray<SSLTokensCacheRecordInfo>& aSnapshot) {
  mozStorageTransaction transaction(aConn, false);
  nsresult rv = transaction.Start();
  NS_ENSURE_SUCCESS(rv, rv);

  rv = aConn->ExecuteSimpleSQL("DELETE FROM ssl_tokens"_ns);
  NS_ENSURE_SUCCESS(rv, rv);

  for (const auto& rec : aSnapshot) {
    mozStorageStatementScoper scoper(aStmtInsert);
    rv = aStmtInsert->BindUTF8StringByName("key"_ns, rec.key);
    NS_ENSURE_SUCCESS(rv, rv);
    rv = aStmtInsert->BindInt64ByName("expiration_time"_ns, rec.expirationTime);
    NS_ENSURE_SUCCESS(rv, rv);
    rv = aStmtInsert->BindInt32ByName("overridable_error"_ns,
                                      rec.overridableError);
    NS_ENSURE_SUCCESS(rv, rv);
    rv = aStmtInsert->BindBlobByName("payload"_ns,
                                     rec.compressedPayload.Elements(),
                                     rec.compressedPayload.Length());
    NS_ENSURE_SUCCESS(rv, rv);
    rv = aStmtInsert->Execute();
    NS_ENSURE_SUCCESS(rv, rv);
  }

  return transaction.Commit();
}

// static
uint32_t SSLTokensCache::LoadValidRecordsFrom(mozIStorageConnection* aConn,
                                              uint32_t aLoadGen,
                                              bool* aCorrupted) {
  nsCOMPtr<mozIStorageStatement> stmt;
  nsresult rv = aConn->CreateStatement(
      nsLiteralCString("SELECT key, expiration_time, overridable_error, "
                       "payload FROM ssl_tokens WHERE expiration_time > "
                       ":now"),
      getter_AddRefs(stmt));
  NS_ENSURE_SUCCESS(rv, 0);

  rv = stmt->BindInt64ByName("now"_ns, PR_Now());
  NS_ENSURE_SUCCESS(rv, 0);

  uint32_t count = 0;
  bool hasResult = false;
  for (;;) {
    rv = stmt->ExecuteStep(&hasResult);
    if (NS_FAILED(rv)) {
      LOG(("SSLTokensCache::LoadValidRecordsFrom: ExecuteStep failed"));
      if (aCorrupted) {
        *aCorrupted = true;
      }
      break;
    }
    if (!hasResult) {
      break;
    }
    SSLTokensCacheRecordInfo rec{};
    uint32_t len = 0;
    const uint8_t* blob = nullptr;
    if (NS_FAILED(stmt->GetUTF8String(0, rec.key)) ||
        NS_FAILED(stmt->GetSharedBlob(3, &len, &blob))) {
      // Corruption the open check can't see; don't pass off a partial load.
      if (aCorrupted) {
        *aCorrupted = true;
      }
      continue;
    }
    rec.expirationTime = stmt->AsInt64(1);
    rec.overridableError = static_cast<uint8_t>(stmt->AsInt32(2));
    rec.compressedPayload.AppendElements(blob, len);
    // Loaded from disk, so restored by definition.
    if (PutFromPersisted(rec, aLoadGen, /* aRestored */ true)) {
      count++;
    }
  }
  return count;
}

// static
nsresult SSLTokensCache::Put(const nsACString& aKey, const uint8_t* aToken,
                             uint32_t aTokenLen,
                             CommonSocketControl* aSocketControl) {
  PRTime expirationTime;
  SSLResumptionTokenInfo tokenInfo;
  if (SSL_GetResumptionTokenInfo(aToken, aTokenLen, &tokenInfo,
                                 sizeof(tokenInfo)) != SECSuccess) {
    LOG(("  cannot get expiration time from the token, NSS error %d",
         PORT_GetError()));
    return NS_ERROR_FAILURE;
  }

  expirationTime = tokenInfo.expirationTime;
  SSL_DestroyResumptionTokenInfo(&tokenInfo);

  return Put(aKey, aToken, aTokenLen, aSocketControl, expirationTime);
}

// static
nsresult SSLTokensCache::Put(const nsACString& aKey, const uint8_t* aToken,
                             uint32_t aTokenLen,
                             CommonSocketControl* aSocketControl,
                             PRTime aExpirationTime) {
  LOG(("SSLTokensCache::Put [key=%s, tokenLen=%u]",
       PromiseFlatCString(aKey).get(), aTokenLen));

  if (!aSocketControl) {
    return NS_ERROR_FAILURE;
  }

  nsCOMPtr<nsITransportSecurityInfo> securityInfo;
  nsresult rv = aSocketControl->GetSecurityInfo(getter_AddRefs(securityInfo));
  if (NS_FAILED(rv)) {
    return rv;
  }

  nsCOMPtr<nsIX509Cert> cert;
  securityInfo->GetServerCert(getter_AddRefs(cert));
  if (!cert) {
    return NS_ERROR_FAILURE;
  }

  nsTArray<uint8_t> certBytes;
  rv = cert->GetRawDER(certBytes);
  if (NS_FAILED(rv)) {
    return rv;
  }

  Maybe<nsTArray<nsTArray<uint8_t>>> succeededCertChainBytes;
  nsTArray<RefPtr<nsIX509Cert>> succeededCertArray;
  rv = securityInfo->GetSucceededCertChain(succeededCertArray);
  if (NS_FAILED(rv)) {
    return rv;
  }

  auto getRawDerAll = [](nsTArray<RefPtr<nsIX509Cert>>& aCerts)
      -> Result<nsTArray<nsTArray<uint8_t>>, nsresult> {
    return TransformIntoNewArrayAbortOnErr(
        aCerts,
        [](const RefPtr<nsIX509Cert>& aCert)
            -> Result<nsTArray<uint8_t>, nsresult> {
          nsTArray<uint8_t> raw;
          MOZ_TRY(aCert->GetRawDER(raw));
          return std::move(raw);
        },
        fallible);
  };

  Maybe<bool> isBuiltCertChainRootBuiltInRoot;
  if (!succeededCertArray.IsEmpty()) {
    auto result = getRawDerAll(succeededCertArray);
    if (result.isErr()) return result.unwrapErr();
    succeededCertChainBytes.emplace(result.unwrap());

    bool builtInRoot = false;
    rv = securityInfo->GetIsBuiltCertChainRootBuiltInRoot(&builtInRoot);
    if (NS_FAILED(rv)) {
      return rv;
    }
    isBuiltCertChainRootBuiltInRoot.emplace(builtInRoot);
  }

  bool isEV;
  rv = securityInfo->GetIsExtendedValidation(&isEV);
  if (NS_FAILED(rv)) {
    return rv;
  }

  uint16_t certificateTransparencyStatus;
  rv = securityInfo->GetCertificateTransparencyStatus(
      &certificateTransparencyStatus);
  if (NS_FAILED(rv)) {
    return rv;
  }

  nsITransportSecurityInfo::OverridableErrorCategory overridableErrorCategory;
  rv = securityInfo->GetOverridableErrorCategory(&overridableErrorCategory);
  if (NS_FAILED(rv)) {
    return rv;
  }

  Maybe<nsTArray<nsTArray<uint8_t>>> handshakeCertificatesBytes;
  nsTArray<RefPtr<nsIX509Cert>> handshakeCertificates;
  rv = securityInfo->GetHandshakeCertificates(handshakeCertificates);
  if (NS_FAILED(rv)) {
    return rv;
  }
  if (!handshakeCertificates.IsEmpty()) {
    auto result = getRawDerAll(handshakeCertificates);
    if (result.isErr()) return result.unwrapErr();
    handshakeCertificatesBytes.emplace(result.unwrap());
  }

  SessionCacheInfo info;
  info.mEVStatus = isEV ? psm::EVStatus::EV : psm::EVStatus::NotEV;
  info.mCertificateTransparencyStatus = certificateTransparencyStatus;
  info.mOverridableErrorCategory = overridableErrorCategory;
  info.mIsBuiltCertChainRootBuiltInRoot = isBuiltCertChainRootBuiltInRoot;
  info.mServerCertBytes = std::move(certBytes);
  info.mSucceededCertChainBytes = std::move(succeededCertChainBytes);
  info.mHandshakeCertificatesBytes = std::move(handshakeCertificatesBytes);

  nsTArray<uint8_t> payload = SerializeRecord({aToken, aTokenLen}, info);
  nsTArray<uint8_t> compressed = CompressRecord(payload);
  if (compressed.IsEmpty()) {
    LOG(("SSLTokensCache::Put: compression failed"));
    return NS_ERROR_FAILURE;
  }

  {
    StaticMutexAutoLock lock(sLock);

    if (!gInstance) {
      LOG(("  service not initialized"));
      return NS_ERROR_NOT_INITIALIZED;
    }

    auto makeRecord = [&]() MOZ_REQUIRES(sLock) {
      auto rec = MakeUnique<TokenCacheRecord>();
      rec->mKey = aKey;
      rec->mExpirationTime = aExpirationTime;
      rec->mOverridableError = static_cast<uint8_t>(overridableErrorCategory);
      rec->mCompressedPayload = std::move(compressed);
      return rec;
    };

    gInstance->InsertRecordLocked(makeRecord());
    gInstance->LogStats();

  }  // sLock released

  return NS_OK;
}

// static
nsresult SSLTokensCache::Get(const nsACString& aKey, nsTArray<uint8_t>& aToken,
                             SessionCacheInfo& aResult, uint64_t* aTokenId) {
  LOG(("SSLTokensCache::Get [key=%s]", PromiseFlatCString(aKey).get()));

  StaticMutexAutoLock lock(sLock);
  if (!gInstance) {
    LOG(("  service not initialized"));
    return NS_ERROR_NOT_INITIALIZED;
  }
  UniquePtr<TokenCacheRecord> owned =
      gInstance->GetRecordLocked(aKey, aTokenId);
  if (!owned) {
    return NS_ERROR_NOT_AVAILABLE;
  }
  nsTArray<uint8_t> payload = DecompressRecord(owned->mCompressedPayload);
  if (payload.IsEmpty() || !DeserializeRecord(payload, aToken, aResult)) {
    return NS_ERROR_FAILURE;
  }
  return NS_OK;
}

UniquePtr<SSLTokensCache::TokenCacheRecord> SSLTokensCache::GetRecordLocked(
    const nsACString& aKey, uint64_t* aTokenId) {
  sLock.AssertCurrentThreadOwns();

  if (!mLoadComplete && mDBActive) {
    LOG(("SSLTokensCache::GetRecordLocked: connection before load complete"));
    mozilla::glean::network::ssl_token_cache_early_connections.Add(1);
  }

  TokenCacheEntry* cacheEntry = nullptr;

  if (mTokenCacheRecords.Get(aKey, &cacheEntry)) {
    if (cacheEntry->RecordCount() == 0) {
      MOZ_ASSERT(false, "Found a cacheEntry with no records");
      mTokenCacheRecords.Remove(aKey);
      return nullptr;
    }

    PRTime now = PR_Now();

    while (cacheEntry->RecordCount() > 0) {
      const UniquePtr<TokenCacheRecord>& rec = cacheEntry->Get();

      if (rec->mExpirationTime > now) {
        uint64_t id = rec->mId;
        uint32_t size = rec->Size();
        UniquePtr<TokenCacheRecord> owned = cacheEntry->RemoveWithId(id);
        if (aTokenId) {
          *aTokenId = id;
        }
        mCacheSize -= size;
        if (cacheEntry->RecordCount() == 0) {
          mTokenCacheRecords.Remove(aKey);
        }
        mozilla::glean::network::ssl_token_cache_hits.Get("hit"_ns).Add(1);
        LOG(("SSLTokensCache::GetRecordLocked: hit [key=%s, load_complete=%s]",
             PromiseFlatCString(aKey).get(), mLoadComplete ? "yes" : "no"));
        return owned;
      }

      LOG(("  skipping expired token [expirationTime=%" PRId64 ", now=%" PRId64
           "]",
           rec->mExpirationTime, now));
      mozilla::glean::network::ssl_token_cache_expired.Add(1);
      uint64_t expiredId = rec->mId;
      mCacheSize -= rec->Size();
      cacheEntry->RemoveWithId(expiredId);
    }

    mTokenCacheRecords.Remove(aKey);
  }

  LOG(("  token not found"));
  mozilla::glean::network::ssl_token_cache_hits.Get("miss"_ns).Add(1);
  return nullptr;
}

// static
nsresult SSLTokensCache::Remove(const nsACString& aKey, uint64_t aId) {
  StaticMutexAutoLock lock(sLock);

  LOG(("SSLTokensCache::Remove [key=%s]", PromiseFlatCString(aKey).get()));

  if (!gInstance) {
    LOG(("  service not initialized"));
    return NS_ERROR_NOT_INITIALIZED;
  }

  return gInstance->RemoveLocked(aKey, aId);
}

nsresult SSLTokensCache::RemoveLocked(const nsACString& aKey, uint64_t aId) {
  sLock.AssertCurrentThreadOwns();

  LOG(("SSLTokensCache::RemoveLocked [key=%s, id=%" PRIu64 "]",
       PromiseFlatCString(aKey).get(), aId));

  TokenCacheEntry* cacheEntry;
  if (!mTokenCacheRecords.Get(aKey, &cacheEntry)) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  UniquePtr<TokenCacheRecord> rec = cacheEntry->RemoveWithId(aId);
  if (!rec) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  mCacheSize -= rec->Size();
  if (cacheEntry->RecordCount() == 0) {
    mTokenCacheRecords.Remove(aKey);
  }

  // Release the record immediately, so mExpirationArray can be also updated.
  rec = nullptr;

  LogStats();

  return NS_OK;
}

// static
nsresult SSLTokensCache::RemoveAll(const nsACString& aKey) {
  StaticMutexAutoLock lock(sLock);

  LOG(("SSLTokensCache::RemoveAll [key=%s]", PromiseFlatCString(aKey).get()));

  if (!gInstance) {
    LOG(("  service not initialized"));
    return NS_ERROR_NOT_INITIALIZED;
  }

  return gInstance->RemoveAllLocked(aKey);
}

nsresult SSLTokensCache::RemoveAllLocked(const nsACString& aKey) {
  sLock.AssertCurrentThreadOwns();

  LOG(("SSLTokensCache::RemoveAllLocked [key=%s]",
       PromiseFlatCString(aKey).get()));

  UniquePtr<TokenCacheEntry> cacheEntry;
  if (!mTokenCacheRecords.Remove(aKey, &cacheEntry)) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  mCacheSize -= cacheEntry->Size();
  cacheEntry = nullptr;

  LogStats();

  return NS_OK;
}

void SSLTokensCache::OnRecordDestroyed(TokenCacheRecord* aRec) {
  // Always called from destructors of map entries while sLock is held.
  sLock.AssertCurrentThreadOwns();
  mExpirationArray.RemoveElement(aRec);
}

void SSLTokensCache::EvictIfNecessary() {
  sLock.AssertCurrentThreadOwns();
  // kilobytes to bytes
  uint32_t capacity = StaticPrefs::network_ssl_tokens_cache_capacity() << 10;
  if (mCacheSize <= capacity) {
    return;
  }

  LOG(("SSLTokensCache::EvictIfNecessary: evicting"));

  mExpirationArray.Sort(ExpirationComparator());

  PRTime now = PR_Now();
  while (mCacheSize > capacity && mExpirationArray.Length() > 0) {
    auto* rec = mExpirationArray[0];
    // Only count evictions of tokens that are still valid; already-expired
    // tokens would be discarded on lookup anyway and are tracked separately
    // by ssl_token_cache_expired.
    if (rec->mExpirationTime > now) {
      mozilla::glean::network::ssl_token_cache_evictions.Add(1);
    }
    DebugOnly<nsresult> rv = RemoveLocked(rec->mKey, rec->mId);
    MOZ_ASSERT(NS_SUCCEEDED(rv),
               "mExpirationArray and mTokenCacheRecords are out of sync!");
  }
}

void SSLTokensCache::LogStats() {
  sLock.AssertCurrentThreadOwns();
  if (!LOG5_ENABLED()) {
    return;
  }
  LOG(("SSLTokensCache::LogStats [count=%zu, cacheSize=%u]",
       mExpirationArray.Length(), mCacheSize));
  for (const auto& ent : mTokenCacheRecords.Values()) {
    const UniquePtr<TokenCacheRecord>& rec = ent->Get();
    LOG(("  [key=%s, count=%d]", rec->mKey.get(), ent->RecordCount()));
  }
}

size_t SSLTokensCache::SizeOfIncludingThis(
    mozilla::MallocSizeOf mallocSizeOf) const {
  size_t n = mallocSizeOf(this);

  n += mTokenCacheRecords.ShallowSizeOfExcludingThis(mallocSizeOf);
  n += mExpirationArray.ShallowSizeOfExcludingThis(mallocSizeOf);

  for (const auto* rec : mExpirationArray) {
    n += mallocSizeOf(rec);
    n += rec->mKey.SizeOfExcludingThisIfUnshared(mallocSizeOf);
    n += rec->mCompressedPayload.ShallowSizeOfExcludingThis(mallocSizeOf);
  }

  return n;
}

MOZ_DEFINE_MALLOC_SIZE_OF(SSLTokensCacheMallocSizeOf)

NS_IMETHODIMP
SSLTokensCache::CollectReports(nsIHandleReportCallback* aHandleReport,
                               nsISupports* aData, bool aAnonymize) {
  StaticMutexAutoLock lock(sLock);

  MOZ_COLLECT_REPORT("explicit/network/ssl-tokens-cache", KIND_HEAP,
                     UNITS_BYTES,
                     SizeOfIncludingThis(SSLTokensCacheMallocSizeOf),
                     "Memory used for the SSL tokens cache.");

  return NS_OK;
}

// static
void SSLTokensCache::ClearPrivateBrowsing() {
  LOG(("SSLTokensCache::ClearPrivateBrowsing"));
  StaticMutexAutoLock lock(sLock);
  if (!gInstance) {
    return;
  }
  gInstance->RemoveMatchingLocked([](const nsACString& aKey) {
    // Non-PBM keys have no '^' OA suffix at all — skip full deserialization.
    if (!aKey.Contains('^')) return false;
    return OAFromPeerId(aKey).mPrivateBrowsingId != 0;
  });
}

template <typename SendFn>
static void ForwardToSocketProcess(SendFn aSend) {
  if (!XRE_IsParentProcess()) {
    return;
  }
  if (nsIOService::UseSocketProcess() && gIOService) {
    gIOService->CallOrWaitForSocketProcess([send = std::move(aSend)]() {
      RefPtr<SocketProcessParent> socketParent =
          SocketProcessParent::GetSingleton();
      if (socketParent) {
        send(socketParent);
      }
    });
  }
}

// static
void SSLTokensCache::ForwardClearToSocketProcess() {
  ForwardToSocketProcess(
      [](SocketProcessParent* p) { (void)p->SendClearSessionCache(); });
}

static void MaybeClearNSSSessionCache() {
  if (NSS_IsInitialized()) {
    SSL_ClearSessionCache();
  }
}

// static
void SSLTokensCache::ClearSessionCacheAndTokens() {
  MaybeClearNSSSessionCache();
  Clear();
  ForwardClearToSocketProcess();
}

// static
void SSLTokensCache::ForwardClearPrivateBrowsingToSocketProcess() {
  ForwardToSocketProcess([](SocketProcessParent* p) {
    (void)p->SendClearPrivateBrowsingSessionCache();
  });
}

// static
void SSLTokensCache::ClearSessionCacheAndPBMTokens() {
  MOZ_ASSERT(!XRE_IsParentProcess());
  MaybeClearNSSSessionCache();
  ClearPrivateBrowsing();
}

// static
void SSLTokensCache::Clear() {
  LOG(("SSLTokensCache::Clear"));

  nsCOMPtr<nsISerialEventTarget> queue;
  {
    StaticMutexAutoLock lock(sLock);
    if (!gInstance) {
      LOG(("  service not initialized"));
      return;
    }

    gInstance->ClearCacheLocked();
    if (gInstance->mDBActive) {
      queue = gInstance->mDBQueue;
    }
  }

  if (queue) {
    // The serial queue guarantees this runs after any pending write.
    queue->Dispatch(
        NS_NewRunnableFunction("SSLTokensCache::ClearDB", [] { ClearDB(); }),
        NS_DISPATCH_EVENT_MAY_BLOCK);
  }
}

void SSLTokensCache::DoWrite(bool aSynchronous) {
  nsCOMPtr<nsISerialEventTarget> queue;
  {
    StaticMutexAutoLock lock(sLock);
    if (!gInstance) {
      return;
    }
    if (mDBActive) {
      queue = mDBQueue;
    }
  }

  if (!queue) {
    if (XRE_IsSocketProcess()) {
      nsTArray<SSLTokensCacheRecordInfo> snapshot;
      {
        StaticMutexAutoLock lock(sLock);
        if (gInstance) {
          gInstance->CollectRecordInfosLocked(snapshot,
                                              /* aFilterForPersistence */ true);
        }
      }
      if (!snapshot.IsEmpty()) {
        NS_DispatchToMainThread(NS_NewRunnableFunction(
            "SSLTokensCache::SendToParent",
            [records = std::move(snapshot)]() mutable {
              auto* child = SocketProcessChild::GetSingleton();
              if (child && child->CanSend()) {
                (void)child->SendSSLTokensCacheData(std::move(records));
              }
            }));
      }
    }
    return;
  }

  if (aSynchronous) {
    // Test-only synchronous path; block until write completes on the queue.
    nsCOMPtr<nsIRunnable> event =
        NS_NewRunnableFunction("SSLTokensCache::DoWriteSync",
                               [] { CollectAndWriteSnapshotIfLoaded(); });
    NS_DispatchAndSpinEventLoopUntilComplete(
        "SSLTokensCache::DoWrite synchronous test-only write"_ns, queue.get(),
        event.forget());
    return;
  }

  queue->Dispatch(
      NS_NewRunnableFunction("SSLTokensCache::DoWriteAsync",
                             [] { CollectAndWriteSnapshotIfLoaded(); }),
      NS_DISPATCH_EVENT_MAY_BLOCK);
}

// static
void SSLTokensCache::CollectAndWriteSnapshotIfLoaded() {
  MOZ_ASSERT(!NS_IsMainThread());
  // The serial queue guarantees this runs after any queued OpenAndLoad.
  nsTArray<SSLTokensCacheRecordInfo> snapshot;
  {
    StaticMutexAutoLock lock(sLock);
    if (!gInstance || !gInstance->mLoadComplete) {
      return;
    }
    gInstance->CollectRecordInfosLocked(snapshot,
                                        /* aFilterForPersistence */ true);
  }
  WriteSnapshotToDB(std::move(snapshot));
}

// static
void SSLTokensCache::DispatchOpenAndLoad(uint32_t aLoadGen) {
  nsCOMPtr<nsISerialEventTarget> queue;
  {
    StaticMutexAutoLock lock(sLock);
    if (!gInstance || !gInstance->mDBActive) {
      return;
    }
    queue = gInstance->mDBQueue;
  }
  queue->Dispatch(NS_NewRunnableFunction("SSLTokensCache::OpenAndLoad",
                                         [aLoadGen]() {
                                           uint32_t count =
                                               LoadFromDB(aLoadGen);
                                           OnLoadCompleteNotify(count);
                                         }),
                  NS_DISPATCH_EVENT_MAY_BLOCK);
}

// static
void SSLTokensCache::OnLoadCompleteNotify(uint32_t aCount) {
  mozilla::glean::network::ssl_token_cache_persistence_records_loaded.Add(
      AssertedCast<int32_t>(aCount));

  TimeDuration elapsed;
  {
    StaticMutexAutoLock lock(sLock);
    if (!gInstance) {
      return;
    }
    gInstance->mLoadComplete = true;
    elapsed = TimeStamp::Now() - gInstance->mLoadStartTime;
  }
  mozilla::glean::network::ssl_token_cache_load_time.AccumulateRawDuration(
      elapsed);
  LOG(("SSLTokensCache::OnLoadCompleteNotify [records=%u, time=%.1fms]", aCount,
       elapsed.ToMilliseconds()));

  // Forward persisted tokens to the socket process. Uses
  // CallOrWaitForSocketProcess so it fires immediately if the socket process
  // is already up, or is deferred until it is ready.
  if (StaticPrefs::network_ssl_tokens_cache_persistence()) {
    NS_DispatchToMainThread(
        NS_NewRunnableFunction("SSLTokensCache::ForwardToSocketProcess", []() {
          if (!gIOService || !nsIOService::UseSocketProcess()) {
            return;
          }
          // Serialize on a background thread, then send on the main thread.
          // No captures: CallOrWaitForSocketProcess copies its callable when
          // deferring, so the lambda must remain copyable.
          gIOService->CallOrWaitForSocketProcess([]() {
            NS_DispatchBackgroundTask(NS_NewRunnableFunction(
                "SSLTokensCache::SerializeForSocket", []() {
                  nsTArray<SSLTokensCacheRecordInfo> records =
                      SSLTokensCache::SerializeForIPC();
                  if (records.IsEmpty()) {
                    return;
                  }
                  NS_DispatchToMainThread(NS_NewRunnableFunction(
                      "SSLTokensCache::SendToSocket",
                      [records = std::move(records)]() mutable {
                        RefPtr<SocketProcessParent> parent =
                            SocketProcessParent::GetSingleton();
                        if (parent && parent->CanSend()) {
                          (void)parent->SendLoadSSLTokensCache(
                              std::move(records));
                        }
                      }));
                }));
          });
        }));
  }
}

// static
UniquePtr<SSLTokensCache::TokenCacheRecord> SSLTokensCache::MakeRecord(
    const nsACString& aKey, PRTime aExpirationTime, uint8_t aOverridableError,
    bool aRestored, nsTArray<uint8_t>&& aCompressedPayload) {
  auto rec = MakeUnique<TokenCacheRecord>();
  rec->mKey = aKey;
  rec->mExpirationTime = aExpirationTime;
  rec->mOverridableError = aOverridableError;
  rec->mRestored = aRestored;
  rec->mCompressedPayload = std::move(aCompressedPayload);
  return rec;
}

// static
bool SSLTokensCache::PutFromPersisted(const SSLTokensCacheRecordInfo& aRec,
                                      uint32_t aExpectedGen, bool aRestored) {
  StaticMutexAutoLock lock(sLock);
  if (!gInstance || gInstance->mLoadGeneration != aExpectedGen) {
    return false;
  }
  auto rec = MakeRecord(aRec.key, static_cast<PRTime>(aRec.expirationTime),
                        aRec.overridableError, aRestored,
                        aRec.compressedPayload.Clone());
  gInstance->InsertRecordLocked(std::move(rec));
  return true;
}

#ifdef DEBUG
// static
void SSLTokensCache::AssertOnDBThread() {
  MOZ_ASSERT(!NS_IsMainThread());
  nsCOMPtr<nsISerialEventTarget> queue;
  {
    StaticMutexAutoLock lock(sLock);
    if (gInstance) {
      queue = gInstance->mDBQueue;
    }
  }
  MOZ_ASSERT(!queue || queue->IsOnCurrentThread());
}
#endif

// static
bool SSLTokensCache::IsPersistenceStillActive() {
  // Re-checked so a task queued before the pref flipped off can't write.
  if (!StaticPrefs::network_ssl_tokens_cache_persistence()) {
    return false;
  }
  StaticMutexAutoLock lock(sLock);
  return gInstance && gInstance->mDBActive;
}

// static
already_AddRefed<nsIFile> SSLTokensCache::GetCachedDBFile() {
  StaticMutexAutoLock lock(sLock);
  if (!gInstance || !gInstance->mDBFile) {
    return nullptr;
  }
  nsCOMPtr<nsIFile> dbFile = gInstance->mDBFile;
  return dbFile.forget();
}

// static
bool SSLTokensCache::RemoveDBFile(nsIFile* aDbFile) {
  MOZ_ASSERT(!NS_IsMainThread());
  return NS_SUCCEEDED(aDbFile->Remove(false));
}

// True if the file itself is unusable, so removal is the only recovery.
static bool DBFileIsUnusable(nsresult aRv) {
  switch (aRv) {
    case NS_ERROR_STORAGE_BUSY:
    case NS_ERROR_FILE_IS_LOCKED:
    case NS_ERROR_FILE_ACCESS_DENIED:
    case NS_ERROR_FILE_READ_ONLY:
    case NS_ERROR_OUT_OF_MEMORY:
    case NS_ERROR_NOT_INITIALIZED:
      return false;
    default:
      return true;
  }
}

// Shared by OpenDB and the test-only connection helper below.
static nsresult OpenUnsharedDatabaseAt(nsIFile* aFile,
                                       mozIStorageConnection** aConn) {
  nsCOMPtr<mozIStorageService> storageService =
      do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID);
  if (!storageService) {
    return NS_ERROR_FAILURE;
  }
  return storageService->OpenUnsharedDatabase(
      aFile, mozIStorageService::CONNECTION_DEFAULT, aConn);
}

// static
bool SSLTokensCache::OpenDB(nsCOMPtr<mozIStorageConnection>& aConn) {
  MOZ_ASSERT(!NS_IsMainThread());
#ifdef DEBUG
  AssertOnDBThread();
#endif
  MOZ_ASSERT(!aConn);

  nsCOMPtr<nsIFile> dbFile = GetCachedDBFile();
  if (!dbFile) {
    return false;
  }

  nsresult rv = OpenUnsharedDatabaseAt(dbFile, getter_AddRefs(aConn));
  // Corruption or a missing encryption key never opens again; retry once.
  if (NS_FAILED(rv) && DBFileIsUnusable(rv)) {
    LOG(("SSLTokensCache::OpenDB: unopenable (0x%" PRIx32 "), removing",
         static_cast<uint32_t>(rv)));
    mozilla::glean::network::ssl_token_cache_db_errors.Get("open"_ns).Add(1);
    aConn = nullptr;
    RemoveDBFile(dbFile);
    rv = OpenUnsharedDatabaseAt(dbFile, getter_AddRefs(aConn));
  }
  if (NS_FAILED(rv)) {
    aConn = nullptr;
    return false;
  }

  // Disposable cache, so skip fsync entirely.
  (void)NS_WARN_IF(
      NS_FAILED(aConn->ExecuteSimpleSQL("PRAGMA synchronous = OFF"_ns)));

  // Tolerate brief contention; matches Places' DATABASE_BUSY_TIMEOUT_MS.
  (void)NS_WARN_IF(
      NS_FAILED(aConn->ExecuteSimpleSQL("PRAGMA busy_timeout = 100"_ns)));

  if (NS_FAILED(EnsureSchema(aConn))) {
    aConn->Close();
    aConn = nullptr;
    // Opened but unusable; drop it so the next open starts clean.
    mozilla::glean::network::ssl_token_cache_db_errors.Get("schema"_ns).Add(1);
    (void)RemoveDBFile(dbFile);
    return false;
  }

  return true;
}

// static
already_AddRefed<mozIStorageConnection> SSLTokensCache::OpenActiveDB() {
  MOZ_ASSERT(!NS_IsMainThread());
  if (!IsPersistenceStillActive()) {
    return nullptr;
  }
  nsCOMPtr<mozIStorageConnection> conn;
  if (!OpenDB(conn)) {
    return nullptr;
  }
  return conn.forget();
}

// static
uint32_t SSLTokensCache::LoadFromDB(uint32_t aLoadGen) {
  nsCOMPtr<mozIStorageConnection> conn = OpenActiveDB();
  if (!conn) {
    return 0;
  }
  bool corrupted = false;
  uint32_t count = LoadValidRecordsFrom(conn, aLoadGen, &corrupted);
  conn->Close();
  if (corrupted) {
    mozilla::glean::network::ssl_token_cache_db_errors.Get("read"_ns).Add(1);
    nsCOMPtr<nsIFile> dbFile = GetCachedDBFile();
    if (dbFile) {
      RemoveDBFile(dbFile);
    }
  }
  return count;
}

// static
void SSLTokensCache::WriteSnapshotToDB(
    nsTArray<SSLTokensCacheRecordInfo>&& aSnapshot) {
  nsCOMPtr<mozIStorageConnection> conn = OpenActiveDB();
  if (!conn) {
    return;
  }
  nsCOMPtr<mozIStorageStatement> stmtInsert;
  nsresult rv = PrepareInsertStatement(conn, stmtInsert);
  if (NS_SUCCEEDED(rv)) {
    rv = WriteSnapshotTo(conn, stmtInsert, aSnapshot);
    stmtInsert = nullptr;
  }
  if (NS_WARN_IF(NS_FAILED(rv))) {
    mozilla::glean::network::ssl_token_cache_db_errors.Get("write"_ns).Add(1);
  }
  conn->Close();
}

// static
void SSLTokensCache::ClearDB() {
  nsCOMPtr<mozIStorageConnection> conn = OpenActiveDB();
  if (!conn) {
    return;
  }
  (void)NS_WARN_IF(
      NS_FAILED(conn->ExecuteSimpleSQL("DELETE FROM ssl_tokens"_ns)));
  conn->Close();
}

// static
void SSLTokensCache::RemoveDBFileSync() {
  nsCOMPtr<nsIFile> dbFile = GetCachedDBFile();
  if (dbFile) {
    (void)RemoveDBFile(dbFile);
  }
}

uint64_t SSLTokensCache::InsertRecordLocked(UniquePtr<TokenCacheRecord> aRec) {
  sLock.AssertCurrentThreadOwns();
  const uint64_t id = ++sRecordId;
  aRec->mId = id;

  // aRec->mKey must be read before AddRecord() moves aRec.
  TokenCacheEntry* cacheEntry = mTokenCacheRecords.GetOrInsertNew(aRec->mKey);
  if (cacheEntry->RecordCount() > 0) {
    mCacheSize -= cacheEntry->Size();
  }
  cacheEntry->AddRecord(std::move(aRec), mExpirationArray);
  mCacheSize += cacheEntry->Size();
  EvictIfNecessary();
  return id;
}

// static
bool SSLTokensCache::ShouldPersistKey(const nsACString& aKey,
                                      uint8_t aOverridableError) {
  return aOverridableError == 0 && OAFromPeerId(aKey).mPrivateBrowsingId == 0;
}

// static
void SSLTokensCache::RemoveByMatchAndOAPattern(
    const nsACString& aValue, const nsACString& aSeparatedValue,
    const mozilla::OriginAttributesPattern& aPattern) {
  StaticMutexAutoLock lock(sLock);
  if (!gInstance) {
    return;
  }
  gInstance->RemoveMatchingLocked(
      [&aValue, &aSeparatedValue, &aPattern](const nsACString& aKey) {
        nsDependentCSubstring host = HostFromBasePart(BasePartFromKey(aKey));
        return !host.IsEmpty() &&
               (host.Equals(aValue) || StringEndsWith(host, aSeparatedValue)) &&
               aPattern.Matches(OAFromPeerId(aKey));
      });
}

// static
void SSLTokensCache::RemoveByHostAndOAPattern(
    const nsACString& aHost, const mozilla::OriginAttributesPattern& aPattern) {
  LOG(("SSLTokensCache::RemoveByHostAndOAPattern"));
  RemoveByMatchAndOAPattern(aHost, ":"_ns + aHost, aPattern);
}

// static
void SSLTokensCache::ClearSessionCacheAndTokensForHost(
    const nsACString& aHost, const mozilla::OriginAttributesPattern& aPattern) {
  LOG(("SSLTokensCache::ClearSessionCacheAndTokensForHost"));
  MaybeClearNSSSessionCache();
  RemoveByHostAndOAPattern(aHost, aPattern);
  ForwardClearToSocketProcess();
}

// static
void SSLTokensCache::RemoveBySiteAndOAPattern(
    const nsACString& aSite, const mozilla::OriginAttributesPattern& aPattern) {
  LOG(("SSLTokensCache::RemoveBySiteAndOAPattern"));

  // Three cases for the host part of a key (after HostFromBasePart):
  //   "example.com"       — exact, no prefix        → Equals(aSite)
  //   "sub.example.com"   — subdomain                → ends with "."_ns + aSite
  //   "anon:example.com"  — connection-type prefix   → ends with ":"_ns + aSite
  // (Prefixed subdomains like "anon:sub.example.com" are caught by dotSite.)
  nsAutoCString dotSite("."_ns + aSite);
  nsAutoCString colonSite(":"_ns + aSite);
  StaticMutexAutoLock lock(sLock);
  if (!gInstance) {
    return;
  }
  gInstance->RemoveMatchingLocked(
      [&aSite, &dotSite, &colonSite, &aPattern](const nsACString& aKey) {
        nsDependentCSubstring host = HostFromBasePart(BasePartFromKey(aKey));
        return !host.IsEmpty() &&
               (host.Equals(aSite) || StringEndsWith(host, dotSite) ||
                StringEndsWith(host, colonSite)) &&
               aPattern.Matches(OAFromPeerId(aKey));
      });
}

#ifdef ENABLE_TESTS

// Throwaway connection at aPath, independent of mDBQueue/OpenDB, so tests
// can exercise persistence without the pref or observers.
static already_AddRefed<mozIStorageConnection> OpenTestConnection(
    const nsACString& aPath) {
  nsCOMPtr<nsIFile> file;
  if (NS_FAILED(NS_NewNativeLocalFile(aPath, getter_AddRefs(file)))) {
    return nullptr;
  }
  nsCOMPtr<mozIStorageConnection> conn;
  if (NS_FAILED(OpenUnsharedDatabaseAt(file, getter_AddRefs(conn)))) {
    return nullptr;
  }
  return conn.forget();
}

// static
void SSLTokensCache::TriggerWriteForTest(const nsACString& aPath) {
  nsTArray<SSLTokensCacheRecordInfo> snapshot;
  {
    StaticMutexAutoLock lock(sLock);
    if (gInstance) {
      gInstance->CollectRecordInfosLocked(snapshot,
                                          /* aFilterForPersistence */ true);
    }
  }

  nsCOMPtr<mozIStorageConnection> conn = OpenTestConnection(aPath);
  if (!conn) {
    return;
  }
  if (NS_FAILED(EnsureSchema(conn))) {
    conn->Close();
    return;
  }

  nsCOMPtr<mozIStorageStatement> stmt;
  if (NS_SUCCEEDED(PrepareInsertStatement(conn, stmt))) {
    (void)NS_WARN_IF(NS_FAILED(WriteSnapshotTo(conn, stmt, snapshot)));
    stmt = nullptr;
  }
  conn->Close();
}

// static
void SSLTokensCache::LoadForTest(const nsACString& aPath) {
  uint32_t loadGen = 0;
  {
    StaticMutexAutoLock lock(sLock);
    if (gInstance) {
      loadGen = gInstance->mLoadGeneration;
    }
  }

  nsCOMPtr<mozIStorageConnection> conn = OpenTestConnection(aPath);
  if (!conn) {
    return;
  }
  bool tableExists = false;
  if (NS_SUCCEEDED(conn->TableExists("ssl_tokens"_ns, &tableExists)) &&
      tableExists) {
    LoadValidRecordsFrom(conn, loadGen);
  }
  conn->Close();
}

// static
uint32_t SSLTokensCache::CountForTest() {
  StaticMutexAutoLock lock(sLock);
  if (!gInstance) {
    return 0;
  }
  return gInstance->mTokenCacheRecords.Count();
}

// static
uint32_t SSLTokensCache::CacheSizeForTest() {
  StaticMutexAutoLock lock(sLock);
  return gInstance ? gInstance->mCacheSize : 0;
}

// static
void SSLTokensCache::PutForTest(const nsACString& aKey) {
  uint32_t gen = 0;
  {
    StaticMutexAutoLock lock(sLock);
    if (gInstance) {
      gen = gInstance->mLoadGeneration;
    }
  }
  uint8_t dummyToken[] = {0xDE, 0xAD, 0xBE, 0xEF};
  SessionCacheInfo info;
  nsTArray<uint8_t> payload =
      SerializeRecord({dummyToken, sizeof(dummyToken)}, info);
  nsTArray<uint8_t> compressed = CompressRecord(payload);
  if (compressed.IsEmpty()) {
    return;
  }
  SSLTokensCacheRecordInfo rec{};
  rec.key = aKey;
  rec.expirationTime = PR_Now() + 3600LL * PR_USEC_PER_SEC;
  rec.compressedPayload = std::move(compressed);
  PutFromPersisted(rec, gen, /* aRestored */ false);
}

#endif  // ENABLE_TESTS

// static
void SSLTokensCache::ReconcilePersistence(const char*, void*) {
  MOZ_ASSERT(XRE_IsParentProcess());
  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
  if (!obs) {
    return;
  }

  bool wantPersistence = StaticPrefs::network_ssl_tokens_cache_persistence();
  // Checked before sLock; see SetupPersistenceLocked's doc comment.
  bool storageServiceAvailable = false;
  if (wantPersistence) {
    nsCOMPtr<mozIStorageService> storageService =
        do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID);
    storageServiceAvailable = storageService != nullptr;
  }

  bool addObservers = false;
  bool removeObservers = false;
  bool loadOnActivation = false;
  uint32_t loadGen = 0;
  RefPtr<SSLTokensCache> instance;
  nsCOMPtr<nsISerialEventTarget> queueForRemoval;
  {
    StaticMutexAutoLock lock(sLock);
    instance = gInstance;
    if (!instance) {
      return;
    }
    bool wasRegistered = instance->mWriteObserversRegistered;
    instance->mWriteObserversRegistered = wantPersistence;
    addObservers = wantPersistence && !wasRegistered;
    removeObservers = !wantPersistence && wasRegistered;
    if (wantPersistence && !instance->mDBActive &&
        SetupPersistenceLocked(loadGen, storageServiceAvailable)) {
      // Only the startup activation loads from disk; a later re-enable
      // resumes writing without reloading a possibly-stale cache.
      loadOnActivation = !instance->mPrefCallbackRegistered;
      instance->mLoadComplete = !loadOnActivation;
    }
    if (!wantPersistence && instance->mDBActive) {
      instance->mDBActive = false;
      // mDBQueue stays alive, only the DB file is removed.
      queueForRemoval = instance->mDBQueue;
      instance->ClearCacheLocked();
    }
  }
  if (!wantPersistence) {
    // Drop any DB left behind by a session that had persistence on.
    if (queueForRemoval) {
      // Ordered after any pending write.
      queueForRemoval->Dispatch(
          NS_NewRunnableFunction("SSLTokensCache::RemoveDB",
                                 []() { RemoveDBFileSync(); }),
          NS_DISPATCH_EVENT_MAY_BLOCK);
    } else if (nsCOMPtr<nsIFile> dbFile = ProfileDBFile()) {
      NS_DispatchBackgroundTask(
          NS_NewRunnableFunction("SSLTokensCache::RemoveDB",
                                 [dbFile]() { (void)RemoveDBFile(dbFile); }));
    }
  }
  if (addObservers) {
    obs->AddObserver(instance, "application-background", false);
    obs->AddObserver(instance, "idle-daily", false);
  } else if (removeObservers) {
    obs->RemoveObserver(instance, "application-background");
    obs->RemoveObserver(instance, "idle-daily");
  }
  if (wantPersistence) {
    if (loadOnActivation) {
      DispatchOpenAndLoad(loadGen);
    }
    instance->RegisterShutdownBlocker();
  }
}

NS_IMETHODIMP
SSLTokensCache::Observe(nsISupports* aSubject, const char* aTopic,
                        const char16_t* aData) {
  if (!strcmp(aTopic, "application-background") ||
      !strcmp(aTopic, "idle-daily")) {
    LOG(("SSLTokensCache::Observe [topic=%s]", aTopic));
    DoWrite(false);
  } else if (!strcmp(aTopic, "profile-after-change")) {
    MOZ_ASSERT(XRE_IsParentProcess());
    LOG(("SSLTokensCache::Observe [topic=profile-after-change]"));
    ReconcilePersistence();
    if (!mPrefCallbackRegistered) {
      // Register for live pref changes now that the JS environment is ready.
      // Registering earlier (in Init()) would fire the callback during startup
      // pref-loading before the JS module loader is initialised.
      Preferences::RegisterCallback(&SSLTokensCache::ReconcilePersistence,
                                    "network.ssl_tokens_cache_persistence");
      mPrefCallbackRegistered = true;
    }
  } else if (!strcmp(aTopic, "last-pb-context-exited")) {
    MOZ_ASSERT(XRE_IsParentProcess());
    LOG(("SSLTokensCache::Observe [topic=last-pb-context-exited]"));
    // Remove only PBM entries from this process's token cache.
    ClearPrivateBrowsing();
    // Forward a PBM-scoped clear to the socket process.
    ForwardClearPrivateBrowsingToSocketProcess();
  }
  return NS_OK;
}

// nsIAsyncShutdownBlocker

NS_IMETHODIMP
SSLTokensCache::BlockShutdown(nsIAsyncShutdownClient* /* aClient */) {
  LOG(("SSLTokensCache::BlockShutdown"));

  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(XRE_IsParentProcess());
  nsCOMPtr<nsISerialEventTarget> queue;
  {
    StaticMutexAutoLock lock(sLock);
    if (mDBActive) {
      queue = mDBQueue;
    }
  }
  if (!queue) {
    RemoveShutdownBlocker();
    return NS_OK;
  }

  // Merge the socket process's tokens, then write the final snapshot. The
  // blocker keeps the main thread alive until that completes.
  RefPtr<SSLTokensCache> self = this;
  auto writeAndRelease = [queue,
                          self](nsTArray<SSLTokensCacheRecordInfo> aRecords) {
    InvokeAsync(queue.get(), __func__,
                [records = std::move(aRecords)]() mutable {
                  if (!records.IsEmpty()) {
                    SSLTokensCache::DeserializeFromIPC(std::move(records),
                                                       /* aRestored */ false);
                  }
                  CollectAndWriteSnapshotIfLoaded();
                  return GenericPromise::CreateAndResolve(true, __func__);
                })
        ->Then(
            GetMainThreadSerialEventTarget(), __func__,
            [self](bool) { self->RemoveShutdownBlocker(); },
            [self](nsresult) { self->RemoveShutdownBlocker(); });
  };

  // If the socket process is alive, flush its token cache first so the
  // persisted DB reflects the most recent handshake data.
  RefPtr<SocketProcessParent> socketParent =
      SocketProcessParent::GetSingleton();
  if (!socketParent || !socketParent->CanSend()) {
    writeAndRelease({});
    return NS_OK;
  }

  socketParent->SendFlushSSLTokensCache()->Then(
      GetMainThreadSerialEventTarget(), __func__,
      [writeAndRelease](nsTArray<SSLTokensCacheRecordInfo>&& aRecords) {
        writeAndRelease(std::move(aRecords));
      },
      [writeAndRelease](mozilla::ipc::ResponseRejectReason) {
        writeAndRelease({});
      });
  return NS_OK;
}

NS_IMETHODIMP
SSLTokensCache::GetName(nsAString& aName) {
  aName.AssignLiteral("SSLTokensCache: writing cache to disk");
  return NS_OK;
}

NS_IMETHODIMP
SSLTokensCache::GetState(nsIPropertyBag** aState) {
  *aState = nullptr;
  return NS_OK;
}

void SSLTokensCache::RegisterShutdownBlocker() {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(XRE_IsParentProcess());
  {
    StaticMutexAutoLock lock(sLock);
    if (!gInstance || !gInstance->mDBQueue) {
      return;
    }
    if (gInstance->mShutdownBarrier) {
      return;
    }
  }
  // sLock is intentionally released before the AsyncShutdown calls below:
  // Service() and AddBlocker may run JS, which mustn't happen under sLock.
  nsCOMPtr<nsIAsyncShutdownService> svc = components::AsyncShutdown::Service();
  if (!svc) {
    return;
  }
  nsCOMPtr<nsIAsyncShutdownClient> client;
  svc->GetProfileBeforeChange(getter_AddRefs(client));
  if (!client) {
    return;
  }
  LOG(("SSLTokensCache::RegisterShutdownBlocker"));
  nsresult rv = client->AddBlocker(
      this, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__, u""_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    // Too late to add a blocker, so don't record one: Shutdown() would
    // assume it is pending and skip its own cleanup.
    return;
  }
  {
    StaticMutexAutoLock lock(sLock);
    mShutdownBarrier = client;
  }
}

void SSLTokensCache::RemoveShutdownBlocker() {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(XRE_IsParentProcess());
  // Shutdown() left gInstance set so BlockShutdown could write, so
  // unregister here instead.
  UnregisterFromServices(this);

  nsCOMPtr<nsIAsyncShutdownClient> barrier;
  {
    StaticMutexAutoLock lock(sLock);
    barrier = std::move(mShutdownBarrier);
    gInstance = nullptr;
  }
  if (barrier) {
    barrier->RemoveBlocker(this);
  }
}

#ifdef ENABLE_TESTS
NS_IMPL_ISUPPORTS(SSLTokensCacheService, nsISSLTokensCache,
                  nsISSLTokensCacheTest)
#else
NS_IMPL_ISUPPORTS(SSLTokensCacheService, nsISSLTokensCache)
#endif

// Most of nsISSLTokensCache is parent-process-only; the socket process has
// no way to serve these requests itself.
static nsresult EnsureParentProcess() {
  MOZ_ASSERT(XRE_IsParentProcess());
  return XRE_IsParentProcess() ? NS_OK : NS_ERROR_NOT_AVAILABLE;
}

NS_IMETHODIMP
SSLTokensCacheService::ClearSSLExternalAndInternalSessionCache() {
  nsresult rv = EnsureParentProcess();
  if (NS_FAILED(rv)) {
    return rv;
  }
  SSLTokensCache::ClearSessionCacheAndTokens();
  return NS_OK;
}

NS_IMETHODIMP
SSLTokensCacheService::AsyncClearSSLExternalAndInternalSessionCache(
    JSContext* aCx, mozilla::dom::Promise** aPromise) {
  nsresult rv = EnsureParentProcess();
  if (NS_FAILED(rv)) {
    return rv;
  }

  nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx);
  if (NS_WARN_IF(!globalObject)) {
    return NS_ERROR_FAILURE;
  }

  ErrorResult result;
  RefPtr<mozilla::dom::Promise> promise =
      mozilla::dom::Promise::Create(globalObject, result);
  if (NS_WARN_IF(result.Failed())) {
    return result.StealNSResult();
  }

  if (nsIOService::UseSocketProcess() && gIOService) {
    gIOService->CallOrWaitForSocketProcess([p = RefPtr{promise}]() {
      RefPtr<SocketProcessParent> socketParent =
          SocketProcessParent::GetSingleton();
      (void)socketParent->SendClearSessionCache()->Then(
          GetCurrentSerialEventTarget(), __func__,
          [promise = RefPtr{p}] { promise->MaybeResolveWithUndefined(); },
          [promise = RefPtr{p}] { promise->MaybeReject(NS_ERROR_UNEXPECTED); });
    });
  } else {
    promise->MaybeResolveWithUndefined();
  }
  SSLTokensCache::ClearSessionCacheAndTokens();
  promise.forget(aPromise);
  return NS_OK;
}

template <typename F>
static nsresult WithParsedOAPattern(const nsAString& aPatternJson, F&& aFunc) {
  nsresult rv = EnsureParentProcess();
  if (NS_FAILED(rv)) {
    return rv;
  }
  mozilla::OriginAttributesPattern pattern;
  if (!pattern.Init(aPatternJson)) {
    return NS_ERROR_INVALID_ARG;
  }
  aFunc(pattern);
  return NS_OK;
}

NS_IMETHODIMP
SSLTokensCacheService::RemoveSSLTokensByHostAndOriginAttributesPattern(
    const nsACString& aHost, const nsAString& aPattern) {
  return WithParsedOAPattern(aPattern, [&aHost](const auto& pattern) {
    SSLTokensCache::RemoveByHostAndOAPattern(aHost, pattern);
  });
}

NS_IMETHODIMP
SSLTokensCacheService::RemoveSSLTokensBySiteAndOriginAttributesPattern(
    const nsACString& aSite, const nsAString& aPattern) {
  return WithParsedOAPattern(aPattern, [&aSite](const auto& pattern) {
    SSLTokensCache::RemoveBySiteAndOAPattern(aSite, pattern);
  });
}

#ifdef ENABLE_TESTS
NS_IMETHODIMP
SSLTokensCacheService::CountSSLTokens(uint32_t* aCount) {
  *aCount = SSLTokensCache::CountForTest();
  return NS_OK;
}

NS_IMETHODIMP
SSLTokensCacheService::PutSSLTokenForTest(const nsACString& aKey) {
  SSLTokensCache::PutForTest(aKey);
  return NS_OK;
}
#endif  // ENABLE_TESTS

}  // namespace net
}  // namespace mozilla
