blob: 83009c352f3a46ba0008156cb42c717a33d9ab6d [file] [log] [blame] [edit]
// Copyright 2019 The Fuchsia 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 <fuchsia/media/cpp/fidl.h>
#include <fuchsia/media/drm/cpp/fidl.h>
#include <fuchsia/sysmem/cpp/fidl.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <lib/fidl/cpp/interface_handle.h>
#include <lib/gtest/real_loop_fixture.h>
#include <zircon/errors.h>
#include <zircon/types.h>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <random>
#include <unordered_map>
#include <variant>
#include <vector>
#include <gtest/gtest.h>
#include <sdk/lib/sys/cpp/testing/test_with_environment.h>
#include "lib/media/codec_impl/codec_impl.h"
#include "lib/media/codec_impl/decryptor_adapter.h"
namespace {
constexpr uint64_t kBufferConstraintsVersionOrdinal = 1;
constexpr uint64_t kBufferLifetimeOrdinal = 1;
constexpr uint64_t kStreamLifetimeOrdinal = 1;
constexpr uint32_t kInputPacketSize = 8 * 1024;
auto CreateDecryptorParams(bool is_secure) {
fuchsia::media::drm::DecryptorParams params;
params.mutable_input_details()->set_format_details_version_ordinal(0);
if (is_secure) {
params.set_require_secure_mode(is_secure);
}
return params;
}
auto CreateStreamBufferPartialSettings(
const fuchsia::media::StreamBufferConstraints& constraints,
fidl::InterfaceHandle<fuchsia::sysmem::BufferCollectionToken> token) {
fuchsia::media::StreamBufferPartialSettings settings;
settings.set_buffer_lifetime_ordinal(kBufferLifetimeOrdinal)
.set_buffer_constraints_version_ordinal(kBufferConstraintsVersionOrdinal)
.set_single_buffer_mode(constraints.default_settings().single_buffer_mode())
.set_packet_count_for_server(constraints.default_settings().packet_count_for_server())
.set_packet_count_for_client(constraints.default_settings().packet_count_for_client())
.set_sysmem_token(std::move(token));
return settings;
}
auto CreateBufferCollectionConstraints(uint32_t cpu_usage) {
fuchsia::sysmem::BufferCollectionConstraints collection_constraints;
collection_constraints.usage.cpu = cpu_usage;
collection_constraints.min_buffer_count_for_camping = 1;
collection_constraints.has_buffer_memory_constraints = true;
collection_constraints.buffer_memory_constraints.min_size_bytes = kInputPacketSize;
// Secure buffers not allowed for test keys
EXPECT_FALSE(collection_constraints.buffer_memory_constraints.secure_required);
return collection_constraints;
}
auto CreateInputFormatDetails(const std::string& scheme, const std::vector<uint8_t>& key_id,
const std::vector<uint8_t>& init_vector) {
constexpr uint64_t kFormatDetailsVersionOrdinal = 0;
fuchsia::media::FormatDetails details;
details.set_format_details_version_ordinal(kFormatDetailsVersionOrdinal);
details.mutable_domain()
->crypto()
.encrypted()
.set_scheme(scheme)
.set_key_id(key_id)
.set_init_vector(init_vector);
return details;
}
const std::map<std::string, std::string> kServices = {
{"fuchsia.sysmem.Allocator",
"fuchsia-pkg://fuchsia.com/sysmem_connector#meta/sysmem_connector.cmx"},
};
class FakeDecryptorAdapter : public DecryptorAdapter {
public:
explicit FakeDecryptorAdapter(std::mutex& lock, CodecAdapterEvents* codec_adapter_events)
: DecryptorAdapter(lock, codec_adapter_events) {}
void set_has_keys(bool has_keys) { has_keys_ = has_keys; }
bool has_keys() const { return has_keys_; }
void set_use_mapped_output(bool use_mapped_output) { use_mapped_output_ = use_mapped_output; }
bool use_mapped_output() const { return use_mapped_output_; }
bool IsCoreCodecMappedBufferUseful(CodecPort port) override {
if (!use_mapped_output_ && port == kOutputPort) {
return false;
} else {
return DecryptorAdapter::IsCoreCodecMappedBufferUseful(port);
}
}
private:
bool has_keys_ = false;
bool use_mapped_output_ = true;
};
class ClearTextDecryptorAdapter : public FakeDecryptorAdapter {
public:
explicit ClearTextDecryptorAdapter(std::mutex& lock, CodecAdapterEvents* codec_adapter_events)
: FakeDecryptorAdapter(lock, codec_adapter_events) {}
std::optional<fuchsia::media::StreamError> Decrypt(const EncryptionParams& params,
const InputBuffer& input,
const OutputBuffer& output,
CodecPacket* output_packet) override {
if (!std::holds_alternative<ClearOutputBuffer>(output)) {
return fuchsia::media::StreamError::DECRYPTOR_UNKNOWN;
}
auto& clear_output = std::get<ClearOutputBuffer>(output);
if (input.data_length != clear_output.data_length) {
return fuchsia::media::StreamError::DECRYPTOR_UNKNOWN;
}
if (!has_keys()) {
return fuchsia::media::StreamError::DECRYPTOR_NO_KEY;
}
std::memcpy(clear_output.data, input.data, input.data_length);
return std::nullopt;
}
};
class FakeSecureDecryptorAdapter : public FakeDecryptorAdapter {
public:
explicit FakeSecureDecryptorAdapter(std::mutex& lock, CodecAdapterEvents* codec_adapter_events)
: FakeDecryptorAdapter(lock, codec_adapter_events) {}
std::optional<fuchsia::media::StreamError> Decrypt(const EncryptionParams& params,
const InputBuffer& input,
const OutputBuffer& output,
CodecPacket* output_packet) override {
// We should not get here, so just return an error.
return fuchsia::media::StreamError::DECRYPTOR_UNKNOWN;
}
};
} // namespace
template <typename DecryptorAdapterT>
class DecryptorAdapterTest : public sys::testing::TestWithEnvironment {
protected:
DecryptorAdapterTest() : random_device_(), prng_(random_device_()) {
std::unique_ptr<sys::testing::EnvironmentServices> services = CreateServices();
for (const auto& [service_name, url] : kServices) {
fuchsia::sys::LaunchInfo launch_info;
launch_info.url = url;
services->AddServiceWithLaunchInfo(std::move(launch_info), service_name);
}
constexpr char kEnvironment[] = "DecryptorAdapterTest";
environment_ = CreateNewEnclosingEnvironment(kEnvironment, std::move(services));
environment_->ConnectToService(allocator_.NewRequest());
PopulateInputData();
allocator_.set_error_handler([this](zx_status_t s) { sysmem_error_ = s; });
decryptor_.set_error_handler([this](zx_status_t s) { decryptor_error_ = s; });
input_collection_.set_error_handler([this](zx_status_t s) { input_collection_error_ = s; });
output_collection_.set_error_handler([this](zx_status_t s) { output_collection_error_ = s; });
decryptor_.events().OnStreamFailed =
fit::bind_member(this, &DecryptorAdapterTest::OnStreamFailed);
decryptor_.events().OnInputConstraints =
fit::bind_member(this, &DecryptorAdapterTest::OnInputConstraints);
decryptor_.events().OnOutputConstraints =
fit::bind_member(this, &DecryptorAdapterTest::OnOutputConstraints);
decryptor_.events().OnOutputFormat =
fit::bind_member(this, &DecryptorAdapterTest::OnOutputFormat);
decryptor_.events().OnOutputPacket =
fit::bind_member(this, &DecryptorAdapterTest::OnOutputPacket);
decryptor_.events().OnFreeInputPacket =
fit::bind_member(this, &DecryptorAdapterTest::OnFreeInputPacket);
decryptor_.events().OnOutputEndOfStream =
fit::bind_member(this, &DecryptorAdapterTest::OnOutputEndOfStream);
}
~DecryptorAdapterTest() {
// Cleanly terminate BufferCollection view to avoid errors as the test halts.
if (input_collection_.is_bound()) {
input_collection_->Close();
}
if (output_collection_.is_bound()) {
output_collection_->Close();
}
}
void ConnectDecryptor(bool is_secure) {
fidl::InterfaceHandle<fuchsia::sysmem::Allocator> allocator;
environment_->ConnectToService(allocator.NewRequest());
codec_impl_ =
std::make_unique<CodecImpl>(std::move(allocator), nullptr, dispatcher(), thrd_current(),
CreateDecryptorParams(is_secure), decryptor_.NewRequest());
auto adapter = std::make_unique<DecryptorAdapterT>(codec_impl_->lock(), codec_impl_.get());
// Grab a non-owning reference to the adapter for test manipulation.
decryptor_adapter_ = adapter.get();
codec_impl_->SetCoreCodecAdapter(std::move(adapter));
codec_impl_->BindAsync([this]() { codec_impl_ = nullptr; });
}
void OnStreamFailed(uint64_t stream_lifetime_ordinal, fuchsia::media::StreamError error) {
stream_error_ = std::move(error);
}
void OnInputConstraints(fuchsia::media::StreamBufferConstraints ic) {
auto settings = BindBufferCollection(
input_collection_, fuchsia::sysmem::cpuUsageWrite | fuchsia::sysmem::cpuUsageWriteOften,
ic);
input_collection_->WaitForBuffersAllocated(
[this](zx_status_t status, fuchsia::sysmem::BufferCollectionInfo_2 info) {
ASSERT_EQ(status, ZX_OK);
input_buffer_info_ = std::move(info);
});
input_collection_->Sync([this, settings = std::move(settings)]() mutable {
decryptor_->SetInputBufferPartialSettings(std::move(settings));
});
input_constraints_ = std::move(ic);
}
void OnOutputConstraints(fuchsia::media::StreamOutputConstraints oc) {
auto settings = BindBufferCollection(
output_collection_, fuchsia::sysmem::cpuUsageRead | fuchsia::sysmem::cpuUsageReadOften,
oc.buffer_constraints());
output_collection_->WaitForBuffersAllocated(
[this](zx_status_t status, fuchsia::sysmem::BufferCollectionInfo_2 info) {
if (status == ZX_OK) {
output_buffer_info_ = std::move(info);
}
});
output_collection_->Sync([this, settings = std::move(settings)]() mutable {
decryptor_->SetOutputBufferPartialSettings(std::move(settings));
decryptor_->CompleteOutputBufferPartialSettings(kBufferLifetimeOrdinal);
});
output_constraints_ = std::move(oc);
}
void OnOutputFormat(fuchsia::media::StreamOutputFormat of) { output_format_ = std::move(of); }
void OnOutputPacket(fuchsia::media::Packet packet, bool error_before, bool error_during) {
EXPECT_FALSE(error_before);
EXPECT_FALSE(error_during);
auto header = fidl::Clone(packet.header());
output_data_.emplace_back(ExtractPayloadData(std::move(packet)));
decryptor_->RecycleOutputPacket(std::move(header));
}
void OnFreeInputPacket(fuchsia::media::PacketHeader header) {
ASSERT_TRUE(header.has_packet_index());
FreePacket(header.packet_index());
if (end_of_stream_set_) {
return;
}
PumpInput();
}
void OnOutputEndOfStream(uint64_t stream_lifetime_ordinal, bool error_before) {
end_of_stream_reached_ = true;
}
void PopulateInputData() {
constexpr size_t kNumInputPackets = 50;
std::uniform_int_distribution<uint8_t> dist;
input_data_.reserve(kNumInputPackets);
for (size_t i = 0; i < kNumInputPackets; i++) {
std::vector<uint8_t> v(kInputPacketSize);
std::generate(v.begin(), v.end(), [this, &dist]() { return dist(prng_); });
input_data_.emplace_back(std::move(v));
}
input_iter_ = input_data_.begin();
}
fuchsia::media::StreamBufferPartialSettings BindBufferCollection(
fuchsia::sysmem::BufferCollectionPtr& collection, uint32_t cpu_usage,
const fuchsia::media::StreamBufferConstraints& constraints) {
fuchsia::sysmem::BufferCollectionTokenPtr client_token;
allocator_->AllocateSharedCollection(client_token.NewRequest());
fidl::InterfaceHandle<fuchsia::sysmem::BufferCollectionToken> decryptor_token;
client_token->Duplicate(std::numeric_limits<uint32_t>::max(), decryptor_token.NewRequest());
allocator_->BindSharedCollection(client_token.Unbind(), collection.NewRequest());
collection->SetConstraints(true, CreateBufferCollectionConstraints(cpu_usage));
return CreateStreamBufferPartialSettings(constraints, std::move(decryptor_token));
}
fuchsia::media::Packet CreateInputPacket(const std::vector<uint8_t>& data) {
fuchsia::media::Packet packet;
static uint64_t timestamp_ish = 42;
uint32_t packet_index;
uint32_t buffer_index;
AllocatePacket(&packet_index, &buffer_index);
auto& vmo = input_buffer_info_->buffers[buffer_index].vmo;
uint64_t offset = input_buffer_info_->buffers[buffer_index].vmo_usable_start;
size_t size = data.size();
// Since test code, no particular reason to bother with mapping.
auto status = vmo.write(data.data(), offset, size);
EXPECT_EQ(status, ZX_OK);
packet.mutable_header()
->set_buffer_lifetime_ordinal(kBufferLifetimeOrdinal)
.set_packet_index(packet_index);
packet.set_buffer_index(buffer_index)
.set_stream_lifetime_ordinal(kStreamLifetimeOrdinal)
.set_start_offset(0)
.set_valid_length_bytes(size)
.set_timestamp_ish(timestamp_ish++)
.set_start_access_unit(true);
return packet;
}
std::vector<uint8_t> ExtractPayloadData(fuchsia::media::Packet packet) {
std::vector<uint8_t> data;
uint32_t buffer_index = packet.buffer_index();
uint32_t offset = packet.start_offset();
uint32_t size = packet.valid_length_bytes();
EXPECT_TRUE(buffer_index < output_buffer_info_->buffer_count);
const auto& buffer = output_buffer_info_->buffers[buffer_index];
data.resize(size);
auto status = buffer.vmo.read(data.data(), offset, size);
EXPECT_EQ(status, ZX_OK);
return data;
}
bool HasFreePackets() { return !free_packets_.empty(); }
void ConfigureInputPackets() {
ASSERT_TRUE(input_buffer_info_);
auto buffer_count = input_buffer_info_->buffer_count;
std::vector<uint32_t> buffers;
std::vector<uint32_t> packets;
for (uint32_t i = 0; i < buffer_count; i++) {
buffers.emplace_back(i);
packets.emplace_back(i);
}
// Shuffle the packet indexes so that they don't align with the buffer indexes
std::shuffle(packets.begin(), packets.end(), prng_);
for (uint32_t i = 0; i < buffer_count; i++) {
free_packets_.emplace(packets[i], buffers[i]);
}
}
void AllocatePacket(uint32_t* packet_index, uint32_t* buffer_index) {
ASSERT_TRUE(HasFreePackets());
ASSERT_TRUE(packet_index);
ASSERT_TRUE(buffer_index);
auto node = free_packets_.extract(free_packets_.begin());
*packet_index = node.key();
*buffer_index = node.mapped();
used_packets_.insert(std::move(node));
}
void FreePacket(uint32_t packet_index) {
free_packets_.insert(used_packets_.extract(packet_index));
}
void PumpInput() {
while (input_iter_ != input_data_.end() && HasFreePackets()) {
decryptor_->QueueInputPacket(CreateInputPacket(*input_iter_));
input_iter_++;
}
if (input_iter_ == input_data_.end() && !end_of_stream_set_) {
decryptor_->QueueInputEndOfStream(kStreamLifetimeOrdinal);
end_of_stream_set_ = true;
}
}
void AssertNoChannelErrors() {
ASSERT_FALSE(decryptor_error_) << "Decryptor error = " << *decryptor_error_;
ASSERT_FALSE(sysmem_error_) << "Sysmem error = " << *sysmem_error_;
ASSERT_FALSE(input_collection_error_)
<< "Input BufferCollection error = " << *input_collection_error_;
ASSERT_FALSE(output_collection_error_)
<< "Output BufferCollection error = " << *output_collection_error_;
}
bool HasChannelErrors() {
return decryptor_error_.has_value() || sysmem_error_.has_value() ||
input_collection_error_.has_value() || output_collection_error_.has_value();
}
std::unique_ptr<sys::testing::EnclosingEnvironment> environment_;
fuchsia::media::StreamProcessorPtr decryptor_;
fuchsia::sysmem::AllocatorPtr allocator_;
std::unique_ptr<CodecImpl> codec_impl_;
DecryptorAdapterT* decryptor_adapter_;
using DataSet = std::vector<std::vector<uint8_t>>;
DataSet input_data_;
DataSet output_data_;
std::optional<fuchsia::media::StreamBufferConstraints> input_constraints_;
std::optional<fuchsia::media::StreamOutputConstraints> output_constraints_;
std::optional<fuchsia::media::StreamOutputFormat> output_format_;
bool end_of_stream_set_ = false;
bool end_of_stream_reached_ = false;
DataSet::const_iterator input_iter_;
fuchsia::sysmem::BufferCollectionPtr input_collection_;
fuchsia::sysmem::BufferCollectionPtr output_collection_;
std::optional<fuchsia::sysmem::BufferCollectionInfo_2> input_buffer_info_;
std::optional<fuchsia::sysmem::BufferCollectionInfo_2> output_buffer_info_;
std::optional<fuchsia::media::StreamError> stream_error_;
std::optional<zx_status_t> sysmem_error_;
std::optional<zx_status_t> decryptor_error_;
std::optional<zx_status_t> input_collection_error_;
std::optional<zx_status_t> output_collection_error_;
using PacketMap = std::unordered_map<uint32_t /*packet_index*/, uint32_t /*buffer_index*/>;
PacketMap free_packets_;
PacketMap used_packets_;
std::random_device random_device_;
std::mt19937 prng_;
};
class ClearDecryptorAdapterTest : public DecryptorAdapterTest<ClearTextDecryptorAdapter> {};
TEST_F(ClearDecryptorAdapterTest, ClearTextDecrypt) {
ConnectDecryptor(false);
decryptor_adapter_->set_has_keys(true);
RunLoopUntil([this]() { return input_buffer_info_.has_value(); });
AssertNoChannelErrors();
ASSERT_TRUE(input_buffer_info_);
ConfigureInputPackets();
decryptor_->QueueInputFormatDetails(kStreamLifetimeOrdinal,
CreateInputFormatDetails("clear", {}, {}));
PumpInput();
RunLoopUntil([this]() { return end_of_stream_reached_; });
AssertNoChannelErrors();
EXPECT_TRUE(input_constraints_);
EXPECT_TRUE(output_constraints_);
EXPECT_TRUE(output_format_);
ASSERT_TRUE(end_of_stream_reached_);
// ClearText decryptor just copies data across
EXPECT_EQ(output_data_, input_data_);
}
TEST_F(ClearDecryptorAdapterTest, NoKeys) {
ConnectDecryptor(false);
decryptor_adapter_->set_has_keys(false);
decryptor_->EnableOnStreamFailed();
RunLoopUntil([this]() { return input_buffer_info_.has_value(); });
AssertNoChannelErrors();
ASSERT_TRUE(input_buffer_info_);
ConfigureInputPackets();
decryptor_->QueueInputFormatDetails(kStreamLifetimeOrdinal,
CreateInputFormatDetails("clear", {}, {}));
PumpInput();
RunLoopUntil([this]() { return stream_error_.has_value(); });
AssertNoChannelErrors();
ASSERT_TRUE(stream_error_.has_value());
EXPECT_EQ(*stream_error_, fuchsia::media::StreamError::DECRYPTOR_NO_KEY);
}
TEST_F(ClearDecryptorAdapterTest, UnmappedOutputBuffers) {
ConnectDecryptor(false);
decryptor_adapter_->set_has_keys(true);
decryptor_adapter_->set_use_mapped_output(false);
RunLoopUntil([this]() { return input_buffer_info_.has_value(); });
AssertNoChannelErrors();
ASSERT_TRUE(input_buffer_info_);
ConfigureInputPackets();
decryptor_->QueueInputFormatDetails(kStreamLifetimeOrdinal,
CreateInputFormatDetails("clear", {}, {}));
PumpInput();
RunLoopUntil([this]() { return HasChannelErrors(); });
// The decryptor should have failed (since unmapped buffers are unsupported),
// and nothing else should have failed.
EXPECT_TRUE(decryptor_error_.has_value());
decryptor_error_.reset();
AssertNoChannelErrors();
}
TEST_F(ClearDecryptorAdapterTest, DecryptorClosesBuffersCleanly) {
ConnectDecryptor(false);
decryptor_adapter_->set_has_keys(true);
RunLoopUntil([this]() { return input_buffer_info_.has_value(); });
AssertNoChannelErrors();
ASSERT_TRUE(input_buffer_info_);
ConfigureInputPackets();
decryptor_->QueueInputFormatDetails(kStreamLifetimeOrdinal,
CreateInputFormatDetails("clear", {}, {}));
// Queue a single input packet.
ASSERT_TRUE(HasFreePackets());
decryptor_->QueueInputPacket(CreateInputPacket(*input_iter_++));
// Wait until the output collection has been allocated.
RunLoopUntil([this]() { return output_buffer_info_.has_value(); });
AssertNoChannelErrors();
ASSERT_TRUE(output_buffer_info_);
// Wait until receive first output packet.
RunLoopUntil([this]() { return !output_data_.empty(); });
AssertNoChannelErrors();
// Drop the decryptor_. This should not cause any buffer collection failures.
decryptor_ = nullptr;
AssertNoChannelErrors();
// If the below fail, the collections have failed.
std::optional<zx_status_t> input_status;
std::optional<zx_status_t> output_status;
EXPECT_TRUE(input_collection_.is_bound());
EXPECT_TRUE(output_collection_.is_bound());
input_collection_->CheckBuffersAllocated([&input_status](zx_status_t s) { input_status = s; });
output_collection_->CheckBuffersAllocated([&output_status](zx_status_t s) { output_status = s; });
RunLoopUntil([&]() { return input_status.has_value() && output_status.has_value(); });
EXPECT_EQ(*input_status, ZX_OK);
EXPECT_EQ(*output_status, ZX_OK);
AssertNoChannelErrors();
EXPECT_FALSE(stream_error_.has_value());
// Buffers are A-OK after dropping decryptor_.
// ClearText decryptor just copies data across, and only one input packet was sent.
EXPECT_EQ(output_data_[0], input_data_[0]);
}
class SecureDecryptorAdapterTest : public DecryptorAdapterTest<FakeSecureDecryptorAdapter> {};
TEST_F(SecureDecryptorAdapterTest, FailsToAcquireSecureBuffers) {
ConnectDecryptor(true);
RunLoopUntil([this]() { return input_buffer_info_.has_value(); });
AssertNoChannelErrors();
ASSERT_TRUE(input_buffer_info_);
ConfigureInputPackets();
decryptor_->QueueInputFormatDetails(kStreamLifetimeOrdinal,
CreateInputFormatDetails("secure", {}, {}));
PumpInput();
// TODO(13678): Once there is a Sysmem fake that allows us to control behavior, we could force it
// to give us back "secure" buffers that aren't really secure and go through more of the flow.
RunLoopUntil(
[this]() { return decryptor_error_.has_value() && output_collection_error_.has_value(); });
EXPECT_TRUE(decryptor_error_.has_value());
EXPECT_TRUE(output_collection_error_.has_value());
EXPECT_TRUE(input_constraints_);
EXPECT_TRUE(output_constraints_);
EXPECT_FALSE(output_format_);
EXPECT_FALSE(end_of_stream_reached_);
}