| // 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 <zircon/types.h> | |
| #include <fbl/algorithm.h> | |
| #include <fbl/array.h> | |
| #include <src/lib/chunked-compression/chunked-archive.h> | |
| #include <zxtest/zxtest.h> | |
| #include "test-utils.h" | |
| namespace chunked_compression { | |
| namespace { | |
| using test_utils::CreateHeader; | |
| } // namespace | |
| TEST(HeaderReader, ZeroState) { | |
| SeekTable header; | |
| EXPECT_EQ(header.DecompressedSize(), 0ul); | |
| EXPECT_EQ(header.Entries().size(), 0ul); | |
| EXPECT_EQ(header.CompressedSize(), kChunkArchiveMinHeaderSize); | |
| EXPECT_EQ(header.SerializedHeaderSize(), kChunkArchiveMinHeaderSize); | |
| } | |
| TEST(HeaderReader, Parse_BadArgs) { | |
| HeaderReader reader; | |
| SeekTable header; | |
| ASSERT_EQ(reader.Parse(nullptr, 0ul, 0ul, &header), kStatusErrInvalidArgs); | |
| uint8_t buf[kChunkArchiveMinHeaderSize]; | |
| ASSERT_EQ(reader.Parse(buf, sizeof(buf) - 1, sizeof(buf) - 1, &header), kStatusErrBufferTooSmall); | |
| ASSERT_EQ(reader.Parse(buf, sizeof(buf), sizeof(buf), nullptr), kStatusErrInvalidArgs); | |
| } | |
| TEST(HeaderReader, Parse_Empty) { | |
| HeaderReader reader; | |
| fbl::Array<uint8_t> buf = CreateHeader(); | |
| SeekTable header; | |
| ASSERT_EQ(reader.Parse(buf.get(), buf.size(), buf.size(), &header), kStatusOk); | |
| EXPECT_EQ(header.DecompressedSize(), 0ul); | |
| EXPECT_EQ(header.Entries().size(), 0ul); | |
| EXPECT_EQ(header.CompressedSize(), kChunkArchiveMinHeaderSize); | |
| EXPECT_EQ(header.SerializedHeaderSize(), kChunkArchiveMinHeaderSize); | |
| } | |
| TEST(HeaderReader, Parse_OneEntry) { | |
| HeaderReader reader; | |
| fbl::Array<uint8_t> buf = CreateHeader({{ | |
| .decompressed_offset = 0ul, | |
| .decompressed_size = 256ul, | |
| .compressed_offset = 100ul, | |
| .compressed_size = 100ul, | |
| }}); | |
| SeekTable header; | |
| ASSERT_EQ(reader.Parse(buf.get(), buf.size(), 200ul, &header), kStatusOk); | |
| EXPECT_EQ(header.CompressedSize(), 200ul); | |
| EXPECT_EQ(header.DecompressedSize(), 256ul); | |
| EXPECT_EQ(header.SerializedHeaderSize(), buf.size()); | |
| ASSERT_EQ(header.Entries().size(), 1ul); | |
| const SeekTableEntry& entry = header.Entries()[0]; | |
| ASSERT_EQ(entry.decompressed_offset, 0ul); | |
| ASSERT_EQ(entry.decompressed_size, 256ul); | |
| ASSERT_EQ(entry.compressed_offset, 100ul); | |
| ASSERT_EQ(entry.compressed_size, 100ul); | |
| } | |
| TEST(HeaderReader, Parse_TwoEntries) { | |
| HeaderReader reader; | |
| fbl::Array<uint8_t> buf = | |
| CreateHeader({{ | |
| .decompressed_offset = 0ul, | |
| .decompressed_size = 256ul, | |
| .compressed_offset = 200ul, | |
| .compressed_size = 10ul, | |
| }, | |
| { | |
| .decompressed_offset = 256ul, | |
| .decompressed_size = 100ul, | |
| // Note the compressed frames are non-contiguous (the second starts at 2000) | |
| .compressed_offset = 2000ul, | |
| .compressed_size = 40ul, | |
| }}); | |
| SeekTable header; | |
| ASSERT_EQ(reader.Parse(buf.get(), buf.size(), 2040ul, &header), kStatusOk); | |
| // Compressed size should be the end of the last frame. | |
| EXPECT_EQ(header.CompressedSize(), 2040ul); | |
| EXPECT_EQ(header.DecompressedSize(), 356ul); | |
| EXPECT_EQ(header.SerializedHeaderSize(), buf.size()); | |
| ASSERT_EQ(header.Entries().size(), 2ul); | |
| const SeekTableEntry& entry1 = header.Entries()[0]; | |
| ASSERT_EQ(entry1.decompressed_offset, 0ul); | |
| ASSERT_EQ(entry1.decompressed_size, 256ul); | |
| ASSERT_EQ(entry1.compressed_offset, 200ul); | |
| ASSERT_EQ(entry1.compressed_size, 10ul); | |
| const SeekTableEntry& entry2 = header.Entries()[1]; | |
| ASSERT_EQ(entry2.decompressed_offset, 256ul); | |
| ASSERT_EQ(entry2.decompressed_size, 100ul); | |
| ASSERT_EQ(entry2.compressed_offset, 2000ul); | |
| ASSERT_EQ(entry2.compressed_size, 40ul); | |
| } | |
| TEST(HeaderReader, Parse_BadMagic) { | |
| HeaderReader reader; | |
| fbl::Array<uint8_t> buf = CreateHeader(); | |
| // Bit flip the first byte in the archive. | |
| buf[0] ^= 0xff; | |
| SeekTable header; | |
| ASSERT_EQ(reader.Parse(buf.get(), buf.size(), buf.size(), &header), kStatusErrIoDataIntegrity); | |
| } | |
| TEST(HeaderReader, Parse_BadVersion) { | |
| HeaderReader reader; | |
| fbl::Array<uint8_t> buf = CreateHeader(); | |
| reinterpret_cast<ArchiveVersionType*>(buf.get() + kChunkArchiveVersionOffset)[0] = 3u; | |
| SeekTable header; | |
| ASSERT_EQ(reader.Parse(buf.get(), buf.size(), buf.size(), &header), kStatusErrInvalidArgs); | |
| } | |
| TEST(HeaderReader, Parse_CorruptSeekTableEntry) { | |
| HeaderReader reader; | |
| fbl::Array<uint8_t> buf = CreateHeader({{ | |
| .decompressed_offset = 0ul, | |
| .decompressed_size = 256ul, | |
| .compressed_offset = 200ul, | |
| .compressed_size = 10ul, | |
| }}); | |
| SeekTableEntry* table = | |
| reinterpret_cast<SeekTableEntry*>(buf.get() + kChunkArchiveSeekTableOffset); | |
| table[0].decompressed_size += 1; | |
| // The checksum should prevent this from parsing. | |
| SeekTable header; | |
| ASSERT_EQ(reader.Parse(buf.get(), buf.size(), buf.size(), &header), kStatusErrIoDataIntegrity); | |
| } | |
| TEST(HeaderReader, Parse_TooManyFrames) { | |
| HeaderReader reader; | |
| fbl::Array<uint8_t> buf = CreateHeader(); | |
| reinterpret_cast<ChunkCountType*>(buf.get() + kChunkArchiveNumChunksOffset)[0] = 1024; | |
| // This can't be distinguished from a corrupt header, so the library treats this as an integrity | |
| // error. | |
| SeekTable header; | |
| ASSERT_EQ(reader.Parse(buf.get(), buf.size(), buf.size(), &header), kStatusErrIoDataIntegrity); | |
| } | |
| // Parse_Invalid_I* tests verify the invariants documented in the header during parsing. | |
| TEST(HeaderReader, Parse_Invalid_I0_DecompressedDataStartsAbove0) { | |
| HeaderReader reader; | |
| fbl::Array<uint8_t> buf = CreateHeader({{ | |
| .decompressed_offset = 1ul, | |
| .decompressed_size = 255ul, | |
| .compressed_offset = 100ul, | |
| .compressed_size = 100ul, | |
| }}); | |
| SeekTable header; | |
| ASSERT_EQ(reader.Parse(buf.get(), buf.size(), 200ul, &header), kStatusErrIoDataIntegrity); | |
| } | |
| TEST(HeaderReader, Parse_Invalid_I1_CompressedDataOverlapsHeader) { | |
| HeaderReader reader; | |
| fbl::Array<uint8_t> buf = CreateHeader({{ | |
| .decompressed_offset = 0ul, | |
| .decompressed_size = 256ul, | |
| .compressed_offset = 47ul, | |
| .compressed_size = 113ul, | |
| }}); | |
| SeekTable header; | |
| ASSERT_EQ(reader.Parse(buf.get(), buf.size(), 160ul, &header), kStatusErrIoDataIntegrity); | |
| } | |
| TEST(HeaderReader, Parse_Invalid_I2_NonContigDecompressedFrames) { | |
| HeaderReader reader; | |
| fbl::Array<uint8_t> buf = CreateHeader({{ | |
| .decompressed_offset = 0ul, | |
| .decompressed_size = 256ul, | |
| .compressed_offset = 100ul, | |
| .compressed_size = 2ul, | |
| }, | |
| { | |
| // Gap between frames | |
| .decompressed_offset = 257ul, | |
| .decompressed_size = 99ul, | |
| .compressed_offset = 102ul, | |
| .compressed_size = 18ul, | |
| }}); | |
| SeekTable header; | |
| ASSERT_EQ(reader.Parse(buf.get(), buf.size(), 120ul, &header), kStatusErrIoDataIntegrity); | |
| buf = CreateHeader({{ | |
| .decompressed_offset = 0ul, | |
| .decompressed_size = 256ul, | |
| .compressed_offset = 100ul, | |
| .compressed_size = 2ul, | |
| }, | |
| { | |
| // Overlap between frames | |
| .decompressed_offset = 255ul, | |
| .decompressed_size = 101ul, | |
| .compressed_offset = 102ul, | |
| .compressed_size = 18ul, | |
| }}); | |
| ASSERT_EQ(reader.Parse(buf.get(), buf.size(), 120ul, &header), kStatusErrIoDataIntegrity); | |
| } | |
| TEST(HeaderReader, Parse_Invalid_I3_OverlappingCompressedFrames) { | |
| HeaderReader reader; | |
| fbl::Array<uint8_t> buf = CreateHeader({{ | |
| .decompressed_offset = 0ul, | |
| .decompressed_size = 256ul, | |
| .compressed_offset = 100ul, | |
| .compressed_size = 20ul, | |
| }, | |
| { | |
| .decompressed_offset = 256ul, | |
| .decompressed_size = 100ul, | |
| // Overlap between frames | |
| .compressed_offset = 119ul, | |
| .compressed_size = 2ul, | |
| }}); | |
| SeekTable header; | |
| ASSERT_EQ(reader.Parse(buf.get(), buf.size(), 121ul, &header), kStatusErrIoDataIntegrity); | |
| } | |
| TEST(HeaderReader, Parse_Invalid_I4_ZeroLengthFrames) { | |
| HeaderReader reader; | |
| fbl::Array<uint8_t> buf = CreateHeader({{ | |
| .decompressed_offset = 0ul, | |
| // Zero-length decompressed frame | |
| .decompressed_size = 0ul, | |
| .compressed_offset = 100ul, | |
| .compressed_size = 40ul, | |
| }}); | |
| SeekTable header; | |
| ASSERT_EQ(reader.Parse(buf.get(), buf.size(), 140ul, &header), kStatusErrIoDataIntegrity); | |
| buf = CreateHeader({{ | |
| .decompressed_offset = 0ul, | |
| .decompressed_size = 100ul, | |
| .compressed_offset = 100ul, | |
| // Zero-length compressed frame | |
| .compressed_size = 0ul, | |
| }}); | |
| ASSERT_EQ(reader.Parse(buf.get(), buf.size(), 100ul, &header), kStatusErrIoDataIntegrity); | |
| } | |
| TEST(HeaderReader, Parse_Invalid_I5_CompressedFrameExceedsFile) { | |
| HeaderReader reader; | |
| fbl::Array<uint8_t> buf = CreateHeader({{ | |
| .decompressed_offset = 0ul, | |
| .decompressed_size = 256ul, | |
| .compressed_offset = 100ul, | |
| .compressed_size = 60ul, | |
| }}); | |
| SeekTable header; | |
| // File claims to be 120 bytes long, but the compressed frame goes from [100, 160)) | |
| ASSERT_EQ(reader.Parse(buf.get(), buf.size(), 120ul, &header), kStatusErrIoDataIntegrity); | |
| } | |
| } // namespace chunked_compression |