|  | // 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 <zircon/assert.h> | 
|  |  | 
|  | #include <algorithm> | 
|  |  | 
|  | #include <fbl/algorithm.h> | 
|  | #include <fbl/array.h> | 
|  | #include <src/lib/chunked-compression/streaming-chunked-compressor.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()); | 
|  | } | 
|  | } | 
|  |  | 
|  | void VerifyData(const uint8_t* compressed_data, size_t compressed_len, unsigned expected_frames, | 
|  | size_t expected_uncompressed_size) { | 
|  | SeekTable table; | 
|  | HeaderReader reader; | 
|  | ASSERT_EQ(reader.Parse(compressed_data, compressed_len, compressed_len, &table), kStatusOk); | 
|  | ASSERT_EQ(table.Entries().size(), expected_frames); | 
|  |  | 
|  | size_t decompressed_size_total = 0; | 
|  | // Include metadata in compressed size | 
|  | size_t compressed_size_total = | 
|  | kChunkArchiveSeekTableOffset + table.Entries().size() * sizeof(SeekTableEntry); | 
|  | for (unsigned i = 0; i < table.Entries().size(); ++i) { | 
|  | SeekTableEntry entry = table.Entries()[i]; | 
|  | decompressed_size_total += entry.decompressed_size; | 
|  | compressed_size_total += entry.compressed_size; | 
|  | } | 
|  | EXPECT_EQ(decompressed_size_total, expected_uncompressed_size); | 
|  | EXPECT_EQ(compressed_size_total, compressed_len); | 
|  | } | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | TEST(StreamingChunkedCompressorTest, ComputeOutputSizeLimit_Minimum) { | 
|  | StreamingChunkedCompressor compressor; | 
|  | // There should always be enough bytes for at least the metadata and one seek table entry. | 
|  | ASSERT_GE(compressor.ComputeOutputSizeLimit(1u), | 
|  | kChunkArchiveSeekTableOffset + sizeof(SeekTableEntry)); | 
|  | } | 
|  |  | 
|  | TEST(StreamingChunkedCompressorTest, Compress_Zeroes_Short) { | 
|  | size_t len = 8192ul; | 
|  | fbl::Array<uint8_t> data(new uint8_t[len], len); | 
|  | memset(data.get(), 0x00, len); | 
|  |  | 
|  | StreamingChunkedCompressor compressor; | 
|  |  | 
|  | size_t output_len = compressor.ComputeOutputSizeLimit(len); | 
|  | fbl::Array<uint8_t> compressed_data(new uint8_t[output_len], output_len); | 
|  |  | 
|  | ASSERT_EQ(compressor.Init(len, compressed_data.get(), compressed_data.size()), kStatusOk); | 
|  | ASSERT_EQ(compressor.Update(data.get(), len), kStatusOk); | 
|  | size_t compressed_len; | 
|  | ASSERT_EQ(compressor.Final(&compressed_len), kStatusOk); | 
|  |  | 
|  | VerifyData(compressed_data.get(), compressed_len, 1u, len); | 
|  | } | 
|  |  | 
|  | TEST(StreamingChunkedCompressorTest, Compress_Random_Short) { | 
|  | size_t len = 8192ul; | 
|  | fbl::Array<uint8_t> data(new uint8_t[len], len); | 
|  | RandomFill(data.get(), len); | 
|  |  | 
|  | StreamingChunkedCompressor compressor; | 
|  |  | 
|  | size_t output_len = compressor.ComputeOutputSizeLimit(len); | 
|  | fbl::Array<uint8_t> compressed_data(new uint8_t[output_len], output_len); | 
|  |  | 
|  | ASSERT_EQ(compressor.Init(len, compressed_data.get(), compressed_data.size()), kStatusOk); | 
|  | ASSERT_EQ(compressor.Update(data.get(), len), kStatusOk); | 
|  | size_t compressed_len; | 
|  | ASSERT_EQ(compressor.Final(&compressed_len), kStatusOk); | 
|  |  | 
|  | VerifyData(compressed_data.get(), compressed_len, 1u, len); | 
|  | } | 
|  |  | 
|  | TEST(StreamingChunkedCompressorTest, Compress_Zeroes_Long) { | 
|  | // 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); | 
|  |  | 
|  | StreamingChunkedCompressor compressor; | 
|  |  | 
|  | size_t output_len = compressor.ComputeOutputSizeLimit(len); | 
|  | fbl::Array<uint8_t> compressed_data(new uint8_t[output_len], output_len); | 
|  |  | 
|  | ASSERT_EQ(compressor.Init(len, compressed_data.get(), compressed_data.size()), kStatusOk); | 
|  | ASSERT_EQ(compressor.Update(data.get(), len), kStatusOk); | 
|  | size_t compressed_len; | 
|  | ASSERT_EQ(compressor.Final(&compressed_len), kStatusOk); | 
|  |  | 
|  | VerifyData(compressed_data.get(), compressed_len, 3u, len); | 
|  | } | 
|  |  | 
|  | TEST(StreamingChunkedCompressorTest, Compress_Zeroes_Long_RandomUpdateSizes) { | 
|  | // 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); | 
|  |  | 
|  | StreamingChunkedCompressor compressor; | 
|  |  | 
|  | size_t output_len = compressor.ComputeOutputSizeLimit(len); | 
|  | fbl::Array<uint8_t> compressed_data(new uint8_t[output_len], output_len); | 
|  |  | 
|  | ASSERT_EQ(compressor.Init(len, compressed_data.get(), compressed_data.size()), kStatusOk); | 
|  |  | 
|  | size_t off = 0; | 
|  | while (off < len) { | 
|  | constexpr size_t kMaxChunkSize = 8192; | 
|  | size_t chunk = (rand() % kMaxChunkSize) + 1; | 
|  | if (chunk > len - off) { | 
|  | chunk = len - off; | 
|  | } | 
|  | ASSERT_EQ(compressor.Update(data.get() + off, chunk), kStatusOk); | 
|  | off += chunk; | 
|  | } | 
|  | size_t compressed_len; | 
|  |  | 
|  | ASSERT_EQ(compressor.Final(&compressed_len), kStatusOk); | 
|  |  | 
|  | VerifyData(compressed_data.get(), compressed_len, 3u, len); | 
|  | } | 
|  |  | 
|  | TEST(StreamingChunkedCompressorTest, Compress_Random_Long) { | 
|  | // 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); | 
|  |  | 
|  | StreamingChunkedCompressor compressor; | 
|  |  | 
|  | size_t output_len = compressor.ComputeOutputSizeLimit(len); | 
|  | fbl::Array<uint8_t> compressed_data(new uint8_t[output_len], output_len); | 
|  |  | 
|  | ASSERT_EQ(compressor.Init(len, compressed_data.get(), compressed_data.size()), kStatusOk); | 
|  | ASSERT_EQ(compressor.Update(data.get(), len), kStatusOk); | 
|  | size_t compressed_len; | 
|  | ASSERT_EQ(compressor.Final(&compressed_len), kStatusOk); | 
|  |  | 
|  | VerifyData(compressed_data.get(), compressed_len, 3u, len); | 
|  | } | 
|  |  | 
|  | TEST(StreamingChunkedCompressorTest, Compress_Random_Long_RandomUpdateSizes) { | 
|  | // 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); | 
|  |  | 
|  | StreamingChunkedCompressor compressor; | 
|  |  | 
|  | size_t output_len = compressor.ComputeOutputSizeLimit(len); | 
|  | fbl::Array<uint8_t> compressed_data(new uint8_t[output_len], output_len); | 
|  |  | 
|  | ASSERT_EQ(compressor.Init(len, compressed_data.get(), compressed_data.size()), kStatusOk); | 
|  |  | 
|  | size_t off = 0; | 
|  | while (off < len) { | 
|  | constexpr size_t kMaxChunkSize = 8192; | 
|  | size_t chunk = (rand() % kMaxChunkSize) + 1; | 
|  | if (chunk > len - off) { | 
|  | chunk = len - off; | 
|  | } | 
|  | ASSERT_EQ(compressor.Update(data.get() + off, chunk), kStatusOk); | 
|  | off += chunk; | 
|  | } | 
|  | size_t compressed_len; | 
|  |  | 
|  | ASSERT_EQ(compressor.Final(&compressed_len), kStatusOk); | 
|  |  | 
|  | VerifyData(compressed_data.get(), compressed_len, 3u, len); | 
|  | } | 
|  |  | 
|  | TEST(StreamingChunkedCompressorTest, Compress_ReuseCompressor) { | 
|  | StreamingChunkedCompressor compressor; | 
|  |  | 
|  | { | 
|  | size_t len = 8192ul; | 
|  | fbl::Array<uint8_t> data(new uint8_t[len], len); | 
|  | memset(data.get(), 0x00, len); | 
|  |  | 
|  | size_t compressed_limit = compressor.ComputeOutputSizeLimit(len); | 
|  | fbl::Array<uint8_t> compressed_data(new uint8_t[compressed_limit], compressed_limit); | 
|  | ASSERT_EQ(compressor.Init(len, compressed_data.get(), compressed_data.size()), kStatusOk); | 
|  | ASSERT_EQ(compressor.Update(data.get(), len), kStatusOk); | 
|  | size_t compressed_len; | 
|  | ASSERT_EQ(compressor.Final(&compressed_len), kStatusOk); | 
|  |  | 
|  | VerifyData(compressed_data.get(), compressed_len, 1u, len); | 
|  | } | 
|  | { | 
|  | size_t len = 8192ul; | 
|  | fbl::Array<uint8_t> data(new uint8_t[len], len); | 
|  | // Set with different input data. | 
|  | memset(data.get(), 0xac, len); | 
|  |  | 
|  | size_t compressed_limit = compressor.ComputeOutputSizeLimit(len); | 
|  | fbl::Array<uint8_t> compressed_data(new uint8_t[compressed_limit], compressed_limit); | 
|  | ASSERT_EQ(compressor.Init(len, compressed_data.get(), compressed_data.size()), kStatusOk); | 
|  | ASSERT_EQ(compressor.Update(data.get(), len), kStatusOk); | 
|  | size_t compressed_len; | 
|  | ASSERT_EQ(compressor.Final(&compressed_len), kStatusOk); | 
|  |  | 
|  | VerifyData(compressed_data.get(), compressed_len, 1u, len); | 
|  | } | 
|  | } | 
|  |  | 
|  | TEST(StreamingChunkedCompressorTest, Compress_UpdateCalledBeforeInit) { | 
|  | size_t len = 8192ul; | 
|  | fbl::Array<uint8_t> data(new uint8_t[len], len); | 
|  | memset(data.get(), 0x00, len); | 
|  |  | 
|  | StreamingChunkedCompressor compressor; | 
|  | ASSERT_EQ(compressor.Update(data.get(), len), kStatusErrBadState); | 
|  | } | 
|  |  | 
|  | TEST(StreamingChunkedCompressorTest, Compress_FinalCalledBeforeInit) { | 
|  | size_t len = 8192ul; | 
|  | fbl::Array<uint8_t> data(new uint8_t[len], len); | 
|  | memset(data.get(), 0x00, len); | 
|  |  | 
|  | StreamingChunkedCompressor compressor; | 
|  | size_t compressed_len; | 
|  | ASSERT_EQ(compressor.Final(&compressed_len), kStatusErrBadState); | 
|  | } | 
|  |  | 
|  | TEST(StreamingChunkedCompressorTest, Compress_FinalCalledEarly) { | 
|  | size_t len = 8192ul; | 
|  | fbl::Array<uint8_t> data(new uint8_t[len], len); | 
|  | memset(data.get(), 0x00, len); | 
|  |  | 
|  | StreamingChunkedCompressor compressor; | 
|  |  | 
|  | size_t output_len = compressor.ComputeOutputSizeLimit(len); | 
|  | fbl::Array<uint8_t> compressed_data(new uint8_t[output_len], output_len); | 
|  |  | 
|  | ASSERT_EQ(compressor.Init(len, compressed_data.get(), compressed_data.size()), kStatusOk); | 
|  |  | 
|  | // All but the last byte was processed | 
|  | ASSERT_EQ(compressor.Update(data.get(), len - 1), kStatusOk); | 
|  |  | 
|  | size_t compressed_len; | 
|  | ASSERT_EQ(compressor.Final(&compressed_len), kStatusErrBadState); | 
|  |  | 
|  | // Process the last byte | 
|  | ASSERT_EQ(compressor.Update(data.get() + len - 1, 1), kStatusOk); | 
|  |  | 
|  | // Now Final() should be successful | 
|  | ASSERT_EQ(compressor.Final(&compressed_len), kStatusOk); | 
|  |  | 
|  | VerifyData(compressed_data.get(), compressed_len, 1u, len); | 
|  | } | 
|  |  | 
|  | TEST(StreamingChunkedCompressorTest, Compress_Update_TooManyBytes) { | 
|  | size_t len = 8192ul; | 
|  | fbl::Array<uint8_t> data(new uint8_t[len], len); | 
|  | memset(data.get(), 0x00, len); | 
|  |  | 
|  | StreamingChunkedCompressor compressor; | 
|  |  | 
|  | size_t output_len = compressor.ComputeOutputSizeLimit(len); | 
|  | fbl::Array<uint8_t> compressed_data(new uint8_t[output_len], output_len); | 
|  |  | 
|  | ASSERT_EQ(compressor.Init(len, compressed_data.get(), compressed_data.size()), kStatusOk); | 
|  |  | 
|  | ASSERT_EQ(compressor.Update(data.get(), len), kStatusOk); | 
|  |  | 
|  | // Processing any extra bytes should fail | 
|  | uint8_t byte = 0x0; | 
|  | ASSERT_EQ(compressor.Update(&byte, sizeof(byte)), kStatusErrInvalidArgs); | 
|  |  | 
|  | // Calling Final() should still succeed | 
|  | size_t compressed_len; | 
|  | ASSERT_EQ(compressor.Final(&compressed_len), kStatusOk); | 
|  |  | 
|  | VerifyData(compressed_data.get(), compressed_len, 1u, len); | 
|  | } | 
|  |  | 
|  | TEST(StreamingChunkedCompressorTest, Compress_MaxSeekTableEntries) { | 
|  | size_t len = 8192ul * kChunkArchiveMaxFrames; | 
|  | fbl::Array<uint8_t> buf(new uint8_t[8192ul], 8192ul); | 
|  | memset(buf.get(), 0x00, buf.size()); | 
|  |  | 
|  | CompressionParams params = {.chunk_size = 8192ul}; | 
|  | StreamingChunkedCompressor compressor(params); | 
|  |  | 
|  | size_t output_len = compressor.ComputeOutputSizeLimit(len); | 
|  | fbl::Array<uint8_t> compressed_data(new uint8_t[output_len], output_len); | 
|  |  | 
|  | ASSERT_EQ(compressor.Init(len, compressed_data.get(), compressed_data.size()), kStatusOk); | 
|  |  | 
|  | size_t consumed = 0; | 
|  | while (consumed < len) { | 
|  | size_t to_consume = std::min(buf.size(), len - consumed); | 
|  | ASSERT_EQ(compressor.Update(buf.get(), to_consume), kStatusOk); | 
|  | consumed += to_consume; | 
|  | } | 
|  |  | 
|  | size_t compressed_len; | 
|  | ASSERT_EQ(compressor.Final(&compressed_len), kStatusOk); | 
|  |  | 
|  | VerifyData(compressed_data.get(), compressed_len, kChunkArchiveMaxFrames, len); | 
|  | } | 
|  |  | 
|  | }  // namespace chunked_compression |