blob: 005613122e6dea7f6d93a3ba87b279d673f9f7ee [file] [log] [blame]
// Copyright 2020 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 <fbl/array.h>
#include <src/lib/chunked-compression/chunked-archive.h>
#include <src/lib/chunked-compression/chunked-compressor.h>
#include <src/lib/chunked-compression/chunked-decompressor.h>
#include <src/lib/chunked-compression/status.h>
#include <zxtest/zxtest.h>
namespace chunked_compression {
namespace {
void RandomFill(uint8_t* data, size_t len) {
size_t off = 0;
size_t rounded_len = fbl::round_down(len, sizeof(int));
for (off = 0; off < rounded_len; off += sizeof(int)) {
*reinterpret_cast<int*>(data + off) = rand();
}
ZX_ASSERT(off == rounded_len);
for (; off < len; ++off) {
data[off] = static_cast<uint8_t>(rand());
}
}
} // namespace
TEST(ChunkedDecompressorTest, Decompress_EmptyArchive) {
srand(zxtest::Runner::GetInstance()->random_seed());
fbl::Array<uint8_t> buf(new uint8_t[kChunkArchiveMinHeaderSize], kChunkArchiveMinHeaderSize);
HeaderWriter writer;
ASSERT_EQ(HeaderWriter::Create(buf.get(), buf.size(), 0, &writer), kStatusOk);
ASSERT_EQ(writer.Finalize(), kStatusOk);
fbl::Array<uint8_t> out_buf;
size_t decompressed_size;
ASSERT_EQ(
ChunkedDecompressor::DecompressBytes(buf.get(), buf.size(), &out_buf, &decompressed_size),
kStatusOk);
EXPECT_EQ(decompressed_size, 0ul);
}
TEST(ChunkedDecompressorTest, Decompress_SingleFrame_Zeroes) {
srand(zxtest::Runner::GetInstance()->random_seed());
size_t len = 8192ul;
fbl::Array<uint8_t> data(new uint8_t[len], len);
memset(data.get(), 0x00, len);
fbl::Array<uint8_t> compressed_data;
size_t compressed_len;
ASSERT_EQ(ChunkedCompressor::CompressBytes(data.get(), len, &compressed_data, &compressed_len),
kStatusOk);
ASSERT_GE(compressed_data.size(), compressed_len);
fbl::Array<uint8_t> out_buf;
size_t decompressed_size;
ASSERT_EQ(ChunkedDecompressor::DecompressBytes(compressed_data.get(), compressed_len, &out_buf,
&decompressed_size),
kStatusOk);
EXPECT_EQ(decompressed_size, 8192l);
EXPECT_BYTES_EQ(data.get(), out_buf.get(), len);
}
TEST(ChunkedDecompressorTest, Decompress_SingleFrame_Random) {
srand(zxtest::Runner::GetInstance()->random_seed());
size_t len = 8192ul;
fbl::Array<uint8_t> data(new uint8_t[len], len);
RandomFill(data.get(), len);
fbl::Array<uint8_t> compressed_data;
size_t compressed_len;
ASSERT_EQ(ChunkedCompressor::CompressBytes(data.get(), len, &compressed_data, &compressed_len),
kStatusOk);
ASSERT_GE(compressed_data.size(), compressed_len);
fbl::Array<uint8_t> out_buf;
size_t decompressed_size;
ASSERT_EQ(ChunkedDecompressor::DecompressBytes(compressed_data.get(), compressed_len, &out_buf,
&decompressed_size),
kStatusOk);
EXPECT_EQ(decompressed_size, 8192l);
EXPECT_BYTES_EQ(data.get(), out_buf.get(), len);
}
TEST(ChunkedDecompressorTest, Decompress_MultiFrame_Zeroes) {
srand(zxtest::Runner::GetInstance()->random_seed());
// 3 data frames, last one partial
size_t len = (2 * CompressionParams::MinChunkSize()) + 42ul;
fbl::Array<uint8_t> data(new uint8_t[len], len);
memset(data.get(), 0x00, len);
fbl::Array<uint8_t> compressed_data;
size_t compressed_len;
ASSERT_EQ(ChunkedCompressor::CompressBytes(data.get(), len, &compressed_data, &compressed_len),
kStatusOk);
ASSERT_GE(compressed_data.size(), compressed_len);
fbl::Array<uint8_t> out_buf;
size_t decompressed_size;
ASSERT_EQ(ChunkedDecompressor::DecompressBytes(compressed_data.get(), compressed_len, &out_buf,
&decompressed_size),
kStatusOk);
EXPECT_EQ(decompressed_size, len);
EXPECT_BYTES_EQ(data.get(), out_buf.get(), len);
}
TEST(ChunkedDecompressorTest, Decompress_MultiFrame_Random) {
srand(zxtest::Runner::GetInstance()->random_seed());
// 3 data frames, last one partial
size_t len = (2 * CompressionParams::MinChunkSize()) + 42ul;
fbl::Array<uint8_t> data(new uint8_t[len], len);
RandomFill(data.get(), len);
fbl::Array<uint8_t> compressed_data;
size_t compressed_len;
ASSERT_EQ(ChunkedCompressor::CompressBytes(data.get(), len, &compressed_data, &compressed_len),
kStatusOk);
ASSERT_GE(compressed_data.size(), compressed_len);
fbl::Array<uint8_t> out_buf;
size_t decompressed_size;
ASSERT_EQ(ChunkedDecompressor::DecompressBytes(compressed_data.get(), compressed_len, &out_buf,
&decompressed_size),
kStatusOk);
EXPECT_EQ(decompressed_size, len);
EXPECT_BYTES_EQ(data.get(), out_buf.get(), len);
}
TEST(ChunkedDecompressorTest, DecompressFrame_MultiFrame_Zeroes) {
srand(zxtest::Runner::GetInstance()->random_seed());
size_t chunk_size = CompressionParams::MinChunkSize();
// 3 data frames, last one partial
size_t len = (2 * chunk_size) + 42ul;
fbl::Array<uint8_t> data(new uint8_t[len], len);
memset(data.get(), 0x00, len);
fbl::Array<uint8_t> compressed_data;
size_t compressed_len;
ASSERT_EQ(ChunkedCompressor::CompressBytes(data.get(), len, &compressed_data, &compressed_len),
kStatusOk);
ASSERT_GE(compressed_data.size(), compressed_len);
fbl::Array<uint8_t> out_buf(new uint8_t[chunk_size], chunk_size);
SeekTable table;
HeaderReader reader;
ASSERT_OK(reader.Parse(compressed_data.get(), compressed_len, compressed_len, &table));
ChunkedDecompressor decompressor;
// Frame 0
uint8_t* frame_start = compressed_data.get() + table.Entries()[0].compressed_offset;
size_t frame_length = table.Entries()[0].compressed_size;
size_t bytes_written;
EXPECT_EQ(decompressor.DecompressFrame(table, 0, frame_start, frame_length, out_buf.get(),
out_buf.size(), &bytes_written),
kStatusOk);
EXPECT_EQ(bytes_written, chunk_size);
EXPECT_BYTES_EQ(data.get(), out_buf.get(), bytes_written);
// Frame 1
frame_start = compressed_data.get() + table.Entries()[1].compressed_offset;
frame_length = table.Entries()[1].compressed_size;
EXPECT_EQ(decompressor.DecompressFrame(table, 1, frame_start, frame_length, out_buf.get(),
out_buf.size(), &bytes_written),
kStatusOk);
EXPECT_EQ(bytes_written, chunk_size);
EXPECT_BYTES_EQ(data.get() + chunk_size, out_buf.get(), bytes_written);
// Frame 2
frame_start = compressed_data.get() + table.Entries()[2].compressed_offset;
frame_length = table.Entries()[2].compressed_size;
EXPECT_EQ(decompressor.DecompressFrame(table, 2, frame_start, frame_length, out_buf.get(),
out_buf.size(), &bytes_written),
kStatusOk);
EXPECT_EQ(bytes_written, 42ul);
EXPECT_BYTES_EQ(data.get() + (2 * chunk_size), out_buf.get(), bytes_written);
}
TEST(ChunkedDecompressorTest, DecompressFrame_MultiFrame_Random) {
srand(zxtest::Runner::GetInstance()->random_seed());
size_t chunk_size = CompressionParams::MinChunkSize();
// 3 data frames, last one partial
size_t len = (2 * chunk_size) + 42ul;
fbl::Array<uint8_t> data(new uint8_t[len], len);
RandomFill(data.get(), len);
fbl::Array<uint8_t> compressed_data;
size_t compressed_len;
ASSERT_EQ(ChunkedCompressor::CompressBytes(data.get(), len, &compressed_data, &compressed_len),
kStatusOk);
ASSERT_GE(compressed_data.size(), compressed_len);
fbl::Array<uint8_t> out_buf(new uint8_t[chunk_size], chunk_size);
SeekTable table;
HeaderReader reader;
ASSERT_OK(reader.Parse(compressed_data.get(), compressed_len, compressed_len, &table));
ChunkedDecompressor decompressor;
// Frame 0
uint8_t* frame_start = compressed_data.get() + table.Entries()[0].compressed_offset;
size_t frame_length = table.Entries()[0].compressed_size;
size_t bytes_written;
EXPECT_EQ(decompressor.DecompressFrame(table, 0, frame_start, frame_length, out_buf.get(),
out_buf.size(), &bytes_written),
kStatusOk);
EXPECT_EQ(bytes_written, chunk_size);
EXPECT_BYTES_EQ(data.get(), out_buf.get(), bytes_written);
// Frame 1
frame_start = compressed_data.get() + table.Entries()[1].compressed_offset;
frame_length = table.Entries()[1].compressed_size;
EXPECT_EQ(decompressor.DecompressFrame(table, 1, frame_start, frame_length, out_buf.get(),
out_buf.size(), &bytes_written),
kStatusOk);
EXPECT_EQ(bytes_written, chunk_size);
EXPECT_BYTES_EQ(data.get() + chunk_size, out_buf.get(), bytes_written);
// Frame 2
frame_start = compressed_data.get() + table.Entries()[2].compressed_offset;
frame_length = table.Entries()[2].compressed_size;
EXPECT_EQ(decompressor.DecompressFrame(table, 2, frame_start, frame_length, out_buf.get(),
out_buf.size(), &bytes_written),
kStatusOk);
EXPECT_EQ(bytes_written, 42ul);
EXPECT_BYTES_EQ(data.get() + (2 * chunk_size), out_buf.get(), bytes_written);
}
TEST(ChunkedDecompressorTest, DecompressFrame_BadFrameNumber) {
srand(zxtest::Runner::GetInstance()->random_seed());
size_t chunk_size = CompressionParams::MinChunkSize();
// 3 data frames, last one partial
size_t len = (2 * chunk_size) + 42ul;
fbl::Array<uint8_t> data(new uint8_t[len], len);
RandomFill(data.get(), len);
fbl::Array<uint8_t> compressed_data;
size_t compressed_len;
ASSERT_EQ(ChunkedCompressor::CompressBytes(data.get(), len, &compressed_data, &compressed_len),
kStatusOk);
ASSERT_GE(compressed_data.size(), compressed_len);
fbl::Array<uint8_t> out_buf(new uint8_t[chunk_size], chunk_size);
SeekTable table;
HeaderReader reader;
ASSERT_OK(reader.Parse(compressed_data.get(), compressed_len, compressed_len, &table));
ChunkedDecompressor decompressor;
uint8_t* frame_start = compressed_data.get() + table.Entries()[0].compressed_offset;
size_t frame_length = table.Entries()[0].compressed_size;
size_t bytes_written;
EXPECT_EQ(decompressor.DecompressFrame(table, 3, frame_start, frame_length, out_buf.get(),
out_buf.size(), &bytes_written),
kStatusErrInvalidArgs);
}
TEST(ChunkedDecompressorTest, DecompressFrame_BadOffset) {
srand(zxtest::Runner::GetInstance()->random_seed());
size_t chunk_size = CompressionParams::MinChunkSize();
// 3 data frames, last one partial
size_t len = (2 * chunk_size) + 42ul;
fbl::Array<uint8_t> data(new uint8_t[len], len);
RandomFill(data.get(), len);
fbl::Array<uint8_t> compressed_data;
size_t compressed_len;
ASSERT_EQ(ChunkedCompressor::CompressBytes(data.get(), len, &compressed_data, &compressed_len),
kStatusOk);
ASSERT_GE(compressed_data.size(), compressed_len);
fbl::Array<uint8_t> out_buf(new uint8_t[chunk_size], chunk_size);
SeekTable table;
HeaderReader reader;
ASSERT_OK(reader.Parse(compressed_data.get(), compressed_len, compressed_len, &table));
ChunkedDecompressor decompressor;
uint8_t* frame_start = compressed_data.get() + table.Entries()[0].compressed_offset;
size_t frame_length = table.Entries()[0].compressed_size;
size_t bytes_written;
// Starting at 1 byte past the actual frame start.
// This looks like a corrupt frame to the decompressor.
EXPECT_EQ(decompressor.DecompressFrame(table, 0, frame_start + 1, frame_length, out_buf.get(),
out_buf.size(), &bytes_written),
kStatusErrIoDataIntegrity);
// Starting at 1 byte before the actual frame start.
// This looks like a corrupt frame to the decompressor.
EXPECT_EQ(decompressor.DecompressFrame(table, 0, frame_start - 1, frame_length, out_buf.get(),
out_buf.size(), &bytes_written),
kStatusErrIoDataIntegrity);
}
TEST(ChunkedDecompressorTest, DecompressFrame_SmallBuffer) {
srand(zxtest::Runner::GetInstance()->random_seed());
size_t chunk_size = CompressionParams::MinChunkSize();
// 3 data frames, last one partial
size_t len = (2 * chunk_size) + 42ul;
fbl::Array<uint8_t> data(new uint8_t[len], len);
RandomFill(data.get(), len);
fbl::Array<uint8_t> compressed_data;
size_t compressed_len;
ASSERT_EQ(ChunkedCompressor::CompressBytes(data.get(), len, &compressed_data, &compressed_len),
kStatusOk);
ASSERT_GE(compressed_data.size(), compressed_len);
fbl::Array<uint8_t> out_buf(new uint8_t[chunk_size], chunk_size);
SeekTable table;
HeaderReader reader;
ASSERT_OK(reader.Parse(compressed_data.get(), compressed_len, compressed_len, &table));
ChunkedDecompressor decompressor;
uint8_t* frame_start = compressed_data.get() + table.Entries()[0].compressed_offset;
size_t frame_length = table.Entries()[0].compressed_size;
size_t bytes_written;
// Compressed buffer 1 byte too small
EXPECT_EQ(decompressor.DecompressFrame(table, 0, frame_start, frame_length - 1, out_buf.get(),
out_buf.size(), &bytes_written),
kStatusErrBufferTooSmall);
// Decompressed buffer 1 byte too small
EXPECT_EQ(decompressor.DecompressFrame(table, 0, frame_start, frame_length, out_buf.get(),
out_buf.size() - 1, &bytes_written),
kStatusErrBufferTooSmall);
}
TEST(ChunkedDecompressorTest, DecompressFrame_CorruptFirstByteInFrame_Checksum) {
srand(zxtest::Runner::GetInstance()->random_seed());
size_t chunk_size = CompressionParams::MinChunkSize();
// 2 data frames
size_t len = (2 * chunk_size);
fbl::Array<uint8_t> data(new uint8_t[len], len);
RandomFill(data.get(), len);
CompressionParams params;
params.frame_checksum = true;
ChunkedCompressor compressor(params);
size_t compressed_limit = compressor.ComputeOutputSizeLimit(len);
fbl::Array<uint8_t> compressed_data(new uint8_t[compressed_limit], compressed_limit);
size_t compressed_len;
ASSERT_EQ(compressor.Compress(data.get(), len, compressed_data.get(), compressed_limit,
&compressed_len),
kStatusOk);
ASSERT_LE(compressed_len, compressed_limit);
fbl::Array<uint8_t> out_buf(new uint8_t[chunk_size], chunk_size);
SeekTable table;
HeaderReader reader;
ASSERT_OK(reader.Parse(compressed_data.get(), compressed_len, compressed_len, &table));
ChunkedDecompressor decompressor;
uint8_t* frame_start = compressed_data.get() + table.Entries()[0].compressed_offset;
size_t frame_length = table.Entries()[0].compressed_size;
// Invert the first byte of the first frame.
frame_start[0] ^= 0xff;
size_t bytes_written;
EXPECT_EQ(decompressor.DecompressFrame(table, 0, frame_start, frame_length, out_buf.get(),
out_buf.size(), &bytes_written),
kStatusErrIoDataIntegrity);
frame_start = compressed_data.get() + table.Entries()[1].compressed_offset;
frame_length = table.Entries()[1].compressed_size;
// Second frame still decompresses.
EXPECT_EQ(decompressor.DecompressFrame(table, 1, frame_start, frame_length, out_buf.get(),
out_buf.size(), &bytes_written),
kStatusOk);
}
TEST(ChunkedDecompressorTest, DecompressFrame_CorruptLastByteInFrame_Checksum) {
srand(zxtest::Runner::GetInstance()->random_seed());
size_t chunk_size = CompressionParams::MinChunkSize();
// 2 data frames
size_t len = (2 * chunk_size);
fbl::Array<uint8_t> data(new uint8_t[len], len);
RandomFill(data.get(), len);
CompressionParams params;
params.frame_checksum = true;
ChunkedCompressor compressor(params);
size_t compressed_limit = compressor.ComputeOutputSizeLimit(len);
fbl::Array<uint8_t> compressed_data(new uint8_t[compressed_limit], compressed_limit);
size_t compressed_len;
ASSERT_EQ(compressor.Compress(data.get(), len, compressed_data.get(), compressed_limit,
&compressed_len),
kStatusOk);
ASSERT_LE(compressed_len, compressed_limit);
fbl::Array<uint8_t> out_buf(new uint8_t[chunk_size], chunk_size);
SeekTable table;
HeaderReader reader;
ASSERT_OK(reader.Parse(compressed_data.get(), compressed_len, compressed_len, &table));
ChunkedDecompressor decompressor;
uint8_t* frame_start = compressed_data.get() + table.Entries()[0].compressed_offset;
size_t frame_length = table.Entries()[0].compressed_size;
// Invert the last byte of the first frame.
frame_start[frame_length - 1] ^= 0xff;
size_t bytes_written;
EXPECT_EQ(decompressor.DecompressFrame(table, 0, frame_start, frame_length, out_buf.get(),
out_buf.size(), &bytes_written),
kStatusErrIoDataIntegrity);
frame_start = compressed_data.get() + table.Entries()[1].compressed_offset;
frame_length = table.Entries()[1].compressed_size;
// Second frame still decompresses.
EXPECT_EQ(decompressor.DecompressFrame(table, 1, frame_start, frame_length, out_buf.get(),
out_buf.size(), &bytes_written),
kStatusOk);
}
TEST(ChunkedDecompressorTest, DecompressFrame_CorruptFirstByteInFrame_NoChecksum) {
srand(zxtest::Runner::GetInstance()->random_seed());
size_t chunk_size = CompressionParams::MinChunkSize();
// 2 data frames
size_t len = (2 * chunk_size);
fbl::Array<uint8_t> data(new uint8_t[len], len);
RandomFill(data.get(), len);
CompressionParams params;
ChunkedCompressor compressor(params);
size_t compressed_limit = compressor.ComputeOutputSizeLimit(len);
fbl::Array<uint8_t> compressed_data(new uint8_t[compressed_limit], compressed_limit);
size_t compressed_len;
ASSERT_EQ(compressor.Compress(data.get(), len, compressed_data.get(), compressed_limit,
&compressed_len),
kStatusOk);
ASSERT_LE(compressed_len, compressed_limit);
fbl::Array<uint8_t> out_buf(new uint8_t[chunk_size], chunk_size);
SeekTable table;
HeaderReader reader;
ASSERT_OK(reader.Parse(compressed_data.get(), compressed_len, compressed_len, &table));
ChunkedDecompressor decompressor;
uint8_t* frame_start = compressed_data.get() + table.Entries()[0].compressed_offset;
size_t frame_length = table.Entries()[0].compressed_size;
// Invert the first byte of the first frame.
frame_start[0] ^= 0xff;
size_t bytes_written;
// Even though we have no checksum, this should always be detected as corruption, because
// ZSTD will not be able to interpret the frame header.
EXPECT_EQ(decompressor.DecompressFrame(table, 0, frame_start, frame_length, out_buf.get(),
out_buf.size(), &bytes_written),
kStatusErrIoDataIntegrity);
frame_start = compressed_data.get() + table.Entries()[1].compressed_offset;
frame_length = table.Entries()[1].compressed_size;
// Second frame still decompresses.
EXPECT_EQ(decompressor.DecompressFrame(table, 1, frame_start, frame_length, out_buf.get(),
out_buf.size(), &bytes_written),
kStatusOk);
}
TEST(ChunkedDecompressorTest, Decompress_CorruptHeader) {
srand(zxtest::Runner::GetInstance()->random_seed());
size_t chunk_size = CompressionParams::MinChunkSize();
// 2 data frames
size_t len = (2 * chunk_size);
fbl::Array<uint8_t> data(new uint8_t[len], len);
RandomFill(data.get(), len);
CompressionParams params;
ChunkedCompressor compressor(params);
size_t compressed_limit = compressor.ComputeOutputSizeLimit(len);
fbl::Array<uint8_t> compressed_data(new uint8_t[compressed_limit], compressed_limit);
size_t compressed_len;
ASSERT_EQ(compressor.Compress(data.get(), len, compressed_data.get(), compressed_limit,
&compressed_len),
kStatusOk);
ASSERT_LE(compressed_len, compressed_limit);
// Invert a random byte in the header.
size_t header_len = HeaderWriter::MetadataSizeForNumFrames(2);
compressed_data[rand() % header_len] ^= 0xff;
fbl::Array<uint8_t> out_buf(new uint8_t[chunk_size], chunk_size);
SeekTable table;
HeaderReader reader;
// We check for *inequality* with kStatusOk, because some types of corruptions may be
// signalled differently. (For example if the number of frames reported by the header was
// corrupted, the library can't distinguish between the client passing too small of a header,
// or the header being corrupted).
EXPECT_NE(reader.Parse(compressed_data.get(), compressed_len, compressed_len, &table), kStatusOk);
}
TEST(ChunkedDecompressorTest, RawDecompressFrame_WrongDecompressionLength) {
srand(zxtest::Runner::GetInstance()->random_seed());
size_t chunk_size = CompressionParams::MinChunkSize();
// 2 data frames
size_t len = (2 * chunk_size);
fbl::Array<uint8_t> data(new uint8_t[len], len);
RandomFill(data.get(), len);
CompressionParams params;
ChunkedCompressor compressor(params);
size_t compressed_limit = compressor.ComputeOutputSizeLimit(len);
fbl::Array<uint8_t> compressed_data(new uint8_t[compressed_limit], compressed_limit);
size_t compressed_len;
ASSERT_EQ(compressor.Compress(data.get(), len, compressed_data.get(), compressed_limit,
&compressed_len),
kStatusOk);
ASSERT_LE(compressed_len, compressed_limit);
fbl::Array<uint8_t> out_buf(new uint8_t[chunk_size], chunk_size);
SeekTable table;
HeaderReader reader;
ASSERT_OK(reader.Parse(compressed_data.get(), compressed_len, compressed_len, &table));
ChunkedDecompressor decompressor;
uint8_t* frame_start = compressed_data.get() + table.Entries()[0].compressed_offset;
size_t frame_length = table.Entries()[0].compressed_size;
size_t output_len = table.Entries()[0].decompressed_size;
size_t bytes_written;
// First frame uses the correct table outputs.
EXPECT_EQ(decompressor.DecompressFrame(frame_start, frame_length, out_buf.get(),
output_len, &bytes_written),
kStatusOk);
frame_start = compressed_data.get() + table.Entries()[1].compressed_offset;
frame_length = table.Entries()[1].compressed_size;
size_t wrong_output_len = table.Entries()[1].decompressed_size + 1;
// Second frame takes an incorrect table output, so we cannot verify that the entire frame was
// decompressed as expected.
EXPECT_EQ(decompressor.DecompressFrame(frame_start, frame_length, out_buf.get(),
wrong_output_len, &bytes_written),
kStatusErrIoDataIntegrity);
}
} // namespace chunked_compression