blob: df99ccdde2fbc4e27a994a34b3f95cb9ee93a18f [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/common/media/cdm_manifest.h"
#include <stdint.h>
#include <memory>
#include <string>
#include <vector>
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/json/json_file_value_serializer.h"
#include "base/stl_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/values.h"
#include "base/version.h"
#include "content/public/common/cdm_info.h"
#include "extensions/common/manifest_constants.h"
#include "media/cdm/api/content_decryption_module.h"
#include "media/cdm/supported_cdm_versions.h"
#include "testing/gtest/include/gtest/gtest.h"
using content::CdmCapability;
namespace {
// These names must match what is used in cdm_manifest.cc.
const char kCdmModuleVersionsName[] = "x-cdm-module-versions";
const char kCdmInterfaceVersionsName[] = "x-cdm-interface-versions";
const char kCdmHostVersionsName[] = "x-cdm-host-versions";
const char kCdmCodecsListName[] = "x-cdm-codecs";
const char kCdmPersistentLicenseSupportName[] =
"x-cdm-persistent-license-support";
const char kCdmSupportedEncryptionSchemesName[] =
"x-cdm-supported-encryption-schemes";
const char kCdmSupportedCdmProxyProtocolsName[] =
"x-cdm-supported-cdm-proxy-protocols";
// Version checking does change over time. Deriving these values from constants
// in the code to ensure they change when the CDM interface changes.
// |kSupportedCdmInterfaceVersion| and |kSupportedCdmHostVersion| are the
// minimum versions supported. There may be versions after them that are also
// supported.
constexpr int kSupportedCdmModuleVersion = CDM_MODULE_VERSION;
constexpr int kSupportedCdmInterfaceVersion =
media::kSupportedCdmInterfaceVersions[0].version;
static_assert(media::kSupportedCdmInterfaceVersions[0].enabled,
"kSupportedCdmInterfaceVersion is not enabled by default.");
constexpr int kSupportedCdmHostVersion = media::kMinSupportedCdmHostVersion;
// Make a string of the values from 0 up to and including |item|.
std::string MakeStringList(int item) {
DCHECK_GT(item, 0);
std::vector<std::string> parts;
for (int i = 0; i <= item; ++i) {
parts.push_back(base::NumberToString(i));
}
return base::JoinString(parts, ",");
}
base::Value MakeListValue(const std::string& item) {
base::Value list(base::Value::Type::LIST);
list.GetList().push_back(base::Value(item));
return list;
}
base::Value MakeListValue(const std::string& item1, const std::string& item2) {
base::Value list(base::Value::Type::LIST);
list.GetList().push_back(base::Value(item1));
list.GetList().push_back(base::Value(item2));
return list;
}
// Create a default manifest with valid values for all entries.
base::Value DefaultManifest() {
base::Value dict(base::Value::Type::DICTIONARY);
dict.SetStringKey(kCdmCodecsListName, "vp8,vp9.0,av01");
dict.SetBoolKey(kCdmPersistentLicenseSupportName, true);
dict.SetKey(kCdmSupportedEncryptionSchemesName,
MakeListValue("cenc", "cbcs"));
dict.SetKey(kCdmSupportedCdmProxyProtocolsName, MakeListValue("intel"));
// The following are dependent on what the current code supports.
EXPECT_TRUE(media::IsSupportedCdmModuleVersion(kSupportedCdmModuleVersion));
EXPECT_TRUE(media::IsSupportedAndEnabledCdmInterfaceVersion(
kSupportedCdmInterfaceVersion));
EXPECT_TRUE(media::IsSupportedCdmHostVersion(kSupportedCdmHostVersion));
dict.SetStringKey(kCdmModuleVersionsName,
base::NumberToString(kSupportedCdmModuleVersion));
dict.SetStringKey(kCdmInterfaceVersionsName,
base::NumberToString(kSupportedCdmInterfaceVersion));
dict.SetStringKey(kCdmHostVersionsName,
base::NumberToString(kSupportedCdmHostVersion));
return dict;
}
void CheckCodecs(const std::vector<media::VideoCodec>& actual,
const std::vector<media::VideoCodec>& expected) {
EXPECT_EQ(expected.size(), actual.size());
for (const auto& codec : expected) {
EXPECT_TRUE(base::Contains(actual, codec));
}
}
void CheckEncryptionSchemes(
const base::flat_set<media::EncryptionScheme>& actual,
const std::vector<media::EncryptionScheme>& expected) {
EXPECT_EQ(expected.size(), actual.size());
for (const auto& encryption_scheme : expected) {
EXPECT_TRUE(base::Contains(actual, encryption_scheme));
}
}
void CheckSessionTypes(const base::flat_set<media::CdmSessionType>& actual,
const std::vector<media::CdmSessionType>& expected) {
EXPECT_EQ(expected.size(), actual.size());
for (const auto& session_type : expected) {
EXPECT_TRUE(base::Contains(actual, session_type));
}
}
void CheckProxyProtocols(
const base::flat_set<media::CdmProxy::Protocol>& actual,
const std::vector<media::CdmProxy::Protocol>& expected) {
EXPECT_EQ(expected.size(), actual.size());
for (const auto& proxy_protocol : expected) {
EXPECT_TRUE(base::Contains(actual, proxy_protocol));
}
}
void WriteManifestToFile(const base::Value& manifest,
const base::FilePath& file_path) {
EXPECT_FALSE(base::PathExists(file_path));
JSONFileValueSerializer serializer(file_path);
EXPECT_TRUE(serializer.Serialize(manifest));
EXPECT_TRUE(base::PathExists(file_path));
}
} // namespace
TEST(CdmManifestTest, IsCompatibleWithChrome) {
base::Value manifest(DefaultManifest());
EXPECT_TRUE(IsCdmManifestCompatibleWithChrome(manifest));
}
TEST(CdmManifestTest, InCompatibleModuleVersion) {
const int kUnsupportedModuleVersion = 0;
EXPECT_FALSE(media::IsSupportedCdmModuleVersion(kUnsupportedModuleVersion));
auto manifest = DefaultManifest();
manifest.SetStringKey(kCdmModuleVersionsName,
base::NumberToString(kUnsupportedModuleVersion));
EXPECT_FALSE(IsCdmManifestCompatibleWithChrome(std::move(manifest)));
}
TEST(CdmManifestTest, InCompatibleInterfaceVersion) {
const int kUnsupportedInterfaceVersion = kSupportedCdmInterfaceVersion - 1;
EXPECT_FALSE(media::IsSupportedAndEnabledCdmInterfaceVersion(
kUnsupportedInterfaceVersion));
auto manifest = DefaultManifest();
manifest.SetStringKey(kCdmInterfaceVersionsName,
base::NumberToString(kUnsupportedInterfaceVersion));
EXPECT_FALSE(IsCdmManifestCompatibleWithChrome(std::move(manifest)));
}
TEST(CdmManifestTest, InCompatibleHostVersion) {
const int kUnsupportedHostVersion = kSupportedCdmHostVersion - 1;
EXPECT_FALSE(media::IsSupportedCdmHostVersion(kUnsupportedHostVersion));
auto manifest = DefaultManifest();
manifest.SetStringKey(kCdmHostVersionsName,
base::NumberToString(kUnsupportedHostVersion));
EXPECT_FALSE(IsCdmManifestCompatibleWithChrome(std::move(manifest)));
}
TEST(CdmManifestTest, IsCompatibleWithMultipleValues) {
auto manifest = DefaultManifest();
manifest.SetStringKey(kCdmModuleVersionsName,
MakeStringList(kSupportedCdmModuleVersion));
manifest.SetStringKey(kCdmInterfaceVersionsName,
MakeStringList(kSupportedCdmInterfaceVersion));
manifest.SetStringKey(kCdmHostVersionsName,
MakeStringList(kSupportedCdmHostVersion));
EXPECT_TRUE(IsCdmManifestCompatibleWithChrome(std::move(manifest)));
}
TEST(CdmManifestTest, ValidManifest) {
auto manifest = DefaultManifest();
CdmCapability capability;
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckCodecs(capability.video_codecs,
{media::VideoCodec::kCodecVP8, media::VideoCodec::kCodecVP9,
media::VideoCodec::kCodecAV1});
CheckEncryptionSchemes(
capability.encryption_schemes,
{media::EncryptionScheme::kCenc, media::EncryptionScheme::kCbcs});
CheckSessionTypes(capability.session_types,
{media::CdmSessionType::kTemporary,
media::CdmSessionType::kPersistentLicense});
CheckProxyProtocols(capability.cdm_proxy_protocols,
{media::CdmProxy::Protocol::kIntel});
}
TEST(CdmManifestTest, EmptyManifest) {
base::Value manifest(base::Value::Type::DICTIONARY);
CdmCapability capability;
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckCodecs(capability.video_codecs, {});
CheckEncryptionSchemes(capability.encryption_schemes,
{media::EncryptionScheme::kCenc});
CheckSessionTypes(capability.session_types,
{media::CdmSessionType::kTemporary});
CheckProxyProtocols(capability.cdm_proxy_protocols, {});
}
TEST(CdmManifestTest, ManifestCodecs) {
auto manifest = DefaultManifest();
// Try each valid value individually.
{
CdmCapability capability;
manifest.SetStringKey(kCdmCodecsListName, "vp8");
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckCodecs(capability.video_codecs, {media::VideoCodec::kCodecVP8});
}
{
CdmCapability capability;
manifest.SetStringKey(kCdmCodecsListName, "vp9.0");
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckCodecs(capability.video_codecs, {media::VideoCodec::kCodecVP9});
EXPECT_FALSE(capability.supports_vp9_profile2);
}
{
CdmCapability capability;
manifest.SetStringKey(kCdmCodecsListName, "vp09");
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckCodecs(capability.video_codecs, {media::VideoCodec::kCodecVP9});
EXPECT_TRUE(capability.supports_vp9_profile2);
}
{
CdmCapability capability;
manifest.SetStringKey(kCdmCodecsListName, "av01");
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckCodecs(capability.video_codecs, {media::VideoCodec::kCodecAV1});
}
#if BUILDFLAG(USE_PROPRIETARY_CODECS)
{
CdmCapability capability;
manifest.SetStringKey(kCdmCodecsListName, "avc1");
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckCodecs(capability.video_codecs, {media::VideoCodec::kCodecH264});
}
#endif
{
// Try list of everything (except proprietary codecs).
CdmCapability capability;
manifest.SetStringKey(kCdmCodecsListName, "vp8,vp9.0,vp09,av01");
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
// Note that kCodecVP9 is returned twice in the list.
CheckCodecs(capability.video_codecs,
{media::VideoCodec::kCodecVP8, media::VideoCodec::kCodecVP9,
media::VideoCodec::kCodecVP9, media::VideoCodec::kCodecAV1});
EXPECT_TRUE(capability.supports_vp9_profile2);
}
{
// Note that invalid codec values are simply skipped.
CdmCapability capability;
manifest.SetStringKey(kCdmCodecsListName, "invalid,av01");
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckCodecs(capability.video_codecs, {media::VideoCodec::kCodecAV1});
}
{
// Wrong types are an error.
CdmCapability capability;
manifest.SetBoolKey(kCdmCodecsListName, true);
EXPECT_FALSE(ParseCdmManifest(manifest, &capability));
}
{
// Missing entry is OK, but list is empty.
CdmCapability capability;
EXPECT_TRUE(manifest.RemoveKey(kCdmCodecsListName));
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckCodecs(capability.video_codecs, {});
}
}
TEST(CdmManifestTest, ManifestEncryptionSchemes) {
auto manifest = DefaultManifest();
// Try each valid value individually.
{
CdmCapability capability;
manifest.SetKey(kCdmSupportedEncryptionSchemesName, MakeListValue("cenc"));
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckEncryptionSchemes(capability.encryption_schemes,
{media::EncryptionScheme::kCenc});
}
{
CdmCapability capability;
manifest.SetKey(kCdmSupportedEncryptionSchemesName, MakeListValue("cbcs"));
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckEncryptionSchemes(capability.encryption_schemes,
{media::EncryptionScheme::kCbcs});
}
{
// Try multiple valid entries.
CdmCapability capability;
manifest.SetKey(kCdmSupportedEncryptionSchemesName,
MakeListValue("cenc", "cbcs"));
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckEncryptionSchemes(
capability.encryption_schemes,
{media::EncryptionScheme::kCenc, media::EncryptionScheme::kCbcs});
}
{
// Invalid encryption schemes are ignored. However, if value specified then
// there must be at least 1 valid value.
CdmCapability capability;
manifest.SetKey(kCdmSupportedEncryptionSchemesName,
MakeListValue("invalid"));
EXPECT_FALSE(ParseCdmManifest(manifest, &capability));
}
{
CdmCapability capability;
manifest.SetKey(kCdmSupportedEncryptionSchemesName,
MakeListValue("invalid", "cenc"));
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckEncryptionSchemes(capability.encryption_schemes,
{media::EncryptionScheme::kCenc});
}
{
// Wrong types are an error.
CdmCapability capability;
manifest.SetBoolKey(kCdmSupportedEncryptionSchemesName, true);
EXPECT_FALSE(ParseCdmManifest(manifest, &capability));
}
{
// Missing values default to "cenc".
CdmCapability capability;
EXPECT_TRUE(manifest.RemoveKey(kCdmSupportedEncryptionSchemesName));
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckEncryptionSchemes(capability.encryption_schemes,
{media::EncryptionScheme::kCenc});
}
}
TEST(CdmManifestTest, ManifestSessionTypes) {
auto manifest = DefaultManifest();
{
// Try false (persistent license not supported).
CdmCapability capability;
manifest.SetBoolKey(kCdmPersistentLicenseSupportName, false);
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckSessionTypes(capability.session_types,
{media::CdmSessionType::kTemporary});
}
{
// Try true (persistent license is supported).
CdmCapability capability;
manifest.SetBoolKey(kCdmPersistentLicenseSupportName, true);
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckSessionTypes(capability.session_types,
{media::CdmSessionType::kTemporary,
media::CdmSessionType::kPersistentLicense});
}
{
// Wrong types are an error.
CdmCapability capability;
manifest.SetStringKey(kCdmPersistentLicenseSupportName, "true");
EXPECT_FALSE(ParseCdmManifest(manifest, &capability));
}
{
// Missing values default to "temporary".
CdmCapability capability;
EXPECT_TRUE(manifest.RemoveKey(kCdmPersistentLicenseSupportName));
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckSessionTypes(capability.session_types,
{media::CdmSessionType::kTemporary});
}
}
TEST(CdmManifestTest, ManifestProxyProtocols) {
auto manifest = DefaultManifest();
{
// Try only supported value.
CdmCapability capability;
manifest.SetKey(kCdmSupportedCdmProxyProtocolsName, MakeListValue("intel"));
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckProxyProtocols(capability.cdm_proxy_protocols,
{media::CdmProxy::Protocol::kIntel});
}
{
// Unrecognized values are ignored.
CdmCapability capability;
manifest.SetKey(kCdmSupportedCdmProxyProtocolsName,
MakeListValue("unknown", "intel"));
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckProxyProtocols(capability.cdm_proxy_protocols,
{media::CdmProxy::Protocol::kIntel});
}
{
// Wrong types are an error.
CdmCapability capability;
manifest.SetStringKey(kCdmSupportedCdmProxyProtocolsName, "intel");
EXPECT_FALSE(ParseCdmManifest(manifest, &capability));
}
{
// Missing values are OK.
CdmCapability capability;
EXPECT_TRUE(manifest.RemoveKey(kCdmSupportedCdmProxyProtocolsName));
EXPECT_TRUE(ParseCdmManifest(manifest, &capability));
CheckProxyProtocols(capability.cdm_proxy_protocols, {});
}
}
TEST(CdmManifestTest, FileManifest) {
const char kVersion[] = "1.2.3.4";
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
auto manifest_path = temp_dir.GetPath().AppendASCII("manifest.json");
// Manifests read from a file also need a version.
auto manifest = DefaultManifest();
manifest.SetStringKey(extensions::manifest_keys::kVersion, kVersion);
WriteManifestToFile(manifest, manifest_path);
base::Version version;
CdmCapability capability;
EXPECT_TRUE(ParseCdmManifestFromPath(manifest_path, &version, &capability));
EXPECT_TRUE(version.IsValid());
EXPECT_EQ(version.GetString(), kVersion);
CheckCodecs(capability.video_codecs,
{media::VideoCodec::kCodecVP8, media::VideoCodec::kCodecVP9,
media::VideoCodec::kCodecAV1});
CheckEncryptionSchemes(
capability.encryption_schemes,
{media::EncryptionScheme::kCenc, media::EncryptionScheme::kCbcs});
CheckSessionTypes(capability.session_types,
{media::CdmSessionType::kTemporary,
media::CdmSessionType::kPersistentLicense});
CheckProxyProtocols(capability.cdm_proxy_protocols,
{media::CdmProxy::Protocol::kIntel});
}
TEST(CdmManifestTest, FileManifestNoVersion) {
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
auto manifest_path = temp_dir.GetPath().AppendASCII("manifest.json");
auto manifest = DefaultManifest();
WriteManifestToFile(manifest, manifest_path);
base::Version version;
CdmCapability capability;
EXPECT_FALSE(ParseCdmManifestFromPath(manifest_path, &version, &capability));
}
TEST(CdmManifestTest, FileManifestBadVersion) {
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
auto manifest_path = temp_dir.GetPath().AppendASCII("manifest.json");
auto manifest = DefaultManifest();
manifest.SetStringKey(extensions::manifest_keys::kVersion, "bad version");
WriteManifestToFile(manifest, manifest_path);
base::Version version;
CdmCapability capability;
EXPECT_FALSE(ParseCdmManifestFromPath(manifest_path, &version, &capability));
}
TEST(CdmManifestTest, FileManifestDoesNotExist) {
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
auto manifest_path = temp_dir.GetPath().AppendASCII("manifest.json");
base::Version version;
CdmCapability capability;
EXPECT_FALSE(ParseCdmManifestFromPath(manifest_path, &version, &capability));
}
TEST(CdmManifestTest, FileManifestEmpty) {
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
auto manifest_path = temp_dir.GetPath().AppendASCII("manifest.json");
base::Value manifest(base::Value::Type::DICTIONARY);
WriteManifestToFile(manifest, manifest_path);
base::Version version;
CdmCapability capability;
EXPECT_FALSE(ParseCdmManifestFromPath(manifest_path, &version, &capability));
}
TEST(CdmManifestTest, FileManifestLite) {
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
auto manifest_path = temp_dir.GetPath().AppendASCII("manifest.json");
// Only a version plus fields to satisfy compatibility are required in the
// manifest to parse correctly.
base::Value manifest(base::Value::Type::DICTIONARY);
manifest.SetStringKey(extensions::manifest_keys::kVersion, "1.2.3.4");
manifest.SetStringKey(kCdmModuleVersionsName,
base::NumberToString(kSupportedCdmModuleVersion));
manifest.SetStringKey(kCdmInterfaceVersionsName,
base::NumberToString(kSupportedCdmInterfaceVersion));
manifest.SetStringKey(kCdmHostVersionsName,
base::NumberToString(kSupportedCdmHostVersion));
WriteManifestToFile(manifest, manifest_path);
base::Version version;
CdmCapability capability;
EXPECT_TRUE(ParseCdmManifestFromPath(manifest_path, &version, &capability));
CheckCodecs(capability.video_codecs, {});
CheckEncryptionSchemes(capability.encryption_schemes,
{media::EncryptionScheme::kCenc});
CheckSessionTypes(capability.session_types,
{media::CdmSessionType::kTemporary});
CheckProxyProtocols(capability.cdm_proxy_protocols, {});
}
TEST(CdmManifestTest, FileManifestNotDictionary) {
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
auto manifest_path = temp_dir.GetPath().AppendASCII("manifest.json");
base::Value manifest("not a dictionary");
WriteManifestToFile(manifest, manifest_path);
base::Version version;
CdmCapability capability;
EXPECT_FALSE(ParseCdmManifestFromPath(manifest_path, &version, &capability));
}