0

Revert "media: Remove MediaLicense* code and usage"

This reverts commit e307cce88c.

Reason for revert: will come up with a strategy and discussion for
handling privacy concerns regarding being able to delete MediaLicense
data.

Original change's description:
> media: Remove MediaLicense* code and usage
>
> This CL deletes a lot of the MediaLicense* code and usages of it from
> the codebase.
>
> OBSOLETE_HISTOGRAMS=No longer using the MediaLicense* code.
>
> Bug: 40272342
> Change-Id: I46a2d8bf53ca684467255b6e21e3cd7e1441c4c4
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5904345
> Commit-Queue: Vikram Pasupathy <vpasupathy@chromium.org>
> Reviewed-by: Evan Liu <evliu@google.com>
> Reviewed-by: Evan Stade <estade@chromium.org>
> Reviewed-by: John Rummell <jrummell@chromium.org>
> Cr-Commit-Position: refs/heads/main@{#1372190}

Bug: 40272342
Change-Id: I1cbb4669d932ec153b1aae59a6b5164a6afde2ea
No-Presubmit: true
No-Tree-Checks: true
No-Try: true
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5955369
Reviewed-by: Evan Stade <estade@chromium.org>
Reviewed-by: Evan Liu <evliu@google.com>
Bot-Commit: Rubber Stamper <rubber-stamper@appspot.gserviceaccount.com>
Commit-Queue: Vikram Pasupathy <vpasupathy@chromium.org>
Reviewed-by: John Rummell <jrummell@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1372853}
This commit is contained in:
Vikram Pasupathy
2024-10-23 18:46:30 +00:00
committed by Chromium LUCI CQ
parent 13d7cb32ad
commit bda8b474d3
24 changed files with 2357 additions and 28 deletions

@ -26,8 +26,6 @@ const base::FilePath::CharType kServiceWorkerDirectory[] =
// The path where media license data is persisted on disk, relative to the path
// for the respective storage bucket.
// TODO(crbug.com/40272342): Remove this and the file name below after the
// deletion logic for the actual MediaLicense.db lands and completes.
const base::FilePath::CharType kMediaLicenseDirectory[] =
FILE_PATH_LITERAL("Media Licenses");

@ -3029,6 +3029,14 @@ source_set("browser") {
"media/cdm_storage_database.h",
"media/cdm_storage_manager.cc",
"media/cdm_storage_manager.h",
"media/media_license_database.cc",
"media/media_license_database.h",
"media/media_license_manager.cc",
"media/media_license_manager.h",
"media/media_license_quota_client.cc",
"media/media_license_quota_client.h",
"media/media_license_storage_host.cc",
"media/media_license_storage_host.h",
]
}

