910e62b5创建于 1月15日历史提交
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/media/cdm_storage_database.h"

#include <algorithm>

#include "base/files/file.h"
#include "base/files/file_util.h"
#include "base/json/values_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "sql/statement.h"

namespace content {

namespace {

// This is the version number of the CdmStorageDatabase. We are currently on
// version two since there has been one change to the schema from when it is
// created. Please increment `kVersionNumber` by 1 every time you change the
// schema, and update the cdm_storage_database_unittest.cc code to keep version
// 2's schema to test the transition between v2 and v3, the same way we are
// testing v1 to v2.
// Change to version 2:
// We have introduced file_size and last_modified to the version Schema, to keep
// track of file size and when a file has been touched, whether it be written or
// read.
static const int kVersionNumber = 2;

const char kUmaPrefix[] = "Media.EME.CdmStorageDatabaseSQLiteError.";

const char kDeleteForTimeFrameError[] = "DeleteForTimeFrameError.";
const char kDeleteForStorageKeyError[] = "DeleteForStorageKeyError.";
const char kDeleteForFilterError[] = "DeleteForFilterError.";
const char kDeleteFileError[] = "DeleteFileError.";

static bool DatabaseIsEmpty(sql::Database* db) {
  static constexpr char kSelectCountSql[] = "SELECT COUNT(*) FROM cdm_storage";
  DCHECK(db->IsSQLValid(kSelectCountSql));

  sql::Statement statement(
      db->GetCachedStatement(SQL_FROM_HERE, kSelectCountSql));
  return (statement.Step() && !statement.ColumnInt(0));
}

}  // namespace

CdmStorageDatabase::CdmStorageDatabase(const base::FilePath& path)
    : path_(path),
      // Use a smaller cache, since access will be fairly infrequent and random.
      // Given the expected record sizes (~100s of bytes) and key sizes (<100
      // bytes) and that we'll typically only be pulling one file at a time
      // (playback), specify a large page size to allow inner nodes can pack
      // many keys, to keep the index B-tree flat.
      db_(sql::DatabaseOptions().set_page_size(32768).set_cache_size(8),
          /*tag=*/"CdmStorage") {
  // base::Unretained is safe because `db_` is owned by `this`
  db_.set_error_callback(base::BindRepeating(
      &CdmStorageDatabase::OnDatabaseError, base::Unretained(this)));
}

CdmStorageDatabase::~CdmStorageDatabase() = default;

CdmStorageOpenError CdmStorageDatabase::EnsureOpen() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // If in use or open successful, returns `CdmStorageOpenError::kOk`.
  return OpenDatabase();
}

std::optional<std::vector<uint8_t>> CdmStorageDatabase::ReadFile(
    const blink::StorageKey& storage_key,
    const media::CdmType& cdm_type,
    const std::string& file_name) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (OpenDatabase() != CdmStorageOpenError::kOk) {
    return std::nullopt;
  }

  // clang-format off
  static constexpr char kSelectSql[] =
      "SELECT data FROM cdm_storage "
          "WHERE storage_key = ? "
            "AND cdm_type = ? "
            "AND file_name = ? ";
  // clang-format on
  DCHECK(db_.IsSQLValid(kSelectSql));

  last_operation_ = "ReadFile";

  sql::Statement statement(db_.GetCachedStatement(SQL_FROM_HERE, kSelectSql));
  statement.BindString(0, storage_key.Serialize());
  statement.BindBlob(1, cdm_type.AsBytes());
  statement.BindString(2, file_name);

  if (!statement.Step()) {
    // Failing here is expected if the "file" has not yet been written to and
    // the row does not yet exist. The Cdm Storage code doesn't distinguish
    // between an empty file and a file which does not exist, so just return
    // an empty file without erroring.
    return std::vector<uint8_t>();
  }

  return statement.ColumnBlobAsVector(0);
}

