blob: e826fd962a36dfc54205bc38a2ae893056f42c29 [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "media/mojo/services/fuchsia_cdm_manager.h"
#include <fuchsia/media/drm/cpp/fidl.h>
#include <fuchsia/media/drm/cpp/fidl_test_base.h>
#include <lib/fidl/cpp/binding_set.h>
#include <lib/fidl/cpp/interface_request.h>
#include <lib/fpromise/promise.h>
#include <map>
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace media {
namespace {
namespace drm = ::fuchsia::media::drm;
using ::testing::_;
using ::testing::Eq;
using ::testing::Invoke;
using ::testing::SaveArg;
using ::testing::WithArgs;
// This is a mock for the Chromium media::ProvisionFetcher (and not Fuchsia's
// similarly named ProvisioningFetcher protocol).
class MockProvisionFetcher : public ProvisionFetcher {
public:
MockProvisionFetcher() = default;
~MockProvisionFetcher() override = default;
MOCK_METHOD(void,
Retrieve,
(const GURL& default_url,
const std::string& request_data,
ResponseCB response_cb),
(override));
};
std::unique_ptr<ProvisionFetcher> CreateMockProvisionFetcher() {
auto mock_provision_fetcher = std::make_unique<MockProvisionFetcher>();
ON_CALL(*mock_provision_fetcher, Retrieve(_, _, _))
.WillByDefault(WithArgs<2>(
Invoke([](ProvisionFetcher::ResponseCB response_callback) {
std::move(response_callback).Run(true, "response");
})));
return mock_provision_fetcher;
}
class MockKeySystem : public drm::testing::KeySystem_TestBase {
public:
MockKeySystem() = default;
~MockKeySystem() override = default;
drm::KeySystemHandle AddBinding() { return bindings_.AddBinding(this); }
fidl::BindingSet<drm::KeySystem>& bindings() { return bindings_; }
void NotImplemented_(const std::string& name) override { FAIL() << name; }
MOCK_METHOD(void,
AddDataStore,
(uint32_t data_store_id,
drm::DataStoreParams params,
AddDataStoreCallback callback),
(override));
MOCK_METHOD(
void,
CreateContentDecryptionModule2,
(uint32_t data_store_id,
fidl::InterfaceRequest<drm::ContentDecryptionModule> cdm_request),
(override));
private:
fidl::BindingSet<drm::KeySystem> bindings_;
};
class FuchsiaCdmManagerTest : public ::testing::Test {
public:
FuchsiaCdmManagerTest() { EXPECT_TRUE(temp_dir_.CreateUniqueTempDir()); }
std::unique_ptr<FuchsiaCdmManager> CreateFuchsiaCdmManager(
std::vector<base::StringPiece> key_systems,
absl::optional<uint64_t> cdm_data_quota_bytes = absl::nullopt) {
FuchsiaCdmManager::CreateKeySystemCallbackMap create_key_system_callbacks;
for (const base::StringPiece& name : key_systems) {
MockKeySystem& key_system = mock_key_systems_[name];
create_key_system_callbacks.emplace(
name, base::BindRepeating(&MockKeySystem::AddBinding,
base::Unretained(&key_system)));
}
return std::make_unique<FuchsiaCdmManager>(
std::move(create_key_system_callbacks), temp_dir_.GetPath(),
cdm_data_quota_bytes);
}
protected:
using MockKeySystemMap = std::map<base::StringPiece, MockKeySystem>;
MockKeySystem& mock_key_system(const base::StringPiece& key_system_name) {
return mock_key_systems_[key_system_name];
}
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::MainThreadType::IO};
MockKeySystemMap mock_key_systems_;
base::ScopedTempDir temp_dir_;
};
TEST_F(FuchsiaCdmManagerTest, NoKeySystems) {
std::unique_ptr<FuchsiaCdmManager> cdm_manager = CreateFuchsiaCdmManager({});
base::RunLoop run_loop;
drm::ContentDecryptionModulePtr cdm_ptr;
cdm_ptr.set_error_handler([&](zx_status_t status) {
EXPECT_EQ(status, ZX_ERR_NOT_FOUND);
run_loop.Quit();
});
cdm_manager->CreateAndProvision(
"com.key_system", url::Origin(),
base::BindRepeating(&CreateMockProvisionFetcher), cdm_ptr.NewRequest());
run_loop.Run();
}
TEST_F(FuchsiaCdmManagerTest, CreateAndProvision) {
constexpr char kKeySystem[] = "com.key_system.a";
std::unique_ptr<FuchsiaCdmManager> cdm_manager =
CreateFuchsiaCdmManager({kKeySystem});
base::RunLoop run_loop;
drm::ContentDecryptionModulePtr cdm_ptr;
cdm_ptr.set_error_handler([&](zx_status_t status) { run_loop.Quit(); });
uint32_t added_data_store_id = 0;
uint32_t cdm_data_store_id = 0;
EXPECT_CALL(mock_key_system(kKeySystem), AddDataStore(_, _, _))
.WillOnce(WithArgs<0, 2>(
Invoke([&](uint32_t data_store_id,
drm::KeySystem::AddDataStoreCallback callback) {
added_data_store_id = data_store_id;
callback(fpromise::ok());
})));
EXPECT_CALL(mock_key_system(kKeySystem), CreateContentDecryptionModule2(_, _))
.WillOnce(SaveArg<0>(&cdm_data_store_id));
cdm_manager->CreateAndProvision(
kKeySystem, url::Origin(),
base::BindRepeating(&CreateMockProvisionFetcher), cdm_ptr.NewRequest());
run_loop.Run();
EXPECT_NE(added_data_store_id, 0u);
EXPECT_EQ(added_data_store_id, cdm_data_store_id);
}
TEST_F(FuchsiaCdmManagerTest, RecreateAfterDisconnect) {
constexpr char kKeySystem[] = "com.key_system.a";
std::unique_ptr<FuchsiaCdmManager> cdm_manager =
CreateFuchsiaCdmManager({kKeySystem});
uint32_t added_data_store_id = 0;
EXPECT_CALL(mock_key_system(kKeySystem), AddDataStore(_, _, _))
.WillOnce(WithArgs<0, 2>(
Invoke([&](uint32_t data_store_id,
drm::KeySystem::AddDataStoreCallback callback) {
added_data_store_id = data_store_id;
callback(fpromise::ok());
})));
// Create a CDM to force a KeySystem binding
base::RunLoop create_run_loop;
drm::ContentDecryptionModulePtr cdm_ptr;
cdm_ptr.set_error_handler(
[&](zx_status_t status) { create_run_loop.Quit(); });
cdm_manager->CreateAndProvision(
kKeySystem, url::Origin(),
base::BindRepeating(&CreateMockProvisionFetcher), cdm_ptr.NewRequest());
create_run_loop.Run();
ASSERT_EQ(mock_key_system(kKeySystem).bindings().size(), 1u);
// Close the KeySystem's bindings and wait until empty
base::RunLoop disconnect_run_loop;
cdm_manager->set_on_key_system_disconnect_for_test_callback(
base::BindLambdaForTesting([&](const std::string& key_system_name) {
if (key_system_name == kKeySystem) {
disconnect_run_loop.Quit();
}
}));
mock_key_system(kKeySystem).bindings().CloseAll();
disconnect_run_loop.Run();
ASSERT_EQ(mock_key_system(kKeySystem).bindings().size(), 0u);
EXPECT_CALL(mock_key_system(kKeySystem),
AddDataStore(Eq(added_data_store_id), _, _))
.WillOnce(
WithArgs<2>(Invoke([](drm::KeySystem::AddDataStoreCallback callback) {
callback(fpromise::ok());
})));
base::RunLoop recreate_run_loop;
cdm_ptr.set_error_handler(
[&](zx_status_t status) { recreate_run_loop.Quit(); });
cdm_manager->CreateAndProvision(
kKeySystem, url::Origin(),
base::BindRepeating(&CreateMockProvisionFetcher), cdm_ptr.NewRequest());
recreate_run_loop.Run();
EXPECT_EQ(mock_key_system(kKeySystem).bindings().size(), 1u);
}
TEST_F(FuchsiaCdmManagerTest, SameOriginShareDataStore) {
constexpr char kKeySystem[] = "com.key_system.a";
std::unique_ptr<FuchsiaCdmManager> cdm_manager =
CreateFuchsiaCdmManager({kKeySystem});
base::RunLoop run_loop;
drm::ContentDecryptionModulePtr cdm1, cdm2;
auto error_handler = [&](zx_status_t status) {
EXPECT_EQ(status, ZX_ERR_PEER_CLOSED);
if (!cdm1.is_bound() && !cdm2.is_bound()) {
run_loop.Quit();
}
};
cdm1.set_error_handler(error_handler);
cdm2.set_error_handler(error_handler);
EXPECT_CALL(mock_key_system(kKeySystem), AddDataStore(Eq(1u), _, _))
.WillOnce(
WithArgs<2>(Invoke([](drm::KeySystem::AddDataStoreCallback callback) {
callback(fpromise::ok());
})));
EXPECT_CALL(mock_key_system(kKeySystem),
CreateContentDecryptionModule2(Eq(1u), _))
.Times(2);
url::Origin origin = url::Origin::Create(GURL("http://origin_a.com"));
cdm_manager->CreateAndProvision(
kKeySystem, origin, base::BindRepeating(&CreateMockProvisionFetcher),
cdm1.NewRequest());
cdm_manager->CreateAndProvision(
kKeySystem, origin, base::BindRepeating(&CreateMockProvisionFetcher),
cdm2.NewRequest());
run_loop.Run();
}
TEST_F(FuchsiaCdmManagerTest, DifferentOriginDoNotShareDataStore) {
constexpr char kKeySystem[] = "com.key_system.a";
std::unique_ptr<FuchsiaCdmManager> cdm_manager =
CreateFuchsiaCdmManager({kKeySystem});
base::RunLoop run_loop;
drm::ContentDecryptionModulePtr cdm1, cdm2;
auto error_handler = [&](zx_status_t status) {
EXPECT_EQ(status, ZX_ERR_PEER_CLOSED);
if (!cdm1.is_bound() && !cdm2.is_bound()) {
run_loop.Quit();
}
};
cdm1.set_error_handler(error_handler);
cdm2.set_error_handler(error_handler);
EXPECT_CALL(mock_key_system(kKeySystem), AddDataStore(Eq(1u), _, _))
.WillOnce(
WithArgs<2>(Invoke([](drm::KeySystem::AddDataStoreCallback callback) {
callback(fpromise::ok());
})));
EXPECT_CALL(mock_key_system(kKeySystem), AddDataStore(Eq(2u), _, _))
.WillOnce(
WithArgs<2>(Invoke([](drm::KeySystem::AddDataStoreCallback callback) {
callback(fpromise::ok());
})));
EXPECT_CALL(mock_key_system(kKeySystem),
CreateContentDecryptionModule2(Eq(1u), _))
.Times(1);
EXPECT_CALL(mock_key_system(kKeySystem),
CreateContentDecryptionModule2(Eq(2u), _))
.Times(1);
url::Origin origin_a = url::Origin::Create(GURL("http://origin_a.com"));
url::Origin origin_b = url::Origin::Create(GURL("http://origin_b.com"));
cdm_manager->CreateAndProvision(
kKeySystem, origin_a, base::BindRepeating(&CreateMockProvisionFetcher),
cdm1.NewRequest());
cdm_manager->CreateAndProvision(
kKeySystem, origin_b, base::BindRepeating(&CreateMockProvisionFetcher),
cdm2.NewRequest());
run_loop.Run();
}
void CreateDummyCdmDirectory(const base::FilePath& cdm_data_path,
base::StringPiece origin,
base::StringPiece key_system,
uint64_t size) {
const base::FilePath path = cdm_data_path.Append(origin).Append(key_system);
CHECK(base::CreateDirectory(path));
if (size) {
std::vector<uint8_t> zeroes(size);
CHECK(base::WriteFile(path.Append("zeroes"), zeroes));
}
}
// Verify that the least recently used CDM data directories are removed, until
// the quota is met. Also verify that old directories are removed regardless
// of whether they are empty or not.
TEST_F(FuchsiaCdmManagerTest, CdmDataQuotaBytes) {
constexpr uint64_t kTestQuotaBytes = 1024;
constexpr char kOriginDirectory1[] = "origin1";
constexpr char kOriginDirectory2[] = "origin2";
constexpr char kKeySystemDirectory1[] = "key_system1";
constexpr char kKeySystemDirectory2[] = "key_system2";
constexpr char kEmptyKeySystemDirectory[] = "empty_key_system";
// Create fake CDM data directories for two origins, each with two key
// systems, with each directory consuming 50% of the total quota, so that
// two directories must be removed to meet quota.
// Create least-recently-used directories & their contents.
const base::FilePath temp_path = temp_dir_.GetPath();
CreateDummyCdmDirectory(temp_path, kOriginDirectory1, kKeySystemDirectory1,
kTestQuotaBytes / 2);
CreateDummyCdmDirectory(temp_path, kOriginDirectory2, kKeySystemDirectory2,
kTestQuotaBytes / 2);
CreateDummyCdmDirectory(temp_path, kOriginDirectory1,
kEmptyKeySystemDirectory, 0);
// Sleep to account for coarse-grained filesystem timestamps.
base::PlatformThread::Sleep(base::Seconds(1));
// Create the recently-used directories.
CreateDummyCdmDirectory(temp_path, kOriginDirectory1, kKeySystemDirectory2,
kTestQuotaBytes / 2);
CreateDummyCdmDirectory(temp_path, kOriginDirectory2, kKeySystemDirectory1,
kTestQuotaBytes / 2);
CreateDummyCdmDirectory(temp_path, kOriginDirectory2,
kEmptyKeySystemDirectory, 0);
// Create the CDM manager, to run the data directory quota enforcement.
std::unique_ptr<FuchsiaCdmManager> cdm_manager =
CreateFuchsiaCdmManager({}, kTestQuotaBytes);
// Use a CreateAndProvision() request as a proxy to wait for quota enforcement
// to finish being applied.
base::RunLoop run_loop;
drm::ContentDecryptionModulePtr cdm_ptr;
cdm_ptr.set_error_handler([&](zx_status_t status) {
EXPECT_EQ(status, ZX_ERR_NOT_FOUND);
run_loop.Quit();
});
cdm_manager->CreateAndProvision(
"com.key_system", url::Origin(),
base::BindRepeating(&CreateMockProvisionFetcher), cdm_ptr.NewRequest());
run_loop.Run();
EXPECT_FALSE(base::PathExists(
temp_path.Append(kOriginDirectory1).Append(kKeySystemDirectory1)));
EXPECT_FALSE(base::PathExists(
temp_path.Append(kOriginDirectory2).Append(kKeySystemDirectory2)));
EXPECT_TRUE(base::PathExists(
temp_path.Append(kOriginDirectory1).Append(kKeySystemDirectory2)));
EXPECT_TRUE(base::PathExists(
temp_path.Append(kOriginDirectory2).Append(kKeySystemDirectory1)));
// Empty directories are currently always treated as old, causing them all to
// be deleted if the CDM data directory exceeds its quota.
EXPECT_FALSE(base::PathExists(
temp_path.Append(kOriginDirectory1).Append(kEmptyKeySystemDirectory)));
EXPECT_FALSE(base::PathExists(
temp_path.Append(kOriginDirectory2).Append(kEmptyKeySystemDirectory)));
}
// Verify that if all key-system sub-directories for a given origin have been
// deleted then the origin's directory is also deleted.
TEST_F(FuchsiaCdmManagerTest, EmptyOriginDirectory) {
constexpr uint64_t kTestQuotaBytes = 1024;
constexpr char kInactiveOriginDirectory[] = "origin1";
constexpr char kActiveOriginDirectory[] = "origin2";
constexpr char kKeySystemDirectory1[] = "key_system1";
constexpr char kKeySystemDirectory2[] = "key_system2";
// Create dummy data for an inactive origin.
const base::FilePath temp_path = temp_dir_.GetPath();
CreateDummyCdmDirectory(temp_path, kInactiveOriginDirectory,
kKeySystemDirectory1, kTestQuotaBytes / 2);
CreateDummyCdmDirectory(temp_path, kInactiveOriginDirectory,
kKeySystemDirectory2, kTestQuotaBytes / 2);
// Sleep to account for coarse-grained filesystem timestamps.
base::PlatformThread::Sleep(base::Seconds(1));
// Create dummy data for a recently-used, active origin.
CreateDummyCdmDirectory(temp_path, kActiveOriginDirectory,
kKeySystemDirectory2, kTestQuotaBytes);
// Create the CDM manager, to run the data directory quota enforcement.
std::unique_ptr<FuchsiaCdmManager> cdm_manager =
CreateFuchsiaCdmManager({}, kTestQuotaBytes);
// Use a CreateAndProvision() request as a proxy to wait for quota enforcement
// to finish being applied.
base::RunLoop run_loop;
drm::ContentDecryptionModulePtr cdm_ptr;
cdm_ptr.set_error_handler([&](zx_status_t status) {
EXPECT_EQ(status, ZX_ERR_NOT_FOUND);
run_loop.Quit();
});
cdm_manager->CreateAndProvision(
"com.key_system", url::Origin(),
base::BindRepeating(&CreateMockProvisionFetcher), cdm_ptr.NewRequest());
run_loop.Run();
EXPECT_FALSE(base::PathExists(temp_path.Append(kInactiveOriginDirectory)));
EXPECT_TRUE(base::PathExists(
temp_path.Append(kActiveOriginDirectory).Append(kKeySystemDirectory2)));
}
} // namespace
} // namespace media