blob: 184f2b74ef8d77ab45887cdf9d244cc91d98a446 [file] [log] [blame]
// Copyright 2018 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 "storage/browser/blob/blob_builder_from_stream.h"
#include <algorithm>
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/rand_util.h"
#include "base/run_loop.h"
#include "base/task/post_task.h"
#include "base/task/task_scheduler/task_scheduler.h"
#include "base/test/bind_test_util.h"
#include "base/test/scoped_task_environment.h"
#include "mojo/public/cpp/bindings/associated_binding.h"
#include "mojo/public/cpp/system/data_pipe_utils.h"
#include "mojo/public/cpp/system/string_data_pipe_producer.h"
#include "storage/browser/blob/blob_data_builder.h"
#include "storage/browser/blob/blob_data_item.h"
#include "storage/browser/blob/blob_storage_context.h"
#include "storage/browser/test/fake_progress_client.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace storage {
namespace {
constexpr size_t kTestBlobStorageMaxBytesDataItemSize = 13;
constexpr size_t kTestBlobStorageMaxBlobMemorySize = 500;
constexpr uint64_t kTestBlobStorageMinFileSizeBytes = 32;
constexpr uint64_t kTestBlobStorageMaxFileSizeBytes = 100;
constexpr uint64_t kTestBlobStorageMaxDiskSpace = 1000;
enum class LengthHintTestType {
kUnknownSize,
kCorrectSize,
kTooLargeSize,
kTooSmallSize
};
} // namespace
class BlobBuilderFromStreamTestWithDelayedLimits
: public testing::TestWithParam<LengthHintTestType> {
public:
void SetUp() override {
ASSERT_TRUE(data_dir_.CreateUniqueTempDir());
context_ = std::make_unique<BlobStorageContext>(
data_dir_.GetPath(),
base::CreateTaskRunnerWithTraits({base::MayBlock()}));
limits_.max_ipc_memory_size = kTestBlobStorageMaxBytesDataItemSize;
limits_.max_shared_memory_size = kTestBlobStorageMaxBytesDataItemSize;
limits_.max_bytes_data_item_size = kTestBlobStorageMaxBytesDataItemSize;
limits_.max_blob_in_memory_space = kTestBlobStorageMaxBlobMemorySize;
limits_.min_page_file_size = kTestBlobStorageMinFileSizeBytes;
limits_.max_file_size = kTestBlobStorageMaxFileSizeBytes;
limits_.desired_max_disk_space = kTestBlobStorageMaxDiskSpace;
limits_.effective_max_disk_space = kTestBlobStorageMaxDiskSpace;
}
void TearDown() override {
// Make sure we clean up files.
base::RunLoop().RunUntilIdle();
base::TaskScheduler::GetInstance()->FlushForTesting();
base::RunLoop().RunUntilIdle();
}
uint64_t GetLengthHint(uint64_t actual_size) {
switch (GetParam()) {
case LengthHintTestType::kUnknownSize:
return 0;
case LengthHintTestType::kCorrectSize:
return actual_size;
case LengthHintTestType::kTooLargeSize:
return actual_size + actual_size / 2;
case LengthHintTestType::kTooSmallSize:
return actual_size / 2;
}
NOTREACHED();
return 0;
}
std::unique_ptr<BlobDataHandle> BuildFromString(
std::string data,
bool initial_allocation_should_succeed = true) {
mojo::DataPipe pipe;
base::RunLoop loop;
std::unique_ptr<BlobDataHandle> result;
uint64_t length_hint = GetLengthHint(data.length());
BlobBuilderFromStream* finished_builder = nullptr;
BlobBuilderFromStream builder(
context_->AsWeakPtr(), kContentType, kContentDisposition, length_hint,
std::move(pipe.consumer_handle), nullptr,
base::BindLambdaForTesting([&](BlobBuilderFromStream* result_builder,
std::unique_ptr<BlobDataHandle> blob) {
finished_builder = result_builder;
result = std::move(blob);
loop.Quit();
}));
// Make sure the initial memory allocation done by the builder matches the
// length hint passed in.
if (initial_allocation_should_succeed &&
GetParam() != LengthHintTestType::kUnknownSize && length_hint != 0) {
EXPECT_EQ(length_hint, context_->memory_controller().memory_usage() +
context_->memory_controller().disk_usage())
<< " memory_usage: " << context_->memory_controller().memory_usage()
<< ", disk_usage: " << context_->memory_controller().disk_usage();
}
mojo::BlockingCopyFromString(data, pipe.producer_handle);
pipe.producer_handle.reset();
loop.Run();
EXPECT_EQ(&builder, finished_builder);
return result;
}
void VerifyBlobContents(base::span<const char> in_memory_data,
base::span<const char> on_disk_data,
const BlobDataSnapshot& blob_data) {
size_t next_memory_offset = 0;
size_t next_file_offset = 0;
for (const auto& item : blob_data.items()) {
if (item->type() == BlobDataItem::Type::kBytes) {
EXPECT_EQ(0u, next_file_offset)
<< "Bytes item after file items is invalid";
EXPECT_LE(item->length(), kTestBlobStorageMaxBlobMemorySize);
ASSERT_LE(next_memory_offset + item->length(), in_memory_data.size());
EXPECT_TRUE(std::equal(
in_memory_data.begin() + next_memory_offset,
in_memory_data.begin() + next_memory_offset + item->length(),
item->bytes().begin(), item->bytes().end()));
next_memory_offset += item->length();
} else if (item->type() == BlobDataItem::Type::kFile) {
EXPECT_EQ(next_memory_offset, in_memory_data.size())
<< "File item before all in memory data was found";
EXPECT_LE(item->length(), kTestBlobStorageMaxFileSizeBytes);
ASSERT_LE(next_file_offset + item->length(), on_disk_data.size());
std::string file_contents;
EXPECT_TRUE(base::ReadFileToString(item->path(), &file_contents));
EXPECT_EQ(item->length(), file_contents.size());
EXPECT_TRUE(
std::equal(on_disk_data.begin() + next_file_offset,
on_disk_data.begin() + next_file_offset + item->length(),
file_contents.begin(), file_contents.end()));
next_file_offset += item->length();
if (next_file_offset < on_disk_data.size()) {
EXPECT_EQ(kTestBlobStorageMaxFileSizeBytes, item->length())
<< "All but the last file should be max sized";
}
} else {
ADD_FAILURE() << "Invalid blob item type: "
<< static_cast<int>(item->type());
}
}
EXPECT_EQ(next_memory_offset, in_memory_data.size());
EXPECT_EQ(next_file_offset, on_disk_data.size());
}
protected:
const std::string kContentType = "content/type";
const std::string kContentDisposition = "disposition";
base::ScopedTempDir data_dir_;
base::test::ScopedTaskEnvironment scoped_task_environment_;
BlobStorageLimits limits_;
std::unique_ptr<BlobStorageContext> context_;
};
class BlobBuilderFromStreamTest
: public BlobBuilderFromStreamTestWithDelayedLimits {
public:
void SetUp() override {
BlobBuilderFromStreamTestWithDelayedLimits::SetUp();
context_->set_limits_for_testing(limits_);
}
};
TEST_P(BlobBuilderFromStreamTest, CallbackCalledOnDeletion) {
mojo::DataPipe pipe;
base::RunLoop loop;
BlobBuilderFromStream* builder_ptr = nullptr;
auto builder = std::make_unique<BlobBuilderFromStream>(
context_->AsWeakPtr(), "", "", GetLengthHint(16),
std::move(pipe.consumer_handle), nullptr,
base::BindLambdaForTesting([&](BlobBuilderFromStream* result_builder,
std::unique_ptr<BlobDataHandle> blob) {
EXPECT_EQ(builder_ptr, result_builder);
EXPECT_FALSE(blob);
loop.Quit();
}));
builder_ptr = builder.get();
builder.reset();
loop.Run();
}
TEST_P(BlobBuilderFromStreamTest, EmptyStream) {
std::unique_ptr<BlobDataHandle> result = BuildFromString("");
ASSERT_TRUE(result);
EXPECT_FALSE(result->uuid().empty());
EXPECT_EQ(BlobStatus::DONE, result->GetBlobStatus());
EXPECT_EQ(kContentType, result->content_type());
EXPECT_EQ(kContentDisposition, result->content_disposition());
EXPECT_EQ(0u, result->size());
// Verify memory usage.
EXPECT_EQ(0u, context_->memory_controller().memory_usage());
EXPECT_EQ(0u, context_->memory_controller().disk_usage());
// Verify blob contents.
VerifyBlobContents(base::span<const char>(), base::span<const char>(),
*result->CreateSnapshot());
}
TEST_P(BlobBuilderFromStreamTest, SmallStream) {
const std::string kData =
base::RandBytesAsString(kTestBlobStorageMaxBytesDataItemSize + 5);
std::unique_ptr<BlobDataHandle> result = BuildFromString(kData);
ASSERT_TRUE(result);
EXPECT_FALSE(result->uuid().empty());
EXPECT_EQ(BlobStatus::DONE, result->GetBlobStatus());
EXPECT_EQ(kData.size(), result->size());
// Verify memory usage.
EXPECT_EQ(kData.size(), context_->memory_controller().memory_usage());
EXPECT_EQ(0u, context_->memory_controller().disk_usage());
// Verify blob contents.
VerifyBlobContents(kData, base::span<const char>(),
*result->CreateSnapshot());
}
TEST_P(BlobBuilderFromStreamTest, MediumStream) {
const std::string kData =
base::RandBytesAsString(kTestBlobStorageMinFileSizeBytes * 3 + 13);
std::unique_ptr<BlobDataHandle> result = BuildFromString(kData);
ASSERT_TRUE(result);
EXPECT_FALSE(result->uuid().empty());
EXPECT_EQ(BlobStatus::DONE, result->GetBlobStatus());
EXPECT_EQ(kData.size(), result->size());
// Verify memory usage.
if (GetParam() == LengthHintTestType::kUnknownSize) {
EXPECT_EQ(2 * kTestBlobStorageMaxBytesDataItemSize,
context_->memory_controller().memory_usage());
EXPECT_EQ(kData.size() - 2 * kTestBlobStorageMaxBytesDataItemSize,
context_->memory_controller().disk_usage());
} else {
EXPECT_EQ(0u, context_->memory_controller().memory_usage());
EXPECT_EQ(kData.size(), context_->memory_controller().disk_usage());
}
// Verify blob contents.
if (GetParam() == LengthHintTestType::kUnknownSize) {
VerifyBlobContents(base::make_span(kData).subspan(
0, 2 * kTestBlobStorageMaxBytesDataItemSize),
base::make_span(kData).subspan(
2 * kTestBlobStorageMaxBytesDataItemSize),
*result->CreateSnapshot());
} else {
VerifyBlobContents(base::span<const char>(), kData,
*result->CreateSnapshot());
}
}
TEST_P(BlobBuilderFromStreamTest, LargeStream) {
const std::string kData = base::RandBytesAsString(
kTestBlobStorageMaxDiskSpace - kTestBlobStorageMinFileSizeBytes);
std::unique_ptr<BlobDataHandle> result =
BuildFromString(kData, GetParam() != LengthHintTestType::kTooLargeSize);
if (GetParam() == LengthHintTestType::kTooLargeSize) {
EXPECT_FALSE(result);
EXPECT_EQ(0u, context_->memory_controller().memory_usage());
EXPECT_EQ(0u, context_->memory_controller().disk_usage());
return;
}
ASSERT_TRUE(result);
EXPECT_FALSE(result->uuid().empty());
EXPECT_EQ(BlobStatus::DONE, result->GetBlobStatus());
EXPECT_EQ(kData.size(), result->size());
// Verify memory usage.
if (GetParam() == LengthHintTestType::kUnknownSize) {
EXPECT_EQ(2 * kTestBlobStorageMaxBytesDataItemSize,
context_->memory_controller().memory_usage());
EXPECT_EQ(kData.size() - 2 * kTestBlobStorageMaxBytesDataItemSize,
context_->memory_controller().disk_usage());
} else {
EXPECT_EQ(0u, context_->memory_controller().memory_usage());
EXPECT_EQ(kData.size(), context_->memory_controller().disk_usage());
}
// Verify blob contents.
if (GetParam() == LengthHintTestType::kUnknownSize) {
VerifyBlobContents(base::make_span(kData).subspan(
0, 2 * kTestBlobStorageMaxBytesDataItemSize),
base::make_span(kData).subspan(
2 * kTestBlobStorageMaxBytesDataItemSize),
*result->CreateSnapshot());
} else {
VerifyBlobContents(base::span<const char>(), kData,
*result->CreateSnapshot());
}
}
TEST_P(BlobBuilderFromStreamTest, TooLargeForQuota) {
const std::string kData = base::RandBytesAsString(
kTestBlobStorageMaxDiskSpace + kTestBlobStorageMaxBlobMemorySize + 1);
std::unique_ptr<BlobDataHandle> result =
BuildFromString(kData, GetParam() == LengthHintTestType::kTooSmallSize);
EXPECT_FALSE(result);
// Make sure we clean up files.
base::RunLoop().RunUntilIdle();
base::TaskScheduler::GetInstance()->FlushForTesting();
base::RunLoop().RunUntilIdle();
EXPECT_EQ(0u, context_->memory_controller().memory_usage());
EXPECT_EQ(0u, context_->memory_controller().disk_usage());
}
TEST_P(BlobBuilderFromStreamTest, TooLargeForQuotaAndNoDisk) {
context_->DisableFilePagingForTesting();
const std::string kData =
base::RandBytesAsString(kTestBlobStorageMaxBlobMemorySize + 1);
std::unique_ptr<BlobDataHandle> result =
BuildFromString(kData, GetParam() == LengthHintTestType::kTooSmallSize);
EXPECT_FALSE(result);
EXPECT_EQ(0u, context_->memory_controller().memory_usage());
EXPECT_EQ(0u, context_->memory_controller().disk_usage());
}
// The next two tests are similar to the previous two, except they don't send
// any data over the datapipe, but should still result in failure as the
// initial memory/file allocation should fail.
TEST_F(BlobBuilderFromStreamTest, HintTooLargeForQuota) {
const uint64_t kLengthHint =
kTestBlobStorageMaxDiskSpace + kTestBlobStorageMaxBlobMemorySize + 1;
mojo::DataPipe pipe;
base::RunLoop loop;
std::unique_ptr<BlobDataHandle> result;
BlobBuilderFromStream builder(
context_->AsWeakPtr(), "", "", kLengthHint,
std::move(pipe.consumer_handle), nullptr,
base::BindLambdaForTesting(
[&](BlobBuilderFromStream*, std::unique_ptr<BlobDataHandle> blob) {
result = std::move(blob);
loop.Quit();
}));
pipe.producer_handle.reset();
loop.Run();
EXPECT_FALSE(result);
EXPECT_EQ(0u, context_->memory_controller().memory_usage());
EXPECT_EQ(0u, context_->memory_controller().disk_usage());
}
TEST_F(BlobBuilderFromStreamTest, HintTooLargeForQuotaAndNoDisk) {
context_->DisableFilePagingForTesting();
const uint64_t kLengthHint = kTestBlobStorageMaxBlobMemorySize + 1;
mojo::DataPipe pipe;
base::RunLoop loop;
std::unique_ptr<BlobDataHandle> result;
BlobBuilderFromStream builder(
context_->AsWeakPtr(), "", "", kLengthHint,
std::move(pipe.consumer_handle), nullptr,
base::BindLambdaForTesting(
[&](BlobBuilderFromStream*, std::unique_ptr<BlobDataHandle> blob) {
result = std::move(blob);
loop.Quit();
}));
pipe.producer_handle.reset();
loop.Run();
EXPECT_FALSE(result);
EXPECT_EQ(0u, context_->memory_controller().memory_usage());
EXPECT_EQ(0u, context_->memory_controller().disk_usage());
}
TEST_P(BlobBuilderFromStreamTest, ProgressEvents) {
const std::string kData =
base::RandBytesAsString(kTestBlobStorageMaxBytesDataItemSize + 5);
FakeProgressClient progress_client;
blink::mojom::ProgressClientAssociatedPtr progress_client_ptr;
mojo::AssociatedBinding<blink::mojom::ProgressClient> progress_binding(
&progress_client,
MakeRequestAssociatedWithDedicatedPipe(&progress_client_ptr));
mojo::DataPipe pipe;
base::RunLoop loop;
std::unique_ptr<BlobDataHandle> result;
BlobBuilderFromStream builder(
context_->AsWeakPtr(), "", "", GetLengthHint(kData.size()),
std::move(pipe.consumer_handle), progress_client_ptr.PassInterface(),
base::BindLambdaForTesting(
[&](BlobBuilderFromStream*, std::unique_ptr<BlobDataHandle> blob) {
result = std::move(blob);
loop.Quit();
}));
mojo::BlockingCopyFromString(kData, pipe.producer_handle);
pipe.producer_handle.reset();
loop.Run();
progress_binding.FlushForTesting();
EXPECT_EQ(kData.size(), progress_client.total_size);
EXPECT_GE(progress_client.call_count, 2);
}
INSTANTIATE_TEST_SUITE_P(BlobBuilderFromStreamTest,
BlobBuilderFromStreamTest,
::testing::Values(LengthHintTestType::kUnknownSize,
LengthHintTestType::kCorrectSize,
LengthHintTestType::kTooLargeSize,
LengthHintTestType::kTooSmallSize));
TEST_F(BlobBuilderFromStreamTestWithDelayedLimits, LargeStream) {
const std::string kData =
base::RandBytesAsString(kDefaultMinPageFileSize + 32);
limits_.desired_max_disk_space = kDefaultMinPageFileSize * 2;
limits_.effective_max_disk_space = kDefaultMinPageFileSize * 2;
mojo::DataPipe pipe;
base::RunLoop loop;
std::unique_ptr<BlobDataHandle> result;
BlobBuilderFromStream builder(
context_->AsWeakPtr(), kContentType, kContentDisposition, kData.size(),
std::move(pipe.consumer_handle), nullptr,
base::BindLambdaForTesting([&](BlobBuilderFromStream* result_builder,
std::unique_ptr<BlobDataHandle> blob) {
result = std::move(blob);
loop.Quit();
}));
context_->set_limits_for_testing(limits_);
auto data_producer = std::make_unique<mojo::StringDataPipeProducer>(
std::move(pipe.producer_handle));
auto* producer_ptr = data_producer.get();
producer_ptr->Write(
kData,
mojo::StringDataPipeProducer::AsyncWritingMode::
STRING_STAYS_VALID_UNTIL_COMPLETION,
base::BindOnce(
base::DoNothing::Once<std::unique_ptr<mojo::StringDataPipeProducer>,
MojoResult>(),
std::move(data_producer)));
pipe.producer_handle.reset();
loop.Run();
ASSERT_TRUE(result);
EXPECT_FALSE(result->uuid().empty());
EXPECT_EQ(BlobStatus::DONE, result->GetBlobStatus());
EXPECT_EQ(kData.size(), result->size());
}
} // namespace storage