bool CdmStorageDatabase::WriteFile(const blink::StorageKey& storage_key,
                                   const media::CdmType& cdm_type,
                                   const std::string& file_name,
                                   const std::vector<uint8_t>& data) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (OpenDatabase() != CdmStorageOpenError::kOk) {
    return false;
  }

  // clang-format off
  static constexpr char kInsertSql[] =
      "INSERT OR REPLACE INTO "
         "cdm_storage(storage_key,cdm_type,file_name,data,file_size,last_modified) "
         "VALUES(?,?,?,?,?,?) ";
  // clang-format on
  DCHECK(db_.IsSQLValid(kInsertSql));

  last_operation_ = "WriteFile";

  sql::Statement statement(db_.GetCachedStatement(SQL_FROM_HERE, kInsertSql));
  statement.BindString(0, storage_key.Serialize());
  statement.BindBlob(1, cdm_type.AsBytes());
  statement.BindString(2, file_name);
  statement.BindBlob(3, data);
  statement.BindInt64(4, data.size());
  statement.BindTime(5, base::Time::Now());

  bool success = statement.Run();

  DVLOG_IF(1, !success) << "Error writing Cdm storage data.";

  if (success) {
    bool large_write = data.size() > (15 * 1024);
    base::UmaHistogramBoolean(
        "Media.EME.CdmStorageDatabase.WriteFileForBigData", large_write);
  }

  return success;
}

std::optional<uint64_t> CdmStorageDatabase::GetSizeForFile(
    const blink::StorageKey& storage_key,
    const media::CdmType& cdm_type,
    const std::string& file_name) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (OpenDatabase() != CdmStorageOpenError::kOk) {
    return std::nullopt;
  }

  // clang-format off
  static constexpr char kSelectSql[] =
      "SELECT file_size FROM cdm_storage "
          "WHERE storage_key = ? "
            "AND cdm_type = ? "
            "AND file_name = ? ";
  // clang-format on
  DCHECK(db_.IsSQLValid(kSelectSql));

  last_operation_ = "GetSizeForFile";

  sql::Statement statement(db_.GetCachedStatement(SQL_FROM_HERE, kSelectSql));
  statement.BindString(0, storage_key.Serialize());
  statement.BindBlob(1, cdm_type.AsBytes());
  statement.BindString(2, file_name);

  if (!statement.Step()) {
    // Failing here is expected if the "file" has not yet been written to and
    // the row does not yet exist. The Cdm Storage code doesn't distinguish
    // between an empty file and a file which does not exist, so just return
    // an empty file size without erroring.
    return 0;
  }

  return statement.ColumnInt64(0);
}

std::optional<uint64_t> CdmStorageDatabase::GetSizeForStorageKey(
    const blink::StorageKey& storage_key,
    const base::Time begin,
    const base::Time end) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (OpenDatabase() != CdmStorageOpenError::kOk) {
    return std::nullopt;
  }

  // clang-format off
  static constexpr char kSelectSql[] =
      "SELECT SUM(file_size) FROM cdm_storage "
         "WHERE storage_key = ? "
         "AND last_modified >= ? "
         "AND last_modified <= ? ";
  // clang-format on

  DCHECK(db_.IsSQLValid(kSelectSql));

  last_operation_ = "GetSizeForStorageKey";

  sql::Statement statement(db_.GetCachedStatement(SQL_FROM_HERE, kSelectSql));
  statement.BindString(0, storage_key.Serialize());
  statement.BindTime(1, begin);
  statement.BindTime(2, end);

  if (!statement.Step()) {
    // Failing here is expected if the "files" are not found in the
    // time frame for the storage key. The Cdm Storage code doesn't distinguish
    // between an empty file and a file which does not exist, so just this
    // returns the Sum() of the file sizes, which are all empty, without
    // erroring.
    return 0;
  }

  return statement.ColumnInt64(0);
}