@ -12,6 +12,7 @@
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_util.h"
#include "content/browser/media/cdm_storage_manager.h"
#include "content/browser/media/media_license_storage_host.h"
#include "media/cdm/cdm_helpers.h"
#include "media/cdm/cdm_type.h"
#include "media/mojo/mojom/cdm_storage.mojom.h"
@ -58,6 +59,21 @@ bool CdmFileImpl::IsValidName(const std::string& name) {
return true;
}
CdmFileImpl::CdmFileImpl(
MediaLicenseStorageHost* host,
const media::CdmType& cdm_type,
const std::string& file_name,
mojo::PendingAssociatedReceiver<media::mojom::CdmFile> pending_receiver)
: file_name_(file_name), cdm_type_(cdm_type), host_(host) {
DVLOG(3) << __func__ << " " << file_name_;
DCHECK(IsValidName(file_name_));
DCHECK(host_);
receiver_.Bind(std::move(pending_receiver));
receiver_.set_disconnect_handler(base::BindOnce(
&CdmFileImpl::OnReceiverDisconnect, weak_factory_.GetWeakPtr()));
}
CdmFileImpl::CdmFileImpl(
CdmStorageManager* manager,
const blink::StorageKey& storage_key,
@ -91,7 +107,7 @@ CdmFileImpl::~CdmFileImpl() {
void CdmFileImpl::Read(ReadCallback callback) {
DVLOG(3) << __func__ << " file: " << file_name_;
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
DCHECK(cdm_storage_manager_);
DCHECK(host_ || cdm_storage_manager_);
// Only 1 Read() or Write() is allowed at any time.
if (read_callback_ || write_callback_) {
@ -103,9 +119,15 @@ void CdmFileImpl::Read(ReadCallback callback) {
read_callback_ = std::move(callback);
start_time_ = base::TimeTicks::Now();
cdm_storage_manager_->ReadFile(
storage_key_, cdm_type_, file_name_,
base::BindOnce(&CdmFileImpl::DidRead, weak_factory_.GetWeakPtr()));
if (host_) {
host_->ReadFile(
cdm_type_, file_name_,
base::BindOnce(&CdmFileImpl::DidRead, weak_factory_.GetWeakPtr()));
} else {
cdm_storage_manager_->ReadFile(
storage_key_, cdm_type_, file_name_,
base::BindOnce(&CdmFileImpl::DidRead, weak_factory_.GetWeakPtr()));
}
}
void CdmFileImpl::DidRead(std::optional<std::vector<uint8_t>> data) {
@ -113,7 +135,7 @@ void CdmFileImpl::DidRead(std::optional<std::vector<uint8_t>> data) {
<< ", success: " << (data.has_value() ? "yes" : "no");
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
DCHECK(read_callback_);
DCHECK(cdm_storage_manager_);
DCHECK(host_ || cdm_storage_manager_);
bool success = data.has_value();
ReportFileOperationUMA(success, kReadFile);
@ -131,7 +153,7 @@ void CdmFileImpl::Write(const std::vector<uint8_t>& data,
WriteCallback callback) {
DVLOG(3) << __func__ << " file: " << file_name_ << ", size: " << data.size();
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
DCHECK(cdm_storage_manager_);
DCHECK(host_ || cdm_storage_manager_);
// Only 1 Read() or Write() is allowed at any time.
if (read_callback_ || write_callback_) {
@ -159,22 +181,29 @@ void CdmFileImpl::Write(const std::vector<uint8_t>& data,
return;
}
cdm_storage_manager_->WriteFile(
storage_key_, cdm_type_, file_name_, data,
base::BindOnce(&CdmFileImpl::DidWrite, weak_factory_.GetWeakPtr()));
if (host_) {
host_->WriteFile(
cdm_type_, file_name_, data,
base::BindOnce(&CdmFileImpl::DidWrite, weak_factory_.GetWeakPtr()));
} else {
cdm_storage_manager_->WriteFile(
storage_key_, cdm_type_, file_name_, data,
base::BindOnce(&CdmFileImpl::DidWrite, weak_factory_.GetWeakPtr()));
}
}
void CdmFileImpl::ReportFileOperationUMA(bool success,
const std::string& operation) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
DCHECK(cdm_storage_manager_);
DCHECK(host_ || cdm_storage_manager_);
// Strings for UMA names.
static const char kUmaPrefix[] = "Media.EME.CdmFileIO";
static const char kTimeTo[] = "TimeTo";
const std::string mode_suffix =
cdm_storage_manager_->in_memory() ? "Incognito" : "Normal";
const bool in_memory =
(host_) ? host_->in_memory() : cdm_storage_manager_->in_memory();
const std::string mode_suffix = in_memory ? "Incognito" : "Normal";
// Records the result to the base histogram as well as splitting it out by
// incognito or normal mode.
@ -199,7 +228,7 @@ void CdmFileImpl::DidWrite(bool success) {
DVLOG(3) << __func__ << " file: " << file_name_;
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
DCHECK(write_callback_);
DCHECK(cdm_storage_manager_);
DCHECK(host_ || cdm_storage_manager_);
ReportFileOperationUMA(success, kWriteFile);
@ -216,20 +245,27 @@ void CdmFileImpl::DeleteFile() {
DVLOG(3) << __func__;
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
DCHECK(write_callback_);
DCHECK(cdm_storage_manager_);
DCHECK(host_ || cdm_storage_manager_);
DVLOG(3) << "Deleting " << file_name_;
cdm_storage_manager_->DeleteFile(
storage_key_, cdm_type_, file_name_,
base::BindOnce(&CdmFileImpl::DidDeleteFile, weak_factory_.GetWeakPtr()));
if (host_) {
host_->DeleteFile(cdm_type_, file_name_,
base::BindOnce(&CdmFileImpl::DidDeleteFile,
weak_factory_.GetWeakPtr()));
} else {
cdm_storage_manager_->DeleteFile(
storage_key_, cdm_type_, file_name_,
base::BindOnce(&CdmFileImpl::DidDeleteFile,
weak_factory_.GetWeakPtr()));
}
}
void CdmFileImpl::DidDeleteFile(bool success) {
DVLOG(3) << __func__ << " file: " << file_name_;
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
DCHECK(write_callback_);
DCHECK(cdm_storage_manager_);
DCHECK(host_ || cdm_storage_manager_);
ReportFileOperationUMA(success, kDeleteFile);
@ -244,11 +280,16 @@ void CdmFileImpl::DidDeleteFile(bool success) {
void CdmFileImpl::OnReceiverDisconnect() {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
DCHECK(cdm_storage_manager_);
DCHECK(host_ || cdm_storage_manager_);
// May delete `this`.
cdm_storage_manager_->OnFileReceiverDisconnect(
file_name_, cdm_type_, storage_key_, base::PassKey<CdmFileImpl>());
if (host_) {
host_->OnFileReceiverDisconnect(file_name_, cdm_type_,
base::PassKey<CdmFileImpl>());
} else {
cdm_storage_manager_->OnFileReceiverDisconnect(
file_name_, cdm_type_, storage_key_, base::PassKey<CdmFileImpl>());
}
}
} // namespace content

@ -20,6 +20,7 @@
#include "third_party/blink/public/common/storage_key/storage_key.h"
namespace content {
class MediaLicenseStorageHost;
class CdmStorageManager;
// This class implements the media::mojom::CdmFile interface.
@ -31,8 +32,14 @@ class CdmFileImpl final : public media::mojom::CdmFile {
// This "file" is actually just an entry in a custom backend for CDM, uniquely
// identified by a storage key, CDM type, and file name. File operations are
// routed through `manager` which is owned by the storage partition.
// This constructor is used by the CdmStorageManager.
// routed through `host` which is owned by the storage partition.
CdmFileImpl(
MediaLicenseStorageHost* host,
const media::CdmType& cdm_type,
const std::string& file_name,
mojo::PendingAssociatedReceiver<media::mojom::CdmFile> pending_receiver);
// As Above. This constructor is used by CdmStorageManager.
CdmFileImpl(
CdmStorageManager* manager,
const blink::StorageKey& storage_key,
@ -62,7 +69,8 @@ class CdmFileImpl final : public media::mojom::CdmFile {
void OnReceiverDisconnect();
// This receiver is associated with the CdmStorageManager which creates it.
// This receiver is associated with the MediaLicenseStorageHost which creates
// it.
mojo::AssociatedReceiver<media::mojom::CdmFile> receiver_{this};
const std::string file_name_;
@ -78,6 +86,10 @@ class CdmFileImpl final : public media::mojom::CdmFile {
base::TimeTicks start_time_;
// Backing store which CDM file operations are routed through.
// Owned by MediaLicenseManager.
const raw_ptr<MediaLicenseStorageHost> host_ = nullptr;
// New backing store which CDM file operations are routed through.
// CdmStorageManager owns the lifetime of this object and will outlive it.
const raw_ptr<CdmStorageManager> cdm_storage_manager_ = nullptr;

@ -51,6 +51,7 @@
#include "base/feature_list.h"
#include "base/metrics/histogram_macros.h"
#include "content/browser/media/cdm_storage_manager.h"
#include "content/browser/media/media_license_manager.h"
#include "media/base/key_system_names.h"
#include "media/mojo/mojom/cdm_service.mojom.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"

@ -0,0 +1,297 @@
// Copyright 2022 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/media_license_database.h"
#include "base/files/file.h"
#include "base/files/file_util.h"
#include "base/metrics/histogram_functions.h"
#include "sql/database.h"
#include "sql/meta_table.h"
#include "sql/sqlite_result_code_values.h"
#include "sql/statement.h"
namespace content {
using MediaLicenseStorageHostOpenError =
MediaLicenseStorageHost::MediaLicenseStorageHostOpenError;
namespace {
static const int kVersionNumber = 1;
const char kUmaPrefix[] = "Media.EME.MediaLicenseDatabaseSQLiteError";
const char kUmaPrefixWithPeriod[] =
"Media.EME.MediaLicenseDatabaseSQLiteError.";
} // namespace
MediaLicenseDatabase::MediaLicenseDatabase(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{.page_size = 32768, .cache_size = 8}) {}
MediaLicenseDatabase::~MediaLicenseDatabase() = default;
MediaLicenseStorageHostOpenError MediaLicenseDatabase::OpenFile(
const media::CdmType& cdm_type,
const std::string& file_name) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// The media license code doesn't distinguish between an empty file and a
// file which does not exist, so don't bother inserting an empty row into
// the database.
return OpenDatabase();
}
std::optional<std::vector<uint8_t>> MediaLicenseDatabase::ReadFile(
const media::CdmType& cdm_type,
const std::string& file_name) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (OpenDatabase() != MediaLicenseStorageHostOpenError::kOk) {
return std::nullopt;
}
static constexpr char kSelectSql[] =
"SELECT data FROM licenses WHERE cdm_type=? AND file_name=?";
DCHECK(db_.IsSQLValid(kSelectSql));
last_operation_ = "ReadFile";
sql::Statement statement(db_.GetCachedStatement(SQL_FROM_HERE, kSelectSql));
statement.BindString(0, cdm_type.ToString());
statement.BindString(1, 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 media license 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 media license data.";
return std::nullopt;
}
last_operation_.reset();
return data;
}
bool MediaLicenseDatabase::WriteFile(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() != MediaLicenseStorageHostOpenError::kOk) {
return false;
}
static constexpr char kInsertSql[] =
// clang-format off
"INSERT OR REPLACE INTO licenses(cdm_type,file_name,data) "
"VALUES(?,?,?)";
// clang-format on
DCHECK(db_.IsSQLValid(kInsertSql));
last_operation_ = "WriteFile";
last_write_file_size_ = data.size();
sql::Statement statement(db_.GetCachedStatement(SQL_FROM_HERE, kInsertSql));
statement.BindString(0, cdm_type.ToString());
statement.BindString(1, file_name);
statement.BindBlob(2, data);
bool success = statement.Run();
if (!success)
DVLOG(1) << "Error writing media license data.";
last_operation_.reset();
return success;
}
bool MediaLicenseDatabase::DeleteFile(const media::CdmType& cdm_type,
const std::string& file_name) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (OpenDatabase() != MediaLicenseStorageHostOpenError::kOk) {
return false;
}
static constexpr char kDeleteSql[] =
"DELETE FROM licenses WHERE cdm_type=? AND file_name=?";
DCHECK(db_.IsSQLValid(kDeleteSql));
last_operation_ = "DeleteFile";
sql::Statement statement(db_.GetCachedStatement(SQL_FROM_HERE, kDeleteSql));
statement.BindString(0, cdm_type.ToString());
statement.BindString(1, file_name);
bool success = statement.Run();
if (!success)
DVLOG(1) << "Error writing media license data.";
last_operation_.reset();
return success;
}
bool MediaLicenseDatabase::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_);
}
uint64_t MediaLicenseDatabase::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;
}
// Opens and sets up a database if one is not already set up.
MediaLicenseStorageHostOpenError MediaLicenseDatabase::OpenDatabase(
bool is_retry) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (db_.is_open())
return MediaLicenseStorageHostOpenError::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 becase |db_| is owned by |this|
db_.set_error_callback(base::BindRepeating(
&MediaLicenseDatabase::OnDatabaseError, base::Unretained(this)));
if (path_.empty()) {
success = db_.OpenInMemory();
} else {
// Ensure `path`'s parent directory exists.
auto error = base::File::Error::FILE_OK;
if (!base::CreateDirectoryAndGetError(path_.DirName(), &error)) {
DVLOG(1) << "Failed to open CDM database: "
<< base::File::ErrorToString(error);
base::UmaHistogramExactLinear(
"Media.EME.MediaLicenseDatabaseCreateDirectoryError", -error,
-base::File::FILE_ERROR_MAX);
return MediaLicenseStorageHostOpenError::kBucketNotFound;
}
DCHECK_EQ(error, base::File::Error::FILE_OK);
success = db_.Open(path_);
}
if (!success) {
DVLOG(1) << "Failed to open CDM database: " << db_.GetErrorMessage();
return MediaLicenseStorageHostOpenError::kDatabaseOpenError;
}
sql::MetaTable meta_table;
if (!meta_table.Init(&db_, kVersionNumber, kVersionNumber)) {
DVLOG(1) << "Could not initialize Media License 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 ? MediaLicenseStorageHostOpenError::kDatabaseRazeError
: OpenDatabase(/*is_retry=*/true);
}
if (meta_table.GetCompatibleVersionNumber() > kVersionNumber) {
// This should only happen if the user downgrades the Chrome channel (for
// example, from Beta to Stable). If that results in an incompatible schema,
// we need to wipe the database and start over.
DVLOG(1) << "Media License database is too new, kVersionNumber"
<< kVersionNumber << ", GetCompatibleVersionNumber="
<< meta_table.GetCompatibleVersionNumber();
db_.Raze();
return is_retry ? MediaLicenseStorageHostOpenError::kDatabaseRazeError
: OpenDatabase(/*is_retry=*/true);
}
// Set up the table.
static constexpr char kCreateTableSql[] =
// clang-format off
"CREATE TABLE IF NOT EXISTS licenses("
"cdm_type TEXT NOT NULL,"
"file_name TEXT NOT NULL,"
"data BLOB NOT NULL,"
"PRIMARY KEY(cdm_type,file_name))";
// clang-format on
DCHECK(db_.IsSQLValid(kCreateTableSql));
if (!db_.Execute(kCreateTableSql)) {
DVLOG(1) << "Failed to execute " << kCreateTableSql;
return MediaLicenseStorageHostOpenError::kSQLExecutionError;
}
return MediaLicenseStorageHostOpenError::kOk;
}
void MediaLicenseDatabase::OnDatabaseError(int error, sql::Statement* stmt) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
sql::UmaHistogramSqliteResult(kUmaPrefix, error);
if (last_operation_) {
sql::UmaHistogramSqliteResult(kUmaPrefixWithPeriod + *last_operation_,
error);
// Log the size of the data in bytes if the error was a full disk error to
// track size of data being rejected by the MediaLicenseDatabase.
if (last_operation_ == "WriteFile" &&
sql::ToSqliteResultCode(error) == sql::SqliteResultCode::kFullDisk &&
last_write_file_size_) {
base::UmaHistogramCustomCounts(
"Media.EME.MediaLicenseDatabase.WriteFile.FullDiskDataSizeBytes",
last_write_file_size_.value(), /*min=*/1,
/*exclusive_max=*/60 * 1024, /*buckets=*/100);
}
last_operation_.reset();
}
}
} // namespace content

@ -0,0 +1,63 @@
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CONTENT_BROWSER_MEDIA_MEDIA_LICENSE_DATABASE_H_
#define CONTENT_BROWSER_MEDIA_MEDIA_LICENSE_DATABASE_H_
#include "base/sequence_checker.h"
#include "content/browser/media/media_license_storage_host.h"
#include "media/cdm/cdm_type.h"
#include "sql/database.h"
namespace content {
// Helper class which encapsulates all database logic for storing media license
// data.
//
// This class must be constructed and used on a sequence which allows blocking.
class MediaLicenseDatabase {
public:
// The database will be in-memory if `path` is empty.
explicit MediaLicenseDatabase(const base::FilePath& path);
~MediaLicenseDatabase();
MediaLicenseStorageHost::MediaLicenseStorageHostOpenError OpenFile(
const media::CdmType& cdm_type,
const std::string& file_name);
std::optional<std::vector<uint8_t>> ReadFile(const media::CdmType& cdm_type,
const std::string& file_name);
bool WriteFile(const media::CdmType& cdm_type,
const std::string& file_name,
const std::vector<uint8_t>& data);
bool DeleteFile(const media::CdmType& cdm_type, const std::string& file_name);
bool ClearDatabase();
uint64_t GetDatabaseSize();
private:
// Opens and sets up a database if one is not already set up.
MediaLicenseStorageHost::MediaLicenseStorageHostOpenError OpenDatabase(
bool is_retry = false);
void OnDatabaseError(int error, sql::Statement* stmt);
SEQUENCE_CHECKER(sequence_checker_);
// Empty if the database is in-memory.
const base::FilePath path_;
// A descriptor of the last SQL statement that was executed, used for metrics.
std::optional<std::string> last_operation_;
// Integer of last file size that the CDM sent to be written, used for
// metrics.
std::optional<int> last_write_file_size_;
sql::Database db_ GUARDED_BY_CONTEXT(sequence_checker_);
};
} // namespace content
#endif // CONTENT_BROWSER_MEDIA_MEDIA_LICENSE_DATABASE_H_

@ -0,0 +1,231 @@
// Copyright 2022 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/media_license_manager.h"
#include <memory>
#include <utility>
#include <vector>
#include "base/containers/flat_map.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/not_fatal_until.h"
#include "base/sequence_checker.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "components/services/storage/public/cpp/buckets/bucket_locator.h"
#include "components/services/storage/public/cpp/buckets/constants.h"
#include "components/services/storage/public/cpp/constants.h"
#include "content/browser/media/media_license_database.h"
#include "content/browser/media/media_license_storage_host.h"
#include "media/cdm/cdm_type.h"
#include "sql/database.h"
#include "sql/sqlite_result_code.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "third_party/blink/public/mojom/quota/quota_types.mojom-shared.h"
#include "url/origin.h"
namespace content {
using MediaLicenseStorageHostOpenError =
MediaLicenseStorageHost::MediaLicenseStorageHostOpenError;
namespace {
// Creates a task runner suitable for running SQLite database operations.
scoped_refptr<base::SequencedTaskRunner> CreateDatabaseTaskRunner() {
// We use a SequencedTaskRunner so that there is a global ordering to a
// storage key's directory operations.
return base::ThreadPool::CreateSequencedTaskRunner({
// Needed for file I/O.
base::MayBlock(),
// Reasonable compromise, given that a few database operations are
// blocking, while most operations are not. We should be able to do better
// when we get scheduling APIs on the Web Platform.
base::TaskPriority::USER_VISIBLE,
// Needed to allow for clearing site data on shutdown.
base::TaskShutdownBehavior::BLOCK_SHUTDOWN,
});
}
} // namespace
MediaLicenseManager::MediaLicenseManager(
bool in_memory,
scoped_refptr<storage::SpecialStoragePolicy> special_storage_policy,
scoped_refptr<storage::QuotaManagerProxy> quota_manager_proxy)
: db_runner_(CreateDatabaseTaskRunner()),
in_memory_(in_memory),
special_storage_policy_(std::move(special_storage_policy)),
quota_manager_proxy_(std::move(quota_manager_proxy)),
// Using a raw pointer is safe since `quota_client_` is owned by
// this instance.
quota_client_(this),
quota_client_receiver_(&quota_client_) {
if (quota_manager_proxy_) {
// Quota client assumes all backends have registered.
quota_manager_proxy_->RegisterClient(
quota_client_receiver_.BindNewPipeAndPassRemote(),
storage::QuotaClientType::kMediaLicense,
{blink::mojom::StorageType::kTemporary});
}
}
MediaLicenseManager::~MediaLicenseManager() = default;
void MediaLicenseManager::OpenCdmStorage(
const CdmStorageBindingContext& binding_context,
mojo::PendingReceiver<media::mojom::CdmStorage> receiver) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
const auto& storage_key = binding_context.storage_key;
auto it_hosts = hosts_.find(storage_key);
if (it_hosts != hosts_.end()) {
// A storage host for this storage key already exists.
it_hosts->second->BindReceiver(binding_context, std::move(receiver));
return;
}
auto& receiver_list = pending_receivers_[storage_key];
receiver_list.emplace_back(binding_context, std::move(receiver));
if (receiver_list.size() > 1) {
// If a pending receiver for this storage key already existed, there is
// an in-flight `UpdateOrCreateBucket()` call for this storage key.
return;
}
// Get the default bucket for `storage_key`.
quota_manager_proxy()->UpdateOrCreateBucket(
storage::BucketInitParams::ForDefaultBucket(storage_key),
base::SequencedTaskRunner::GetCurrentDefault(),
base::BindOnce(&MediaLicenseManager::DidGetBucket,
weak_factory_.GetWeakPtr(), storage_key));
}
void MediaLicenseManager::DidGetBucket(
const blink::StorageKey& storage_key,
storage::QuotaErrorOr<storage::BucketInfo> result) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto it = pending_receivers_.find(storage_key);
CHECK(it != pending_receivers_.end(), base::NotFatalUntil::M130);
auto receivers_list = std::move(it->second);
pending_receivers_.erase(it);
DCHECK_GT(receivers_list.size(), 0u);
storage::BucketLocator bucket_locator;
if (result.has_value()) {
bucket_locator = result->ToBucketLocator();
} else {
// Use the null locator, but update the `storage_key` field so
// `storage_host` can be identified when it is to be removed from `hosts_`.
// We could consider falling back to using an in-memory database in this
// case, but failing here seems easier to reason about from a website
// author's point of view.
sql::UmaHistogramSqliteResult(
"Media.EME.MediaLicenseDatabaseOpenSQLiteError",
result.error().sqlite_error);
base::UmaHistogramEnumeration(
"Media.EME.MediaLicenseDatabaseOpenQuotaError",
result.error().quota_error);
MediaLicenseStorageHost::ReportDatabaseOpenError(
MediaLicenseStorageHostOpenError::kBucketLocatorError, in_memory());
DCHECK(bucket_locator.id.is_null());
bucket_locator.storage_key = storage_key;
}
// All receivers associated with `storage_key` will be bound to the same host.
auto storage_host =
std::make_unique<MediaLicenseStorageHost>(this, bucket_locator);
for (auto& context_and_receiver : receivers_list) {
storage_host->BindReceiver(context_and_receiver.first,
std::move(context_and_receiver.second));
}
hosts_.emplace(storage_key, std::move(storage_host));
}
void MediaLicenseManager::DeleteBucketData(
const storage::BucketLocator& bucket,
storage::mojom::QuotaClient::DeleteBucketDataCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto it_hosts = hosts_.find(bucket.storage_key);
if (it_hosts != hosts_.end()) {
// Let the host gracefully handle data deletion.
it_hosts->second->DeleteBucketData(
base::BindOnce(&MediaLicenseManager::DidDeleteBucketData,
weak_factory_.GetWeakPtr(), std::move(callback)));
return;
}
// If we have an in-memory profile, any data for the storage key would have
// lived in the associated MediaLicenseStorageHost.
if (in_memory()) {
std::move(callback).Run(blink::mojom::QuotaStatusCode::kOk);
return;
}
// Otherwise delete database file.
auto path = GetDatabasePath(bucket);
db_runner()->PostTaskAndReplyWithResult(
FROM_HERE, base::BindOnce(&sql::Database::Delete, path),
base::BindOnce(&MediaLicenseManager::DidDeleteBucketData,
weak_factory_.GetWeakPtr(), std::move(callback)));
}
void MediaLicenseManager::DidDeleteBucketData(
storage::mojom::QuotaClient::DeleteBucketDataCallback callback,
bool success) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
std::move(callback).Run(success ? blink::mojom::QuotaStatusCode::kOk
: blink::mojom::QuotaStatusCode::kUnknown);
}
base::FilePath MediaLicenseManager::GetDatabasePath(
const storage::BucketLocator& bucket_locator) {
if (in_memory())
return base::FilePath();
auto media_license_dir = quota_manager_proxy()->GetClientBucketPath(
bucket_locator, storage::QuotaClientType::kMediaLicense);
return media_license_dir.Append(storage::kMediaLicenseDatabaseFileName);
}
void MediaLicenseManager::OnHostReceiverDisconnect(
MediaLicenseStorageHost* host,
base::PassKey<MediaLicenseStorageHost> pass_key) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(host);
if (in_memory()) {
// Don't delete `host` for an in-memory profile, since the data is not safe
// to delete yet. For example, a site may be re-visited within the same
// incognito session. `host` will be destroyed when `this` is destroyed.
return;
}
DCHECK_GT(hosts_.count(host->storage_key()), 0ul);
DCHECK_EQ(hosts_[host->storage_key()].get(), host);
if (!host->has_empty_receiver_set())
return;
size_t count_removed = hosts_.erase(host->storage_key());
DCHECK_EQ(count_removed, 1u);
}
} // namespace content

@ -0,0 +1,137 @@
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CONTENT_BROWSER_MEDIA_MEDIA_LICENSE_MANAGER_H_
#define CONTENT_BROWSER_MEDIA_MEDIA_LICENSE_MANAGER_H_
#include <memory>
#include <vector>
#include "base/containers/flat_map.h"
#include "base/files/file_path.h"
#include "base/functional/callback_forward.h"
#include "base/memory/scoped_refptr.h"
#include "base/task/sequenced_task_runner.h"
#include "base/types/pass_key.h"
#include "components/services/storage/public/cpp/buckets/bucket_locator.h"
#include "content/browser/media/cdm_storage_common.h"
#include "content/browser/media/cdm_storage_manager.h"
#include "content/browser/media/media_license_quota_client.h"
#include "content/common/content_export.h"
#include "media/cdm/cdm_type.h"
#include "media/mojo/mojom/cdm_storage.mojom.h"
#include "storage/browser/quota/quota_manager_proxy.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
namespace content {
class MediaLicenseStorageHost;
// Each StoragePartition owns exactly one instance of this class. This class
// creates and destroys MediaLicenseStorageHost instances to meet the
// demands for CDM from different storage keys.
//
// This class is not thread-safe, and all access to an instance must happen on
// the same sequence.
class CONTENT_EXPORT MediaLicenseManager {
public:
MediaLicenseManager(
bool in_memory,
scoped_refptr<storage::SpecialStoragePolicy> special_storage_policy,
scoped_refptr<storage::QuotaManagerProxy> quota_manager_proxy);
MediaLicenseManager(const MediaLicenseManager&) = delete;
MediaLicenseManager& operator=(const MediaLicenseManager&) = delete;
~MediaLicenseManager();
void OpenCdmStorage(const CdmStorageBindingContext& binding_context,
mojo::PendingReceiver<media::mojom::CdmStorage> receiver);
// Called by the MediaLicenseQuotaClient.
void DeleteBucketData(
const storage::BucketLocator& bucket,
storage::mojom::QuotaClient::DeleteBucketDataCallback callback);
// Returns an empty path if the database is in-memory.
base::FilePath GetDatabasePath(const storage::BucketLocator& bucket_locator);
// Called when a receiver is disconnected from a MediaLicenseStorageHost.
//
// `host` must be owned by this manager. `host` may be deleted.
void OnHostReceiverDisconnect(
MediaLicenseStorageHost* host,
base::PassKey<MediaLicenseStorageHost> pass_key);
const scoped_refptr<storage::QuotaManagerProxy>& quota_manager_proxy() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return quota_manager_proxy_;
}
const scoped_refptr<base::SequencedTaskRunner>& db_runner() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return db_runner_;
}
bool in_memory() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return in_memory_;
}
void set_cdm_storage_manager(CdmStorageManager* cdm_storage_manager) {
cdm_storage_manager_ = cdm_storage_manager;
}
CdmStorageManager* cdm_storage_manager() { return cdm_storage_manager_; }
private:
void DidGetBucket(const blink::StorageKey& storage_key,
storage::QuotaErrorOr<storage::BucketInfo> result);
void DidDeleteBucketData(
storage::mojom::QuotaClient::DeleteBucketDataCallback callback,
bool success);
SEQUENCE_CHECKER(sequence_checker_);
// Task runner which all database operations are routed through.
const scoped_refptr<base::SequencedTaskRunner> db_runner_;
const bool in_memory_;
// Tracks special rights for apps and extensions, may be null.
const scoped_refptr<storage::SpecialStoragePolicy> special_storage_policy_;
const scoped_refptr<storage::QuotaManagerProxy> quota_manager_proxy_;
base::flat_map<blink::StorageKey, std::unique_ptr<MediaLicenseStorageHost>>
hosts_ GUARDED_BY_CONTEXT(sequence_checker_);
// Maps storage keys to a list of receivers which are awaiting bucket
// information from the quota system before they can be bound.
base::flat_map<
blink::StorageKey,
std::vector<std::pair<CdmStorageBindingContext,
mojo::PendingReceiver<media::mojom::CdmStorage>>>>
pending_receivers_;
MediaLicenseQuotaClient quota_client_ GUARDED_BY_CONTEXT(sequence_checker_);
// Once the QuotaClient receiver is destroyed, the underlying mojo connection
// is closed. Callbacks associated with mojo calls received over this
// connection may only be dropped after the connection is closed. For this
// reason, it's preferable to have the receiver be destroyed as early as
// possible during the MediaLicenseManager destruction process.
mojo::Receiver<storage::mojom::QuotaClient> quota_client_receiver_
GUARDED_BY_CONTEXT(sequence_checker_);
// Owned by 'StoragePartitionImpl' so we don't have to handle deletion.
// This pointer is safe because `StoragePartitionImpl` declares the
// `cdm_storage_manager` before the `media_license_manager`, and members are
// destructed in reverse order of declaration.
raw_ptr<CdmStorageManager> cdm_storage_manager_ = nullptr;
base::WeakPtrFactory<MediaLicenseManager> weak_factory_{this};
};
} // namespace content
#endif // CONTENT_BROWSER_MEDIA_MEDIA_LICENSE_MANAGER_H_

