#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 {
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));
}
}
CdmStorageDatabase::CdmStorageDatabase(const base::FilePath& path)
: path_(path),
db_(sql::DatabaseOptions().set_page_size(32768).set_cache_size(8),
"CdmStorage") {
db_.set_error_callback(base::BindRepeating(
&CdmStorageDatabase::OnDatabaseError, base::Unretained(this)));
}
CdmStorageDatabase::~CdmStorageDatabase() = default;
CdmStorageOpenError CdmStorageDatabase::EnsureOpen() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
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;
}
static constexpr char kSelectSql[] =
"SELECT data FROM cdm_storage "
"WHERE storage_key = ? "
"AND cdm_type = ? "
"AND file_name = ? ";
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()) {
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;
}
static constexpr char kInsertSql[] =
"INSERT OR REPLACE INTO "
"cdm_storage(storage_key,cdm_type,file_name,data,file_size,last_modified) "
"VALUES(?,?,?,?,?,?) ";
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;
}
static constexpr char kSelectSql[] =
"SELECT file_size FROM cdm_storage "
"WHERE storage_key = ? "
"AND cdm_type = ? "
"AND file_name = ? ";
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()) {
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;
}
static constexpr char kSelectSql[] =
"SELECT SUM(file_size) FROM cdm_storage "
"WHERE storage_key = ? "
"AND last_modified >= ? "
"AND last_modified <= ? ";
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()) {
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;
}
static constexpr char kSelectSql[] =
"SELECT SUM(file_size) FROM cdm_storage "
"WHERE last_modified >= ? "
"AND last_modified <= ? ";
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()) {
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;
}
static constexpr char kDeleteSql[] =
"DELETE FROM cdm_storage "
"WHERE storage_key = ? "
"AND cdm_type = ? "
"AND file_name = ? ";
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";
static constexpr char kDeleteSql[] =
"DELETE FROM cdm_storage "
"WHERE storage_key = ? "
"AND last_modified >= ? "
"AND last_modified <= ? ";
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";
static constexpr char kDeleteSql[] =
"DELETE FROM cdm_storage "
"WHERE last_modified >= ? "
"AND last_modified <= ? ";
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()) {
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.";
db_.Raze();
return is_retry ? CdmStorageOpenError::kDatabaseRazeError
: OpenDatabase(true);
}
if (meta_table.GetCompatibleVersionNumber() < kVersionNumber) {
return (!UpgradeDatabaseSchema(&meta_table) || is_retry)
? CdmStorageOpenError::kAlterTableError
: OpenDatabase(true);
}
if (meta_table.GetCompatibleVersionNumber() > kVersionNumber) {
DVLOG(1) << "Cdm Storage database is too new, kVersionNumber"
<< kVersionNumber << ", GetCompatibleVersionNumber="
<< meta_table.GetCompatibleVersionNumber();
db_.Raze();
return is_retry ? CdmStorageOpenError::kDatabaseRazeError
: OpenDatabase(true);
}
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))";
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_);
if (meta_table->GetCompatibleVersionNumber() == 1 &&
meta_table->GetVersionNumber() == 2) {
return meta_table->SetCompatibleVersionNumber(2);
}
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:
break;
}
last_operation_.reset();
}
}
}