std::optional<uint64_t> CdmStorageDatabase::GetSizeForTimeFrame(
    const base::Time begin,
    const base::Time end) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (OpenDatabase() != CdmStorageOpenError::kOk) {
    return std::nullopt;
  }

  // clang-format off
  static constexpr char kSelectSql[] =
      "SELECT SUM(file_size) FROM cdm_storage "
         "WHERE last_modified >= ? "
         "AND last_modified <= ? ";
  // clang-format on
  DCHECK(db_.IsSQLValid(kSelectSql));

  last_operation_ = "GetSizeForTimeFrame";

  sql::Statement statement(db_.GetCachedStatement(SQL_FROM_HERE, kSelectSql));
  statement.BindTime(0, begin);
  statement.BindTime(1, end);

  if (!statement.Step()) {
    // Failing here is expected if the "files" are not found in the
    // time frame. The Cdm Storage code doesn't distinguish between an
    // empty file and a file which does not exist, so just this returns the
    // Sum() of the file sizes, which are all empty, without erroring.
    return 0;
  }

  return statement.ColumnInt64(0);
}

CdmStorageKeyUsageSize CdmStorageDatabase::GetUsagePerAllStorageKeys(
    const base::Time begin,
    const base::Time end) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  CdmStorageKeyUsageSize usage_per_storage_keys;

  if (OpenDatabase() != CdmStorageOpenError::kOk) {
    return usage_per_storage_keys;
  }

  static constexpr char kSelectStorageKeySql[] =
      "SELECT DISTINCT storage_key FROM cdm_storage "
      "WHERE last_modified >= ? "
      "AND last_modified <= ? ";

  sql::Statement get_all_storage_keys_statement(
      db_.GetCachedStatement(SQL_FROM_HERE, kSelectStorageKeySql));
  get_all_storage_keys_statement.BindTime(0, begin);
  get_all_storage_keys_statement.BindTime(1, end);

  while (get_all_storage_keys_statement.Step()) {
    std::optional<blink::StorageKey> maybe_storage_key =
        blink::StorageKey::Deserialize(
            get_all_storage_keys_statement.ColumnStringView(0));
    if (maybe_storage_key) {
      auto storage_key = maybe_storage_key.value();
      usage_per_storage_keys.emplace_back(
          storage_key, GetSizeForStorageKey(storage_key).value_or(0));
    }
  }

  return usage_per_storage_keys;
}

bool CdmStorageDatabase::DeleteFile(const blink::StorageKey& storage_key,
                                    const media::CdmType& cdm_type,
                                    const std::string& file_name) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (OpenDatabase() != CdmStorageOpenError::kOk) {
    return false;
  }

  // clang-format off
  static constexpr char kDeleteSql[] =
      "DELETE FROM cdm_storage "
         "WHERE storage_key = ? "
         "AND cdm_type = ? "
         "AND file_name = ? ";
  // clang-format on
  DCHECK(db_.IsSQLValid(kDeleteSql));

  last_operation_ = "DeleteFile";

  bool success;
  {
    sql::Statement statement(db_.GetCachedStatement(SQL_FROM_HERE, kDeleteSql));
    statement.BindString(0, storage_key.Serialize());
    statement.BindBlob(1, cdm_type.AsBytes());
    statement.BindString(2, file_name);
    success = statement.Run();
  }
  DVLOG_IF(1, !success) << "Error deleting Cdm storage data.";

  base::UmaHistogramBoolean(
      GetCdmStorageManagerHistogramName(kDeleteFileError, in_memory()),
      !success);

  return DeleteIfEmptyDatabase(success);
}

bool CdmStorageDatabase::DeleteData(
    const StoragePartition::StorageKeyMatcherFunction& storage_key_matcher,
    const blink::StorageKey& storage_key,
    const base::Time begin,
    const base::Time end) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (!storage_key_matcher.is_null()) {
    return DeleteDataForFilter(storage_key_matcher, begin, end);
  } else if (!storage_key.origin().opaque()) {
    return DeleteDataForStorageKey(storage_key, begin, end);
  }
  return DeleteDataForTimeFrame(begin, end);
}