@ -0,0 +1,421 @@
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/342213636): Remove this and spanify to fix the errors.
#pragma allow_unsafe_buffers
#endif
#include "content/browser/media/media_license_manager.h"
#include <string_view>
#include <vector>
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/memory/scoped_refptr.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/gmock_expected_support.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "components/services/storage/public/cpp/buckets/bucket_locator.h"
#include "components/services/storage/public/cpp/constants.h"
#include "content/browser/media/cdm_storage_common.h"
#include "content/browser/media/media_license_quota_client.h"
#include "content/public/browser/storage_partition.h"
#include "media/cdm/cdm_type.h"
#include "media/mojo/mojom/cdm_storage.mojom-forward.h"
#include "mojo/public/cpp/bindings/associated_remote.h"
#include "storage/browser/file_system/file_system_context.h"
#include "storage/browser/test/mock_quota_manager.h"
#include "storage/browser/test/mock_quota_manager_proxy.h"
#include "storage/browser/test/test_file_system_context.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
namespace content {
namespace {
const media::CdmType kCdmType{1234, 5678};
const char kExampleOrigin[] = "https://example.com";
} // namespace
class MediaLicenseManagerTest : public testing::Test {
public:
MediaLicenseManagerTest() : in_memory_(false) {}
explicit MediaLicenseManagerTest(bool in_memory) : in_memory_(in_memory) {}
void SetUp() override {
ASSERT_TRUE(profile_path_.CreateUniqueTempDir());
quota_manager_ = base::MakeRefCounted<storage::MockQuotaManager>(
in_memory_, in_memory_ ? base::FilePath() : profile_path_.GetPath(),
base::SingleThreadTaskRunner::GetCurrentDefault().get(),
/*special storage policy=*/nullptr);
quota_manager_proxy_ = base::MakeRefCounted<storage::MockQuotaManagerProxy>(
static_cast<storage::MockQuotaManager*>(quota_manager_.get()),
base::SingleThreadTaskRunner::GetCurrentDefault());
manager_ = std::make_unique<MediaLicenseManager>(
in_memory_,
/*special storage policy=*/nullptr, quota_manager_proxy_);
}
void TearDown() override {
// Let the client go away before dropping a ref of the quota manager proxy.
quota_manager_ = nullptr;
quota_manager_proxy_ = nullptr;
task_environment_.RunUntilIdle();
EXPECT_TRUE(profile_path_.Delete());
}
// Hard-coded to the default bucket, since this API should never be used in
// non-default buckets anyways.
storage::QuotaErrorOr<storage::BucketLocator> GetOrCreateBucket(
const blink::StorageKey& storage_key) {
base::test::TestFuture<storage::QuotaErrorOr<storage::BucketInfo>> future;
quota_manager_->UpdateOrCreateBucket(
storage::BucketInitParams::ForDefaultBucket(storage_key),
future.GetCallback());
return future.Take().transform(&storage::BucketInfo::ToBucketLocator);
}
mojo::AssociatedRemote<media::mojom::CdmFile> OpenCdmFile(
const mojo::Remote<media::mojom::CdmStorage>& storage,
const std::string& file_name) {
mojo::AssociatedRemote<media::mojom::CdmFile> cdm_file;
base::test::TestFuture<media::mojom::CdmStorage::Status,
mojo::PendingAssociatedRemote<media::mojom::CdmFile>>
open_future;
storage->Open(file_name, open_future.GetCallback());
auto result = open_future.Take();
EXPECT_EQ(std::get<0>(result), media::mojom::CdmStorage::Status::kSuccess);
cdm_file.Bind(std::move(std::get<1>(result)));
return cdm_file;
}
void Write(const mojo::AssociatedRemote<media::mojom::CdmFile>& cdm_file,
const std::string& data) {
base::test::TestFuture<media::mojom::CdmFile::Status> write_future;
cdm_file->Write(
std::vector<uint8_t>(data.data(), data.data() + data.size()),
write_future.GetCallback());
EXPECT_EQ(write_future.Get(), media::mojom::CdmFile::Status::kSuccess);
}
// Reads the previously opened `cdm_file` and check that its contents match
// `expected_data`.
void ExpectFileContents(
const mojo::AssociatedRemote<media::mojom::CdmFile>& cdm_file,
std::string_view expected_data) {
base::test::TestFuture<media::mojom::CdmFile::Status, std::vector<uint8_t>>
future;
cdm_file->Read(future.GetCallback<media::mojom::CdmFile::Status,
const std::vector<uint8_t>&>());
media::mojom::CdmFile::Status status = future.Get<0>();
auto data = future.Get<1>();
EXPECT_EQ(status, media::mojom::CdmFile::Status::kSuccess);
EXPECT_EQ(std::string_view(reinterpret_cast<const char*>(data.data()),
data.size()),
expected_data);
}
base::FilePath FindMediaLicenseDatabase() {
base::FileEnumerator file_enumerator(profile_path_.GetPath(),
/*recursive=*/true,
base::FileEnumerator::FILES);
base::FilePath file;
while (!(file = file_enumerator.Next()).empty()) {
if (file.BaseName().value() == storage::kMediaLicenseDatabaseFileName) {
return file;
}
}
return base::FilePath();
}
protected:
const bool in_memory_;
scoped_refptr<storage::MockQuotaManager> quota_manager_;
scoped_refptr<storage::QuotaManagerProxy> quota_manager_proxy_;
// This must be above MediaLicenseManager, to ensure that no file is accessed
// when the temporary directory is deleted.
base::ScopedTempDir profile_path_;
base::test::TaskEnvironment task_environment_;
std::unique_ptr<MediaLicenseManager> manager_;
};
TEST_F(MediaLicenseManagerTest, DeleteBucketData) {
const std::string kTestData("Test Data");
mojo::Remote<media::mojom::CdmStorage> remote;
blink::StorageKey storage_key =
blink::StorageKey::CreateFromStringForTesting(kExampleOrigin);
ASSERT_OK_AND_ASSIGN(storage::BucketLocator bucket,
GetOrCreateBucket(storage_key));
CdmStorageBindingContext binding_context(storage_key, kCdmType);
// Open CDM storage for a storage key.
manager_->OpenCdmStorage(binding_context,
remote.BindNewPipeAndPassReceiver());
auto cdm_file = OpenCdmFile(remote, "test_file");
// Write some data.
Write(cdm_file, kTestData);
auto database_file = FindMediaLicenseDatabase();
EXPECT_FALSE(database_file.empty());
// Delete data for this storage key.
base::test::TestFuture<blink::mojom::QuotaStatusCode> delete_future;
manager_->DeleteBucketData(bucket, delete_future.GetCallback());
EXPECT_EQ(delete_future.Get(), blink::mojom::QuotaStatusCode::kOk);
// Confirm that the database was deleted, but the Media License directory was
// not.
EXPECT_FALSE(base::PathExists(database_file));
EXPECT_TRUE(base::DirectoryExists(database_file.DirName()));
// Confirm that the file is now empty.
ExpectFileContents(cdm_file, "");
}
TEST_F(MediaLicenseManagerTest, DeleteBucketDataClosedStorage) {
const std::string kTestData("Test Data");
mojo::Remote<media::mojom::CdmStorage> remote;
blink::StorageKey storage_key =
blink::StorageKey::CreateFromStringForTesting(kExampleOrigin);
ASSERT_OK_AND_ASSIGN(storage::BucketLocator bucket,
GetOrCreateBucket(storage_key));
CdmStorageBindingContext binding_context(storage_key, kCdmType);
// Open CDM storage for a storage key.
manager_->OpenCdmStorage(binding_context,
remote.BindNewPipeAndPassReceiver());
auto cdm_file = OpenCdmFile(remote, "test_file");
Write(cdm_file, kTestData);
auto database_file = FindMediaLicenseDatabase();
EXPECT_FALSE(database_file.empty());
// We should still be able to wipe data to a closed storage.
cdm_file.reset();
remote.reset();
EXPECT_TRUE(base::PathExists(database_file));
// Delete data for this storage key.
base::test::TestFuture<blink::mojom::QuotaStatusCode> delete_future;
manager_->DeleteBucketData(bucket, delete_future.GetCallback());
EXPECT_EQ(delete_future.Get(), blink::mojom::QuotaStatusCode::kOk);
// Confirm that the database was deleted, but the Media License
// directory was not.
EXPECT_FALSE(base::PathExists(database_file));
EXPECT_TRUE(base::DirectoryExists(database_file.DirName()));
}
TEST_F(MediaLicenseManagerTest, DeleteBucketDataOpenConnection) {
const std::string kTestData("Test Data");
mojo::Remote<media::mojom::CdmStorage> remote;
blink::StorageKey storage_key =
blink::StorageKey::CreateFromStringForTesting(kExampleOrigin);
ASSERT_OK_AND_ASSIGN(storage::BucketLocator bucket,
GetOrCreateBucket(storage_key));
CdmStorageBindingContext binding_context(storage_key, kCdmType);
// Open CDM storage for a storage key.
manager_->OpenCdmStorage(binding_context,
remote.BindNewPipeAndPassReceiver());
auto cdm_file = OpenCdmFile(remote, "test_file");
Write(cdm_file, kTestData);
auto database_file = FindMediaLicenseDatabase();
EXPECT_FALSE(database_file.empty());
// Delete data for this storage key.
base::test::TestFuture<blink::mojom::QuotaStatusCode> delete_future;
manager_->DeleteBucketData(bucket, delete_future.GetCallback());
EXPECT_EQ(delete_future.Get(), blink::mojom::QuotaStatusCode::kOk);
// Confirm that the database was deleted, but the Media License directory was
// not.
EXPECT_FALSE(base::PathExists(database_file));
EXPECT_TRUE(base::DirectoryExists(database_file.DirName()));
// Confirm that the file is now empty.
ExpectFileContents(cdm_file, "");
// Write some more data. This should succeed.
Write(cdm_file, kTestData);
EXPECT_TRUE(base::PathExists(database_file));
EXPECT_TRUE(base::PathExists(database_file.DirName()));
}
TEST_F(MediaLicenseManagerTest, BucketCreationFailed) {
const std::string kTestData("Test Data");
mojo::Remote<media::mojom::CdmStorage> remote;
blink::StorageKey storage_key =
blink::StorageKey::CreateFromStringForTesting(kExampleOrigin);
ASSERT_OK_AND_ASSIGN(storage::BucketLocator bucket,
GetOrCreateBucket(storage_key));
CdmStorageBindingContext binding_context(storage_key, kCdmType);
// Disable the quota database, causing GetOrCreateBucket() to fail.
quota_manager_->SetDisableDatabase(/*disable=*/true);
// Open CDM storage for a storage key.
manager_->OpenCdmStorage(binding_context,
remote.BindNewPipeAndPassReceiver());
// Opening a CDM file should fail.
base::test::TestFuture<media::mojom::CdmStorage::Status,
mojo::PendingAssociatedRemote<media::mojom::CdmFile>>
open_future;
remote->Open("test_file", open_future.GetCallback());
auto result = open_future.Take();
EXPECT_EQ(std::get<0>(result), media::mojom::CdmStorage::Status::kFailure);
EXPECT_FALSE(std::get<1>(result).is_valid());
}
class MediaLicenseManagerIncognitoTest : public MediaLicenseManagerTest {
public:
MediaLicenseManagerIncognitoTest()
: MediaLicenseManagerTest(/*in_memory=*/true) {}
};
TEST_F(MediaLicenseManagerIncognitoTest, DeleteBucketData) {
const std::string kTestData("Test Data");
mojo::Remote<media::mojom::CdmStorage> remote;
blink::StorageKey storage_key =
blink::StorageKey::CreateFromStringForTesting(kExampleOrigin);
ASSERT_OK_AND_ASSIGN(storage::BucketLocator bucket,
GetOrCreateBucket(storage_key));
CdmStorageBindingContext binding_context(storage_key, kCdmType);
// Open CDM storage for a storage key.
manager_->OpenCdmStorage(binding_context,
remote.BindNewPipeAndPassReceiver());
auto cdm_file = OpenCdmFile(remote, "test_file");
// Write some data.
Write(cdm_file, kTestData);
// We should be able to read the written file.
ExpectFileContents(cdm_file, kTestData);
// Delete data for this storage key.
base::test::TestFuture<blink::mojom::QuotaStatusCode> delete_future;
manager_->DeleteBucketData(bucket, delete_future.GetCallback());
EXPECT_EQ(delete_future.Get(), blink::mojom::QuotaStatusCode::kOk);
// Confirm that the file is now empty.
ExpectFileContents(cdm_file, "");
}
TEST_F(MediaLicenseManagerIncognitoTest, DeleteBucketDataClosedStorage) {
const std::string kTestData("Test Data");
mojo::Remote<media::mojom::CdmStorage> remote;
blink::StorageKey storage_key =
blink::StorageKey::CreateFromStringForTesting(kExampleOrigin);
ASSERT_OK_AND_ASSIGN(storage::BucketLocator bucket,
GetOrCreateBucket(storage_key));
CdmStorageBindingContext binding_context(storage_key, kCdmType);
// Open CDM storage for a storage key.
manager_->OpenCdmStorage(binding_context,
remote.BindNewPipeAndPassReceiver());
auto cdm_file = OpenCdmFile(remote, "test_file");
Write(cdm_file, kTestData);
// There should be no db_file since its in memory.
EXPECT_TRUE(FindMediaLicenseDatabase().empty());
// We should still be able to wipe data to a closed storage.
cdm_file.reset();
remote.reset();
// Delete data for this storage key.
base::test::TestFuture<blink::mojom::QuotaStatusCode> delete_future;
manager_->DeleteBucketData(bucket, delete_future.GetCallback());
EXPECT_EQ(delete_future.Get(), blink::mojom::QuotaStatusCode::kOk);
}
TEST_F(MediaLicenseManagerIncognitoTest, DeleteBucketDataOpenConnection) {
const std::string kTestData("Test Data");
mojo::Remote<media::mojom::CdmStorage> remote;
blink::StorageKey storage_key =
blink::StorageKey::CreateFromStringForTesting(kExampleOrigin);
ASSERT_OK_AND_ASSIGN(storage::BucketLocator bucket,
GetOrCreateBucket(storage_key));
CdmStorageBindingContext binding_context(storage_key, kCdmType);
// Open CDM storage for a storage key.
manager_->OpenCdmStorage(binding_context,
remote.BindNewPipeAndPassReceiver());
auto cdm_file = OpenCdmFile(remote, "test_file");
Write(cdm_file, kTestData);
ExpectFileContents(cdm_file, kTestData);
// There should be no db file since its in memory.
EXPECT_TRUE(FindMediaLicenseDatabase().empty());
// Delete data for this storage key.
base::test::TestFuture<blink::mojom::QuotaStatusCode> delete_future;
manager_->DeleteBucketData(bucket, delete_future.GetCallback());
EXPECT_EQ(delete_future.Get(), blink::mojom::QuotaStatusCode::kOk);
// Confirm that the file is now empty.
ExpectFileContents(cdm_file, "");
// Write some more data. This should succeed.
Write(cdm_file, kTestData);
// Check that no file was created.
EXPECT_TRUE(FindMediaLicenseDatabase().empty());
}
TEST_F(MediaLicenseManagerIncognitoTest, BucketCreationFailed) {
const std::string kTestData("Test Data");
mojo::Remote<media::mojom::CdmStorage> remote;
blink::StorageKey storage_key =
blink::StorageKey::CreateFromStringForTesting(kExampleOrigin);
ASSERT_OK_AND_ASSIGN(storage::BucketLocator bucket,
GetOrCreateBucket(storage_key));
CdmStorageBindingContext binding_context(storage_key, kCdmType);
// Disable the quota database, causing GetOrCreateBucket() to fail.
quota_manager_->SetDisableDatabase(/*disable=*/true);
// Open CDM storage for a storage key.
manager_->OpenCdmStorage(binding_context,
remote.BindNewPipeAndPassReceiver());
// Opening a CDM file should fail.
base::test::TestFuture<media::mojom::CdmStorage::Status,
mojo::PendingAssociatedRemote<media::mojom::CdmFile>>
open_future;
remote->Open("test_file", open_future.GetCallback());
auto result = open_future.Take();
EXPECT_EQ(std::get<0>(result), media::mojom::CdmStorage::Status::kFailure);
EXPECT_FALSE(std::get<1>(result).is_valid());
}
} // namespace content

