| // 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 "src/storage/volume_image/utils/lz4_decompress_reader.h" |
| |
| #include <lib/stdcompat/span.h> |
| #include <sys/types.h> |
| |
| #include <cstdint> |
| #include <string_view> |
| |
| #include <gmock/gmock.h> |
| #include <gtest/gtest.h> |
| |
| #include "src/storage/fvm/fvm_sparse.h" |
| #include "src/storage/volume_image/fvm/fvm_sparse_image.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_compressor.h" |
| #include "src/storage/volume_image/utils/lz4_decompressor.h" |
| |
| namespace storage::volume_image { |
| namespace { |
| // Path to a compressed sparse image. |
| constexpr std::string_view kFvmSparseImagePath = |
| STORAGE_VOLUME_IMAGE_ADAPTER_TEST_IMAGE_PATH "test_fvm.sparse.blk"; |
| |
| constexpr std::string_view kLoremIpsum = |
| R"(Lorem ipsum dolor sit amet, consectetur adipiscing elit, |
| sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. |
| Elit pellentesque habitant morbi tristique senectus et netus et. Blandit |
| aliquam etiam erat velit scelerisque in. Placerat orci nulla pellentesque |
| dignissim enim sit amet. Suspendisse ultrices gravida dictum fusce ut placerat |
| orci. Pretium aenean pharetra magna ac placerat vestibulum lectus mauris ultrices. |
| Nibh venenatis cras sed felis eget velit aliquet sagittis. Risus quis varius quam |
| quisque id diam vel. Sed enim ut sem viverra. Fusce id velit ut tortor pretium. |
| Amet dictum sit amet justo donec enim diam vulputate ut. Faucibus scelerisque eleifend |
| donec pretium vulputate sapien nec. Curabitur gravida arcu ac tortor dignissim |
| convallis aenean. Morbi non arcu risus quis varius quam quisque. Vitae suscipit |
| tellus mauris a diam maecenas. Mattis enim ut tellus elementum sagittis vitae et leo |
| duis. Lacinia quis vel eros donec ac odio. |
| |
| Feugiat in ante metus dictum at. Amet nisl suscipit adipiscing bibendum est. |
| Bibendum ut tristique et egestas quis ipsum suspendisse ultrices. Sed euismod nisi |
| porta lorem mollis aliquam ut porttitor leo. Libero id faucibus nisl tincidunt eget. |
| Gravida in fermentum et sollicitudin ac orci. Accumsan sit amet nulla facilisi morbi |
| tempus. Sed euismod nisi porta lorem mollis aliquam ut. Sed velit dignissim sodales |
| ut eu sem integer. Purus in massa tempor nec feugiat nisl pretium. Eros in cursus |
| turpis massa. |
| |
| A diam maecenas sed enim ut. Leo in vitae turpis massa sed. Lobortis scelerisque |
| fermentum dui faucibus in ornare. Nullam eget felis eget nunc lobortis mattis. A cras |
| semper auctor neque vitae tempus. Dignissim suspendisse in est ante in nibh mauris |
| cursus. Dictumst quisque sagittis purus sit amet volutpat consequat mauris nunc. Vel |
| quam elementum pulvinar etiam non quam lacus suspendisse faucibus. Libero just |
| laoreet sit amet cursus sit amet. Imperdiet dui accumsan sit amet nulla. Platea |
| dictumst quisque sagittis purus. Lobortis mattis aliquam faucibus purus in massa. Nec |
| sagittis aliquam malesuada bibendum. Eu sem integer vitae justo. Sit amet dictum sit |
| amet justo donec enim. Aliquet sagittis id consectetur purus ut faucibus pulvinar |
| elementum integer. Diam vulputate ut pharetra sit amet aliquam. At consectetur lorem |
| donec massa sapien faucibus et.)"; |
| |
| fpromise::result<std::vector<uint8_t>, std::string> CompressedData( |
| cpp20::span<const uint8_t> source_data) { |
| std::vector<uint8_t> compressed_data; |
| Lz4Compressor compressor; |
| if (auto result = |
| compressor.Prepare([&compressed_data](cpp20::span<const uint8_t> compressed_chunk) { |
| compressed_data.insert(compressed_data.end(), compressed_chunk.begin(), |
| compressed_chunk.end()); |
| return fpromise::ok(); |
| }); |
| result.is_error()) { |
| return result.take_error_result(); |
| } |
| |
| if (auto result = compressor.Compress(source_data); result.is_error()) { |
| return result.take_error_result(); |
| } |
| |
| if (auto result = compressor.Finalize(); result.is_error()) { |
| return result.take_error_result(); |
| } |
| |
| return fpromise::ok(compressed_data); |
| } |
| |
| // Compressed Reader. |
| class FakeReader : public Reader { |
| public: |
| FakeReader(std::vector<uint8_t> data) : data_(data) {} |
| |
| uint64_t length() const final { return data_.size(); } |
| |
| fpromise::result<void, std::string> Read(uint64_t offset, |
| cpp20::span<uint8_t> buffer) const final { |
| if (buffer.empty()) { |
| return fpromise::ok(); |
| } |
| if (offset + buffer.size() > data_.size()) { |
| return fpromise::error("FakeReader::Read out of bounds."); |
| } |
| memcpy(buffer.data(), data_.data() + offset, buffer.size()); |
| return fpromise::ok(); |
| } |
| |
| private: |
| std::vector<uint8_t> data_; |
| }; |
| |
| constexpr uint64_t kUncompressedDataPrefix = 128; |
| constexpr uint64_t kDecompressedLength = kUncompressedDataPrefix + kLoremIpsum.size(); |
| constexpr uint64_t kMaxBufferLength = kUncompressedDataPrefix + 1; |
| // constexpr uint64_t kMaxReadBufferLength = kUncompressedDataPrefix / 3; |
| |
| fpromise::result<std::vector<uint8_t>, std::string> GetData() { |
| auto data_or = CompressedData(cpp20::span<const uint8_t>( |
| reinterpret_cast<const uint8_t*>(kLoremIpsum.data()), kLoremIpsum.size())); |
| if (data_or.is_error()) { |
| return data_or.take_error_result(); |
| } |
| auto data = data_or.take_value(); |
| |
| data.insert(data.begin(), kLoremIpsum.begin(), kLoremIpsum.begin() + kUncompressedDataPrefix); |
| return fpromise::ok(data); |
| } |
| |
| void CheckRangeMatch(uint64_t offset, const Reader& reader, |
| cpp20::span<const uint8_t> expected_data) { |
| uint64_t bytes_to_read = reader.length() - offset; |
| if (bytes_to_read > expected_data.size()) { |
| bytes_to_read = expected_data.size(); |
| } |
| |
| std::vector<uint8_t> data; |
| data.resize(bytes_to_read, 0); |
| |
| auto result = reader.Read(offset, data); |
| ASSERT_TRUE(result.is_ok()) << result.error(); |
| |
| EXPECT_TRUE(memcmp(data.data(), expected_data.data(), bytes_to_read) == 0); |
| |
| if (data.empty()) { |
| return; |
| } |
| } |
| |
| TEST(Lz4DecompressReaderTest, ReadingUncompressedAreaIsOk) { |
| auto data_or = GetData(); |
| ASSERT_TRUE(data_or.is_ok()) << data_or.error(); |
| auto data = data_or.take_value(); |
| |
| std::shared_ptr<FakeReader> compressed_reader = std::make_shared<FakeReader>(data); |
| Lz4DecompressReader decompressed_reader(kUncompressedDataPrefix, kDecompressedLength, |
| compressed_reader); |
| auto init_result = decompressed_reader.Initialize(kMaxBufferLength); |
| ASSERT_TRUE(init_result.is_ok()) << init_result.error(); |
| |
| auto view = cpp20::span<uint8_t>(data); |
| |
| // Read part of uncompressed data only. |
| ASSERT_NO_FATAL_FAILURE( |
| CheckRangeMatch(0, decompressed_reader, view.subspan(0, kUncompressedDataPrefix / 4))); |
| |
| // The entire uncompressed data. |
| ASSERT_NO_FATAL_FAILURE( |
| CheckRangeMatch(0, decompressed_reader, view.subspan(0, kUncompressedDataPrefix))); |
| } |
| |
| TEST(Lz4DecompressReaderTest, ReadingCompressedAreaIsOk) { |
| auto data_or = GetData(); |
| ASSERT_TRUE(data_or.is_ok()) << data_or.error(); |
| auto data = data_or.take_value(); |
| |
| std::shared_ptr<FakeReader> compressed_reader = std::make_shared<FakeReader>(data); |
| Lz4DecompressReader decompressed_reader(kUncompressedDataPrefix, kDecompressedLength, |
| compressed_reader); |
| auto init_result = decompressed_reader.Initialize(kMaxBufferLength); |
| ASSERT_TRUE(init_result.is_ok()) << init_result.error(); |
| |
| auto lorem_ipsum = cpp20::span<const uint8_t>( |
| reinterpret_cast<const uint8_t*>(kLoremIpsum.data()), kLoremIpsum.size()); |
| |
| // Random chunk. |
| ASSERT_NO_FATAL_FAILURE(CheckRangeMatch(kUncompressedDataPrefix + 500, decompressed_reader, |
| lorem_ipsum.subspan(500))); |
| |
| // Read part of uncompressed data only. |
| ASSERT_NO_FATAL_FAILURE( |
| CheckRangeMatch(kUncompressedDataPrefix, decompressed_reader, lorem_ipsum.subspan(0, 1))); |
| |
| // The entire uncompressed data. |
| ASSERT_NO_FATAL_FAILURE( |
| CheckRangeMatch(kUncompressedDataPrefix, decompressed_reader, lorem_ipsum)); |
| } |
| |
| TEST(Lz4DecompressReaderTest, ReadingBothAreasIsOk) { |
| auto data_or = GetData(); |
| ASSERT_TRUE(data_or.is_ok()) << data_or.error(); |
| auto data = data_or.take_value(); |
| |
| std::shared_ptr<FakeReader> compressed_reader = std::make_shared<FakeReader>(data); |
| Lz4DecompressReader decompressed_reader(kUncompressedDataPrefix, kDecompressedLength, |
| compressed_reader); |
| auto init_result = decompressed_reader.Initialize(kMaxBufferLength); |
| ASSERT_TRUE(init_result.is_ok()) << init_result.error(); |
| |
| std::array<uint8_t, 2> expected_data = {data[kUncompressedDataPrefix - 1], kLoremIpsum[0]}; |
| |
| // The entire uncompressed data. |
| ASSERT_NO_FATAL_FAILURE( |
| CheckRangeMatch(kUncompressedDataPrefix - 1, decompressed_reader, expected_data)); |
| } |
| |
| TEST(Lz4DecompressReaderTest, DecompressingSparseFvmIsOk) { |
| auto decompressed_image_or = TempFile::Create(); |
| ASSERT_TRUE(decompressed_image_or.is_ok()) << decompressed_image_or.error(); |
| auto decompressed_image = decompressed_image_or.take_value(); |
| |
| auto compressed_reader_or = FdReader::Create(kFvmSparseImagePath); |
| ASSERT_TRUE(compressed_reader_or.is_ok()) << compressed_reader_or.error(); |
| auto compressed_reader = compressed_reader_or.take_value(); |
| |
| auto decompressed_writer_or = FdWriter::Create(decompressed_image.path()); |
| ASSERT_TRUE(decompressed_writer_or.is_ok()) << decompressed_writer_or.error(); |
| auto decompressed_writer = decompressed_writer_or.take_value(); |
| |
| auto decompress_result = FvmSparseDecompressImage(0, compressed_reader, decompressed_writer); |
| ASSERT_TRUE(decompress_result.is_ok()) << decompress_result.error(); |
| ASSERT_TRUE(decompress_result.value()); |
| |
| // Read the header. |
| fvm::SparseImage header; |
| cpp20::span<uint8_t> header_buffer(reinterpret_cast<uint8_t*>(&header), sizeof(header)); |
| auto header_read_result = compressed_reader.Read(0, header_buffer); |
| ASSERT_TRUE(header_read_result.is_ok()) << header_read_result.error(); |
| |
| uint64_t compressed_data_offset = header.header_length; |
| |
| auto expected_decompressed_reader_or = FdReader::Create(decompressed_image.path()); |
| ASSERT_TRUE(expected_decompressed_reader_or.is_ok()) << expected_decompressed_reader_or.error(); |
| auto expected_decompressed_reader = expected_decompressed_reader_or.take_value(); |
| |
| std::shared_ptr<Reader> shared_compressed_reader = |
| std::make_shared<FdReader>(std::move(compressed_reader)); |
| // For a fvm sparse image, we can either decompress and calculate the length in a single pass, |
| // or calculate the expected uncompressed size based on the accumulated extent length. |
| Lz4DecompressReader decompressed_reader( |
| compressed_data_offset, expected_decompressed_reader.length(), shared_compressed_reader); |
| ASSERT_TRUE(decompressed_reader.Initialize().is_ok()); |
| |
| // Now compare offsets. |
| constexpr uint64_t kDecompressedBufferSize = 64u << 10; |
| std::vector<uint8_t> actual_decompressed_buffer; |
| actual_decompressed_buffer.resize(kDecompressedBufferSize, 0); |
| |
| std::vector<uint8_t> expected_decompressed_buffer; |
| expected_decompressed_buffer.resize(kDecompressedBufferSize, 0); |
| |
| // We skip the header itself, since some flags might be different, from the compressed and the non |
| // compressed. Though this sectionis not compressed. |
| uint64_t read_bytes = sizeof(fvm::SparseImage); |
| while (read_bytes < decompressed_reader.length()) { |
| uint64_t bytes_to_read = kDecompressedBufferSize; |
| if (bytes_to_read > decompressed_reader.length() - read_bytes) { |
| bytes_to_read = decompressed_reader.length() - read_bytes; |
| } |
| auto actual_decompressed_view = |
| cpp20::span<uint8_t>(actual_decompressed_buffer).subspan(0, bytes_to_read); |
| auto read_result = decompressed_reader.Read(read_bytes, actual_decompressed_view); |
| ASSERT_TRUE(read_result.is_ok()) << read_result.error(); |
| |
| auto expected_decompressed_view = |
| cpp20::span<uint8_t>(expected_decompressed_buffer).subspan(0, bytes_to_read); |
| auto expected_read_result = |
| expected_decompressed_reader.Read(read_bytes, expected_decompressed_view); |
| ASSERT_TRUE(expected_read_result.is_ok()) << expected_read_result.error(); |
| |
| EXPECT_TRUE(memcmp(actual_decompressed_view.data(), expected_decompressed_view.data(), |
| actual_decompressed_view.size()) == 0) |
| << " offset " << read_bytes << " size " << bytes_to_read; |
| read_bytes += bytes_to_read; |
| } |
| |
| // Check that read_bytes contain all data from the decompressed image. |
| EXPECT_EQ(read_bytes, expected_decompressed_reader.length()); |
| } |
| |
| } // namespace |
| } // namespace storage::volume_image |