bool CdmStorageDatabase::DeleteDataForFilter(
    StoragePartition::StorageKeyMatcherFunction storage_key_matcher,
    const base::Time begin,
    const base::Time end) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  CdmStorageKeyUsageSize usage_per_storage_keys =
      GetUsagePerAllStorageKeys(begin, end);

  for (auto [storage_key, _] : usage_per_storage_keys) {
    if (storage_key_matcher.Run(storage_key)) {
      DeleteDataForStorageKey(storage_key, begin, end);
    }
  }

  base::UmaHistogramBoolean(
      GetCdmStorageManagerHistogramName(kDeleteForFilterError, in_memory()),
      false);

  return DeleteIfEmptyDatabase(true);
}

bool CdmStorageDatabase::DeleteDataForStorageKey(
    const blink::StorageKey& storage_key,
    const base::Time begin,
    const base::Time end) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (OpenDatabase() != CdmStorageOpenError::kOk) {
    return false;
  }

  last_operation_ = "DeleteForStorageKey";

  // clang-format off
  static constexpr char kDeleteSql[] =
      "DELETE FROM cdm_storage "
         "WHERE storage_key = ? "
         "AND last_modified >= ? "
         "AND last_modified <= ? ";
  // clang-format on
  DCHECK(db_.IsSQLValid(kDeleteSql));

  bool success;
  {
    sql::Statement statement(db_.GetCachedStatement(SQL_FROM_HERE, kDeleteSql));
    statement.BindString(0, storage_key.Serialize());
    statement.BindTime(1, begin);
    statement.BindTime(2, end);
    success = statement.Run();
  }

  DVLOG_IF(1, !success) << "Error deleting Cdm storage data.";

  base::UmaHistogramBoolean(
      GetCdmStorageManagerHistogramName(kDeleteForStorageKeyError, in_memory()),
      !success);

  return DeleteIfEmptyDatabase(success);
}

bool CdmStorageDatabase::DeleteDataForTimeFrame(const base::Time begin,
                                                const base::Time end) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (OpenDatabase() != CdmStorageOpenError::kOk) {
    return false;
  }

  last_operation_ = "DeleteForTimeFrame";

  // clang-format off
  static constexpr char kDeleteSql[] =
      "DELETE FROM cdm_storage "
         "WHERE last_modified >= ? "
         "AND last_modified <= ? ";
  // clang-format on
  DCHECK(db_.IsSQLValid(kDeleteSql));

  bool success;
  {
    sql::Statement statement(db_.GetCachedStatement(SQL_FROM_HERE, kDeleteSql));
    statement.BindTime(0, begin);
    statement.BindTime(1, end);
    success = statement.Run();
  }

  DVLOG_IF(1, !success)
      << "Error deleting Cdm storage data for specified time frame.";

  base::UmaHistogramBoolean(GetCdmStorageManagerHistogramName(
                                kDeleteForTimeFrameError, path_.empty()),
                            !success);

  return DeleteIfEmptyDatabase(success);
}

bool CdmStorageDatabase::ClearDatabase() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  last_operation_ = "ClearDatabase";

  db_.Close();

  if (in_memory()) {
    // Memory associated with an in-memory database will be released when the
    // database is closed above.
    return true;
  }

  return sql::Database::Delete(path_);
}