@ -0,0 +1,58 @@
// Copyright 2022 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/media_license_quota_client.h"
#include "base/sequence_checker.h"
#include "content/browser/media/media_license_manager.h"
namespace content {
MediaLicenseQuotaClient::MediaLicenseQuotaClient(MediaLicenseManager* manager)
: manager_(manager) {}
MediaLicenseQuotaClient::~MediaLicenseQuotaClient() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
void MediaLicenseQuotaClient::GetBucketUsage(
const storage::BucketLocator& bucket,
GetBucketUsageCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Media license data does not count against quota.
// TODO(crbug.com/40218094): Consider counting this data against quota.
std::move(callback).Run(0);
}
void MediaLicenseQuotaClient::GetStorageKeysForType(
blink::mojom::StorageType type,
GetStorageKeysForTypeCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK_EQ(type, blink::mojom::StorageType::kTemporary);
// This method is only used to bootstrap existing Storage API data into the
// QuotaDatabase. Since this is a new backend, there is no existing data to
// bootstrap.
std::move(callback).Run({});
}
void MediaLicenseQuotaClient::DeleteBucketData(
const storage::BucketLocator& bucket,
DeleteBucketDataCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
manager_->DeleteBucketData(bucket, std::move(callback));
}
void MediaLicenseQuotaClient::PerformStorageCleanup(
blink::mojom::StorageType type,
PerformStorageCleanupCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Nothing to do here.
std::move(callback).Run();
}
} // namespace content

