| // 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 "base/files/file.h" |
| #include "base/files/file_util.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "sql/meta_table.h" |
| #include "sql/statement.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| // This is the version number of the CdmStorageDatabase. We are currently on |
| // version one since there have been no changes to the schema from when it is |
| // created. Please increment `kVersionNumber` by 1 every time you change the |
| // schema. |
| static const int kVersionNumber = 1; |
| |
| } // namespace |
| |
| using CdmStorageHostOpenError = CdmStorageHost::CdmStorageHostOpenError; |
| |
| 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{.exclusive_locking = true, |
| .page_size = 32768, |
| .cache_size = 8}) {} |
| |
| CdmStorageHostOpenError CdmStorageDatabase::EnsureOpenForTesting() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // If in use or open successful, returns `CdmStorageHostOpenError::kOk`. |
| return OpenDatabase(); |
| } |
| |
| absl::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() != CdmStorageHostOpenError::kOk) { |
| return absl::nullopt; |
| } |
| |
| static constexpr char kSelectSql[] = |
| // clang-format off |
| "SELECT data FROM cdm_storage " |
| "WHERE storage_key=? " |
| "AND cdm_type=? " |
| "AND file_name=? "; |
| // clang-format on |
| DCHECK(db_.IsSQLValid(kSelectSql)); |
| |
| 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>(); |
| } |
| |
| std::vector<uint8_t> data; |
| if (!statement.ColumnBlobAsVector(0, &data)) { |
| DVLOG(1) << "Error reading Cdm storage data."; |
| return absl::nullopt; |
| } |
| |
| return data; |
| } |
| |
| 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() != CdmStorageHostOpenError::kOk) { |
| return false; |
| } |
| |
| static constexpr char kInsertSql[] = |
| // clang-format off |
| "INSERT OR REPLACE INTO cdm_storage(storage_key,cdm_type,file_name,data) " |
| "VALUES(?,?,?,?)"; |
| // clang-format on |
| DCHECK(db_.IsSQLValid(kInsertSql)); |
| |
| 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); |
| bool success = statement.Run(); |
| |
| DVLOG_IF(1, !success) << "Error writing Cdm storage data."; |
| |
| return success; |
| } |
| |
| 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() != CdmStorageHostOpenError::kOk) { |
| return false; |
| } |
| |
| static constexpr char kDeleteSql[] = |
| // clang-format off |
| "DELETE FROM cdm_storage " |
| "WHERE storage_key=? " |
| "AND cdm_type=? " |
| "AND file_name=? "; |
| // clang-format on |
| DCHECK(db_.IsSQLValid(kDeleteSql)); |
| |
| 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); |
| bool success = statement.Run(); |
| |
| DVLOG_IF(1, !success) << "Error deleting Cdm storage data."; |
| |
| return success; |
| } |
| |
| bool CdmStorageDatabase::DeleteDataForStorageKey( |
| const blink::StorageKey& storage_key, |
| const media::CdmType& cdm_type) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (OpenDatabase() != CdmStorageHostOpenError::kOk) { |
| return false; |
| } |
| |
| static constexpr char kDeleteSql[] = |
| "DELETE FROM cdm_storage WHERE storage_key=?"; |
| DCHECK(db_.IsSQLValid(kDeleteSql)); |
| |
| sql::Statement statement(db_.GetCachedStatement(SQL_FROM_HERE, kDeleteSql)); |
| statement.BindString(0, storage_key.Serialize()); |
| bool success = statement.Run(); |
| |
| DVLOG_IF(1, !success) << "Error deleting Cdm storage data."; |
| |
| return success; |
| } |
| |
| bool CdmStorageDatabase::ClearDatabase() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| db_.Close(); |
| |
| if (path_.empty()) { |
| // Memory associated with an in-memory database will be released when the |
| // database is closed above. |
| return true; |
| } |
| |
| return sql::Database::Delete(path_); |
| } |
| |
| CdmStorageHostOpenError CdmStorageDatabase::OpenDatabase(bool is_retry) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (db_.is_open()) { |
| return CdmStorageHostOpenError::kOk; |
| } |
| |
| bool success = false; |
| |
| // If this is not the first call to `OpenDatabase()` because we are re-trying |
| // initialization, then the error callback will have previously been set. |
| db_.reset_error_callback(); |
| |
| // base::Unretained is safe because `db_` is owned by `this` |
| db_.set_error_callback(base::BindRepeating( |
| &CdmStorageDatabase::OnDatabaseError, base::Unretained(this))); |
| |
| if (path_.empty()) { |
| success = db_.OpenInMemory(); |
| } else { |
| success = db_.Open(path_); |
| } |
| |
| if (!success) { |
| DVLOG(1) << "Failed to open CDM database: " << db_.GetErrorMessage(); |
| return CdmStorageHostOpenError::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 ? CdmStorageHostOpenError::kDatabaseRazeError |
| : 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/1454512) Add UMA to report if incompatible database |
| // version occurs. |
| db_.Raze(); |
| return is_retry ? CdmStorageHostOpenError::kDatabaseRazeError |
| : OpenDatabase(/*is_retry=*/true); |
| } |
| |
| // Set up the table. |
| static constexpr char kCreateTableSql[] = |
| // clang-format off |
| "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," |
| "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 CdmStorageHostOpenError::kSQLExecutionError; |
| } |
| |
| return CdmStorageHostOpenError::kOk; |
| } |
| |
| void CdmStorageDatabase::OnDatabaseError(int error, sql::Statement* stmt) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| sql::UmaHistogramSqliteResult("Media.EME.CdmStorageDatabaseSQLiteError", |
| error); |
| } |
| |
| } // namespace content |