| // 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 |