@ -0,0 +1,42 @@
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CONTENT_BROWSER_MEDIA_MEDIA_LICENSE_QUOTA_CLIENT_H_
#define CONTENT_BROWSER_MEDIA_MEDIA_LICENSE_QUOTA_CLIENT_H_
#include "components/services/storage/public/mojom/quota_client.mojom.h"
#include "content/common/content_export.h"
namespace content {
class MediaLicenseManager;
// Integrates media licenses with the quota system.
//
// Each MediaLicenseManager owns exactly one MediaLicenseQuotaClient.
class CONTENT_EXPORT MediaLicenseQuotaClient
: public storage::mojom::QuotaClient {
public:
explicit MediaLicenseQuotaClient(MediaLicenseManager* manager);
~MediaLicenseQuotaClient() override;
// storage::mojom::QuotaClient implementation:
void GetBucketUsage(const storage::BucketLocator& bucket,
GetBucketUsageCallback callback) override;
void GetStorageKeysForType(blink::mojom::StorageType type,
GetStorageKeysForTypeCallback callback) override;
void DeleteBucketData(const storage::BucketLocator& bucket,
DeleteBucketDataCallback callback) override;
void PerformStorageCleanup(blink::mojom::StorageType type,
PerformStorageCleanupCallback callback) override;
private:
SEQUENCE_CHECKER(sequence_checker_);
const raw_ptr<MediaLicenseManager> manager_
GUARDED_BY_CONTEXT(sequence_checker_);
};
} // namespace content
#endif // CONTENT_BROWSER_MEDIA_MEDIA_LICENSE_QUOTA_CLIENT_H_

@ -0,0 +1,312 @@
// Copyright 2022 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/media_license_storage_host.h"
#include <algorithm>
#include <memory>
#include <utility>
#include <vector>
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/sequence_checker.h"
#include "base/task/sequenced_task_runner.h"
#include "base/types/pass_key.h"
#include "components/services/storage/public/cpp/buckets/bucket_locator.h"
#include "content/browser/media/cdm_file_impl.h"
#include "content/browser/media/media_license_database.h"
#include "content/browser/media/media_license_manager.h"
#include "media/cdm/cdm_type.h"
#include "media/mojo/mojom/cdm_storage.mojom.h"
#include "mojo/public/cpp/bindings/pending_associated_remote.h"
#include "storage/browser/file_system/file_system_context.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
namespace content {
namespace {
constexpr uint64_t kBytesPerKB = 1024;
constexpr int kMinDatabaseSizeKB = 0;
// Used for histogram reporting, the max size of the database we expect in KB.
constexpr uint64_t kMaxDatabaseSizeKB = 512000 * 10;
constexpr int kSizeKBBuckets = 1000;
} // namespace
// static
void MediaLicenseStorageHost::ReportDatabaseOpenError(
MediaLicenseStorageHostOpenError error,
bool is_incognito) {
DCHECK_NE(error, MediaLicenseStorageHostOpenError::kOk);
const std::string kDatabaseOpenErrorUmaName =
"Media.EME.MediaLicenseStorageHostOpenError";
base::UmaHistogramEnumeration(kDatabaseOpenErrorUmaName, error);
if (is_incognito) {
base::UmaHistogramEnumeration(kDatabaseOpenErrorUmaName + ".Incognito",
error);
} else {
base::UmaHistogramEnumeration(kDatabaseOpenErrorUmaName + ".NotIncognito",
error);
}
}
MediaLicenseStorageHost::MediaLicenseStorageHost(
MediaLicenseManager* manager,
const storage::BucketLocator& bucket_locator)
: manager_(manager),
bucket_locator_(bucket_locator),
db_(manager_->db_runner(), manager_->GetDatabasePath(bucket_locator_)) {
DCHECK(manager_);
// base::Unretained is safe here because this MediaLicenseStorageHost owns
// `receivers_`. So, the unretained MediaLicenseStorageHost is guaranteed to
// outlive `receivers_` and the closure that it uses.
receivers_.set_disconnect_handler(base::BindRepeating(
&MediaLicenseStorageHost::OnReceiverDisconnect, base::Unretained(this)));
}
MediaLicenseStorageHost::~MediaLicenseStorageHost() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
void MediaLicenseStorageHost::Open(const std::string& file_name,
OpenCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (bucket_locator_.id.is_null()) {
DVLOG(1) << "Could not retrieve valid bucket.";
ReportDatabaseOpenError(MediaLicenseStorageHostOpenError::kInvalidBucket,
in_memory());
std::move(callback).Run(Status::kFailure, mojo::NullAssociatedRemote());
return;
}
if (file_name.empty()) {
DVLOG(1) << "No file specified.";
ReportDatabaseOpenError(MediaLicenseStorageHostOpenError::kNoFileSpecified,
in_memory());
std::move(callback).Run(Status::kFailure, mojo::NullAssociatedRemote());
return;
}
if (!CdmFileImpl::IsValidName(file_name)) {
ReportDatabaseOpenError(MediaLicenseStorageHostOpenError::kInvalidFileName,
in_memory());
std::move(callback).Run(Status::kFailure, mojo::NullAssociatedRemote());
return;
}
const CdmStorageBindingContext& binding_context =
receivers_.current_context();
db_.AsyncCall(&MediaLicenseDatabase::OpenFile)
.WithArgs(binding_context.cdm_type, file_name)
.Then(base::BindOnce(&MediaLicenseStorageHost::DidOpenFile,
weak_factory_.GetWeakPtr(), file_name,
binding_context, std::move(callback)));
}
void MediaLicenseStorageHost::BindReceiver(
const CdmStorageBindingContext& binding_context,
mojo::PendingReceiver<media::mojom::CdmStorage> receiver) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK_EQ(binding_context.storage_key, bucket_locator_.storage_key);
receivers_.Add(this, std::move(receiver), binding_context);
}
void MediaLicenseStorageHost::DidOpenFile(
const std::string& file_name,
CdmStorageBindingContext binding_context,
OpenCallback callback,
MediaLicenseStorageHostOpenError error) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (error != MediaLicenseStorageHostOpenError::kOk) {
ReportDatabaseOpenError(error, in_memory());
std::move(callback).Run(Status::kFailure, mojo::NullAssociatedRemote());
return;
}
// Check whether this CDM file is in-use.
CdmFileId id(file_name, binding_context.cdm_type);
if (base::Contains(cdm_files_, id)) {
std::move(callback).Run(Status::kInUse, mojo::NullAssociatedRemote());
return;
}
// File was opened successfully, so create the binding and return success.
mojo::PendingAssociatedRemote<media::mojom::CdmFile> cdm_file;
// `this` is safe here since `cdm_file_impl` is owned by this instance.
cdm_files_.emplace(id, std::make_unique<CdmFileImpl>(
this, binding_context.cdm_type, file_name,
cdm_file.InitWithNewEndpointAndPassReceiver()));
// We don't actually touch the database here, but notify the quota system
// anyways since conceptually we're creating an empty file.
manager_->quota_manager_proxy()->NotifyBucketModified(
storage::QuotaClientType::kMediaLicense, bucket_locator_, /*delta=*/0,
/*modification_time=*/base::Time::Now(),
base::SequencedTaskRunner::GetCurrentDefault(),
base::BindOnce(std::move(callback), Status::kSuccess,
std::move(cdm_file)));
}
void MediaLicenseStorageHost::ReadFile(const media::CdmType& cdm_type,
const std::string& file_name,
ReadFileCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
manager_->quota_manager_proxy()->NotifyBucketAccessed(
bucket_locator_,
/*access_time=*/base::Time::Now());
// This is to read from the cdm_storage database when the migration is active
// and we've already migrated the data from the cdm_storage database to the
// media_license database.
if (manager_->cdm_storage_manager() &&
base::Contains(files_migrated_,
CdmFileIdTwo{file_name, cdm_type, storage_key()})) {
manager_->cdm_storage_manager()->ReadFile(storage_key(), cdm_type,
file_name, std::move(callback));
return;
}
db_.AsyncCall(&MediaLicenseDatabase::ReadFile)
.WithArgs(cdm_type, file_name)
.Then(base::BindOnce(&MediaLicenseStorageHost::DidReadFile,
weak_factory_.GetWeakPtr(), cdm_type, file_name,
std::move(callback)));
}
void MediaLicenseStorageHost::DidReadFile(
const media::CdmType& cdm_type,
const std::string& file_name,
ReadFileCallback callback,
std::optional<std::vector<uint8_t>> data) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// The code only reaches this callback during the migration when this is our
// first time reading this specific storage_key, cdm_type, and file name. If
// the data has value, then we write it to the cdm_storage database and from
// then on, read from the cdm_storage database through the media_license code
// when the migration is active.
if (data.has_value() && manager_->cdm_storage_manager()) {
manager_->cdm_storage_manager()->WriteFile(
storage_key(), cdm_type, file_name, data.value(), base::DoNothing());
files_migrated_.emplace_back(file_name, cdm_type, storage_key());
}
if (!database_size_reported_) {
db_.AsyncCall(&MediaLicenseDatabase::GetDatabaseSize)
.Then(base::BindOnce(&MediaLicenseStorageHost::DidGetDatabaseSize,
weak_factory_.GetWeakPtr()));
}
std::move(callback).Run(data);
}
void MediaLicenseStorageHost::DidGetDatabaseSize(const uint64_t size) {
// One time report DatabaseSize.
base::UmaHistogramCustomCounts(
"Media.EME.MediaLicenseStorageHost.CurrentDatabaseUsageKB",
size / kBytesPerKB, kMinDatabaseSizeKB, kMaxDatabaseSizeKB,
kSizeKBBuckets);
database_size_reported_ = true;
}
void MediaLicenseStorageHost::WriteFile(const media::CdmType& cdm_type,
const std::string& file_name,
const std::vector<uint8_t>& data,
WriteFileCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (manager_->cdm_storage_manager()) {
// We don't populate the callback because we want the
// `MediaLicenseStorageHost` to still maintain control. We just call in to
// the `CdmStorageManager` object to be able to update the
// `CdmStorageDatabase` to keep it in line with `MediaLicenseDatabase`.
manager_->cdm_storage_manager()->WriteFile(
storage_key(), cdm_type, file_name, data, base::DoNothing());
}
db_.AsyncCall(&MediaLicenseDatabase::WriteFile)
.WithArgs(cdm_type, file_name, data)
.Then(base::BindOnce(&MediaLicenseStorageHost::DidWriteFile,
weak_factory_.GetWeakPtr(), std::move(callback)));
}
void MediaLicenseStorageHost::DidWriteFile(WriteFileCallback callback,
bool success) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!success) {
manager_->quota_manager_proxy()->OnClientWriteFailed(storage_key());
std::move(callback).Run(false);
return;
}
// Pass `delta`=0 since media license data does not count against quota.
// TODO(crbug.com/40218094): Consider counting this data against quota.
manager_->quota_manager_proxy()->NotifyBucketModified(
storage::QuotaClientType::kMediaLicense, bucket_locator_, /*delta=*/0,
/*modification_time=*/base::Time::Now(),
base::SequencedTaskRunner::GetCurrentDefault(),
base::BindOnce(std::move(callback), success));
}
void MediaLicenseStorageHost::DeleteFile(const media::CdmType& cdm_type,
const std::string& file_name,
DeleteFileCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (manager_->cdm_storage_manager()) {
// We don't populate the callback because we want the
// `MediaLicenseStorageHost` to still maintain control. We just call in to
// the `CdmStorageManager` object to be able to update the
// `CdmStorageDatabase` to keep it in line with `MediaLicenseDatabase`.
// TODO(crbug.com/40272342): Create UMA to track failures from the
// MediaLicense* path, as we choose to fail silently to not affect the
// current code-path's behavior and affect CDM operations.
manager_->cdm_storage_manager()->DeleteFile(storage_key(), cdm_type,
file_name, base::DoNothing());
}
db_.AsyncCall(&MediaLicenseDatabase::DeleteFile)
.WithArgs(cdm_type, file_name)
.Then(base::BindOnce(&MediaLicenseStorageHost::DidWriteFile,
weak_factory_.GetWeakPtr(), std::move(callback)));
}
void MediaLicenseStorageHost::DeleteBucketData(
base::OnceCallback<void(bool)> callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
db_.AsyncCall(&MediaLicenseDatabase::ClearDatabase).Then(std::move(callback));
}
void MediaLicenseStorageHost::OnReceiverDisconnect() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// May delete `this`.
manager_->OnHostReceiverDisconnect(this,
base::PassKey<MediaLicenseStorageHost>());
}
void MediaLicenseStorageHost::OnFileReceiverDisconnect(
const std::string& name,
const media::CdmType& cdm_type,
base::PassKey<CdmFileImpl> /*pass_key*/) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto count = cdm_files_.erase(CdmFileId(name, cdm_type));
DCHECK_GT(count, 0u);
}
} // namespace content

