Files
ce_cdm/core/src/cdm_usage_table.cpp
2024-03-28 19:15:22 -07:00

1440 lines
55 KiB
C++

// Copyright 2018 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine License
// Agreement.
#include "cdm_usage_table.h"
#include <algorithm>
#include <limits>
#include "cdm_random.h"
#include "crypto_session.h"
#include "license.h"
#include "log.h"
#include "wv_cdm_constants.h"
namespace wvcdm {
namespace {
using TableLock = std::unique_lock<std::mutex>;
const std::string kEmptyString;
const wvcdm::CdmKeySetId kDummyKeySetId = "DummyKsid";
constexpr int64_t kDefaultExpireDuration = 33 * 24 * 60 * 60; // 33 Days
// Fraction of table capacity of number of unexpired offline licenses
// before they are considered to be removed. This could occur if
// there are not enough expired offline or streaming licenses to
// remove. This threshold is set to prevent thrashing in the case that
// there are a very large number of unexpired offline licenses and few
// expired / streaming licenses (ie, number of unexpired licenses
// nears the capacity of the usage table).
constexpr double kLruUnexpiredThresholdFraction = 0.75;
// Maximum number of entries to be moved during a defrag operation.
// This is to prevent the system from stalling too long if the defrag
// occurs during an active application session.
constexpr size_t kMaxDefragEntryMoves = 5;
// Convert |license_message| -> SignedMessage -> License.
bool ParseLicenseFromLicenseMessage(const CdmKeyResponse& license_message,
video_widevine::License* license) {
using video_widevine::SignedMessage;
if (license == nullptr) {
LOGE("Output parameter |license| is null");
return false;
}
SignedMessage signed_license_response;
if (!signed_license_response.ParseFromString(license_message)) {
LOGW("Unabled to parse signed license response");
return false;
}
if (signed_license_response.type() != SignedMessage::LICENSE) {
LOGW("Unexpected signed message: type = %d, expected_type = %d",
static_cast<int>(signed_license_response.type()),
static_cast<int>(SignedMessage::LICENSE));
return false;
}
if (!signed_license_response.has_signature()) {
LOGW("License response message is not signed");
return false;
}
if (!license->ParseFromString(signed_license_response.msg())) {
LOGW("Failed to parse license");
return false;
}
return true;
}
bool RetrieveOfflineLicense(DeviceFiles* device_files,
const std::string& key_set_id,
CdmKeyResponse* license_message,
UsageEntryIndex* entry_index) {
if (device_files == nullptr) {
LOGE("DeviceFiles handle is null");
return false;
}
if (license_message == nullptr) {
LOGE("Output parameter |license_message| is null");
return false;
}
if (entry_index == nullptr) {
LOGE("Output parameter |entry_index| is null");
return false;
}
DeviceFiles::CdmLicenseData license_data;
DeviceFiles::ResponseType result = DeviceFiles::kNoError;
if (!device_files->RetrieveLicense(key_set_id, &license_data, &result)) {
LOGW("Failed to retrieve license: key_set_id = %s, result = %s",
IdToString(key_set_id), DeviceFiles::ResponseTypeToString(result));
return false;
}
*license_message = std::move(license_data.license);
*entry_index = license_data.usage_entry_index;
return true;
}
bool RetrieveUsageInfoLicense(DeviceFiles* device_files,
const std::string& usage_info_file_name,
const std::string& key_set_id,
CdmKeyResponse* license_message,
UsageEntryIndex* entry_index) {
if (device_files == nullptr) {
LOGE("DeviceFiles handle is null");
return false;
}
if (license_message == nullptr) {
LOGE("Output parameter |license_message| is null");
return false;
}
if (entry_index == nullptr) {
LOGE("Output parameter |entry_index| is null");
return false;
}
UsageEntry entry;
std::string provider_session_token;
CdmKeyMessage license_request;
std::string drm_certificate;
CryptoWrappedKey wrapped_private_key;
if (!device_files->RetrieveUsageInfoByKeySetId(
usage_info_file_name, key_set_id, &provider_session_token,
&license_request, license_message, &entry, entry_index,
&drm_certificate, &wrapped_private_key)) {
LOGW(
"Failed to retrieve usage information: "
"key_set_id = %s, usage_info_file_name = %s",
IdToString(key_set_id), IdToString(usage_info_file_name));
return false;
}
return true;
}
bool EntryIsUsageInfo(const CdmUsageEntryInfo& info) {
// Used for stl filters.
return info.storage_type == kStorageUsageInfo;
}
bool EntryIsOfflineLicense(const CdmUsageEntryInfo& info) {
// Used for stl filters.
return info.storage_type == kStorageLicense;
}
bool IsValidCdmSecurityLevelForUsageInfo(CdmSecurityLevel security_level) {
return security_level == kSecurityLevelL1 ||
security_level == kSecurityLevelL3;
}
RequestedSecurityLevel CdmSecurityLevelToRequestedLevel(
CdmSecurityLevel security_level) {
return security_level == kSecurityLevelL3 ? kLevel3 : kLevelDefault;
}
} // namespace
CdmUsageTable::CdmUsageTable() : clock_ref_(&clock_) {
file_system_.reset(new wvutil::FileSystem());
device_files_.reset(new DeviceFiles(file_system_.get()));
}
bool CdmUsageTable::Init(CdmSecurityLevel security_level,
CryptoSession* crypto_session) {
LOGD("security_level = %s", CdmSecurityLevelToString(security_level));
if (crypto_session == nullptr) {
LOGE("No crypto session provided");
return false;
}
if (is_initialized_) {
LOGE("Cannot reinitialize usage table: security_level = %s",
CdmSecurityLevelToString(security_level));
return false;
}
if (!IsValidCdmSecurityLevelForUsageInfo(security_level)) {
LOGE("Invalid security level provided: security_level = %s",
CdmSecurityLevelToString(security_level));
return false;
}
security_level_ = security_level;
requested_security_level_ = CdmSecurityLevelToRequestedLevel(security_level);
if (!OpenSessionCheck(crypto_session)) {
return false;
}
if (!DetermineTableCapacity(crypto_session)) {
return false;
}
if (!device_files_->Init(security_level)) {
LOGE("Failed to initialize device files");
return false;
}
// Attempt restoring first, if unable to restore, then create a new
// table.
return RestoreTable(crypto_session) || CreateNewTable(crypto_session);
}
bool CdmUsageTable::RestoreTable(CryptoSession* const crypto_session) {
bool run_lru_upgrade = false;
if (!device_files_->RetrieveUsageTableInfo(&header_, &entry_info_list_,
&run_lru_upgrade)) {
LOGW("Could not retrieve usage table");
return false;
}
LOGI("Found usage table to restore: entry_count = %zu",
entry_info_list_.size());
const CdmResponseType status =
crypto_session->LoadUsageTableHeader(requested_security_level_, header_);
if (status != NO_ERROR) {
LOGE("Failed to load usage table header: sts = %d", status.ToInt());
return false;
}
// If the saved usage entries/meta data is missing LRU information,
// then the entries and their meta data must be updated.
if (run_lru_upgrade && !LruUpgradeAllUsageEntries()) {
LOGE("Failed to perform LRU upgrade to usage entry table");
return false;
}
if (!CapacityCheck(crypto_session)) {
LOGE("Cannot restore table due to failing capacity check");
return false;
}
is_initialized_ = true;
return true;
}
bool CdmUsageTable::CreateNewTable(CryptoSession* const crypto_session) {
LOGD("Removing all usage table files");
// Existing files need to be deleted to avoid attempts to restore
// licenses which no longer have a usage entry.
device_files_->DeleteAllLicenses();
device_files_->DeleteAllUsageInfo();
device_files_->DeleteUsageTableInfo();
entry_info_list_.clear();
header_.clear();
const CdmResponseType status = crypto_session->CreateUsageTableHeader(
requested_security_level_, &header_);
if (status != NO_ERROR) {
LOGE("Failed to create new usage table header");
return false;
}
if (!StoreTable()) {
LOGE("Failed to store new usage table header");
return false;
}
is_initialized_ = true;
return true;
}
CdmResponseType CdmUsageTable::AddEntry(CryptoSession* crypto_session,
bool persistent_license,
const CdmKeySetId& key_set_id,
const std::string& usage_info_file_name,
const CdmKeyResponse& license_message,
UsageEntryIndex* entry_index) {
LOGD("key_set_id = %s, type = %s, current_size = %zu", IdToString(key_set_id),
persistent_license ? "OfflineLicense" : "Streaming",
entry_info_list_.size());
metrics::CryptoMetrics* metrics = crypto_session->GetCryptoMetrics();
if (metrics == nullptr) metrics = &alternate_crypto_metrics_;
TableLock auto_lock(lock_);
CdmResponseType status = CreateEntry(crypto_session, entry_index);
if (status == INSUFFICIENT_CRYPTO_RESOURCES) {
LOGW("Usage table may be full, releasing oldest entry: size = %zu",
entry_info_list_.size());
status = ReleaseOldestEntry(metrics);
if (status == NO_ERROR) {
status = CreateEntry(crypto_session, entry_index);
}
}
if (status != NO_ERROR) return status;
status = RelocateNewEntry(crypto_session, entry_index);
if (status != NO_ERROR) return status;
if (persistent_license) {
SetOfflineEntryInfo(*entry_index, key_set_id, license_message);
} else {
SetUsageInfoEntryInfo(*entry_index, key_set_id, usage_info_file_name);
}
status = RefitTable(crypto_session);
if (status != NO_ERROR) {
entry_info_list_[*entry_index].Clear();
return status;
}
// Call to update the usage table header, but don't store the usage
// entry. If the entry is used by the CDM, the CDM session will make
// subsequent calls to update the usage entry and store that entry.
UsageEntry entry;
status = crypto_session->UpdateUsageEntry(&header_, &entry);
if (status != NO_ERROR) {
LOGE("Failed to update new usage entry: entry_index = %u", *entry_index);
entry_info_list_[*entry_index].Clear();
return status;
}
StoreTable();
return CdmResponseType(NO_ERROR);
}
CdmResponseType CdmUsageTable::LoadEntry(CryptoSession* crypto_session,
const UsageEntry& entry,
UsageEntryIndex entry_index) {
{
LOGD("entry_index = %u", entry_index);
std::unique_lock<std::mutex> auto_lock(lock_);
if (entry_index >= entry_info_list_.size()) {
LOGE(
"Requested usage entry index is larger than table size: "
"entry_index = %u, table_size = %zu",
entry_index, entry_info_list_.size());
return CdmResponseType(USAGE_INVALID_LOAD_ENTRY);
}
}
const CdmResponseType status =
crypto_session->LoadUsageEntry(entry_index, entry);
if (status == NO_ERROR) {
entry_info_list_[entry_index].last_use_time = GetCurrentTime();
}
return status;
}
CdmResponseType CdmUsageTable::UpdateEntry(UsageEntryIndex entry_index,
CryptoSession* crypto_session,
UsageEntry* entry) {
LOGD("entry_index = %u", entry_index);
std::unique_lock<std::mutex> auto_lock(lock_);
if (entry_index >= entry_info_list_.size()) {
LOGE("Usage entry index %u is larger than usage entry size %zu",
entry_index, entry_info_list_.size());
return CdmResponseType(USAGE_INVALID_PARAMETERS_2);
}
CdmResponseType status = crypto_session->UpdateUsageEntry(&header_, entry);
if (status != NO_ERROR) return status;
entry_info_list_[entry_index].last_use_time = GetCurrentTime();
StoreTable();
return CdmResponseType(NO_ERROR);
}
CdmResponseType CdmUsageTable::InvalidateEntry(
UsageEntryIndex entry_index, bool defrag_table, DeviceFiles* device_files,
metrics::CryptoMetrics* metrics) {
LOGD("entry_index = %u", entry_index);
TableLock auto_lock(lock_);
return InvalidateEntryInternal(entry_index, defrag_table, device_files,
metrics);
}
CdmResponseType CdmUsageTable::InvalidateEntryInternal(
UsageEntryIndex entry_index, bool defrag_table, DeviceFiles* device_files,
metrics::CryptoMetrics* metrics) {
// OEMCrypto does not have any concept of "deleting" an entry.
// Instead, the CDM marks the entry's meta data as invalid (storage
// type unknown) and then performs a "defrag" of the OEMCrypto table.
if (entry_index >= entry_info_list_.size()) {
LOGE(
"Usage entry index is larger than table size: "
"entry_index = %u, table_size = %zu",
entry_index, entry_info_list_.size());
return CdmResponseType(USAGE_INVALID_PARAMETERS_1);
}
entry_info_list_[entry_index].Clear();
if (defrag_table) {
// The defrag operation calls many OEMCrypto functions that are
// unrelated to the caller, the only error that will be returned is
// a SYSTEM_INVALIDATED_ERROR. As long as the storage type is
// properly set to unknown, the operation is considered successful.
// SYSTEM_INVALIDATED_ERROR is a special type of error that must be
// sent back to the caller for the CDM as a whole to handle.
const uint32_t pre_defrag_store_counter = store_table_counter_;
const CdmResponseType status = DefragTable(device_files, metrics);
if (status != NO_ERROR) {
LOGW("Failed to defrag usage table: sts = %s", status.ToString().c_str());
}
if (pre_defrag_store_counter == store_table_counter_) {
// It is possible that DefragTable() does not result in any
// changes to the table, and as a result, it will not store the
// invalidated entry.
LOGD("Table was not stored during defrag, storing now");
StoreTable();
}
if (status == SYSTEM_INVALIDATED_ERROR) {
LOGE("Invalidate entry failed due to system invalidation error");
return CdmResponseType(SYSTEM_INVALIDATED_ERROR);
}
} else {
StoreTable();
}
return CdmResponseType(NO_ERROR);
}
size_t CdmUsageTable::UsageInfoCount() const {
LOGV("Locking to count usage info (streaming license) entries");
return std::count_if(entry_info_list_.cbegin(), entry_info_list_.cend(),
EntryIsUsageInfo);
}
size_t CdmUsageTable::OfflineEntryCount() const {
LOGV("Locking to count offline license entries");
return std::count_if(entry_info_list_.cbegin(), entry_info_list_.cend(),
EntryIsOfflineLicense);
}
bool CdmUsageTable::OpenSessionCheck(CryptoSession* const crypto_session) {
// The CdmUsageTable for the specified |requested_security_level_|
// MUST be initialized before any sessions are opened.
size_t session_count = 0;
const CdmResponseType status = crypto_session->GetNumberOfOpenSessions(
requested_security_level_, &session_count);
if (status != NO_ERROR || session_count > 0) {
LOGE(
"Cannot initialize usage table header with open crypto session: "
"status = %d, count = %zu",
status.ToInt(), session_count);
return false;
}
return true;
}
bool CdmUsageTable::CapacityCheck(CryptoSession* const crypto_session) {
// If the table is around capacity or if unlimited and the table is
// larger than the minimally required capacity, then a test must be
// performed to ensure that the usage table is not in a state which
// will prevent create operations.
const size_t capacity_threshold = HasUnlimitedTableCapacity()
? kMinimumUsageTableEntriesSupported
: potential_table_capacity();
if (entry_info_list_.size() <= capacity_threshold) {
// No need to perform test if below capacity.
return true;
}
metrics::CryptoMetrics* metrics = crypto_session->GetCryptoMetrics();
if (metrics == nullptr) metrics = &alternate_crypto_metrics_;
// |local_crypto_session| points to an object whose scope is this
// method or a test object whose scope is the lifetime of this class
CryptoSession* local_crypto_session = test_crypto_session_.get();
std::unique_ptr<CryptoSession> scoped_crypto_session;
if (local_crypto_session == nullptr) {
scoped_crypto_session.reset(CryptoSession::MakeCryptoSession(metrics));
local_crypto_session = scoped_crypto_session.get();
}
CdmResponseType status =
local_crypto_session->Open(requested_security_level_);
if (status != NO_ERROR) {
LOGE("Failed to open crypto session for capacity test: sts = %d",
status.ToInt());
return false;
}
UsageEntryIndex temporary_entry_index;
status = AddEntry(local_crypto_session, true, kDummyKeySetId, kEmptyString,
kEmptyString, &temporary_entry_index);
if (status != NO_ERROR) {
LOGE("Failed to add entry for capacity test: sts = %d", status.ToInt());
return false;
}
// Session must be closed before invalidating, otherwise Shrink() will
// fail in call to InvalidateEntry().
local_crypto_session->Close();
status =
InvalidateEntry(temporary_entry_index,
/* defrag_table = */ true, device_files_.get(), metrics);
if (status != NO_ERROR) {
LOGE("Failed to invalidate entry for capacity test: sts = %d",
status.ToInt());
return false;
}
if (entry_info_list_.size() > temporary_entry_index) {
// The entry should have been deleted from the usage table,
// not just marked as type unknown. Failure to call
// Shrink() may be an indicator of other issues.
LOGE(
"Failed to shrink table for capacity test: "
"post_check_size = %zu, check_usage_entry_number = %u",
entry_info_list_.size(), temporary_entry_index);
return false;
}
return true;
}
bool CdmUsageTable::DetermineTableCapacity(CryptoSession* crypto_session) {
if (!crypto_session->GetMaximumUsageTableEntries(
requested_security_level_, &potential_table_capacity_)) {
LOGW(
"Could not determine usage table capacity, assuming default: "
"default = %zu",
kMinimumUsageTableEntriesSupported);
potential_table_capacity_ = kMinimumUsageTableEntriesSupported;
} else if (potential_table_capacity_ == 0) {
LOGD("capacity = unlimited, security_level = %s",
CdmSecurityLevelToString(security_level_));
} else if (potential_table_capacity_ < kMinimumUsageTableEntriesSupported) {
LOGW(
"Reported usage table capacity is smaller than minimally required: "
"capacity = %zu, minimum = %zu",
potential_table_capacity_, kMinimumUsageTableEntriesSupported);
potential_table_capacity_ = kMinimumUsageTableEntriesSupported;
} else {
LOGD("capacity = %zu, security_level = %s", potential_table_capacity_,
CdmSecurityLevelToString(security_level_));
}
return true;
}
CdmResponseType CdmUsageTable::CreateEntry(CryptoSession* const crypto_session,
UsageEntryIndex* entry_index) {
const CdmResponseType status = crypto_session->CreateUsageEntry(entry_index);
if (status != NO_ERROR) return status;
// If the new entry index is smaller than expected, then the usage
// table may be out of sync or OEMCrypto has been rolled back.
// Not safe to continue.
if (*entry_index < entry_info_list_.size()) {
LOGE(
"New entry index is smaller than table size: "
"entry_index = %u, table_size = %zu",
*entry_index, entry_info_list_.size());
return CdmResponseType(USAGE_INVALID_NEW_ENTRY);
}
LOGI("entry_index = %u", *entry_index);
const size_t previous_size = entry_info_list_.size();
entry_info_list_.resize(*entry_index + 1);
if (*entry_index > previous_size) {
LOGW(
"New entry index is larger than table size, resizing: "
"entry_index = %u, table_size = %zu",
*entry_index, previous_size);
for (size_t i = previous_size; i < entry_info_list_.size() - 1; ++i) {
entry_info_list_[i].Clear();
}
}
entry_info_list_[*entry_index].Clear();
return CdmResponseType(NO_ERROR);
}
CdmResponseType CdmUsageTable::RelocateNewEntry(
CryptoSession* const crypto_session, UsageEntryIndex* entry_index) {
static constexpr UsageEntryIndex kMinimumEntryNumber = 0;
const UsageEntryIndex initial_entry_index = *entry_index;
if (initial_entry_index == kMinimumEntryNumber) {
// First entry in the table.
return CdmResponseType(NO_ERROR);
}
UsageEntryIndex unoccupied_entry_index = initial_entry_index;
for (UsageEntryIndex i = kMinimumEntryNumber; i < initial_entry_index; i++) {
if (IsEntryUnoccupied(i)) {
unoccupied_entry_index = i;
break;
}
}
if (unoccupied_entry_index == initial_entry_index) {
// No open position.
return CdmResponseType(NO_ERROR);
}
const CdmResponseType status =
crypto_session->MoveUsageEntry(unoccupied_entry_index);
if (status == MOVE_USAGE_ENTRY_DESTINATION_IN_USE) {
// Not unexpected, there is a window of time between releasing the
// entry and closing the OEMCrypto session.
LOGD("Released entry still in use: index = %u", unoccupied_entry_index);
return CdmResponseType(NO_ERROR);
}
if (status != NO_ERROR) return status;
LOGI("Entry moved: from_index = %u, to_index = %u", initial_entry_index,
unoccupied_entry_index);
*entry_index = unoccupied_entry_index;
entry_info_list_[unoccupied_entry_index] =
std::move(entry_info_list_[initial_entry_index]);
entry_info_list_[initial_entry_index].Clear();
return CdmResponseType(NO_ERROR);
}
bool CdmUsageTable::IsEntryUnoccupied(const UsageEntryIndex entry_index) const {
// TODO(sigquit): Check that entry is not in use by another session.
// NOTE: The |storage_type| check will protect the integrity of the
// entry. Attempting to use an entry index that is used by another
// session is recoverable and will not affect any opened sessions.
return entry_info_list_[entry_index].storage_type == kStorageTypeUnknown;
}
void CdmUsageTable::SetOfflineEntryInfo(const UsageEntryIndex entry_index,
const std::string& key_set_id,
const CdmKeyResponse& license_message) {
CdmUsageEntryInfo& entry_info = entry_info_list_[entry_index];
entry_info.Clear();
entry_info.storage_type = kStorageLicense;
entry_info.key_set_id = key_set_id;
entry_info.last_use_time = GetCurrentTime();
// Need to determine the expire time for offline licenses.
video_widevine::License license;
if (!license_message.empty() &&
ParseLicenseFromLicenseMessage(license_message, &license)) {
const video_widevine::License::Policy& policy = license.policy();
entry_info.offline_license_expiry_time = license.license_start_time() +
policy.rental_duration_seconds() +
policy.playback_duration_seconds();
} else {
// If the license duration cannot be determined for any reason, it
// is assumed to last at most 33 days.
entry_info.offline_license_expiry_time =
entry_info.last_use_time + kDefaultExpireDuration;
}
}
void CdmUsageTable::SetUsageInfoEntryInfo(
const UsageEntryIndex entry_index, const std::string& key_set_id,
const std::string& usage_info_file_name) {
CdmUsageEntryInfo& entry_info = entry_info_list_[entry_index];
entry_info.Clear();
entry_info.storage_type = kStorageUsageInfo;
entry_info.key_set_id = key_set_id;
entry_info.last_use_time = GetCurrentTime();
entry_info.usage_info_file_name = usage_info_file_name;
}
CdmResponseType CdmUsageTable::RefitTable(CryptoSession* const crypto_session) {
// Remove all unoccupied entries at end of the table.
uint32_t entries_to_remove = 0;
const uint32_t old_size = static_cast<uint32_t>(entry_info_list_.size());
for (uint32_t i = 0; i < old_size; i++) {
const UsageEntryIndex entry_index = old_size - i - 1;
if (!IsEntryUnoccupied(entry_index)) break;
++entries_to_remove;
}
if (entries_to_remove == 0) return CdmResponseType(NO_ERROR);
const uint32_t new_size = old_size - entries_to_remove;
const CdmResponseType status = crypto_session->ShrinkUsageTableHeader(
requested_security_level_, new_size, &header_);
if (status == SHRINK_USAGE_TABLE_HEADER_ENTRY_IN_USE) {
// This error likely indicates that another session has released
// its entry via a call to InvalidateEntry(), but has yet to close
// its OEMCrypto session.
// Safe to assume table state is not invalidated.
LOGW("Unexpected entry in use: range = [%u, %zu]", new_size,
entry_info_list_.size() - 1);
return CdmResponseType(NO_ERROR);
}
if (status != NO_ERROR) return status;
LOGD("Table shrunk: old_size = %zu, new_size = %u", entry_info_list_.size(),
new_size);
entry_info_list_.resize(new_size);
return CdmResponseType(NO_ERROR);
}
CdmResponseType CdmUsageTable::MoveEntry(UsageEntryIndex from_entry_index,
const UsageEntry& from_entry,
UsageEntryIndex to_entry_index,
DeviceFiles* device_files,
metrics::CryptoMetrics* metrics) {
LOGD("from_entry_index = %u, to_entry_index = %u", from_entry_index,
to_entry_index);
// crypto_session points to an object whose scope is this method or a test
// object whose scope is the lifetime of this class
std::unique_ptr<CryptoSession> scoped_crypto_session;
CryptoSession* crypto_session = test_crypto_session_.get();
if (crypto_session == nullptr) {
scoped_crypto_session.reset(CryptoSession::MakeCryptoSession(metrics));
crypto_session = scoped_crypto_session.get();
}
CdmResponseType status = crypto_session->Open(requested_security_level_);
if (status != NO_ERROR) {
LOGE("Cannot open session for move: entry_index = %u", from_entry_index);
return status;
}
status = crypto_session->LoadUsageEntry(from_entry_index, from_entry);
if (status != NO_ERROR) {
LOGE("Failed to load usage entry: entry_index = %u", from_entry_index);
return status;
}
status = crypto_session->MoveUsageEntry(to_entry_index);
if (status != NO_ERROR) {
LOGE(
"Failed to move usage entry: "
"from_entry_index = %u, to_entry_index = %u",
from_entry_index, to_entry_index);
return status;
}
entry_info_list_[to_entry_index] = entry_info_list_[from_entry_index];
entry_info_list_[from_entry_index].Clear();
UsageEntry entry;
status = crypto_session->UpdateUsageEntry(&header_, &entry);
if (status != NO_ERROR) {
LOGE("Failed to update usage entry: entry_index = %u", to_entry_index);
return status;
}
// Store the usage table and usage entry after successful move.
StoreTable();
StoreEntry(to_entry_index, device_files, entry);
return CdmResponseType(NO_ERROR);
}
CdmResponseType CdmUsageTable::GetEntry(UsageEntryIndex entry_index,
DeviceFiles* device_files,
UsageEntry* entry) {
LOGD("Getting entry_index = %u, storage_type = %s", entry_index,
CdmUsageEntryStorageTypeToString(
entry_index < entry_info_list_.size()
? entry_info_list_[entry_index].storage_type
: kStorageTypeUnknown));
UsageEntryIndex retrieved_entry_index;
switch (entry_info_list_[entry_index].storage_type) {
case kStorageLicense: {
DeviceFiles::CdmLicenseData license_data;
DeviceFiles::ResponseType sub_error_code = DeviceFiles::kNoError;
if (!device_files->RetrieveLicense(
entry_info_list_[entry_index].key_set_id, &license_data,
&sub_error_code)) {
LOGE("Failed to retrieve license: status = %d",
static_cast<int>(sub_error_code));
return CdmResponseType(USAGE_GET_ENTRY_RETRIEVE_LICENSE_FAILED);
}
retrieved_entry_index = license_data.usage_entry_index;
*entry = std::move(license_data.usage_entry);
break;
}
case kStorageUsageInfo: {
std::string provider_session_token;
CdmKeyMessage license_request;
CdmKeyResponse license;
std::string drm_certificate;
CryptoWrappedKey wrapped_private_key;
if (!device_files->RetrieveUsageInfoByKeySetId(
entry_info_list_[entry_index].usage_info_file_name,
entry_info_list_[entry_index].key_set_id, &provider_session_token,
&license_request, &license, entry, &retrieved_entry_index,
&drm_certificate, &wrapped_private_key)) {
LOGE("Failed to retrieve usage information");
return CdmResponseType(USAGE_GET_ENTRY_RETRIEVE_USAGE_INFO_FAILED);
}
break;
}
case kStorageTypeUnknown:
default:
LOGE(
"Cannot retrieve usage information with unknown storage type: "
"storage_type = %d",
static_cast<int>(entry_info_list_[entry_index].storage_type));
return CdmResponseType(USAGE_GET_ENTRY_RETRIEVE_INVALID_STORAGE_TYPE);
}
if (entry_index != retrieved_entry_index) {
LOGE(
"Usage entry index mismatch: expected_entry_index = %u, "
"retrieved_entry_index = %u",
entry_index, retrieved_entry_index);
return CdmResponseType(USAGE_ENTRY_NUMBER_MISMATCH);
}
return CdmResponseType(NO_ERROR);
}
CdmResponseType CdmUsageTable::StoreEntry(UsageEntryIndex entry_index,
DeviceFiles* device_files,
const UsageEntry& entry) {
LOGD("entry_index = %u, storage_type = %s", entry_index,
CdmUsageEntryStorageTypeToString(
entry_index < entry_info_list_.size()
? entry_info_list_[entry_index].storage_type
: kStorageTypeUnknown));
switch (entry_info_list_[entry_index].storage_type) {
case kStorageLicense: {
DeviceFiles::CdmLicenseData license_data;
DeviceFiles::ResponseType sub_error_code = DeviceFiles::kNoError;
if (!device_files->RetrieveLicense(
entry_info_list_[entry_index].key_set_id, &license_data,
&sub_error_code)) {
LOGE("Failed to retrieve license: status = %s",
DeviceFiles::ResponseTypeToString(sub_error_code));
return CdmResponseType(USAGE_STORE_ENTRY_RETRIEVE_LICENSE_FAILED);
}
// Update.
license_data.usage_entry = entry;
license_data.usage_entry_index = entry_index;
if (!device_files->StoreLicense(license_data, &sub_error_code)) {
LOGE("Failed to store license: status = %s",
DeviceFiles::ResponseTypeToString(sub_error_code));
return CdmResponseType(USAGE_STORE_LICENSE_FAILED);
}
break;
}
case kStorageUsageInfo: {
UsageEntry retrieved_entry;
UsageEntryIndex retrieved_entry_index;
std::string provider_session_token, init_data, key_request, key_response,
key_renewal_request;
std::string drm_certificate;
CryptoWrappedKey wrapped_private_key;
if (!device_files->RetrieveUsageInfoByKeySetId(
entry_info_list_[entry_index].usage_info_file_name,
entry_info_list_[entry_index].key_set_id, &provider_session_token,
&key_request, &key_response, &retrieved_entry,
&retrieved_entry_index, &drm_certificate, &wrapped_private_key)) {
LOGE("Failed to retrieve usage information");
return CdmResponseType(USAGE_STORE_ENTRY_RETRIEVE_USAGE_INFO_FAILED);
}
device_files->DeleteUsageInfo(
entry_info_list_[entry_index].usage_info_file_name,
entry_info_list_[entry_index].key_set_id);
if (!device_files->StoreUsageInfo(
provider_session_token, key_request, key_response,
entry_info_list_[entry_index].usage_info_file_name,
entry_info_list_[entry_index].key_set_id, entry, entry_index,
drm_certificate, wrapped_private_key)) {
LOGE("Failed to store usage information");
return CdmResponseType(USAGE_STORE_USAGE_INFO_FAILED);
}
break;
}
case kStorageTypeUnknown:
default:
LOGE(
"Cannot retrieve usage information with unknown storage type: "
"storage_type = %d",
static_cast<int>(entry_info_list_[entry_index].storage_type));
return CdmResponseType(USAGE_STORE_ENTRY_RETRIEVE_INVALID_STORAGE_TYPE);
}
return CdmResponseType(NO_ERROR);
}
bool CdmUsageTable::StoreTable() {
LOGV("Storing usage table information");
const bool result =
device_files_->StoreUsageTableInfo(header_, entry_info_list_);
if (result) {
++store_table_counter_;
} else {
LOGW("Failed to store usage table info");
}
return result;
}
CdmResponseType CdmUsageTable::Shrink(
metrics::CryptoMetrics* metrics,
uint32_t number_of_usage_entries_to_delete) {
LOGD("table_size = %zu, number_to_delete = %u", entry_info_list_.size(),
number_of_usage_entries_to_delete);
if (entry_info_list_.empty()) {
LOGE("Usage entry info table unexpectedly empty");
return CdmResponseType(NO_USAGE_ENTRIES);
}
if (entry_info_list_.size() < number_of_usage_entries_to_delete) {
LOGW(
"Cannot delete more entries than the table size, reducing to current "
"table size: table_size = %zu, number_to_delete = %u",
entry_info_list_.size(), number_of_usage_entries_to_delete);
number_of_usage_entries_to_delete =
static_cast<uint32_t>(entry_info_list_.size());
}
if (number_of_usage_entries_to_delete == 0) return CdmResponseType(NO_ERROR);
// crypto_session points to an object whose scope is this method or a test
// object whose scope is the lifetime of this class
std::unique_ptr<CryptoSession> scoped_crypto_session;
CryptoSession* crypto_session = test_crypto_session_.get();
if (crypto_session == nullptr) {
scoped_crypto_session.reset(CryptoSession::MakeCryptoSession(metrics));
crypto_session = scoped_crypto_session.get();
}
const uint32_t new_size = static_cast<uint32_t>(entry_info_list_.size()) -
number_of_usage_entries_to_delete;
const CdmResponseType status = crypto_session->ShrinkUsageTableHeader(
requested_security_level_, new_size, &header_);
if (status == NO_ERROR) {
entry_info_list_.resize(new_size);
StoreTable();
}
return status;
}
CdmResponseType CdmUsageTable::DefragTable(DeviceFiles* device_files,
metrics::CryptoMetrics* metrics) {
LOGV("current_size = %zu", entry_info_list_.size());
// Defragging the usage table involves moving valid entries near the
// end of the usage table to the position of invalid entries near the
// front of the table. After the entries are moved, the CDM shrinks
// the table to cut off all trailing invalid entries at the end of
// the table.
// Special case 0: Empty table, do nothing.
if (entry_info_list_.empty()) {
LOGD("Table empty, nothing to defrag");
return CdmResponseType(NO_ERROR);
}
// Step 1: Create a list of entries to be removed from the table.
// Priority is given to the entries near the beginning of the table.
// To avoid large delays from the swapping process, we limit the
// quantity of entries to remove to |kMaxDefragEntryMoves| or fewer.
std::vector<UsageEntryIndex> entries_to_remove;
for (UsageEntryIndex i = 0; i < entry_info_list_.size() &&
entries_to_remove.size() < kMaxDefragEntryMoves;
++i) {
if (entry_info_list_[i].storage_type == kStorageTypeUnknown) {
entries_to_remove.push_back(i);
}
}
// Special case 1: There are no entries that are invalid; nothing
// needs to be done.
if (entries_to_remove.empty()) {
LOGD("No entries are invalid");
return CdmResponseType(NO_ERROR);
}
// Step 2: Create a list of entries to be moved from the end of the
// table to the positions identified for removal.
std::vector<UsageEntryIndex> entries_to_move;
for (uint32_t i = 0; i < entry_info_list_.size() &&
entries_to_move.size() < entries_to_remove.size();
++i) {
// Search from the end of the table.
const UsageEntryIndex entry_index =
static_cast<UsageEntryIndex>(entry_info_list_.size()) - i - 1;
if (entry_info_list_[entry_index].storage_type != kStorageTypeUnknown) {
entries_to_move.push_back(entry_index);
}
}
// Special case 2: There are no valid entries in the table. In this case,
// the whole table can be removed.
if (entries_to_move.empty()) {
LOGD("No valid entries found, shrinking entire table: size = %zu",
entry_info_list_.size());
const CdmResponseType status =
Shrink(metrics, static_cast<uint32_t>(entry_info_list_.size()));
if (status != NO_ERROR) {
LOGE("Failed to shrink table: sts = %s", status.ToString().c_str());
}
return status;
}
// Step 3: Ignore invalid entries that are after the last valid
// entry. No entry is to be moved to a greater index than it already
// has, and entries after the last valid entry will be removed when
// the shrink operation is applied to the table.
// Note: Special case 4 will handle any non-trivial cases related to
// interweaving of valid and invalid entries.
const UsageEntryIndex last_valid_entry = entries_to_move.front();
while (!entries_to_remove.empty() &&
entries_to_remove.back() > last_valid_entry) {
entries_to_remove.pop_back();
}
// Special case 3: All of the invalid entries are after the last valid
// entry. In this case, no movement is required and the table can just
// be shrunk to the last valid entry.
if (entries_to_remove.empty()) {
const UsageEntryIndex to_remove =
static_cast<UsageEntryIndex>(entry_info_list_.size()) -
last_valid_entry - 1;
LOGD("Removing all entries after the last valid entry: count = %u",
to_remove);
const CdmResponseType status = Shrink(metrics, to_remove);
if (status != NO_ERROR) {
LOGE("Failed to shrink table: sts = %s", status.ToString().c_str());
}
return status;
}
// Step 4: Move the valid entries to overwrite the invalid entries.
// Moving the highest indexed valid entry to the lowest indexed
// invalid entry.
// Reversing vectors to make accessing and popping easier. This
// will put the lowest index invalid entry and the highest index
// valid entry at the back of their respective vectors.
std::reverse(entries_to_remove.begin(), entries_to_remove.end());
std::reverse(entries_to_move.begin(), entries_to_move.end());
while (!entries_to_remove.empty() && !entries_to_move.empty()) {
// Entries are popped after use only.
const UsageEntryIndex to_entry_index = entries_to_remove.back();
const UsageEntryIndex from_entry_index = entries_to_move.back();
// Special case 4: We don't want to move any entries to a higher
// index than their current. Once this occurs, we can stop the
// loop.
if (to_entry_index > from_entry_index) {
LOGD("Entries will not be moved further down the table");
break;
}
UsageEntry from_entry;
CdmResponseType status =
GetEntry(from_entry_index, device_files, &from_entry);
if (status != NO_ERROR) {
LOGW("Could not get entry: entry_index = %u", from_entry_index);
// It is unlikely that an unretrievable entry will suddenly
// become retrievable later on when it is needed.
entry_info_list_[from_entry_index].Clear();
entries_to_move.pop_back();
continue;
}
status = MoveEntry(from_entry_index, from_entry, to_entry_index,
device_files, metrics);
switch (status.code()) {
case NO_ERROR: {
entries_to_remove.pop_back();
entries_to_move.pop_back();
break;
}
// Handle errors associated with the valid "from" entry.
case LOAD_USAGE_ENTRY_INVALID_SESSION: {
// This is a special error code when returned from LoadEntry()
// indicating that the entry is already in use in a different
// session. In this case, skip the entry and move on.
LOGD("From entry already in use: from_entry_index = %u",
from_entry_index);
entries_to_move.pop_back();
break;
}
case LOAD_USAGE_ENTRY_GENERATION_SKEW:
case LOAD_USAGE_ENTRY_SIGNATURE_FAILURE:
case LOAD_USAGE_ENTRY_UNKNOWN_ERROR: {
// The entry (from the CDM's point of view) is invalid and
// can no longer be used. Safe to continue loop.
// TODO(b/152256186): Remove local files associated with this
// entry.
entry_info_list_[from_entry_index].Clear();
LOGW("From entry was corrupted: from_entry_index = %u",
from_entry_index);
entries_to_move.pop_back();
break;
}
// Handle errors associated with the invalid "to" entry.
case MOVE_USAGE_ENTRY_DESTINATION_IN_USE: {
// The usage entry specified by |to_entry_index| is currently
// being used by another session. This is unlikely, but still
// possible. Given that this entry is already marked as unknown
// storage type, it will likely be removed at a later time.
LOGD("To entry already in use: to_entry_index = %u", to_entry_index);
entries_to_remove.pop_back();
break;
}
case MOVE_USAGE_ENTRY_UNKNOWN_ERROR: {
// Something else wrong occurred when moving to the destination
// entry. This could be a problem with from entry or the to
// entry. Both should be skipped on the next iteration.
LOGW(
"Move failed, skipping both to entry and from entry: "
"to_entry_index = %u, from_entry_index = %u",
to_entry_index, from_entry_index);
entries_to_remove.pop_back();
entries_to_move.pop_back();
break;
}
// Handle other possible errors from the operations.
case INSUFFICIENT_CRYPTO_RESOURCES: {
// Cannot open any new sessions. The loop should end, but
// an attempt to shrink the table should still be made.
LOGW("Cannot open new session for table clean up");
entries_to_remove.clear();
entries_to_move.clear();
break;
}
default: {
// For all other cases, it may not be safe to proceed, even to
// shrink the table.
LOGE("Unrecoverable error occurred while defragging table: status = %d",
status.ToInt());
return status;
}
} // End switch case.
} // End while loop.
// Step 5: Find the new last valid entry.
UsageEntryIndex new_last_valid_entry =
static_cast<UsageEntryIndex>(entry_info_list_.size());
for (size_t i = 0; i < entry_info_list_.size(); ++i) {
const UsageEntryIndex entry_index =
static_cast<UsageEntryIndex>(entry_info_list_.size() - i) - 1;
if (entry_info_list_[entry_index].storage_type != kStorageTypeUnknown) {
new_last_valid_entry = entry_index;
break;
}
}
// Special case 5: No entries in the table are valid. This could
// have occurred if entries during the move process were found to be
// invalid. In this case, remove the whole table.
if (new_last_valid_entry == entry_info_list_.size()) {
LOGD(
"All entries have been invalidated, shrinking entire table: size = %zu",
entry_info_list_.size());
const CdmResponseType status =
Shrink(metrics, static_cast<uint32_t>(entry_info_list_.size()));
if (status != NO_ERROR) {
LOGE("Failed to shrink table: sts = %s", status.ToString().c_str());
}
return status;
}
const UsageEntryIndex to_remove =
static_cast<UsageEntryIndex>(entry_info_list_.size()) -
new_last_valid_entry - 1;
// Special case 6: It is possible that the last entry in the table
// is valid and currently loaded in the table by another session.
// The loop above would have tried to move it but had failed. In
// this case, nothing more to do.
if (to_remove == 0) {
LOGD("Defrag completed without shrinking table");
StoreTable();
return CdmResponseType(NO_ERROR);
}
// Step 6: Shrink table to the new size.
LOGD("Clean up complete, shrinking table: count = %u", to_remove);
const CdmResponseType status = Shrink(metrics, to_remove);
if (status != NO_ERROR) {
LOGE("Failed to shrink table: sts = %s", status.ToString().c_str());
}
return status;
} // End Defrag().
CdmResponseType CdmUsageTable::ReleaseOldestEntry(
metrics::CryptoMetrics* metrics) {
LOGV("Releasing oldest entry");
UsageEntryIndex entry_index_to_delete;
if (!GetRemovalCandidate(&entry_index_to_delete)) {
LOGE("Could not determine which license to remove");
return CdmResponseType(UNKNOWN_ERROR);
}
const CdmUsageEntryInfo& entry_info = entry_info_list_[entry_index_to_delete];
const int64_t current_time = GetCurrentTime();
// Capture metric values now, as the |entry_info| reference will
// change after the call to invalidate.
const int64_t staleness = current_time - entry_info.last_use_time;
const CdmUsageEntryStorageType storage_type = entry_info.storage_type;
const CdmResponseType status =
InvalidateEntryInternal(entry_index_to_delete, /* defrag_table = */ true,
device_files_.get(), metrics);
if (status != NO_ERROR) {
LOGE("Failed to invalidate oldest entry: status = %d", status.ToInt());
return status;
}
// Record metrics on success.
RecordLruEventMetrics(metrics, staleness, storage_type);
return CdmResponseType(NO_ERROR);
}
// Test only method.
void CdmUsageTable::InvalidateEntryForTest(UsageEntryIndex entry_index) {
LOGD("entry_index = %u", entry_index);
if (entry_index >= entry_info_list_.size()) {
LOGE(
"Requested usage entry index is larger than table size: "
"entry_index = %u, table_size = %zu",
entry_index, entry_info_list_.size());
return;
}
// Move last entry into invalidated entry location and shrink usage
// entries.
entry_info_list_[entry_index] = entry_info_list_[entry_info_list_.size() - 1];
entry_info_list_.resize(entry_info_list_.size() - 1);
}
bool CdmUsageTable::LruUpgradeAllUsageEntries() {
LOGV("Upgrading all usage entries with LRU information");
if (entry_info_list_.size() == 0) return true; // Nothing to upgrade.
// For each entry, the status upgrading that entry is stored. At the
// end, all problematic licenses will be marked as invalid.
std::vector<UsageEntryIndex> bad_license_file_entries;
for (UsageEntryIndex entry_index = 0; entry_index < entry_info_list_.size();
++entry_index) {
CdmUsageEntryInfo& entry_info = entry_info_list_[entry_index];
UsageEntryIndex retrieved_entry_index;
CdmKeyResponse license_message;
bool retrieve_response = false;
switch (entry_info.storage_type) {
case kStorageLicense: {
retrieve_response =
RetrieveOfflineLicense(device_files_.get(), entry_info.key_set_id,
&license_message, &retrieved_entry_index);
break;
}
case kStorageUsageInfo: {
retrieve_response = RetrieveUsageInfoLicense(
device_files_.get(), entry_info.usage_info_file_name,
entry_info.key_set_id, &license_message, &retrieved_entry_index);
break;
}
case kStorageTypeUnknown:
bad_license_file_entries.push_back(entry_index);
continue;
default: {
LOGW("Unknown usage entry storage type: %d, entry_index = %u",
static_cast<int>(entry_info.storage_type), entry_index);
bad_license_file_entries.push_back(entry_index);
continue;
}
}
if (!retrieve_response) {
LOGW("Could not retrieve license message: entry_index = %u", entry_index);
bad_license_file_entries.push_back(entry_index);
continue;
}
if (retrieved_entry_index != entry_index) {
LOGW(
"Usage entry index mismatched: entry_index = %u, "
"retrieved_entry_index = %u",
entry_index, retrieved_entry_index);
bad_license_file_entries.push_back(entry_index);
continue;
}
video_widevine::License license;
if (!ParseLicenseFromLicenseMessage(license_message, &license)) {
LOGW("Could not parse license: entry_index = %u", entry_index);
bad_license_file_entries.push_back(entry_index);
continue;
}
// If |license_start_time| is 0, then this entry will be considered
// for replacement above all others.
entry_info.last_use_time = license.license_start_time();
if (entry_info.storage_type == kStorageLicense) {
// Only offline licenses need |offline_license_expiry_time| set.
const video_widevine::License::Policy& policy = license.policy();
// TODO(b/139372190): Change how these fields are set once feature is
// implemented.
if (policy.license_duration_seconds() == 0) {
// Zero implies unlimited license duration.
entry_info.offline_license_expiry_time =
license.license_start_time() + policy.rental_duration_seconds() +
policy.playback_duration_seconds();
} else {
entry_info.offline_license_expiry_time =
license.license_start_time() + policy.license_duration_seconds();
}
} else {
entry_info.offline_license_expiry_time = 0;
}
} // End for loop.
if (bad_license_file_entries.size() == entry_info_list_.size()) {
LOGE("Failed to perform LRU upgrade for every entry: count = %zu",
entry_info_list_.size());
return false;
}
// Maps <usage_info_file_name> -> [<key_set_id>].
std::map<std::string, std::vector<std::string>> usage_info_clean_up;
for (UsageEntryIndex entry_index : bad_license_file_entries) {
CdmUsageEntryInfo& entry_info = entry_info_list_[entry_index];
if (entry_info.storage_type == kStorageLicense) {
device_files_->DeleteLicense(entry_info.key_set_id);
} else if (entry_info.storage_type == kStorageUsageInfo) {
// To reduce write cycles, the deletion of usage info will be done
// in bulk.
auto it = usage_info_clean_up.find(entry_info.usage_info_file_name);
if (it == usage_info_clean_up.end()) {
it = usage_info_clean_up
.emplace(entry_info.usage_info_file_name,
std::vector<std::string>())
.first;
}
it->second.push_back(entry_info.key_set_id);
} // else kStorageUnknown { Nothing special }.
entry_info.Clear();
}
for (const auto& p : usage_info_clean_up) {
device_files_->DeleteMultipleUsageInfoByKeySetIds(p.first, p.second);
}
return true;
}
bool CdmUsageTable::GetRemovalCandidate(UsageEntryIndex* entry_to_remove) {
const size_t lru_unexpired_threshold =
HasUnlimitedTableCapacity()
? kLruUnexpiredThresholdFraction * size()
: kLruUnexpiredThresholdFraction * potential_table_capacity();
return DetermineLicenseToRemove(entry_info_list_, GetCurrentTime(),
lru_unexpired_threshold, entry_to_remove);
}
void CdmUsageTable::RecordLruEventMetrics(
metrics::CryptoMetrics* metrics, uint64_t staleness,
CdmUsageEntryStorageType storage_type) {
if (metrics == nullptr) return;
metrics->usage_table_header_lru_usage_info_count_.Record(UsageInfoCount());
metrics->usage_table_header_lru_offline_license_count_.Record(
OfflineEntryCount());
metrics->usage_table_header_lru_evicted_entry_staleness_.Record(staleness);
metrics->usage_table_header_lru_evicted_entry_type_.Record(
static_cast<int>(storage_type));
}
// Static.
bool CdmUsageTable::DetermineLicenseToRemove(
const std::vector<CdmUsageEntryInfo>& entry_info_list, int64_t current_time,
size_t unexpired_threshold, UsageEntryIndex* entry_to_remove) {
if (entry_to_remove == nullptr) {
LOGE("Output parameter |entry_to_remove| is null");
return false;
}
if (entry_info_list.empty()) {
return false;
}
// Returns true if entry of first index is more stale than the
// entry of the second index.
const auto is_more_stale = [&](UsageEntryIndex i, UsageEntryIndex j) -> bool {
return entry_info_list[i].last_use_time < entry_info_list[j].last_use_time;
};
// Find the most stale expired offline / streaming license and the
// most stale unexpired offline entry. Count the number of unexpired
// entries. If any entry is of storage type unknown, then it should
// be removed.
constexpr UsageEntryIndex kNoEntry =
std::numeric_limits<UsageEntryIndex>::max();
UsageEntryIndex stalest_expired_offline_license = kNoEntry;
UsageEntryIndex stalest_unexpired_offline_license = kNoEntry;
UsageEntryIndex stalest_streaming_license = kNoEntry;
size_t unexpired_license_count = 0;
for (UsageEntryIndex entry_index = 0; entry_index < entry_info_list.size();
++entry_index) {
const CdmUsageEntryInfo& entry_info = entry_info_list[entry_index];
if (entry_info.storage_type != kStorageLicense &&
entry_info.storage_type != kStorageUsageInfo) {
// Unknown storage type entries. Remove this entry.
*entry_to_remove = entry_index;
return true;
}
if (entry_info.storage_type == kStorageLicense &&
entry_info.offline_license_expiry_time > current_time) {
// Unexpired offline.
++unexpired_license_count;
if (stalest_unexpired_offline_license == kNoEntry ||
is_more_stale(entry_index, stalest_unexpired_offline_license)) {
stalest_unexpired_offline_license = entry_index;
}
} else if (entry_info.storage_type == kStorageLicense) {
// Expired offline.
if (stalest_expired_offline_license == kNoEntry ||
is_more_stale(entry_index, stalest_expired_offline_license)) {
stalest_expired_offline_license = entry_index;
}
} else {
// Streaming.
if (stalest_streaming_license == kNoEntry ||
is_more_stale(entry_index, stalest_streaming_license)) {
stalest_streaming_license = entry_index;
}
}
}
if (stalest_expired_offline_license == kNoEntry &&
stalest_streaming_license == kNoEntry &&
unexpired_license_count <= unexpired_threshold) {
// Unexpected situation, could be an issue with the threshold.
LOGW(
"Table only contains unexpired offline licenses, "
"but threshold not met: size = %zu, count = %zu, threshold = %zu",
entry_info_list.size(), unexpired_license_count, unexpired_threshold);
*entry_to_remove = stalest_unexpired_offline_license;
return true;
}
const auto select_most_stale = [&](UsageEntryIndex a,
UsageEntryIndex b) -> UsageEntryIndex {
if (a == kNoEntry) return b;
if (b == kNoEntry) return a;
return is_more_stale(a, b) ? a : b;
};
// Only consider an unexpired entry if the threshold is reached.
if (unexpired_license_count > unexpired_threshold) {
const UsageEntryIndex temp = select_most_stale(
stalest_unexpired_offline_license, stalest_streaming_license);
*entry_to_remove = select_most_stale(temp, stalest_expired_offline_license);
} else {
*entry_to_remove = select_most_stale(stalest_streaming_license,
stalest_expired_offline_license);
}
if (*entry_to_remove == kNoEntry) {
// Illegal state check. The loop above should have found at least
// one entry given that |entry_info_list| is not empty.
LOGE("No entry could be used for removal: size = %zu",
entry_info_list.size());
return false;
}
return true;
}
} // namespace wvcdm