blob: 52408f66bb91f92ca79e1e63d0a214cbe5537f64 [file] [log] [blame]
// Copyright 2023 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 "src/storage/blobfs/delivery_blob.h"
#include <zircon/assert.h>
#include <zircon/errors.h>
#include <zircon/status.h>
#include <memory>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "src/storage/blobfs/blobfs.h"
#include "src/storage/blobfs/common.h"
#include "src/storage/blobfs/format.h"
#include "src/storage/blobfs/mkfs.h"
#include "src/storage/blobfs/test/blob_utils.h"
#include "src/storage/blobfs/test/blobfs_test_setup.h"
#include "src/storage/lib/block_client/cpp/fake_block_device.h"
namespace blobfs {
namespace {
constexpr uint32_t kTestDeviceBlockSize = 512;
constexpr uint32_t kTestDeviceNumBlocks = 400 * kBlobfsBlockSize / kTestDeviceBlockSize;
constexpr size_t kSmallBlobSize = 1024;
constexpr size_t kLargeBlobSize = kBlobfsBlockSize * 20;
// Large blobs must cover at least two levels in the Merkle tree to cover all branches.
static_assert(kLargeBlobSize > kBlobfsBlockSize);
struct DeliveryBlobTestParams {
// Blob layout format that the Blobfs instance should be formatted with.
BlobLayoutFormat format;
// If true, specify that the delivery blob should be compressed.
bool compress;
// Size of the blob the test case should write.
size_t blob_size;
using ParamsAsTuple = std::tuple</*format*/ BlobLayoutFormat, /*compress*/ bool,
/*blob_size*/ size_t>;
explicit DeliveryBlobTestParams(ParamsAsTuple params)
: format(std::get<0>(params)),
compress(std::get<1>(params)),
blob_size(std::get<2>(params)) {}
static auto GetTestCombinations() {
return testing::ConvertGenerator<ParamsAsTuple>(testing::Combine(
/*format*/ testing::Values(BlobLayoutFormat::kCompactMerkleTreeAtEnd,
BlobLayoutFormat::kDeprecatedPaddedMerkleTreeAtStart),
/*compress*/ testing::Bool(),
/*blob_size*/ testing::Values(0, kSmallBlobSize, kLargeBlobSize)));
}
static std::string GetTestParamName(const DeliveryBlobTestParams& params) {
// These tests use rather large parameter names, so we use a more compact format when describing
// which blob format the test case is using.
std::string format_name;
switch (params.format) {
case blobfs::BlobLayoutFormat::kDeprecatedPaddedMerkleTreeAtStart:
format_name = "DeprecatedFormat";
break;
case blobfs::BlobLayoutFormat::kCompactMerkleTreeAtEnd:
format_name = "CompactFormat";
break;
}
return format_name + std::string(params.compress ? "Compressed" : "Uncompressed") +
std::string(params.blob_size > 0 ? std::to_string(params.blob_size) : "NullBlob");
}
};
class DeliveryBlobTest : public BlobfsTestSetup,
public testing::TestWithParam<DeliveryBlobTestParams> {
public:
void SetUp() override {
auto device =
std::make_unique<block_client::FakeBlockDevice>(kTestDeviceNumBlocks, kTestDeviceBlockSize);
const FilesystemOptions filesystem_options{
.blob_layout_format = GetParam().format,
};
ASSERT_EQ(FormatFilesystem(device.get(), filesystem_options), ZX_OK);
ASSERT_EQ(ZX_OK, Mount(std::move(device), {}));
ASSERT_EQ(ZX_OK, blobfs()->OpenRootNode(&root_));
}
void TearDown() override {
if (root_) {
ASSERT_EQ(ZX_OK, root_->Close());
}
}
const fbl::RefPtr<fs::Vnode>& root() const {
ZX_ASSERT(root_);
return root_;
}
private:
fbl::RefPtr<fs::Vnode> root_ = nullptr;
};
TEST_P(DeliveryBlobTest, WriteAll) {
const std::unique_ptr<BlobInfo> info =
GenerateRandomBlob(/*mount_path*/ "", GetParam().blob_size);
const fbl::Array<uint8_t> delivery_blob =
GenerateDeliveryBlobType1({info->data.get(), info->size_data}, GetParam().compress).value();
zx::result file =
root()->Create(GetDeliveryBlobPath(info->GetMerkleRoot()), fs::CreationType::kFile);
ASSERT_TRUE(file.is_ok()) << file.status_string();
ASSERT_EQ(ZX_OK, file->Truncate(delivery_blob.size()));
size_t out_actual;
ASSERT_EQ(ZX_OK, file->Write(delivery_blob.data(), delivery_blob.size(), 0, &out_actual));
ASSERT_EQ(out_actual, delivery_blob.size());
ASSERT_EQ(ZX_OK, file->Close());
// Try to open the newly created blob.
fbl::RefPtr<fs::Vnode> file_ptr;
ASSERT_EQ(ZX_OK, root()->Lookup(info->GetMerkleRoot(), &file_ptr));
ASSERT_EQ(ZX_OK, file_ptr->Open(&*file));
// Validate file contents.
if (GetParam().blob_size > 0) {
std::vector<uint8_t> file_contents(info->size_data);
ASSERT_EQ(ZX_OK, file->Read(file_contents.data(), info->size_data, 0, &out_actual));
ASSERT_EQ(out_actual, info->size_data);
ASSERT_EQ(std::memcmp(info->data.get(), file_contents.data(), info->size_data), 0)
<< "Blob contents don't match after writing to disk.";
}
ASSERT_EQ(ZX_OK, file->Close());
}
TEST_P(DeliveryBlobTest, WriteChunked) {
const std::unique_ptr<BlobInfo> info =
GenerateRandomBlob(/*mount_path*/ "", GetParam().blob_size);
const fbl::Array<uint8_t> delivery_blob =
GenerateDeliveryBlobType1({info->data.get(), info->size_data}, GetParam().compress).value();
zx::result file =
root()->Create(GetDeliveryBlobPath(info->GetMerkleRoot()), fs::CreationType::kFile);
ASSERT_TRUE(file.is_ok()) << file.status_string();
ASSERT_EQ(file->Truncate(delivery_blob.size()), ZX_OK);
// Write the delivery blob in chunks. We use a very small chunk size to cover more edge cases.
constexpr size_t kChunkSize = 4;
size_t bytes_written = 0;
while (bytes_written < delivery_blob.size()) {
const size_t to_write = std::min(kChunkSize, delivery_blob.size() - bytes_written);
size_t out_actual;
ASSERT_EQ(ZX_OK, file->Write(delivery_blob.data() + bytes_written, to_write, bytes_written,
&out_actual))
<< "Failed to write " << to_write << " bytes at offset " << bytes_written;
ASSERT_EQ(out_actual, to_write);
bytes_written += out_actual;
}
ASSERT_EQ(bytes_written, delivery_blob.size());
ASSERT_EQ(file->Close(), ZX_OK);
// Try to open the newly created blob.
fbl::RefPtr<fs::Vnode> file_ptr;
ASSERT_EQ(ZX_OK, root()->Lookup(info->GetMerkleRoot(), &file_ptr));
ASSERT_EQ(ZX_OK, file_ptr->Open(&*file));
// Validate file contents.
if (GetParam().blob_size > 0) {
std::vector<uint8_t> file_contents(info->size_data);
size_t out_actual;
ASSERT_EQ(ZX_OK, file->Read(file_contents.data(), info->size_data, 0, &out_actual));
ASSERT_EQ(out_actual, info->size_data);
ASSERT_EQ(std::memcmp(info->data.get(), file_contents.data(), info->size_data), 0)
<< "Blob contents don't match after writing to disk.";
}
ASSERT_EQ(ZX_OK, file->Close());
}
// Verify that CalculateDeliveryBlobDigest works with all types of generated delivery blobs.
TEST_P(DeliveryBlobTest, CalculateDeliveryBlobDigest) {
const std::unique_ptr<BlobInfo> info =
GenerateRandomBlob(/*mount_path*/ "", GetParam().blob_size);
const fbl::Array<uint8_t> delivery_blob =
GenerateDeliveryBlobType1({info->data.get(), info->size_data}, GetParam().compress).value();
zx::result<digest::Digest> digest = CalculateDeliveryBlobDigest(delivery_blob);
ASSERT_TRUE(digest.is_ok()) << digest.status_string() << "Data length: " << delivery_blob.size();
const auto digest_str = digest->ToString();
ASSERT_EQ(info->GetMerkleRoot(), digest_str);
}
std::string GetTestParamName(const ::testing::TestParamInfo<DeliveryBlobTestParams>& p) {
return DeliveryBlobTestParams::GetTestParamName(p.param);
}
INSTANTIATE_TEST_SUITE_P(
/*no prefix*/, DeliveryBlobTest, DeliveryBlobTestParams::GetTestCombinations(),
GetTestParamName);
} // namespace
} // namespace blobfs