@ -0,0 +1,165 @@
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CONTENT_BROWSER_MEDIA_MEDIA_LICENSE_STORAGE_HOST_H_
#define CONTENT_BROWSER_MEDIA_MEDIA_LICENSE_STORAGE_HOST_H_
#include "base/containers/unique_ptr_adapters.h"
#include "base/files/file_path.h"
#include "base/functional/callback_forward.h"
#include "base/thread_annotations.h"
#include "base/threading/sequence_bound.h"
#include "base/types/pass_key.h"
#include "components/services/storage/public/cpp/buckets/bucket_locator.h"
#include "components/services/storage/public/mojom/quota_client.mojom.h"
#include "content/browser/media/cdm_storage_common.h"
#include "content/browser/media/media_license_manager.h"
#include "content/common/content_export.h"
#include "media/mojo/mojom/cdm_storage.mojom.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "mojo/public/cpp/bindings/unique_associated_receiver_set.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
namespace content {
class CdmFileImpl;
class MediaLicenseDatabase;
// Per-storage-key backend for media license (CDM) files. MediaLicenseManager
// owns an instance of this class for each storage key that is actively using
// CDM files. Each instance owns all CdmStorage receivers for the corresponding
// storage key.
class CONTENT_EXPORT MediaLicenseStorageHost : public media::mojom::CdmStorage {
public:
using ReadFileCallback =
base::OnceCallback<void(std::optional<std::vector<uint8_t>>)>;
using WriteFileCallback = base::OnceCallback<void(bool)>;
using DeleteFileCallback = base::OnceCallback<void(bool)>;
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class MediaLicenseStorageHostOpenError {
kOk = -1,
kInvalidBucket = 0, // The database's path could not be determined
// because the default storage bucket for the
// StorageKey could not be retrieved.
kNoFileSpecified = 1, // No file was specified.
kInvalidFileName = 2, // File name specified was invalid.
kDatabaseOpenError = 3, // Error occurred at the Database level.
kBucketNotFound = 4, // If the default Storage Bucket for the StorageKey
// is not found.
kDatabaseRazeError = 5, // The database was in an invalid state and failed
// to be razed.
kSQLExecutionError = 6, // Error executing the SQL statement.
kBucketLocatorError = 7, // Error with the bucket locator. This error was
// introduced after the previous errors so that
// we can drill down deeper on the source of the
// errors.
kMaxValue = kBucketLocatorError
};
static void ReportDatabaseOpenError(MediaLicenseStorageHostOpenError error,
bool in_memory);
MediaLicenseStorageHost(MediaLicenseManager* manager,
const storage::BucketLocator& bucket_locator);
~MediaLicenseStorageHost() override;
// media::mojom::CdmStorage implementation.
void Open(const std::string& file_name, OpenCallback callback) final;
void BindReceiver(const CdmStorageBindingContext& binding_context,
mojo::PendingReceiver<media::mojom::CdmStorage> receiver);
// CDM file operations.
void ReadFile(const media::CdmType& cdm_type,
const std::string& file_name,
ReadFileCallback callback);
void WriteFile(const media::CdmType& cdm_type,
const std::string& file_name,
const std::vector<uint8_t>& data,
WriteFileCallback callback);
void DeleteFile(const media::CdmType& cdm_type,
const std::string& file_name,
DeleteFileCallback callback);
void DeleteBucketData(base::OnceCallback<void(bool)> callback);
void OnFileReceiverDisconnect(const std::string& name,
const media::CdmType& cdm_type,
base::PassKey<CdmFileImpl> pass_key);
// True if there are no receivers connected to this host.
//
// The MediaLicenseManagerImpl that owns this host is expected to destroy the
// host when it isn't serving any receivers.
bool has_empty_receiver_set() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return receivers_.empty();
}
const blink::StorageKey& storage_key() { return bucket_locator_.storage_key; }
bool in_memory() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return manager_->in_memory();
}
private:
void OnReceiverDisconnect();
void DidOpenFile(const std::string& file_name,
CdmStorageBindingContext binding_context,
OpenCallback callback,
MediaLicenseStorageHostOpenError error);
void DidGetDatabaseSize(const uint64_t size);
void DidReadFile(const media::CdmType& cdm_type,
const std::string& file_name,
ReadFileCallback callback,
std::optional<std::vector<uint8_t>> data);
void DidWriteFile(WriteFileCallback callback, bool success);
SEQUENCE_CHECKER(sequence_checker_);
// Track MediaLicenseDatabaseSize
bool database_size_reported_ = false;
// MediaLicenseManager instance which owns this object.
const raw_ptr<MediaLicenseManager> manager_
GUARDED_BY_CONTEXT(sequence_checker_);
// Media licenses are only supported from the default bucket.
// `bucket_locator_` corresponds to the default bucket for the StorageKey this
// host represents.
const storage::BucketLocator bucket_locator_;
// This keeps track of the 'CdmFileIdTwo' values that have been migrated to
// the CdmStorageDatabase after the first read, so that when the Cdm goes to
// read during the migration, the second time and onwards, we read from the
// CdmStorageDatabase instead of the MediaLicenseDatabase. Note that this is
// not a permanent storage, so it has to be repopulated when user restarts
// Chrome.
std::vector<CdmFileIdTwo> files_migrated_
GUARDED_BY_CONTEXT(sequence_checker_);
// All file operations are run through this member.
base::SequenceBound<MediaLicenseDatabase> db_
GUARDED_BY_CONTEXT(sequence_checker_);
// All receivers for frames and workers whose storage key is `storage_key()`.
mojo::ReceiverSet<media::mojom::CdmStorage, CdmStorageBindingContext>
receivers_ GUARDED_BY_CONTEXT(sequence_checker_);
// Keep track of all media::mojom::CdmFile receivers, as each CdmFileImpl
// object keeps a reference to |this|. If |this| goes away unexpectedly,
// all remaining CdmFile receivers will be closed.
std::map<CdmFileId, std::unique_ptr<CdmFileImpl>> cdm_files_
GUARDED_BY_CONTEXT(sequence_checker_);
base::WeakPtrFactory<MediaLicenseStorageHost> weak_factory_
GUARDED_BY_CONTEXT(sequence_checker_){this};
};
} // namespace content
#endif // CONTENT_BROWSER_MEDIA_MEDIA_LICENSE_STORAGE_HOST_H_