uint64_t CdmStorageDatabase::GetDatabaseSize() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  static constexpr char kPageCountSql[] = "PRAGMA page_count";
  DCHECK(db_.IsSQLValid(kPageCountSql));

  last_operation_ = "QueryPageCount";

  sql::Statement statement_count(
      db_.GetCachedStatement(SQL_FROM_HERE, kPageCountSql));
  statement_count.Step();

  uint64_t page_count = statement_count.ColumnInt(0);

  static constexpr char kPageSizeSql[] = "PRAGMA page_size";
  DCHECK(db_.IsSQLValid(kPageSizeSql));

  last_operation_ = "QueryPageSize";

  sql::Statement statement_size(
      db_.GetCachedStatement(SQL_FROM_HERE, kPageSizeSql));
  statement_size.Step();

  uint64_t page_size = statement_size.ColumnInt(0);

  last_operation_.reset();

  return page_count * page_size;
}

void CdmStorageDatabase::CloseDatabaseForTesting() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  db_.Close();
}

bool CdmStorageDatabase::DeleteIfEmptyDatabase(bool last_operation_success) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (!last_operation_success || OpenDatabase() != CdmStorageOpenError::kOk) {
    return false;
  }

  last_operation_ = "DeleteIfEmptyDatabase";

  if (!DatabaseIsEmpty(&db_)) {
    return true;
  }

  return ClearDatabase();
}

CdmStorageOpenError CdmStorageDatabase::OpenDatabase(bool is_retry) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (db_.is_open()) {
    return CdmStorageOpenError::kOk;
  }

  bool success = false;

  last_operation_ = "OpenDatabase";

  if (in_memory()) {
    success = db_.OpenInMemory();
  } else {
    success = db_.Open(path_);
  }

  if (!success) {
    DVLOG(1) << "Failed to open CDM database: " << db_.GetErrorMessage();
    return CdmStorageOpenError::kDatabaseOpenError;
  }

  sql::MetaTable meta_table;
  if (!meta_table.Init(&db_, kVersionNumber, kVersionNumber)) {
    DVLOG(1) << "Could not initialize Cdm Storage database metadata table.";
    // Wipe the database and start over. If we've already wiped the database
    // and are still failing, just return false.
    db_.Raze();
    return is_retry ? CdmStorageOpenError::kDatabaseRazeError
                    : OpenDatabase(/*is_retry=*/true);
  }

  // TODO(crbug.com/40272342): Remove once histogram shows that there are no
  // more incompatible databases. This scenario happens when the database has
  // the v1 schema without the 'file_size' and 'last_modified' columns.
  if (meta_table.GetCompatibleVersionNumber() < kVersionNumber) {
    return (!UpgradeDatabaseSchema(&meta_table) || is_retry)
               ? CdmStorageOpenError::kAlterTableError
               : OpenDatabase(/*is_retry=*/true);
  }

  if (meta_table.GetCompatibleVersionNumber() > kVersionNumber) {
    // This should only happen if the user downgrades the version. If that
    // results in an incompatible schema, we need to wipe the database and start
    // over.
    DVLOG(1) << "Cdm Storage database is too new, kVersionNumber"
             << kVersionNumber << ", GetCompatibleVersionNumber="
             << meta_table.GetCompatibleVersionNumber();
    // TODO(crbug.com/40272342) Add UMA to report if incompatible database
    // version occurs.
    db_.Raze();
    return is_retry ? CdmStorageOpenError::kDatabaseRazeError
                    : OpenDatabase(/*is_retry=*/true);
  }

  // Set up the table.
  // clang-format off
  static constexpr char kCreateTableSql[] =
      "CREATE TABLE IF NOT EXISTS cdm_storage("
          "storage_key TEXT NOT NULL,"
          "cdm_type BLOB NOT NULL,"
          "file_name TEXT NOT NULL,"
          "data BLOB NOT NULL,"
          "file_size INTEGER NOT NULL,"
          "last_modified INTEGER NOT NULL,"
          "PRIMARY KEY(storage_key,cdm_type,file_name))";
  // clang-format on
  DCHECK(db_.IsSQLValid(kCreateTableSql));

  if (!db_.Execute(kCreateTableSql)) {
    DVLOG(1) << "Failed to execute " << kCreateTableSql;
    return CdmStorageOpenError::kSQLExecutionError;
  }

  return CdmStorageOpenError::kOk;
}

