| // 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 |