@ -0,0 +1,359 @@
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <memory>
#include <optional>
#include <vector>
#include "base/files/file.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "content/browser/media/cdm_storage_common.h"
#include "content/browser/media/media_license_manager.h"
#include "content/browser/storage_partition_impl.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_renderer_host.h"
#include "media/cdm/cdm_type.h"
#include "media/mojo/mojom/cdm_storage.mojom.h"
#include "mojo/public/cpp/bindings/associated_remote.h"
#include "mojo/public/cpp/bindings/pending_associated_remote.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "url/gurl.h"
using media::mojom::CdmFile;
using media::mojom::CdmStorage;
namespace content {
namespace {
const media::CdmType kTestCdmType{1234, 5678};
const char kTestOrigin[] = "http://www.test.com";
// Helper functions to manipulate RenderFrameHosts.
void SimulateNavigation(raw_ptr<RenderFrameHost, DanglingUntriaged>* rfh,
const GURL& url) {
auto navigation_simulator =
NavigationSimulator::CreateRendererInitiated(url, *rfh);
navigation_simulator->Commit();
*rfh = navigation_simulator->GetFinalRenderFrameHost();
}
} // namespace
class CdmStorageTest : public RenderViewHostTestHarness {
public:
CdmStorageTest()
: RenderViewHostTestHarness(
content::BrowserTaskEnvironment::REAL_IO_THREAD) {
}
protected:
void SetUp() final {
RenderViewHostTestHarness::SetUp();
rfh_ = web_contents()->GetPrimaryMainFrame();
RenderFrameHostTester::For(rfh_)->InitializeRenderFrameIfNeeded();
SimulateNavigation(&rfh_, GURL(kTestOrigin));
cdm_storage_manager()->OpenCdmStorage(
CdmStorageBindingContext(
blink::StorageKey::CreateFromStringForTesting(kTestOrigin),
kTestCdmType),
cdm_storage_.BindNewPipeAndPassReceiver());
}
// Open the file |name|. Returns true if the file returned is valid, false
// otherwise. On success |cdm_file| is bound to the CdmFileImpl object.
bool Open(const std::string& name,
mojo::AssociatedRemote<CdmFile>& cdm_file) {
DVLOG(3) << __func__;
base::test::TestFuture<CdmStorage::Status,
mojo::PendingAssociatedRemote<CdmFile>>
future;
cdm_storage_->Open(name, future.GetCallback());
CdmStorage::Status status = future.Get<0>();
mojo::PendingAssociatedRemote<CdmFile> actual_file =
std::move(std::get<1>(future.Take()));
if (!actual_file) {
DCHECK_NE(status, CdmStorage::Status::kSuccess);
return false;
}
// Open() returns a mojo::PendingAssociatedRemote<CdmFile>, so bind it to
// the mojo::AssociatedRemote<CdmFileAssociated> provided.
mojo::AssociatedRemote<CdmFile> cdm_file_remote;
cdm_file_remote.Bind(std::move(actual_file));
cdm_file = std::move(cdm_file_remote);
return status == CdmStorage::Status::kSuccess;
}
// Reads the contents of the previously opened |cdm_file|. If successful,
// true is returned and |data| is updated with the contents of the file.
// If unable to read the file, false is returned.
bool Read(CdmFile* cdm_file, std::vector<uint8_t>& data) {
DVLOG(3) << __func__;
base::test::TestFuture<CdmFile::Status, std::vector<uint8_t>> future;
cdm_file->Read(
future.GetCallback<CdmFile::Status, const std::vector<uint8_t>&>());
CdmFile::Status status = future.Get<0>();
data = future.Get<1>();
return status == CdmFile::Status::kSuccess;
}
// Writes |data| to the previously opened |cdm_file|, replacing the contents
// of the file. Returns true if successful, false otherwise.
bool Write(CdmFile* cdm_file, const std::vector<uint8_t>& data) {
DVLOG(3) << __func__;
base::test::TestFuture<CdmFile::Status> future;
cdm_file->Write(data, future.GetCallback());
CdmFile::Status status = future.Get();
return status == CdmFile::Status::kSuccess;
}
MediaLicenseManager* media_license_manager() const {
auto* media_license_manager =
static_cast<StoragePartitionImpl*>(rfh_->GetStoragePartition())
->GetMediaLicenseManager();
DCHECK(media_license_manager);
return media_license_manager;
}
CdmStorageManager* cdm_storage_manager() const {
auto* cdm_storage_manager = static_cast<CdmStorageManager*>(
static_cast<StoragePartitionImpl*>(rfh_->GetStoragePartition())
->GetCdmStorageDataModel());
DCHECK(cdm_storage_manager);
return cdm_storage_manager;
}
raw_ptr<RenderFrameHost, DanglingUntriaged> rfh_ = nullptr;
mojo::Remote<CdmStorage> cdm_storage_;
base::test::ScopedFeatureList scoped_feature_list_;
};
TEST_F(CdmStorageTest, InvalidFileName) {
// Anything other than ASCII letter, digits, and -._ will fail. Add a
// Unicode character to the name.
const char kFileName[] = "openfile\u1234";
mojo::AssociatedRemote<CdmFile> cdm_file;
EXPECT_FALSE(Open(kFileName, cdm_file));
ASSERT_FALSE(cdm_file.is_bound());
}
TEST_F(CdmStorageTest, InvalidFileNameEmpty) {
const char kFileName[] = "";
mojo::AssociatedRemote<CdmFile> cdm_file;
EXPECT_FALSE(Open(kFileName, cdm_file));
ASSERT_FALSE(cdm_file.is_bound());
}
TEST_F(CdmStorageTest, InvalidFileNameStartWithUnderscore) {
const char kFileName[] = "_invalid";
mojo::AssociatedRemote<CdmFile> cdm_file;
EXPECT_FALSE(Open(kFileName, cdm_file));
ASSERT_FALSE(cdm_file.is_bound());
}
TEST_F(CdmStorageTest, InvalidFileNameTooLong) {
// Limit is 256 characters, so try a file name with 257.
const std::string kFileName(257, 'a');
mojo::AssociatedRemote<CdmFile> cdm_file;
EXPECT_FALSE(Open(kFileName, cdm_file));
ASSERT_FALSE(cdm_file.is_bound());
}
TEST_F(CdmStorageTest, OpenFile) {
const char kFileName[] = "test_file_name";
mojo::AssociatedRemote<CdmFile> cdm_file;
EXPECT_TRUE(Open(kFileName, cdm_file));
ASSERT_TRUE(cdm_file.is_bound());
}
TEST_F(CdmStorageTest, OpenFileLocked) {
const char kFileName[] = "test_file_name";
mojo::AssociatedRemote<CdmFile> cdm_file1;
EXPECT_TRUE(Open(kFileName, cdm_file1));
ASSERT_TRUE(cdm_file1.is_bound());
// Second attempt on the same file should fail as the file is locked.
mojo::AssociatedRemote<CdmFile> cdm_file2;
EXPECT_FALSE(Open(kFileName, cdm_file2));
ASSERT_FALSE(cdm_file2.is_bound());
// Now close the first file and try again. It should be free now.
cdm_file1.reset();
mojo::AssociatedRemote<CdmFile> cdm_file3;
EXPECT_TRUE(Open(kFileName, cdm_file3));
ASSERT_TRUE(cdm_file3.is_bound());
}
TEST_F(CdmStorageTest, MultipleFiles) {
const char kFileName1[] = "file1";
mojo::AssociatedRemote<CdmFile> cdm_file1;
EXPECT_TRUE(Open(kFileName1, cdm_file1));
ASSERT_TRUE(cdm_file1.is_bound());
const char kFileName2[] = "file2";
mojo::AssociatedRemote<CdmFile> cdm_file2;
EXPECT_TRUE(Open(kFileName2, cdm_file2));
ASSERT_TRUE(cdm_file2.is_bound());
const char kFileName3[] = "file3";
mojo::AssociatedRemote<CdmFile> cdm_file3;
EXPECT_TRUE(Open(kFileName3, cdm_file3));
ASSERT_TRUE(cdm_file3.is_bound());
}
TEST_F(CdmStorageTest, WriteThenReadFile) {
const char kFileName[] = "test_file_name";
mojo::AssociatedRemote<CdmFile> cdm_file;
EXPECT_TRUE(Open(kFileName, cdm_file));
ASSERT_TRUE(cdm_file.is_bound());
// Write several bytes and read them back.
std::vector<uint8_t> kTestData = {'r', 'a', 'n', 'd', 'o', 'm'};
EXPECT_TRUE(Write(cdm_file.get(), kTestData));
std::vector<uint8_t> data_read;
EXPECT_TRUE(Read(cdm_file.get(), data_read));
EXPECT_EQ(kTestData, data_read);
}
TEST_F(CdmStorageTest, ReadThenWriteEmptyFile) {
const char kFileName[] = "empty_file_name";
mojo::AssociatedRemote<CdmFile> cdm_file;
EXPECT_TRUE(Open(kFileName, cdm_file));
ASSERT_TRUE(cdm_file.is_bound());
// New file should be empty.
std::vector<uint8_t> data_read;
EXPECT_TRUE(Read(cdm_file.get(), data_read));
EXPECT_EQ(0u, data_read.size());
// Write nothing.
EXPECT_TRUE(Write(cdm_file.get(), std::vector<uint8_t>()));
// Should still be empty.
EXPECT_TRUE(Read(cdm_file.get(), data_read));
EXPECT_EQ(0u, data_read.size());
}
TEST_F(CdmStorageTest, ParallelRead) {
const char kFileName[] = "duplicate_read_file_name";
mojo::AssociatedRemote<CdmFile> cdm_file;
EXPECT_TRUE(Open(kFileName, cdm_file));
ASSERT_TRUE(cdm_file.is_bound());
// Attempts to reads the contents of the previously opened |cdm_file| twice.
// We don't really care about the data, just that the first read succeeds.
base::test::TestFuture<CdmFile::Status, std::vector<uint8_t>> future1;
base::test::TestFuture<CdmFile::Status, std::vector<uint8_t>> future2;
cdm_file->Read(
future1.GetCallback<CdmFile::Status, const std::vector<uint8_t>&>());
cdm_file->Read(
future2.GetCallback<CdmFile::Status, const std::vector<uint8_t>&>());
EXPECT_TRUE(future1.Wait());
EXPECT_TRUE(future2.Wait());
CdmFile::Status status1 = future1.Get<0>();
CdmFile::Status status2 = future2.Get<0>();
// The first call should succeed. The second call might fail, if its blocked
// by our locking system, or might succeed, if the reads are processed faster
// than expected.
EXPECT_TRUE(status1 == CdmFile::Status::kSuccess &&
(status2 == CdmFile::Status::kFailure ||
status2 == CdmFile::Status::kSuccess))
<< "status 1: " << status1 << ", status2: " << status2;
}
TEST_F(CdmStorageTest, ParallelWrite) {
const char kFileName[] = "duplicate_write_file_name";
mojo::AssociatedRemote<CdmFile> cdm_file;
EXPECT_TRUE(Open(kFileName, cdm_file));
ASSERT_TRUE(cdm_file.is_bound());
// Attempts to write the contents of the previously opened |cdm_file| twice.
// We don't really care about the data, just that the first write succeeds.
base::test::TestFuture<CdmFile::Status> future1;
base::test::TestFuture<CdmFile::Status> future2;
cdm_file->Write({1, 2, 3}, future1.GetCallback());
cdm_file->Write({4, 5, 6}, future2.GetCallback());
EXPECT_TRUE(future1.Wait());
EXPECT_TRUE(future2.Wait());
CdmFile::Status status1 = future1.Get();
CdmFile::Status status2 = future2.Get();
// The first call should succeed. The second call might fail, if its blocked
// by our locking system, or might succeed, if the writes are processed
// faster than expected.
EXPECT_TRUE(status1 == CdmFile::Status::kSuccess &&
(status2 == CdmFile::Status::kSuccess ||
status2 == CdmFile::Status::kFailure))
<< "status 1: " << status1 << ", status2: " << status2;
}
TEST_F(CdmStorageTest, VerifyMigrationWorks) {
const char kFileName[] = "test_file_name";
mojo::AssociatedRemote<CdmFile> cdm_file;
EXPECT_TRUE(Open(kFileName, cdm_file));
ASSERT_TRUE(cdm_file.is_bound());
// Write several bytes and read them back.
std::vector<uint8_t> kTestData = {'r', 'a', 'n', 'd', 'o', 'm'};
EXPECT_TRUE(Write(cdm_file.get(), kTestData));
std::vector<uint8_t> data_read;
EXPECT_TRUE(Read(cdm_file.get(), data_read));
EXPECT_EQ(data_read, kTestData);
base::test::TestFuture<std::optional<std::vector<uint8_t>>> read_future;
cdm_storage_manager()->ReadFile(
blink::StorageKey::CreateFromStringForTesting(kTestOrigin),
kTestCdmType, kFileName, read_future.GetCallback());
EXPECT_EQ(read_future.Get(), kTestData);
// Write nothing.
EXPECT_TRUE(Write(cdm_file.get(), std::vector<uint8_t>()));
// Should still be empty.
EXPECT_TRUE(Read(cdm_file.get(), data_read));
EXPECT_THAT(data_read, testing::IsEmpty());
base::test::TestFuture<std::optional<std::vector<uint8_t>>>
read_empty_file_future;
cdm_storage_manager()->ReadFile(
blink::StorageKey::CreateFromStringForTesting(kTestOrigin),
kTestCdmType, kFileName, read_empty_file_future.GetCallback());
EXPECT_THAT(read_empty_file_future.Get().value(), testing::IsEmpty());
}
} // namespace content

