blob: e0331c15ddddf91bfa7daffaca949d12a081fc91 [file] [log] [blame]
// 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 testing::WithParamInterface<std::tuple<bool, bool>> {
public:
CdmStorageTest()
: RenderViewHostTestHarness(
content::BrowserTaskEnvironment::REAL_IO_THREAD) {
// Enable the 'kCdmStorageDatabaseMigration' feature and the
// 'kCdmStorageDatabase' feature so that 'cdm_storage_manager()' doesn't
// return null.
std::vector<base::test::FeatureRef> feature_list = {};
if (std::get<0>(GetParam())) {
feature_list.emplace_back(features::kCdmStorageDatabase);
}
if (std::get<1>(GetParam())) {
feature_list.emplace_back(features::kCdmStorageDatabaseMigration);
}
scoped_feature_list_.InitWithFeatures(feature_list, {});
}
protected:
void SetUp() final {
RenderViewHostTestHarness::SetUp();
rfh_ = web_contents()->GetPrimaryMainFrame();
RenderFrameHostTester::For(rfh_)->InitializeRenderFrameIfNeeded();
SimulateNavigation(&rfh_, GURL(kTestOrigin));
if (base::FeatureList::IsEnabled(features::kCdmStorageDatabase) &&
base::FeatureList::IsEnabled(features::kCdmStorageDatabaseMigration)) {
media_license_manager()->set_cdm_storage_manager(cdm_storage_manager());
}
if (base::FeatureList::IsEnabled(features::kCdmStorageDatabase) &&
!base::FeatureList::IsEnabled(features::kCdmStorageDatabaseMigration)) {
cdm_storage_manager()->OpenCdmStorage(
CdmStorageBindingContext(
blink::StorageKey::CreateFromStringForTesting(kTestOrigin),
kTestCdmType),
cdm_storage_.BindNewPipeAndPassReceiver());
} else {
media_license_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_P(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_P(CdmStorageTest, InvalidFileNameEmpty) {
const char kFileName[] = "";
mojo::AssociatedRemote<CdmFile> cdm_file;
EXPECT_FALSE(Open(kFileName, cdm_file));
ASSERT_FALSE(cdm_file.is_bound());
}
TEST_P(CdmStorageTest, InvalidFileNameStartWithUnderscore) {
const char kFileName[] = "_invalid";
mojo::AssociatedRemote<CdmFile> cdm_file;
EXPECT_FALSE(Open(kFileName, cdm_file));
ASSERT_FALSE(cdm_file.is_bound());
}
TEST_P(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_P(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_P(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_P(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_P(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_P(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_P(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_P(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_P(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);
// If 'kCdmStorageDatabase` is enabled, the Write() should have opulated in
// the CdmStorageDatabase. Else, we should check that CdmStorageManager is a
// nullptr since it would not have been created.
if (base::FeatureList::IsEnabled(features::kCdmStorageDatabase)) {
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);
} else {
EXPECT_THAT(static_cast<StoragePartitionImpl*>(rfh_->GetStoragePartition())
->GetCdmStorageDataModel(),
testing::IsNull());
}
// 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());
if (base::FeatureList::IsEnabled(features::kCdmStorageDatabase)) {
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());
} else {
EXPECT_THAT(static_cast<StoragePartitionImpl*>(rfh_->GetStoragePartition())
->GetCdmStorageDataModel(),
testing::IsNull());
}
}
INSTANTIATE_TEST_SUITE_P(,
CdmStorageTest,
/*are_kCdmStorageDatabaseMigration_features_enabled=*/
testing::Combine(testing::Bool(), testing::Bool()));
} // namespace content