blob: eabca58b92438bef37f2a7a8f76e0b8a65295d10 [file] [log] [blame]
// Copyright 2018 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/compression/compressor.h"
#include <stdlib.h>
#include <zircon/assert.h>
#include <zircon/errors.h>
#include <algorithm>
#include <memory>
#include <block-client/cpp/fake-device.h>
#include <digest/digest.h>
#include <digest/merkle-tree.h>
#include <gtest/gtest.h>
#include "src/storage/blobfs/blobfs.h"
#include "src/storage/blobfs/common.h"
#include "src/storage/blobfs/compression/blob-compressor.h"
#include "src/storage/blobfs/compression/decompressor.h"
#include "src/storage/blobfs/compression/lz4.h"
#include "src/storage/blobfs/compression/zstd-plain.h"
#include "src/storage/blobfs/compression/zstd-seekable.h"
#include "src/storage/blobfs/format.h"
#include "src/storage/blobfs/mkfs.h"
#include "src/storage/blobfs/test/blob_utils.h"
namespace blobfs {
namespace {
enum class DataType {
Compressible,
Random,
};
std::unique_ptr<char[]> GenerateInput(DataType data_type, unsigned seed, size_t size) {
std::unique_ptr<char[]> input(new char[size]);
switch (data_type) {
case DataType::Compressible: {
size_t i = 0;
while (i < size) {
size_t run_length = 1 + (rand_r(&seed) % (size - i));
char value = static_cast<char>(rand_r(&seed) % std::numeric_limits<char>::max());
memset(input.get() + i, value, run_length);
i += run_length;
}
break;
}
case DataType::Random:
for (size_t i = 0; i < size; i++) {
input[i] = static_cast<char>(rand_r(&seed));
}
break;
default:
EXPECT_TRUE(false) << "Bad Data Type";
}
return input;
}
void CompressionHelper(CompressionAlgorithm algorithm, const char* input, size_t size, size_t step,
std::optional<BlobCompressor>* out) {
CompressionSettings settings{.compression_algorithm = algorithm};
auto compressor = BlobCompressor::Create(settings, size);
ASSERT_TRUE(compressor);
size_t offset = 0;
while (offset != size) {
const void* data = reinterpret_cast<void*>(reinterpret_cast<uintptr_t>(input) + offset);
const size_t incremental_size = std::min(step, size - offset);
ASSERT_EQ(compressor->Update(data, incremental_size), ZX_OK);
offset += incremental_size;
}
ASSERT_EQ(compressor->End(), ZX_OK);
EXPECT_GT(compressor->Size(), 0ul);
*out = std::move(compressor);
}
void DecompressionHelper(CompressionAlgorithm algorithm, const void* compressed_buf,
size_t compressed_size, const void* expected, size_t expected_size) {
std::unique_ptr<char[]> uncompressed_buf(new char[expected_size]);
size_t uncompressed_size = expected_size;
std::unique_ptr<Decompressor> decompressor;
ASSERT_EQ(Decompressor::Create(algorithm, &decompressor), ZX_OK);
ASSERT_EQ(decompressor->Decompress(uncompressed_buf.get(), &uncompressed_size, compressed_buf,
compressed_size),
ZX_OK);
EXPECT_EQ(expected_size, uncompressed_size);
EXPECT_EQ(memcmp(expected, uncompressed_buf.get(), expected_size), 0);
}
// Tests a contained case of compression and decompression.
//
// size: The size of the input buffer.
// step: The step size of updating the compression buffer.
void RunCompressDecompressTest(CompressionAlgorithm algorithm, DataType data_type, size_t size,
size_t step) {
ASSERT_LE(step, size) << "Step size too large";
// Generate input.
std::unique_ptr<char[]> input(GenerateInput(data_type, 0, size));
// Compress a buffer.
std::optional<BlobCompressor> compressor;
CompressionHelper(algorithm, input.get(), size, step, &compressor);
ASSERT_TRUE(compressor);
// Decompress the buffer.
DecompressionHelper(algorithm, compressor->Data(), compressor->Size(), input.get(), size);
}
TEST(CompressorTests, CompressDecompressLZ4Random1) {
RunCompressDecompressTest(CompressionAlgorithm::LZ4, DataType::Random, 1 << 0, 1 << 0);
}
TEST(CompressorTests, CompressDecompressLZ4Random2) {
RunCompressDecompressTest(CompressionAlgorithm::LZ4, DataType::Random, 1 << 1, 1 << 0);
}
TEST(CompressorTests, CompressDecompressLZ4Random3) {
RunCompressDecompressTest(CompressionAlgorithm::LZ4, DataType::Random, 1 << 10, 1 << 5);
}
TEST(CompressorTests, CompressDecompressLZ4Random4) {
RunCompressDecompressTest(CompressionAlgorithm::LZ4, DataType::Random, 1 << 15, 1 << 10);
}
TEST(CompressorTests, CompressDecompressLZ4Compressible1) {
RunCompressDecompressTest(CompressionAlgorithm::LZ4, DataType::Random, 1 << 0, 1 << 0);
}
TEST(CompressorTests, CompressDecompressLZ4Compressible2) {
RunCompressDecompressTest(CompressionAlgorithm::LZ4, DataType::Random, 1 << 1, 1 << 0);
}
TEST(CompressorTests, CompressDecompressLZ4Compressible3) {
RunCompressDecompressTest(CompressionAlgorithm::LZ4, DataType::Random, 1 << 10, 1 << 5);
}
TEST(CompressorTests, CompressDecompressLZ4Compressible4) {
RunCompressDecompressTest(CompressionAlgorithm::LZ4, DataType::Random, 1 << 15, 1 << 10);
}
TEST(CompressorTests, CompressDecompressZSTDRandom1) {
RunCompressDecompressTest(CompressionAlgorithm::ZSTD, DataType::Random, 1 << 0, 1 << 0);
}
TEST(CompressorTests, CompressDecompressZSTDRandom2) {
RunCompressDecompressTest(CompressionAlgorithm::ZSTD, DataType::Random, 1 << 1, 1 << 0);
}
TEST(CompressorTests, CompressDecompressZSTDRandom3) {
RunCompressDecompressTest(CompressionAlgorithm::ZSTD, DataType::Random, 1 << 10, 1 << 5);
}
TEST(CompressorTests, CompressDecompressZSTDRandom4) {
RunCompressDecompressTest(CompressionAlgorithm::ZSTD, DataType::Random, 1 << 15, 1 << 10);
}
TEST(CompressorTests, CompressDecompressZSTDCompressible1) {
RunCompressDecompressTest(CompressionAlgorithm::ZSTD, DataType::Random, 1 << 0, 1 << 0);
}
TEST(CompressorTests, CompressDecompressZSTDCompressible2) {
RunCompressDecompressTest(CompressionAlgorithm::ZSTD, DataType::Random, 1 << 1, 1 << 0);
}
TEST(CompressorTests, CompressDecompressZSTDCompressible3) {
RunCompressDecompressTest(CompressionAlgorithm::ZSTD, DataType::Random, 1 << 10, 1 << 5);
}
TEST(CompressorTests, CompressDecompressZSTDCompressible4) {
RunCompressDecompressTest(CompressionAlgorithm::ZSTD, DataType::Random, 1 << 15, 1 << 10);
}
TEST(CompressorTests, CompressDecompressZSTDSeekableRandom1) {
RunCompressDecompressTest(CompressionAlgorithm::ZSTD_SEEKABLE, DataType::Random, 1 << 0, 1 << 0);
}
TEST(CompressorTests, CompressDecompressZSTDSeekableRandom2) {
RunCompressDecompressTest(CompressionAlgorithm::ZSTD_SEEKABLE, DataType::Random, 1 << 1, 1 << 0);
}
TEST(CompressorTests, CompressDecompressZSTDSeekableRandom3) {
RunCompressDecompressTest(CompressionAlgorithm::ZSTD_SEEKABLE, DataType::Random, 1 << 10, 1 << 5);
}
TEST(CompressorTests, CompressDecompressZSTDSeekableRandom4) {
RunCompressDecompressTest(CompressionAlgorithm::ZSTD_SEEKABLE, DataType::Random, 1 << 15,
1 << 10);
}
TEST(CompressorTests, CompressDecompressZSTDSeekableCompressible1) {
RunCompressDecompressTest(CompressionAlgorithm::ZSTD_SEEKABLE, DataType::Random, 1 << 0, 1 << 0);
}
TEST(CompressorTests, CompressDecompressZSTDSeekableCompressible2) {
RunCompressDecompressTest(CompressionAlgorithm::ZSTD_SEEKABLE, DataType::Random, 1 << 1, 1 << 0);
}
TEST(CompressorTests, CompressDecompressZSTDSeekableCompressible3) {
RunCompressDecompressTest(CompressionAlgorithm::ZSTD_SEEKABLE, DataType::Random, 1 << 10, 1 << 5);
}
TEST(CompressorTests, CompressDecompressZSTDSeekableCompressible4) {
RunCompressDecompressTest(CompressionAlgorithm::ZSTD_SEEKABLE, DataType::Random, 1 << 15,
1 << 10);
}
TEST(CompressorTests, CompressDecompressChunkRandom1) {
RunCompressDecompressTest(CompressionAlgorithm::CHUNKED, DataType::Random, 1 << 0, 1 << 0);
}
TEST(CompressorTests, CompressDecompressChunkRandom2) {
RunCompressDecompressTest(CompressionAlgorithm::CHUNKED, DataType::Random, 1 << 1, 1 << 0);
}
TEST(CompressorTests, CompressDecompressChunkRandom3) {
RunCompressDecompressTest(CompressionAlgorithm::CHUNKED, DataType::Random, 1 << 10, 1 << 5);
}
TEST(CompressorTests, CompressDecompressChunkRandom4) {
RunCompressDecompressTest(CompressionAlgorithm::CHUNKED, DataType::Random, 1 << 15, 1 << 10);
}
TEST(CompressorTests, CompressDecompressChunkCompressible1) {
RunCompressDecompressTest(CompressionAlgorithm::CHUNKED, DataType::Random, 1 << 0, 1 << 0);
}
TEST(CompressorTests, CompressDecompressChunkCompressible2) {
RunCompressDecompressTest(CompressionAlgorithm::CHUNKED, DataType::Random, 1 << 1, 1 << 0);
}
TEST(CompressorTests, CompressDecompressChunkCompressible3) {
RunCompressDecompressTest(CompressionAlgorithm::CHUNKED, DataType::Random, 1 << 10, 1 << 5);
}
TEST(CompressorTests, CompressDecompressChunkCompressible4) {
RunCompressDecompressTest(CompressionAlgorithm::CHUNKED, DataType::Random, 1 << 15, 1 << 10);
}
void RunUpdateNoDataTest(CompressionAlgorithm algorithm) {
const size_t input_size = 1024;
CompressionSettings settings{.compression_algorithm = algorithm};
auto compressor = BlobCompressor::Create(settings, input_size);
ASSERT_TRUE(compressor);
std::unique_ptr<char[]> input(new char[input_size]);
memset(input.get(), 'a', input_size);
// Test that using "Update(data, 0)" acts a no-op, rather than corrupting the buffer.
ASSERT_EQ(compressor->Update(input.get(), 0), ZX_OK);
ASSERT_EQ(compressor->Update(input.get(), input_size), ZX_OK);
ASSERT_EQ(compressor->End(), ZX_OK);
// Ensure that even with the addition of a zero-length buffer, we still decompress
// to the expected output.
DecompressionHelper(algorithm, compressor->Data(), compressor->Size(), input.get(), input_size);
}
TEST(CompressorTests, UpdateNoDataLZ4) { RunUpdateNoDataTest(CompressionAlgorithm::LZ4); }
TEST(CompressorTests, UpdateNoDataZSTD) { RunUpdateNoDataTest(CompressionAlgorithm::ZSTD); }
TEST(CompressorTests, UpdateNoDataZSTDSeekable) {
RunUpdateNoDataTest(CompressionAlgorithm::ZSTD_SEEKABLE);
}
void DecompressionRoundHelper(CompressionAlgorithm algorithm, const void* compressed_buf,
size_t rounded_compressed_size, const void* expected,
size_t expected_size) {
std::unique_ptr<char[]> uncompressed_buf(new char[expected_size]);
size_t uncompressed_size = expected_size;
size_t compressed_size = rounded_compressed_size;
std::unique_ptr<Decompressor> decompressor;
ASSERT_EQ(Decompressor::Create(algorithm, &decompressor), ZX_OK);
ASSERT_EQ(decompressor->Decompress(uncompressed_buf.get(), &uncompressed_size, compressed_buf,
compressed_size),
ZX_OK);
EXPECT_EQ(expected_size, uncompressed_size);
EXPECT_EQ(memcmp(expected, uncompressed_buf.get(), expected_size), 0);
}
// Tests decompression's ability to handle receiving a compressed size that is rounded
// up to the nearest block size. This mimics blobfs' usage, where the exact compressed size
// is not stored explicitly.
//
// size: The size of the input buffer.
// step: The step size of updating the compression buffer.
void RunCompressRoundDecompressTest(CompressionAlgorithm algorithm, DataType data_type, size_t size,
size_t step) {
ASSERT_LE(step, size) << "Step size too large";
// Generate input.
std::unique_ptr<char[]> input(GenerateInput(data_type, 0, size));
// Compress a buffer.
std::optional<BlobCompressor> compressor;
CompressionHelper(algorithm, input.get(), size, step, &compressor);
ASSERT_TRUE(compressor);
// Round up compressed size to nearest block size;
size_t rounded_size = fbl::round_up(compressor->Size(), kBlobfsBlockSize);
// Decompress the buffer while giving the rounded compressed size.
DecompressionRoundHelper(algorithm, compressor->Data(), rounded_size, input.get(), size);
}
TEST(CompressorTests, CompressRoundDecompressLZ4Random1) {
RunCompressRoundDecompressTest(CompressionAlgorithm::LZ4, DataType::Random, 1 << 0, 1 << 0);
}
TEST(CompressorTests, CompressRoundDecompressLZ4Random2) {
RunCompressRoundDecompressTest(CompressionAlgorithm::LZ4, DataType::Random, 1 << 1, 1 << 0);
}
TEST(CompressorTests, CompressRoundDecompressLZ4Random3) {
RunCompressRoundDecompressTest(CompressionAlgorithm::LZ4, DataType::Random, 1 << 10, 1 << 5);
}
TEST(CompressorTests, CompressRoundDecompressLZ4Random4) {
RunCompressRoundDecompressTest(CompressionAlgorithm::LZ4, DataType::Random, 1 << 15, 1 << 10);
}
TEST(CompressorTests, CompressRoundDecompressZSTDRandom1) {
RunCompressRoundDecompressTest(CompressionAlgorithm::ZSTD, DataType::Random, 1 << 0, 1 << 0);
}
TEST(CompressorTests, CompressRoundDecompressZSTDRandom2) {
RunCompressRoundDecompressTest(CompressionAlgorithm::ZSTD, DataType::Random, 1 << 1, 1 << 0);
}
TEST(CompressorTests, CompressRoundDecompressZSTDRandom3) {
RunCompressRoundDecompressTest(CompressionAlgorithm::ZSTD, DataType::Random, 1 << 10, 1 << 5);
}
TEST(CompressorTests, CompressRoundDecompressZSTDRandom4) {
RunCompressRoundDecompressTest(CompressionAlgorithm::ZSTD, DataType::Random, 1 << 15, 1 << 10);
}
TEST(CompressorTests, CompressRoundDecompressZSTDSeekableRandom1) {
RunCompressRoundDecompressTest(CompressionAlgorithm::ZSTD_SEEKABLE, DataType::Random, 1 << 0,
1 << 0);
}
TEST(CompressorTests, CompressRoundDecompressZSTDSeekableRandom2) {
RunCompressRoundDecompressTest(CompressionAlgorithm::ZSTD_SEEKABLE, DataType::Random, 1 << 1,
1 << 0);
}
TEST(CompressorTests, CompressRoundDecompressZSTDSeekableRandom3) {
RunCompressRoundDecompressTest(CompressionAlgorithm::ZSTD_SEEKABLE, DataType::Random, 1 << 10,
1 << 5);
}
TEST(CompressorTests, CompressRoundDecompressZSTDSeekableRandom4) {
RunCompressRoundDecompressTest(CompressionAlgorithm::ZSTD_SEEKABLE, DataType::Random, 1 << 15,
1 << 10);
}
// Regression test class: Ensure that decompression that is not advancing buffers terminates, even
// if the "size of next recommended input" hint from zstd is a non-zero, non-error value. It turns
// out, this hint is not intended to be authoritative, in the sense that it can be a non-zero,
// non-error value even though subsequent invocations of `ZSTD_decompressStream` will make no
// progress.
class NonZeroHintNonAdvancingZSTDDecompressor : public AbstractZSTDDecompressor {
public:
NonZeroHintNonAdvancingZSTDDecompressor() = default;
// AbstractZSTDDecompressor interface.
size_t DecompressStream(ZSTD_DStream* zds, ZSTD_outBuffer* output,
ZSTD_inBuffer* input) const final {
// Do not advance streams, but return non-zero, non-error value.
EXPECT_FALSE(ZSTD_isError(kDecompressStreamReturn));
return kDecompressStreamReturn;
}
private:
static const size_t kDecompressStreamReturn = 1;
};
// Regression test for fxbug.dev/44603.
// This test prevents regressing to the following *incorrect* logic:
//
// do { ... r = ZSTD_decompressStream(...) ... } while (r != 0);
//
// The value of `r`, when not an error code, is a hint at the size of the next chunk to pass to
// `ZSTD_decompressStream`. Sometimes, this value is non-zero even though invoking
// `ZSTD_decompressStream` again will make no progress, inducing an infinite loop.
// See fxbug.dev/44603 for details.
TEST(CompressorTests, DecompressZSTDNonZeroNonAdvancing) {
constexpr size_t kCompressedSize = 1;
constexpr size_t kUncompressedSize = 2;
uint8_t compressed_buf[kCompressedSize] = {0x00};
uint8_t uncompressed_buf[kUncompressedSize] = {0x00, 0x00};
size_t compressed_size = kCompressedSize;
size_t uncompressed_size = kUncompressedSize;
NonZeroHintNonAdvancingZSTDDecompressor decompressor;
ASSERT_EQ(decompressor.Decompress(uncompressed_buf, &uncompressed_size, compressed_buf,
compressed_size),
ZX_OK);
}
class BlobfsTestFixture : public testing::Test {
protected:
BlobfsTestFixture() {
constexpr uint64_t kBlockCount = 1024;
auto device = std::make_unique<block_client::FakeBlockDevice>(kBlockCount, kBlobfsBlockSize);
EXPECT_EQ(FormatFilesystem(device.get(), FilesystemOptions{}), ZX_OK);
EXPECT_EQ(Blobfs::Create(nullptr, std::move(device), blobfs::MountOptions(), zx::resource(),
&blobfs_),
ZX_OK);
fbl::RefPtr<fs::Vnode> root;
EXPECT_EQ(blobfs_->OpenRootNode(&root), ZX_OK);
root_ = fbl::RefPtr<Directory>::Downcast(std::move(root));
}
fbl::RefPtr<fs::Vnode> AddBlobToBlobfs(size_t data_size, DataType type) {
std::unique_ptr<BlobInfo> blob_info = GenerateBlob(
[type](uint8_t* data, size_t length) {
auto generated_data = GenerateInput(type, 0, length);
memcpy(data, generated_data.get(), length);
},
"", data_size);
fbl::RefPtr<fs::Vnode> file;
zx_status_t status = root_->Create(blob_info->path + 1, 0, &file);
EXPECT_EQ(status, ZX_OK) << "Could not create file";
if (status != ZX_OK) {
return nullptr;
}
status = file->Truncate(data_size);
EXPECT_EQ(status, ZX_OK) << "Could not truncate file";
if (status != ZX_OK) {
return nullptr;
}
size_t actual = 0;
status = file->Write(blob_info->data.get(), data_size, 0, &actual);
EXPECT_EQ(status, ZX_OK) << "Could not write file";
if (status != ZX_OK) {
return nullptr;
}
EXPECT_EQ(actual, data_size) << "Unexpected amount of written data";
if (actual != data_size) {
return nullptr;
}
return file;
}
private:
std::unique_ptr<blobfs::Blobfs> blobfs_;
fbl::RefPtr<Directory> root_;
};
using CompressorBlobfsTests = BlobfsTestFixture;
// Test that we do compress small blobs with compressible content.
TEST_F(CompressorBlobfsTests, CompressSmallCompressibleBlobs) {
struct TestCase {
size_t data_size;
size_t expected_max_storage_size;
};
TestCase test_cases[] = {
{
16 * 1024 - 1,
16 * 1024,
},
{
16 * 1024,
16 * 1024,
},
{
16 * 1024 + 1,
16 * 1024,
},
};
for (const TestCase& test_case : test_cases) {
printf("Test case: data size %zu\n", test_case.data_size);
fbl::RefPtr<fs::Vnode> file = AddBlobToBlobfs(test_case.data_size, DataType::Compressible);
fs::VnodeAttributes attributes;
ASSERT_EQ(file->GetAttributes(&attributes), ZX_OK);
EXPECT_EQ(attributes.content_size, test_case.data_size);
EXPECT_LE(attributes.storage_size, test_case.expected_max_storage_size);
ASSERT_EQ(file->Close(), ZX_OK);
}
}
TEST_F(CompressorBlobfsTests, DoNotInflateIncompressibleBlobs) {
size_t data_sizes[] = {
8 * 1024 - 1, 8 * 1024, 8 * 1024 + 1, 16 * 1024 - 1, 16 * 1024, 16 * 1024 + 1, 128 * 8192 + 1,
};
for (size_t data_size : data_sizes) {
if (data_size != 8193)
continue;
printf("Test case: data size %zu\n", data_size);
fbl::RefPtr<fs::Vnode> file = AddBlobToBlobfs(data_size, DataType::Random);
fs::VnodeAttributes attributes;
ASSERT_EQ(file->GetAttributes(&attributes), ZX_OK);
EXPECT_EQ(attributes.content_size, data_size);
// Beyond 1 block, we need 1 block for the Merkle tree.
size_t expected_max_storage_size = fbl::round_up(data_size, kBlobfsBlockSize) +
(data_size > kBlobfsBlockSize ? kBlobfsBlockSize : 0);
EXPECT_LE(attributes.storage_size, expected_max_storage_size);
ASSERT_EQ(file->Close(), ZX_OK);
}
}
} // namespace
} // namespace blobfs