bool CdmStorageDatabase::UpgradeDatabaseSchema(sql::MetaTable* meta_table) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // Previously in UpgradeDatabaseSchema, we were setting the version number for
  // the meta table, but not the compatible version number, which should have
  // been updated as well. This caused a crash, since UpgradeDatabaseSchema
  // would be called all the time since we compare meta_table's compatible
  // version number to kVersionNumber. This fixes this change by setting it
  // correctly in the cases where this was incorrectly set.
  if (meta_table->GetCompatibleVersionNumber() == 1 &&
      meta_table->GetVersionNumber() == 2) {
    return meta_table->SetCompatibleVersionNumber(2);
  }

  // Histogram to track when incompatible version schema detected.
  base::UmaHistogramBoolean(
      "Media.EME.CdmStorageDatabase.IncompatibleDatabaseDetected", true);

  static constexpr char kAlterFileSizeSql[] =
      "ALTER TABLE cdm_storage ADD COLUMN file_size INTEGER NOT NULL DEFAULT "
      "1";
  DCHECK(db_.IsSQLValid(kAlterFileSizeSql));

  last_operation_ = "AlterDatabaseForFileSize";

  sql::Statement file_size_statement(db_.GetUniqueStatement(kAlterFileSizeSql));

  if (!file_size_statement.Run()) {
    return false;
  }

  const std::string alter_last_modified_string = base::StrCat(
      {"ALTER TABLE cdm_storage ADD COLUMN last_modified INTEGER NOT NULL "
       "DEFAULT ",
       base::NumberToString(
           sql::Statement::TimeToSqlValue(base::Time::Now()))});
  DCHECK(db_.IsSQLValid(alter_last_modified_string));

  sql::Statement last_modified_statement(
      db_.GetUniqueStatement(alter_last_modified_string));

  last_operation_ = "AlterDatabaseForLastModified";

  if (!last_modified_statement.Run()) {
    return false;
  }

  return meta_table->SetVersionNumber(kVersionNumber) &&
         meta_table->SetCompatibleVersionNumber(kVersionNumber);
}

void CdmStorageDatabase::OnDatabaseError(int error, sql::Statement* stmt) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  sql::UmaHistogramSqliteResult("Media.EME.CdmStorageDatabaseSQLiteError",
                                error);

  if (last_operation_) {
    sql::UmaHistogramSqliteResult(kUmaPrefix + *last_operation_, error);

    switch (sql::ToSqliteLoggedResultCode(error)) {
      case sql::SqliteLoggedResultCode::kCantOpen:
        base::UmaHistogramSparse(
            base::StrCat({kUmaPrefix, *last_operation_, ".CantOpen.Errno"}),
            db_.GetLastErrno());
        break;
      case sql::SqliteLoggedResultCode::kFullDisk:
        base::UmaHistogramSparse(
            base::StrCat({kUmaPrefix, *last_operation_, ".FullDisk.Errno"}),
            db_.GetLastErrno());
        break;
      case sql::SqliteLoggedResultCode::kGeneric:
        base::UmaHistogramSparse(
            base::StrCat({kUmaPrefix, *last_operation_, ".Generic.Errno"}),
            db_.GetLastErrno());
        break;
      case sql::SqliteLoggedResultCode::kIoTruncate:
        base::UmaHistogramSparse(
            base::StrCat({kUmaPrefix, *last_operation_, ".IoTruncate.Errno"}),
            db_.GetLastErrno());
        break;
      case sql::SqliteLoggedResultCode::kBusy:
        base::UmaHistogramSparse(
            base::StrCat({kUmaPrefix, *last_operation_, ".Busy.Errno"}),
            db_.GetLastErrno());
        break;
      default:
        // Currently, we don't care what happens with other SqliteErrors, so
        // just break.
        break;
    }

    last_operation_.reset();
  }
}

}  // namespace content