blob: 6884336237718a325ff42a01d3f7a676121ba99f [file] [log] [blame]
// Copyright 2021 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 <lib/fdio/directory.h>
#include <stdio.h>
#include <array>
#include <cstdint>
#include <cstdlib>
#include <filesystem>
#include <fbl/unique_fd.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "src/storage/fvm/format.h"
#include "src/storage/fvm/fvm_check.h"
#include "src/storage/volume_image/adapter/commands.h"
#include "src/storage/volume_image/adapter/commands/file_client.h"
#include "src/storage/volume_image/fvm/fvm_sparse_image.h"
#include "src/storage/volume_image/fvm/options.h"
#include "src/storage/volume_image/utils/fd_reader.h"
#include "src/storage/volume_image/utils/fd_test_helper.h"
#include "src/storage/volume_image/utils/fd_writer.h"
#include "src/storage/volume_image/utils/lz4_decompressor.h"
namespace storage::volume_image {
namespace {
constexpr std::string_view kBlobfsImagePath =
STORAGE_VOLUME_IMAGE_ADAPTER_TEST_IMAGE_PATH "test_blobfs.blk";
constexpr std::string_view kMinfsImagePath =
STORAGE_VOLUME_IMAGE_ADAPTER_TEST_IMAGE_PATH "test_minfs.blk";
constexpr uint64_t kImageSize{UINT64_C(350) * (1 << 20)};
constexpr uint64_t kInitialImageSize{UINT64_C(150) * (1 << 20)};
constexpr uint64_t kSliceSize{UINT64_C(32) * (1u << 10)};
CreateParams MakeParams() {
CreateParams params;
params.output_path = "some_path";
params.format = FvmImageFormat::kBlockImage;
params.is_output_embedded = false;
params.fvm_options.target_volume_size = kInitialImageSize;
params.fvm_options.max_volume_size = kImageSize;
params.fvm_options.slice_size = kSliceSize;
params.fvm_options.compression.schema = CompressionSchema::kNone;
PartitionParams minfs;
minfs.encrypted = false;
minfs.format = PartitionImageFormat::kMinfs;
minfs.source_image_path = kMinfsImagePath;
params.partitions.push_back(minfs);
PartitionParams blobfs;
blobfs.encrypted = false;
blobfs.format = PartitionImageFormat::kBlobfs;
blobfs.source_image_path = kBlobfsImagePath;
params.partitions.push_back(blobfs);
return params;
}
TEST(CreateCommandTest, NoOutputPathIsError) {
CreateParams params = MakeParams();
params.output_path = "";
ASSERT_TRUE(Create(params).is_error());
}
TEST(CreateCommandTest, EmbeddedOutputWithoutOffsetIsError) {
CreateParams params = MakeParams();
params.is_output_embedded = true;
params.offset = std::nullopt;
params.length = kInitialImageSize;
ASSERT_TRUE(Create(params).is_error());
}
TEST(CreateCommandTest, EmbeddedOutputWithoutLengthIsError) {
CreateParams params = MakeParams();
params.is_output_embedded = true;
params.offset = 12345;
params.length = std::nullopt;
ASSERT_TRUE(Create(params).is_error());
}
TEST(CreateCommandTest, SliceSizeZeroIsError) {
CreateParams params = MakeParams();
params.fvm_options.slice_size = 0;
ASSERT_TRUE(Create(params).is_error());
}
TEST(CreateCommandTest, SliceSizeNoMultipleOfFvmBlockIsError) {
CreateParams params = MakeParams();
params.fvm_options.slice_size = fvm::kBlockSize + 1;
ASSERT_TRUE(Create(params).is_error());
}
TEST(CreateCommandTest, UnableToCreateOutputPathIsError) {
CreateParams params = MakeParams();
params.output_path = "/absolute/path/doesnt/exist/file.txt";
ASSERT_TRUE(Create(params).is_error());
}
TEST(CreateCommandTest, PartitionOptionsArePropagated) {
auto output_file_or = TempFile::Create();
ASSERT_TRUE(output_file_or.is_ok());
// Set really small max bytes for a partiton, and it should fail if they are propagated.
CreateParams params = MakeParams();
params.output_path = output_file_or.value().path();
params.format = FvmImageFormat::kBlockImage;
params.fvm_options.compression.schema = CompressionSchema::kNone;
// Max bytes.
// We only test max bytes, because is the only one that has hard failure.
params.partitions.front().options.max_bytes = 1;
ASSERT_TRUE(Create(params).is_error());
}
TEST(CreateCommandTest, CreateFvmBlockImageIsOk) {
auto output_file_or = TempFile::Create();
ASSERT_TRUE(output_file_or.is_ok());
CreateParams params = MakeParams();
params.output_path = output_file_or.value().path();
params.format = FvmImageFormat::kBlockImage;
params.fvm_options.compression.schema = CompressionSchema::kNone;
ASSERT_TRUE(Create(params).is_ok());
zx::result file = OpenFile(params.output_path.c_str());
ASSERT_TRUE(file.is_ok()) << file.status_string();
fvm::Checker fvm_checker(file.value(), 8 * (1 << 10), true);
ASSERT_TRUE(fvm_checker.Validate());
}
fpromise::result<void, std::string> Decompress(std::string_view path) {
auto output_file_or = TempFile::Create();
if (output_file_or.is_error()) {
return output_file_or.take_error_result();
}
Lz4Decompressor decompressor;
auto reader_or = FdReader::Create(path);
if (reader_or.is_error()) {
return reader_or.take_error_result();
}
auto reader = reader_or.take_value();
auto writer_or = FdWriter::Create(output_file_or.value().path());
if (writer_or.is_error()) {
return writer_or.take_error_result();
}
auto writer = writer_or.take_value();
uint64_t decompressed_bytes = 0;
if (auto result = decompressor.Prepare([&decompressed_bytes, &writer](auto decompressed_data)
-> fpromise::result<void, std::string> {
if (auto result = writer.Write(decompressed_bytes, decompressed_data); result.is_error()) {
return result;
}
decompressed_bytes += decompressed_data.size();
return fpromise::ok();
});
result.is_error()) {
return result;
}
std::vector<uint8_t> read_buffer;
read_buffer.resize(1 << 20, 0);
uint64_t read_bytes = 0;
uint64_t hint = read_buffer.size();
while (read_bytes < reader.length()) {
uint64_t view_size = read_buffer.size();
if (view_size > reader.length() - read_bytes) {
view_size = reader.length() - read_bytes;
}
if (view_size > hint) {
view_size = hint;
}
decompressor.ProvideSizeHint(view_size);
auto read_view = cpp20::span<uint8_t>(read_buffer).subspan(0, view_size);
if (auto result = reader.Read(read_bytes, read_view); result.is_error()) {
return result;
}
auto decompress_result = decompressor.Decompress(read_view);
if (decompress_result.is_error()) {
return decompress_result.take_error_result();
}
// How much was actually consumed from the input buffer.
auto [result_hint, consumed_bytes] = decompress_result.value();
read_bytes += consumed_bytes;
if (hint == 0) {
break;
}
hint = result_hint;
}
if (auto result = decompressor.Finalize(); result.is_error()) {
return result;
}
if (rename(output_file_or.value().path().data(), path.data()) == -1) {
return fpromise::error(
"Failed to move decompressed data to final destination. More specifically: " +
std::string(strerror(errno)));
}
return fpromise::ok();
}
TEST(CreateCommandTest, CreateCompressedFvmBlockImageIsOk) {
auto output_file_or = TempFile::Create();
ASSERT_TRUE(output_file_or.is_ok());
CreateParams params = MakeParams();
params.output_path = output_file_or.value().path();
params.format = FvmImageFormat::kBlockImage;
params.fvm_options.compression.schema = CompressionSchema::kLz4;
auto create_result = Create(params);
ASSERT_TRUE(create_result.is_ok()) << create_result.error();
auto result = Decompress(output_file_or.value().path());
ASSERT_TRUE(result.is_ok()) << result.error();
zx::result file = OpenFile(params.output_path.c_str());
ASSERT_TRUE(file.is_ok()) << file.status_string();
fvm::Checker fvm_checker(file.value(), 8 * (1 << 10), true);
ASSERT_TRUE(fvm_checker.Validate());
}
TEST(CreateCommandTest, CreateNonCompressedFvmSpareImageIsOk) {
auto output_file_or = TempFile::Create();
ASSERT_TRUE(output_file_or.is_ok());
CreateParams params = MakeParams();
params.output_path = output_file_or.value().path();
params.format = FvmImageFormat::kSparseImage;
params.fvm_options.compression.schema = CompressionSchema::kNone;
// Add an empty partition.
PartitionParams empty_partition;
empty_partition.format = PartitionImageFormat::kEmptyPartition;
empty_partition.label = "my-empty-partition";
empty_partition.encrypted = false;
empty_partition.options.max_bytes = 1;
params.partitions.push_back(empty_partition);
for (auto& partition_param : params.partitions) {
if (partition_param.format == PartitionImageFormat::kBlobfs) {
partition_param.encrypted = false;
}
if (partition_param.format == PartitionImageFormat::kMinfs) {
partition_param.encrypted = true;
}
}
ASSERT_TRUE(Create(params).is_ok());
auto fvm_reader_or = FdReader::Create(output_file_or.value().path());
ASSERT_TRUE(fvm_reader_or.is_ok()) << fvm_reader_or.error();
std::unique_ptr<Reader> fvm_reader = std::make_unique<FdReader>(fvm_reader_or.take_value());
auto descriptor_or = FvmSparseReadImage(0, std::move(fvm_reader));
ASSERT_TRUE(descriptor_or.is_ok()) << descriptor_or.error();
// Check that minfs is flagged as encrypted and that blobfs is not.
// This is set as the default params.
int checked_count = 0;
for (const auto& partition : descriptor_or.value().partitions()) {
if (partition.volume().name == "blobfs") {
ASSERT_EQ(partition.volume().encryption, EncryptionType::kNone);
checked_count++;
continue;
}
if (partition.volume().name == "data") {
ASSERT_EQ(partition.volume().encryption, EncryptionType::kZxcrypt);
checked_count++;
continue;
}
if (partition.volume().name == "my-empty-partition") {
ASSERT_EQ(partition.volume().encryption, EncryptionType::kNone);
checked_count++;
continue;
}
}
EXPECT_EQ(checked_count, 3);
}
TEST(CreateCommandTest, CreateNonCompressedFvmSpareImageWithNonEncryptedPartitionsIsOk) {
auto output_file_or = TempFile::Create();
ASSERT_TRUE(output_file_or.is_ok());
CreateParams params = MakeParams();
params.output_path = output_file_or.value().path();
params.format = FvmImageFormat::kSparseImage;
params.fvm_options.compression.schema = CompressionSchema::kNone;
for (auto& partition_param : params.partitions) {
if (partition_param.format == PartitionImageFormat::kBlobfs) {
partition_param.encrypted = false;
}
if (partition_param.format == PartitionImageFormat::kMinfs) {
partition_param.encrypted = false;
}
}
ASSERT_TRUE(Create(params).is_ok());
auto fvm_reader_or = FdReader::Create(output_file_or.value().path());
ASSERT_TRUE(fvm_reader_or.is_ok()) << fvm_reader_or.error();
std::unique_ptr<Reader> fvm_reader = std::make_unique<FdReader>(fvm_reader_or.take_value());
auto descriptor_or = FvmSparseReadImage(0, std::move(fvm_reader));
ASSERT_TRUE(descriptor_or.is_ok()) << descriptor_or.error();
// Check that minfs is flagged as encrypted and that blobfs is not.
// This is set as the default params.
EXPECT_EQ(descriptor_or.value().partitions().size(), 2u);
for (const auto& partition : descriptor_or.value().partitions()) {
ASSERT_EQ(partition.volume().encryption, EncryptionType::kNone);
}
}
TEST(CreateCommandTest, CreateCompressedFvmSpareImageIsOk) {
auto output_file_or = TempFile::Create();
ASSERT_TRUE(output_file_or.is_ok());
CreateParams params = MakeParams();
params.output_path = output_file_or.value().path();
params.format = FvmImageFormat::kSparseImage;
params.fvm_options.compression.schema = CompressionSchema::kLz4;
for (auto& partition_param : params.partitions) {
if (partition_param.format == PartitionImageFormat::kBlobfs) {
partition_param.encrypted = false;
}
if (partition_param.format == PartitionImageFormat::kMinfs) {
partition_param.encrypted = true;
}
}
ASSERT_TRUE(Create(params).is_ok());
auto fvm_reader_or = FdReader::Create(output_file_or.value().path());
ASSERT_TRUE(fvm_reader_or.is_ok()) << fvm_reader_or.error();
std::unique_ptr<Reader> fvm_reader = std::make_unique<FdReader>(fvm_reader_or.take_value());
auto decompressed_image_or = TempFile::Create();
ASSERT_TRUE(decompressed_image_or.is_ok());
auto fvm_writer_or = FdWriter::Create(decompressed_image_or.value().path());
ASSERT_TRUE(fvm_writer_or.is_ok()) << fvm_writer_or.error();
FdWriter fvm_writer = fvm_writer_or.take_value();
auto decompressed_or = FvmSparseDecompressImage(0, *fvm_reader, fvm_writer);
ASSERT_TRUE(decompressed_or.is_ok()) << decompressed_or.error();
// Should be a compressed image.
ASSERT_TRUE(decompressed_or.value());
auto decompresed_fvm_reader_or = FdReader::Create(decompressed_image_or.value().path());
ASSERT_TRUE(decompresed_fvm_reader_or.is_ok()) << decompresed_fvm_reader_or.error();
std::unique_ptr<Reader> decompresed_fvm_reader =
std::make_unique<FdReader>(decompresed_fvm_reader_or.take_value());
auto descriptor_or = FvmSparseReadImage(0, std::move(decompresed_fvm_reader));
ASSERT_TRUE(descriptor_or.is_ok()) << descriptor_or.error();
// Check that minfs is flagged as encrypted and that blobfs is not.
// This is set as the default params.
int checked_count = 0;
for (const auto& partition : descriptor_or.value().partitions()) {
if (partition.volume().name == "blobfs") {
ASSERT_EQ(partition.volume().encryption, EncryptionType::kNone);
checked_count++;
continue;
}
if (partition.volume().name == "data") {
ASSERT_EQ(partition.volume().encryption, EncryptionType::kZxcrypt);
checked_count++;
continue;
}
}
EXPECT_EQ(checked_count, 2);
}
TEST(CreateCommandTest, CreateEmbeddedFvmImageIsOk) {
auto output_file_or = TempFile::Create();
ASSERT_TRUE(output_file_or.is_ok());
constexpr uint64_t kImageSize{UINT64_C(10) * (1 << 20)};
CreateParams params = MakeParams();
params.output_path = output_file_or.value().path();
params.format = FvmImageFormat::kBlockImage;
params.fvm_options.compression.schema = CompressionSchema::kNone;
params.is_output_embedded = true;
params.offset = kImageSize / 2;
params.length = kImageSize;
ASSERT_EQ(truncate(std::string(output_file_or.value().path()).c_str(), 2 * kImageSize), 0);
for (auto& partition_param : params.partitions) {
if (partition_param.format == PartitionImageFormat::kBlobfs) {
partition_param.encrypted = false;
}
if (partition_param.format == PartitionImageFormat::kMinfs) {
partition_param.encrypted = false;
}
}
// Add poison values before the offset and after the length to verify afterwards.
std::array<uint8_t, 10> canary = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
auto output_writer_or = FdWriter::Create(params.output_path);
ASSERT_TRUE(output_writer_or.is_ok());
ASSERT_TRUE(output_writer_or.value().Write(params.offset.value() - canary.size(), canary));
ASSERT_TRUE(
output_writer_or.value().Write(params.offset.value() + params.length.value(), canary));
ASSERT_TRUE(Create(params).is_ok());
// Copy the range into a new file and do fvm check on it.
// Also check that the beginning and end are zeroes.
auto fvm_reader_or = FdReader::Create(output_file_or.value().path());
ASSERT_TRUE(fvm_reader_or.is_ok()) << fvm_reader_or.error();
std::unique_ptr<Reader> fvm_reader = std::make_unique<FdReader>(fvm_reader_or.take_value());
// Check canaries
std::array<uint8_t, 10> canary_buffer;
ASSERT_TRUE(fvm_reader->Read(params.offset.value() - canary.size(), canary_buffer));
ASSERT_TRUE(memcmp(canary_buffer.data(), canary.data(), canary.size()) == 0);
ASSERT_TRUE(fvm_reader->Read(params.offset.value() + params.length.value(), canary_buffer));
ASSERT_TRUE(memcmp(canary_buffer.data(), canary.data(), canary.size()) == 0);
uint64_t current_offset = params.offset.value();
std::vector<uint8_t> buffer;
buffer.resize(1 << 20, 0);
auto copy_file_or = TempFile::Create();
ASSERT_TRUE(copy_file_or.is_ok());
auto fvm_copy_or = FdWriter::Create(copy_file_or.value().path());
ASSERT_TRUE(fvm_copy_or.is_ok()) << fvm_copy_or.error();
std::unique_ptr<FdWriter> fvm_copy = std::make_unique<FdWriter>(fvm_copy_or.take_value());
// Copy contents into a new file to run fvm check.
while (current_offset < params.offset.value() + params.length.value()) {
auto buffer_view = cpp20::span<uint8_t>(buffer).subspan(
0, std::min(params.offset.value() + params.length.value() - current_offset,
static_cast<uint64_t>(buffer.size())));
;
ASSERT_TRUE(fvm_reader->Read(current_offset, buffer_view).is_ok());
ASSERT_TRUE(fvm_copy->Write(current_offset - params.offset.value(), buffer_view).is_ok());
current_offset += buffer_view.size();
}
zx::result file = OpenFile(std::string(copy_file_or.value().path()).c_str());
ASSERT_TRUE(file.is_ok()) << file.status_string();
fvm::Checker fvm_checker(file.value(), 8 * (1 << 10), true);
ASSERT_TRUE(fvm_checker.Validate());
}
TEST(CreateCommandTest, TooBigEmbeddedFvmImageIsError) {
auto output_file_or = TempFile::Create();
ASSERT_TRUE(output_file_or.is_ok());
CreateParams params = MakeParams();
params.output_path = output_file_or.value().path();
params.format = FvmImageFormat::kBlockImage;
params.fvm_options.compression.schema = CompressionSchema::kNone;
params.is_output_embedded = true;
params.offset = kInitialImageSize / 2;
params.length = 1;
ASSERT_EQ(truncate(std::string(output_file_or.value().path()).c_str(), 2 * kInitialImageSize), 0);
for (auto& partition_param : params.partitions) {
if (partition_param.format == PartitionImageFormat::kBlobfs) {
partition_param.encrypted = false;
}
if (partition_param.format == PartitionImageFormat::kMinfs) {
partition_param.encrypted = false;
}
}
// Add poison values before the offset and after the length to verify afterwards.
auto output_writer_or = FdWriter::Create(params.output_path);
ASSERT_TRUE(output_writer_or.is_ok());
ASSERT_TRUE(Create(params).is_error());
}
} // namespace
} // namespace storage::volume_image