@ -172,6 +172,7 @@
#if BUILDFLAG(ENABLE_LIBRARY_CDMS)
#include "content/browser/media/cdm_storage_common.h"
#include "content/browser/media/cdm_storage_manager.h"
#include "content/browser/media/media_license_manager.h"
#include "content/public/browser/cdm_storage_data_model.h"
#endif // BUILDFLAG(ENABLE_LIBRARY_CDMS)
@ -788,6 +789,9 @@ storage::QuotaClientTypes StoragePartitionImpl::GenerateQuotaClientTypes(
if (remove_mask & StoragePartition::REMOVE_DATA_MASK_BACKGROUND_FETCH) {
quota_client_types.insert(storage::QuotaClientType::kBackgroundFetch);
}
if (remove_mask & StoragePartition::REMOVE_DATA_MASK_MEDIA_LICENSES) {
quota_client_types.insert(storage::QuotaClientType::kMediaLicense);
}
return quota_client_types;
}
@ -1841,6 +1845,11 @@ void StoragePartitionImpl::SetFontAccessManagerForTesting(
}
#if BUILDFLAG(ENABLE_LIBRARY_CDMS)
MediaLicenseManager* StoragePartitionImpl::GetMediaLicenseManager() {
DCHECK(initialized_);
return media_license_manager_.get();
}
CdmStorageDataModel* StoragePartitionImpl::GetCdmStorageDataModel() {
DCHECK(initialized_);
return cdm_storage_manager_.get();
@ -2815,7 +2824,7 @@ void StoragePartitionImpl::DataDeletionHelper::ClearDataOnUIThread(
}
#if BUILDFLAG(ENABLE_LIBRARY_CDMS)
if ((remove_mask_ & REMOVE_DATA_MASK_MEDIA_LICENSES)) {
if ((remove_mask_ & REMOVE_DATA_MASK_MEDIA_LICENSES) && cdm_storage_manager) {
auto cdm_deletion_callback = base::BindOnce(
base::IgnoreArgs<bool>(mojo::WrapCallbackWithDefaultInvokeIfNotRun(
CreateTaskCompletionClosure(TracingDataType::kCdmStorage))));
@ -2825,11 +2834,14 @@ void StoragePartitionImpl::DataDeletionHelper::ClearDataOnUIThread(
}
#endif // BUILDFLAG(ENABLE_LIBRARY_CDMS)
// TODO(crbug.com/40272342): Remove REMOVE_DATA_MASK_MEDIA_LICENSES from here
// when MediaLicense is removed from Quota types.
if (remove_mask_ & REMOVE_DATA_MASK_INDEXEDDB ||
remove_mask_ & REMOVE_DATA_MASK_WEBSQL ||
remove_mask_ & REMOVE_DATA_MASK_FILE_SYSTEMS ||
remove_mask_ & REMOVE_DATA_MASK_SERVICE_WORKERS ||
remove_mask_ & REMOVE_DATA_MASK_CACHE_STORAGE) {
remove_mask_ & REMOVE_DATA_MASK_CACHE_STORAGE ||
remove_mask_ & REMOVE_DATA_MASK_MEDIA_LICENSES) {
GetIOThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&DataDeletionHelper::ClearQuotaManagedDataOnIOThread,

@ -93,6 +93,7 @@ class CacheStorageControlWrapper;
#if BUILDFLAG(ENABLE_LIBRARY_CDMS)
class CdmStorageDataModel;
class CdmStorageManager;
class MediaLicenseManager;
#endif // BUILDFLAG(ENABLE_LIBRARY_CDMS)
class CookieDeprecationLabelManagerImpl;
class CookieStoreManager;
@ -292,6 +293,9 @@ class CONTENT_EXPORT StoragePartitionImpl
const std::string& GetPartitionDomain() const;
AggregationService* GetAggregationService();
FontAccessManager* GetFontAccessManager();
#if BUILDFLAG(ENABLE_LIBRARY_CDMS)
MediaLicenseManager* GetMediaLicenseManager();
#endif // BUILDFLAG(ENABLE_LIBRARY_CDMS)
storage::SharedStorageManager* GetSharedStorageManager() override;
PrivateAggregationManager* GetPrivateAggregationManager();
@ -778,6 +782,10 @@ class CONTENT_EXPORT StoragePartitionImpl
std::unique_ptr<AggregationService> aggregation_service_;
#if BUILDFLAG(ENABLE_LIBRARY_CDMS)
std::unique_ptr<CdmStorageManager> cdm_storage_manager_;
// TODO(crbug.com/40272342): Remove MediaLicenseManager once migration has
// been completed.
std::unique_ptr<MediaLicenseManager> media_license_manager_;
#endif // BUILDFLAG(ENABLE_LIBRARY_CDMS)
// Owning pointer to the SharedStorageManager for this partition.

@ -3291,6 +3291,8 @@ test("content_unittests") {
"../browser/media/cdm_storage_database_unittest.cc",
"../browser/media/cdm_storage_manager_unittest.cc",
"../browser/media/key_system_support_impl_unittest.cc",
"../browser/media/media_license_manager_unittest.cc",
"../browser/media/media_license_storage_host_unittest.cc",
]
}

@ -16,6 +16,7 @@ const QuotaClientTypes& AllQuotaClientTypes() {
QuotaClientType::kServiceWorkerCache,
QuotaClientType::kServiceWorker,
QuotaClientType::kBackgroundFetch,
QuotaClientType::kMediaLicense,
}};
return *all;
}

@ -21,6 +21,7 @@ enum class QuotaClientType {
kServiceWorkerCache = 4,
kServiceWorker = 5,
kBackgroundFetch = 6,
kMediaLicense = 7,
};
// Set of QuotaClientType values.

@ -30,6 +30,8 @@ base::FilePath CreateClientBucketPath(const base::FilePath& profile_path,
return bucket_directory.Append(kCacheStorageDirectory);
case QuotaClientType::kServiceWorker:
return bucket_directory.Append(kScriptCacheDirectory);
case QuotaClientType::kMediaLicense:
return bucket_directory.Append(kMediaLicenseDirectory);
case QuotaClientType::kDatabase:
NOTREACHED() << "Unsupported QuotaClientType";
}

@ -305,6 +305,13 @@ void UsageTracker::AccumulateClientUsageWithBreakdown(
case QuotaClientType::kBackgroundFetch:
info->usage_breakdown->backgroundFetch += total_usage;
break;
case QuotaClientType::kMediaLicense:
// Media license data does not count against quota and should always
// report 0 usage.
// TODO(crbug.com/40218094): Consider counting media license data against
// quota.
DCHECK_EQ(total_usage, 0);
break;
}
std::move(barrier_callback).Run();

@ -1191,6 +1191,17 @@ chromium-metrics-reviews@google.com.
<int value="12" label="Exit picture in picture"/>
</enum>
<enum name="MediaLicenseStorageHostOpenError">
<int value="0" label="InvalidBucket"/>
<int value="1" label="kNoFileSpecified"/>
<int value="2" label="kInvalidFileName"/>
<int value="3" label="kDatabaseOpenError"/>
<int value="4" label="kBucketNotFound"/>
<int value="5" label="kDatabaseRazeError"/>
<int value="6" label="kSQLExecutionError"/>
<int value="7" label="kBucketLocatorError"/>
</enum>
<enum name="MediaLoadType">
<int value="0" label="URL"/>
<int value="1" label="MediaSource"/>

@ -2916,6 +2916,146 @@ chromium-metrics-reviews@google.com.
</summary>
</histogram>
<histogram
name="Media.EME.MediaLicenseDatabase.WriteFile.FullDiskDataSizeBytes"
units="bytes" expires_after="2025-02-27">
<owner>vpasupathy@chromium.org</owner>
<owner>media-dev-uma@chromium.org</owner>
<summary>
This UMA records the bytes of the data that fail to be written to the
MediaLicenseDatabase during a full disk error. This histogram is logged when
a write fails with a full disk error.
</summary>
</histogram>
<histogram name="Media.EME.MediaLicenseDatabaseCreateDirectoryError"
enum="PlatformFileError" expires_after="2025-02-27">
<owner>vpasupathy@chromium.org</owner>
<owner>media-dev-uma@chromium.org</owner>
<summary>
Platform File specific errors reported while attempting to open the Media
License Database.
</summary>
</histogram>
<histogram name="Media.EME.MediaLicenseDatabaseOpenQuotaError"
enum="QuotaError" expires_after="2025-02-27">
<owner>vpasupathy@chromium.org</owner>
<owner>media-dev-uma@chromium.org</owner>
<summary>
Quota errors reported while attempting to unsuccessfully open the Media
License Database.
</summary>
</histogram>
<histogram name="Media.EME.MediaLicenseDatabaseOpenSQLiteError"
enum="SqliteLoggedResultCode" expires_after="2025-02-27">
<owner>vpasupathy@chromium.org</owner>
<owner>media-dev-uma@chromium.org</owner>
<summary>
SQLite specific errors reported while attempting to unsuccessfully open the
Media License Database.
</summary>
</histogram>
<histogram name="Media.EME.MediaLicenseDatabaseSQLiteError"
enum="SqliteLoggedResultCode" expires_after="2025-02-27">
<owner>vpasupathy@chromium.org</owner>
<owner>media-dev-uma@chromium.org</owner>
<summary>
SQLite specific errors reported while opening and using the Media License
Database.
</summary>
</histogram>
<histogram name="Media.EME.MediaLicenseDatabaseSQLiteError.DeleteFile"
enum="SqliteLoggedResultCode" expires_after="2025-02-27">
<owner>vpasupathy@chromium.org</owner>
<owner>media-dev-uma@chromium.org</owner>
<summary>
This UMA records SQLite specific errors that occur when deleting a file from
the Media License Database. This UMA is recorded on every error for this
operation.
</summary>
</histogram>
<histogram name="Media.EME.MediaLicenseDatabaseSQLiteError.QueryPageCount"
enum="SqliteLoggedResultCode" expires_after="2025-02-27">
<owner>vpasupathy@chromium.org</owner>
<owner>media-dev-uma@chromium.org</owner>
<summary>
This UMA records SQLite specific errors that occur when querying for
page_count from the Media License Database. This UMA is recorded on every
error for this operation.
</summary>
</histogram>
<histogram name="Media.EME.MediaLicenseDatabaseSQLiteError.QueryPageSize"
enum="SqliteLoggedResultCode" expires_after="2025-02-27">
<owner>vpasupathy@chromium.org</owner>
<owner>media-dev-uma@chromium.org</owner>
<summary>
This UMA records SQLite specific errors that occur when querying for page
size from the Media License Database. This UMA is recorded on every error
for this operation.
</summary>
</histogram>
<histogram name="Media.EME.MediaLicenseDatabaseSQLiteError.ReadFile"
enum="SqliteLoggedResultCode" expires_after="2025-02-27">
<owner>vpasupathy@chromium.org</owner>
<owner>media-dev-uma@chromium.org</owner>
<summary>
This UMA records SQLite specific errors that occur when reading a file from
the Media License Database. This UMA is recorded on every error for this
operation.
</summary>
</histogram>
<histogram name="Media.EME.MediaLicenseDatabaseSQLiteError.WriteFile"
enum="SqliteLoggedResultCode" expires_after="2025-02-27">
<owner>vpasupathy@chromium.org</owner>
<owner>media-dev-uma@chromium.org</owner>
<summary>
This UMA records SQLite specific errors that occur when writing a file to
the Media License Database. This UMA is recorded on every error for this
operation.
</summary>
</histogram>
<histogram name="Media.EME.MediaLicenseStorageHost.CurrentDatabaseUsageKB"
units="KB" expires_after="2025-02-27">
<owner>vpasupathy@chromium.org</owner>
<owner>media-dev-uma@chromium.org</owner>
<summary>
This UMA records the current MediaLicenseDatabase size in KB. This UMA is
recorded only once, on the first read, to retrieve usage levels of the
MediaLicenseDatabase.
</summary>
</histogram>
<histogram name="Media.EME.MediaLicenseStorageHostOpenError.Incognito"
enum="MediaLicenseStorageHostOpenError" expires_after="2025-02-27">
<owner>vpasupathy@chromium.org</owner>
<owner>media-dev-uma@chromium.org</owner>
<summary>
General errors reported while attempting to open the MediaLicenseDatabase.
This UMA further breaks down the UMA above by only reporting errors if the
media license storage host error occurs during an incognito process.
</summary>
</histogram>
<histogram name="Media.EME.MediaLicenseStorageHostOpenError.NotIncognito"
enum="MediaLicenseStorageHostOpenError" expires_after="2025-02-27">
<owner>vpasupathy@chromium.org</owner>
<owner>media-dev-uma@chromium.org</owner>
<summary>
General errors reported while attempting to open the MediaLicenseDatabase.
This UMA further breaks down the UMA above by only reporting errors if the
media license storage host error occurs during a non incognito process.
</summary>
</histogram>
<histogram name="Media.EME.MojoCdm.ConnectionError"
enum="BooleanConnectionError" expires_after="2025-02-27">
<owner>xhwang@chromium.org</owner>