| // Copyright 2026 Google LLC |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // https://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| // |
| |
| #include "formats/zip/reader.h" |
| |
| #include <algorithm> |
| #include <cstdint> |
| #include <cstdlib> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "absl/status/status.h" |
| #include "absl/status/status_matchers.h" |
| #include "absl/strings/str_cat.h" |
| #include "absl/strings/string_view.h" |
| #include "formats/byte_range.h" |
| #include "gmock/gmock.h" |
| #include "gtest/gtest.h" |
| #include "riegeli/base/types.h" |
| #include "riegeli/bytes/cfile_reader.h" |
| #include "riegeli/bytes/reader.h" |
| #include "riegeli/bytes/string_reader.h" |
| |
| namespace credentio { |
| namespace { |
| |
| using ::absl_testing::IsOk; |
| using ::absl_testing::StatusIs; |
| using ::testing::AllOf; |
| using ::testing::Contains; |
| using ::testing::Field; |
| using ::testing::HasSubstr; |
| |
| void write_le16(std::string& s, uint16_t val) { |
| s.push_back(static_cast<char>(val & 0xff)); |
| s.push_back(static_cast<char>((val >> 8) & 0xff)); |
| } |
| |
| void write_le32(std::string& s, uint32_t val) { |
| s.push_back(static_cast<char>(val & 0xff)); |
| s.push_back(static_cast<char>((val >> 8) & 0xff)); |
| s.push_back(static_cast<char>((val >> 16) & 0xff)); |
| s.push_back(static_cast<char>((val >> 24) & 0xff)); |
| } |
| |
| std::string MakeLfh(absl::string_view filename) { |
| std::string lfh; |
| lfh.append("PK\x03\x04", 4); |
| write_le16(lfh, 10); // version needed |
| write_le16(lfh, 0); // GP flag |
| write_le16(lfh, 0); // comp method |
| write_le16(lfh, 0); // last mod time |
| write_le16(lfh, 0); // last mod date |
| write_le32(lfh, 0); // crc32 |
| write_le32(lfh, 0); // compressed size |
| write_le32(lfh, 0); // uncompressed size |
| write_le16(lfh, filename.size()); |
| write_le16(lfh, 0); // extra field length |
| lfh.append(filename); |
| return lfh; |
| } |
| |
| std::string MakeCdEntry(absl::string_view filename, |
| uint32_t local_header_offset) { |
| std::string entry; |
| entry.append("PK\x01\x02", 4); |
| write_le16(entry, 0); // version made by |
| write_le16(entry, 0); // version needed |
| write_le16(entry, 0); // GP flag |
| write_le16(entry, 0); // comp method |
| write_le16(entry, 0); // last mod time |
| write_le16(entry, 0); // last mod date |
| write_le32(entry, 0); // crc32 |
| write_le32(entry, 0); // compressed size |
| write_le32(entry, 0); // uncompressed size |
| write_le16(entry, filename.size()); |
| write_le16(entry, 0); // extra field length |
| write_le16(entry, 0); // file comment length |
| write_le16(entry, 0); // disk number start |
| write_le16(entry, 0); // internal attr |
| write_le32(entry, 0); // external attr |
| write_le32(entry, local_header_offset); |
| entry.append(filename); |
| return entry; |
| } |
| |
| std::string MakeEocd(uint32_t cd_size, uint32_t cd_offset, |
| uint16_t entry_count) { |
| std::string eocd; |
| eocd.append("PK\x05\x06", 4); |
| write_le16(eocd, 0); // disk number |
| write_le16(eocd, 0); // disk where CD starts |
| write_le16(eocd, entry_count); // CD records on this disk |
| write_le16(eocd, entry_count); // total CD records |
| write_le32(eocd, cd_size); |
| write_le32(eocd, cd_offset); |
| write_le16(eocd, 0); // comment length |
| return eocd; |
| } |
| |
| std::string GetTestDataPath(absl::string_view relative_path) { |
| const char* test_srcdir = std::getenv("TEST_SRCDIR"); |
| const char* test_workspace = std::getenv("TEST_WORKSPACE"); |
| if (test_srcdir == nullptr || test_workspace == nullptr) { |
| return std::string(relative_path); |
| } |
| return absl::StrCat(test_srcdir, "/", test_workspace, "/", relative_path); |
| } |
| |
| constexpr char kEmptyZipFile[] = { |
| 'P', 'K', '\5', '\6', '\0', '\0', '\0', '\0', '\0', '\0', '\0', |
| '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', |
| }; |
| |
| TEST(ZipReaderTest, FileTooSmall) { |
| std::string file_contents = "PK\x03\x04\xab\xcd\xab\xcd\xab\xcd"; |
| riegeli::StringReader<> input(file_contents); |
| |
| EXPECT_THAT(ZipReader::Create(&input), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Input is too small to be a ZIP file"))); |
| } |
| |
| TEST(ZipReaderTest, InvalidEocdSignature) { |
| std::string file_contents = "PKPKPK12345678901234567890"; |
| riegeli::StringReader<> input(file_contents); |
| |
| EXPECT_THAT( |
| ZipReader::Create(&input), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Invalid End of Central Directory record signature"))); |
| } |
| |
| TEST(ZipReaderTest, InvalidCentralDirectorySignature) { |
| std::string file_contents = std::string( |
| "PK\x01\x03" |
| "PK\x05\x06" |
| "\x00\x00" |
| "\x00\x00" |
| "\x01\x00" |
| "\x01\x00" |
| "\x04\x00\x00\x00" |
| "\x00\x00\x00\x00" |
| "\x00\x00", |
| 26); |
| riegeli::StringReader<> input(file_contents); |
| |
| EXPECT_THAT(ZipReader::Create(&input), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Central directory signature not found"))); |
| } |
| |
| TEST(ZipReaderTest, EmptyZipFile) { |
| std::string empty_zip_file_contents(kEmptyZipFile, sizeof(kEmptyZipFile)); |
| riegeli::StringReader<> input(empty_zip_file_contents); |
| auto reader_or = ZipReader::Create(&input); |
| ASSERT_THAT(reader_or, IsOk()); |
| auto reader = std::move(*reader_or); |
| |
| EXPECT_FALSE(reader->HasNext()); |
| } |
| |
| TEST(ZipReaderTest, Zip64Unsupported) { |
| std::string zip = GetTestDataPath( |
| "formats/zip/testing/" |
| "asset_unsupported_zip64.zip"); |
| riegeli::CFileReader<> input(zip); |
| ASSERT_THAT(input.status(), IsOk()); |
| |
| EXPECT_THAT(ZipReader::Create(&input), |
| StatusIs(absl::StatusCode::kUnimplemented, |
| HasSubstr("ZIP64 format is not supported"))); |
| } |
| |
| TEST(ZipReaderTest, DetectsDataDescriptorCorrectly) { |
| // clang-format off |
| constexpr char kZipWithDataDescriptor[] = { |
| // Local File Header |
| 'P', 'K', '\x03', '\x04', |
| '\x00', '\x00', // Version needed to extract |
| '\x08', '\x00', // General purpose bit flag |
| '\x00', '\x00', // Compression method |
| '\x00', '\x00', // Last mod file time |
| '\x00', '\x00', // Last mod file date |
| '\x00', '\x00', '\x00', '\x00', // CRC-32 |
| '\x00', '\x00', '\x00', '\x00', // Compressed size |
| '\x00', '\x00', '\x00', '\x00', // Uncompressed size |
| '\x01', '\x00', // File name length (1 byte) |
| '\x00', '\x00', // Extra field length |
| 'a', // File name (1 byte) |
| // Central Directory Record |
| 'P', 'K', '\x01', '\x02', |
| '\x00', '\x00', // Version made by |
| '\x00', '\x00', // Version needed to extract |
| '\x08', '\x00', // General purpose bit flag (Bit 3 set) |
| '\x00', '\x00', // Compression method |
| '\x00', '\x00', // Last mod file time |
| '\x00', '\x00', // Last mod file date |
| '\x00', '\x00', '\x00', '\x00', // CRC-32 |
| '\x00', '\x00', '\x00', '\x00', // Compressed size |
| '\x00', '\x00', '\x00', '\x00', // Uncompressed size |
| '\x01', '\x00', // File name length (1 byte) |
| '\x00', '\x00', // Extra field length |
| '\x00', '\x00', // File comment length |
| '\x00', '\x00', // Disk number start |
| '\x00', '\x00', // Internal file attributes |
| '\x00', '\x00', '\x00', '\x00', // External file attributes |
| '\x00', '\x00', '\x00', '\x00', // Relative offset of local header |
| 'a', // File name (1 byte) |
| // End of central directory record (EOCD) |
| 'P', 'K', '\x05', '\x06', |
| '\x00', '\x00', // Number of this disk |
| '\x00', '\x00', // Disk where CD starts |
| '\x01', '\x00', // Number of CD records on this disk |
| '\x01', '\x00', // Total number of CD records |
| '\x2f', '\x00', '\x00', '\x00', // Size of CD (47 bytes) |
| '\x1f', '\x00', '\x00', '\x00', // Offset of start of CD (31 bytes) |
| '\x00', '\x00', // ZIP file comment length (0 bytes) |
| }; |
| // clang-format on |
| std::string zip_contents(kZipWithDataDescriptor, |
| sizeof(kZipWithDataDescriptor)); |
| riegeli::StringReader<> input(zip_contents); |
| auto reader_or = ZipReader::Create(&input); |
| ASSERT_THAT(reader_or, IsOk()); |
| auto reader = std::move(*reader_or); |
| |
| ASSERT_TRUE(reader->HasNext()); |
| auto entry_or = reader->Next(); |
| ASSERT_THAT(entry_or, IsOk()); |
| auto entry = std::move(*entry_or); |
| |
| EXPECT_TRUE(entry.has_data_descriptor); |
| } |
| |
| TEST(ZipReaderTest, ReadsZipFile) { |
| std::string zip = GetTestDataPath("formats/zip/testing/asset_no_c2pa.zip"); |
| riegeli::CFileReader<> input(zip); |
| ASSERT_THAT(input.status(), IsOk()); |
| auto reader_or = ZipReader::Create(&input); |
| ASSERT_THAT(reader_or, IsOk()); |
| auto reader = std::move(*reader_or); |
| |
| std::vector<ZipReader::FileEntry> entries; |
| while (reader->HasNext()) { |
| auto entry_or = reader->Next(); |
| ASSERT_THAT(entry_or, IsOk()); |
| entries.push_back(std::move(*entry_or)); |
| } |
| |
| EXPECT_THAT(entries, |
| testing::UnorderedElementsAre( |
| ZipReader::FileEntry{ |
| .central_directory_header_offset = 139, |
| .local_file_header_offset = 0, |
| .file_name = "file_1.txt", |
| .file_range = ByteRange{.offset = 40, .length = 44}, |
| .has_data_descriptor = false, |
| .general_purpose_bit_flag = 0, |
| .compression_method = 0, |
| .uncompressed_size = 44, |
| }, |
| ZipReader::FileEntry{ |
| .central_directory_header_offset = 195, |
| .local_file_header_offset = 84, |
| .file_name = "file_2.txt", |
| .file_range = ByteRange{.offset = 124, .length = 15}, |
| .has_data_descriptor = false, |
| .general_purpose_bit_flag = 0, |
| .compression_method = 0, |
| .uncompressed_size = 15, |
| })); |
| EXPECT_THAT(reader->Next(), |
| StatusIs(absl::StatusCode::kOutOfRange, |
| HasSubstr("No more file entries to read"))); |
| } |
| |
| TEST(ZipReaderTest, ReadsPptxFile) { |
| std::string pptx = |
| GetTestDataPath("formats/zip/testing/no_c2pa_032_asset.pptx"); |
| riegeli::CFileReader<> input(pptx); |
| ASSERT_THAT(input.status(), IsOk()); |
| auto reader_or = ZipReader::Create(&input); |
| ASSERT_THAT(reader_or, IsOk()); |
| auto reader = std::move(*reader_or); |
| |
| std::vector<ZipReader::FileEntry> entries; |
| while (reader->HasNext()) { |
| auto entry_or = reader->Next(); |
| ASSERT_THAT(entry_or, IsOk()); |
| entries.push_back(std::move(*entry_or)); |
| } |
| |
| EXPECT_EQ(entries.size(), 46); |
| // OOXML files should have a [Content_Types].xml file entry with a data |
| // descriptor. |
| EXPECT_THAT( |
| entries, |
| Contains( |
| AllOf(Field(&ZipReader::FileEntry::file_name, "[Content_Types].xml"), |
| Field(&ZipReader::FileEntry::has_data_descriptor, true)))); |
| } |
| |
| TEST(ZipReaderTest, CentralDirectoryOffsetEmptyZip) { |
| std::string empty_zip_file_contents(kEmptyZipFile, sizeof(kEmptyZipFile)); |
| riegeli::StringReader<> input(empty_zip_file_contents); |
| auto reader_or = ZipReader::Create(&input); |
| ASSERT_THAT(reader_or, IsOk()); |
| auto reader = std::move(*reader_or); |
| |
| EXPECT_EQ(reader->central_directory_offset(), 0); |
| } |
| |
| TEST(ZipReaderTest, CentralDirectoryOffset) { |
| std::string zip = GetTestDataPath("formats/zip/testing/asset_no_c2pa.zip"); |
| riegeli::CFileReader<> input(zip); |
| ASSERT_THAT(input.status(), IsOk()); |
| auto reader_or = ZipReader::Create(&input); |
| ASSERT_THAT(reader_or, IsOk()); |
| auto reader = std::move(*reader_or); |
| |
| EXPECT_EQ(reader->central_directory_offset(), 139); |
| } |
| |
| TEST(ZipReaderTest, LocalFileHeaderExtraFieldMismatchRejected) { |
| std::string zip; |
| // Local File Header for file_1.txt |
| zip.append("PK\x03\x04", 4); |
| write_le16(zip, 10); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 10); |
| write_le32(zip, 10); |
| std::string filename = "file_1.txt"; |
| write_le16(zip, filename.size()); |
| write_le16(zip, 8); // LFH Extra field length = 8! |
| zip.append(filename); |
| zip.append("12345678"); // LFH Extra field data |
| zip.append("abcdefghij"); // Actual file data (10 bytes) |
| |
| uint32_t cd_offset = zip.size(); |
| // Central Directory Record for file_1.txt |
| zip.append("PK\x01\x02", 4); |
| write_le16(zip, 10); |
| write_le16(zip, 10); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 10); |
| write_le32(zip, 10); |
| write_le16(zip, filename.size()); |
| write_le16(zip, 0); // CD Extra field length = 0! Mismatched! |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 0); |
| zip.append(filename); |
| |
| uint32_t cd_size = zip.size() - cd_offset; |
| // End of Central Directory (EOCD) |
| zip.append("PK\x05\x06", 4); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 1); |
| write_le16(zip, 1); |
| write_le32(zip, cd_size); |
| write_le32(zip, cd_offset); |
| write_le16(zip, 0); |
| |
| riegeli::StringReader<> input(zip); |
| EXPECT_THAT( |
| ZipReader::Create(&input), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Local File Header extra field length does not match " |
| "Central Directory"))); |
| } |
| |
| TEST(ZipReaderTest, LocalFileHeaderInvalidSignatureRejected) { |
| std::string zip; |
| // Local File Header with corrupted signature |
| zip.append("PK\x03\x05", 4); // PK\x03\x05 instead of PK\x03\x04! |
| write_le16(zip, 10); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 10); |
| write_le32(zip, 10); |
| std::string filename = "file_1.txt"; |
| write_le16(zip, filename.size()); |
| write_le16(zip, 0); |
| zip.append(filename); |
| zip.append("abcdefghij"); |
| |
| uint32_t cd_offset = zip.size(); |
| // Central Directory Record |
| zip.append("PK\x01\x02", 4); |
| write_le16(zip, 10); |
| write_le16(zip, 10); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 10); |
| write_le32(zip, 10); |
| write_le16(zip, filename.size()); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 0); // Local Header Offset = 0 |
| zip.append(filename); |
| |
| uint32_t cd_size = zip.size() - cd_offset; |
| // End of Central Directory (EOCD) |
| zip.append("PK\x05\x06", 4); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 1); |
| write_le16(zip, 1); |
| write_le32(zip, cd_size); |
| write_le32(zip, cd_offset); |
| write_le16(zip, 0); |
| |
| riegeli::StringReader<> input(zip); |
| EXPECT_THAT(ZipReader::Create(&input), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Invalid local file header signature"))); |
| } |
| |
| TEST(ZipReaderTest, LocalFileHeaderSeekFailureRejected) { |
| std::string zip; |
| // No LFH at offset 0 (we start with CD) |
| uint32_t cd_offset = zip.size(); |
| std::string filename = "file_1.txt"; |
| // Central Directory Record |
| zip.append("PK\x01\x02", 4); |
| write_le16(zip, 10); |
| write_le16(zip, 10); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 10); |
| write_le32(zip, 10); |
| write_le16(zip, filename.size()); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 9999); // Local Header Offset points to invalid offset 9999! |
| zip.append(filename); |
| |
| uint32_t cd_size = zip.size() - cd_offset; |
| // End of Central Directory (EOCD) |
| zip.append("PK\x05\x06", 4); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 1); |
| write_le16(zip, 1); |
| write_le32(zip, cd_size); |
| write_le32(zip, cd_offset); |
| write_le16(zip, 0); |
| |
| riegeli::StringReader<> input(zip); |
| EXPECT_THAT( |
| ZipReader::Create(&input), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Failed to seek to local file header offset"))); |
| } |
| |
| TEST(ZipReaderTest, VulnerabilityZipEntryOverlapDetected) { |
| auto write_le16 = [](std::string& s, uint16_t val) { |
| s.push_back(static_cast<char>(val & 0xff)); |
| s.push_back(static_cast<char>((val >> 8) & 0xff)); |
| }; |
| auto write_le32 = [](std::string& s, uint32_t val) { |
| s.push_back(static_cast<char>(val & 0xff)); |
| s.push_back(static_cast<char>((val >> 8) & 0xff)); |
| s.push_back(static_cast<char>((val >> 16) & 0xff)); |
| s.push_back(static_cast<char>((val >> 24) & 0xff)); |
| }; |
| |
| std::string zip; |
| std::string filename = "file_1.txt"; |
| // Local File Header at offset 0 |
| zip.append("PK\x03\x04", 4); |
| write_le16(zip, 10); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 10); |
| write_le32(zip, 10); |
| write_le16(zip, filename.size()); |
| write_le16(zip, 0); |
| zip.append(filename); |
| zip.append("0123456789", 10); |
| |
| uint32_t cd_offset = zip.size(); |
| // Central Directory Record 1 pointing to offset 0 |
| zip.append("PK\x01\x02", 4); |
| write_le16(zip, 10); |
| write_le16(zip, 10); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 10); |
| write_le32(zip, 10); |
| write_le16(zip, filename.size()); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 0); |
| zip.append(filename); |
| |
| // Central Directory Record 2 duplicates offset 0 (overlapping/duplicate) |
| zip.append("PK\x01\x02", 4); |
| write_le16(zip, 10); |
| write_le16(zip, 10); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 10); |
| write_le32(zip, 10); |
| write_le16(zip, filename.size()); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 0); |
| zip.append(filename); |
| |
| uint32_t cd_size = zip.size() - cd_offset; |
| // End of Central Directory (EOCD) |
| zip.append("PK\x05\x06", 4); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 2); |
| write_le16(zip, 2); |
| write_le32(zip, cd_size); |
| write_le32(zip, cd_offset); |
| write_le16(zip, 0); |
| |
| riegeli::StringReader<> input(zip); |
| EXPECT_THAT( |
| ZipReader::Create(&input), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Overlapping or duplicate ZIP entries detected"))); |
| } |
| |
| TEST(ZipReaderTest, CompressedSizeExceedsFileLimitsRejected) { |
| auto write_le16 = [](std::string& s, uint16_t val) { |
| s.push_back(static_cast<char>(val & 0xff)); |
| s.push_back(static_cast<char>((val >> 8) & 0xff)); |
| }; |
| auto write_le32 = [](std::string& s, uint32_t val) { |
| s.push_back(static_cast<char>(val & 0xff)); |
| s.push_back(static_cast<char>((val >> 8) & 0xff)); |
| s.push_back(static_cast<char>((val >> 16) & 0xff)); |
| s.push_back(static_cast<char>((val >> 24) & 0xff)); |
| }; |
| |
| std::string zip; |
| // Local File Header for file_1.txt |
| zip.append("PK\x03\x04", 4); |
| write_le16(zip, 10); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 10); |
| write_le32(zip, 10); |
| std::string filename = "file_1.txt"; |
| write_le16(zip, filename.size()); |
| write_le16(zip, 0); |
| zip.append(filename); |
| zip.append("abcdefghij"); |
| |
| uint32_t cd_offset = zip.size(); |
| // Central Directory Record for file_1.txt |
| zip.append("PK\x01\x02", 4); |
| write_le16(zip, 10); |
| write_le16(zip, 10); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 0x7FFFFFFF); // CD Compressed size exceeds file size |
| write_le32(zip, 10); |
| write_le16(zip, filename.size()); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 0); |
| zip.append(filename); |
| |
| uint32_t cd_size = zip.size() - cd_offset; |
| // End of Central Directory (EOCD) |
| zip.append("PK\x05\x06", 4); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 1); |
| write_le16(zip, 1); |
| write_le32(zip, cd_size); |
| write_le32(zip, cd_offset); |
| write_le16(zip, 0); |
| |
| riegeli::StringReader<> input(zip); |
| EXPECT_THAT(ZipReader::Create(&input), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Compressed size exceeds file limits"))); |
| } |
| |
| class MockLargeZipReader : public riegeli::Reader { |
| public: |
| MockLargeZipReader(std::string lfh1, std::string lfh2, std::string cd, |
| std::string eocd, uint64_t lfh2_offset, uint64_t cd_offset, |
| uint64_t eocd_offset) |
| : lfh1_(lfh1), |
| lfh2_(lfh2), |
| cd_(cd), |
| eocd_(eocd), |
| lfh2_offset_(lfh2_offset), |
| cd_offset_(cd_offset), |
| eocd_offset_(eocd_offset), |
| size_(eocd_offset + eocd.size()) { |
| set_limit_pos(0); |
| } |
| |
| bool SupportsRandomAccess() override { return true; } |
| bool SupportsSize() override { return true; } |
| |
| protected: |
| bool PullSlow(size_t min_length, size_t recommended_length) override { |
| uint64_t current_pos = pos(); |
| if (current_pos >= size_) return false; |
| |
| if (current_pos < lfh1_.size()) { |
| set_buffer(lfh1_.data(), lfh1_.size(), current_pos); |
| set_limit_pos(lfh1_.size()); |
| return true; |
| } |
| if (current_pos >= lfh2_offset_ && |
| current_pos < lfh2_offset_ + lfh2_.size()) { |
| set_buffer(lfh2_.data(), lfh2_.size(), current_pos - lfh2_offset_); |
| set_limit_pos(lfh2_offset_ + lfh2_.size()); |
| return true; |
| } |
| if (current_pos >= cd_offset_ && current_pos < cd_offset_ + cd_.size()) { |
| set_buffer(cd_.data(), cd_.size(), current_pos - cd_offset_); |
| set_limit_pos(cd_offset_ + cd_.size()); |
| return true; |
| } |
| if (current_pos >= eocd_offset_ && |
| current_pos < eocd_offset_ + eocd_.size()) { |
| set_buffer(eocd_.data(), eocd_.size(), current_pos - eocd_offset_); |
| set_limit_pos(eocd_offset_ + eocd_.size()); |
| return true; |
| } |
| |
| // Gap handling |
| uint64_t next_offset = size_; |
| if (current_pos < lfh2_offset_) { |
| next_offset = lfh2_offset_; |
| } else if (current_pos < cd_offset_) { |
| next_offset = cd_offset_; |
| } else if (current_pos < eocd_offset_) { |
| next_offset = eocd_offset_; |
| } |
| |
| uint64_t remaining = next_offset - current_pos; |
| size_t chunk = |
| std::min(sizeof(kZeroBuffer), static_cast<size_t>(remaining)); |
| set_buffer(kZeroBuffer, chunk, 0); |
| set_limit_pos(current_pos + chunk); |
| return true; |
| } |
| |
| bool SeekSlow(riegeli::Position new_pos) override { |
| if (new_pos > size_) { |
| set_buffer(); |
| set_limit_pos(size_); |
| return false; |
| } |
| set_buffer(); |
| set_limit_pos(new_pos); |
| return true; |
| } |
| |
| std::optional<riegeli::Position> SizeImpl() override { return size_; } |
| |
| private: |
| static constexpr char kZeroBuffer[1024] = {0}; |
| |
| std::string lfh1_; |
| std::string lfh2_; |
| std::string cd_; |
| std::string eocd_; |
| uint64_t lfh2_offset_; |
| uint64_t cd_offset_; |
| uint64_t eocd_offset_; |
| uint64_t size_; |
| }; |
| |
| TEST(ZipReaderTest, LargeZipFile) { |
| // We want to test that ZipReader can handle file entries that start after |
| // 4GB. We construct a mock ZIP file layout: |
| // - LFH1 at 0 (size 40) |
| // - Large file data of size 4294967063 (approx 4GB) |
| // - LFH2 at 4294967103 (size 43) |
| // - File 2 (manifest) data of size 100 |
| // - CD at 4294967250 (starts before 4GB, ends after 4GB) |
| // - CD Entry 1 (for large_file) starts at 4294967250, size 56. |
| // - CD Entry 2 (for manifest.json) starts at 4294967306 (after 4GB!), |
| // size 59. |
| // - EOCD at 4294967365 (size 22) |
| // Total size = 4294967387. |
| |
| std::string filename1 = "large_file"; |
| std::string filename2 = "manifest.json"; |
| |
| std::string lfh1 = MakeLfh(filename1); |
| std::string lfh2 = MakeLfh(filename2); |
| |
| uint64_t lfh2_offset = 4294967103; |
| uint64_t cd_offset = 4294967250; |
| |
| std::string cd_entry1 = MakeCdEntry(filename1, 0); |
| std::string cd_entry2 = MakeCdEntry(filename2, lfh2_offset); |
| std::string cd = cd_entry1 + cd_entry2; |
| |
| uint64_t eocd_offset = cd_offset + cd.size(); |
| std::string eocd = MakeEocd(cd.size(), cd_offset, 2); |
| |
| MockLargeZipReader input(lfh1, lfh2, cd, eocd, lfh2_offset, cd_offset, |
| eocd_offset); |
| |
| auto reader_or = ZipReader::Create(&input); |
| ASSERT_THAT(reader_or, IsOk()); |
| auto reader = std::move(*reader_or); |
| |
| std::vector<ZipReader::FileEntry> entries; |
| while (reader->HasNext()) { |
| auto entry_or = reader->Next(); |
| ASSERT_THAT(entry_or, IsOk()); |
| entries.push_back(std::move(*entry_or)); |
| } |
| |
| EXPECT_THAT( |
| entries, |
| testing::UnorderedElementsAre( |
| ZipReader::FileEntry{ |
| .central_directory_header_offset = cd_offset, |
| .local_file_header_offset = 0, |
| .file_name = filename1, |
| .file_range = ByteRange{.offset = 40, .length = 0}, |
| .has_data_descriptor = false, |
| }, |
| ZipReader::FileEntry{ |
| .central_directory_header_offset = cd_offset + cd_entry1.size(), |
| .local_file_header_offset = lfh2_offset, |
| .file_name = filename2, |
| .file_range = |
| ByteRange{.offset = lfh2_offset + 30 + filename2.size(), |
| .length = 0}, |
| .has_data_descriptor = false, |
| })); |
| } |
| |
| TEST(ZipReaderTest, LocalFileHeaderSizeMismatchRejected) { |
| auto write_le16 = [](std::string& s, uint16_t val) { |
| s.push_back(static_cast<char>(val & 0xff)); |
| s.push_back(static_cast<char>((val >> 8) & 0xff)); |
| }; |
| auto write_le32 = [](std::string& s, uint32_t val) { |
| s.push_back(static_cast<char>(val & 0xff)); |
| s.push_back(static_cast<char>((val >> 8) & 0xff)); |
| s.push_back(static_cast<char>((val >> 16) & 0xff)); |
| s.push_back(static_cast<char>((val >> 24) & 0xff)); |
| }; |
| |
| std::string zip; |
| // Local File Header with compressed_size = 999 |
| zip.append("PK\x03\x04", 4); |
| write_le16(zip, 10); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 999); // LFH compressed size = 999! |
| write_le32(zip, 10); |
| std::string filename = "file_1.txt"; |
| write_le16(zip, filename.size()); |
| write_le16(zip, 0); |
| zip.append(filename); |
| zip.append("abcdefghij"); |
| |
| uint32_t cd_offset = zip.size(); |
| // Central Directory Record with compressed_size = 10 |
| zip.append("PK\x01\x02", 4); |
| write_le16(zip, 10); |
| write_le16(zip, 10); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 10); // CD compressed size = 10! |
| write_le32(zip, 10); |
| write_le16(zip, filename.size()); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 0); |
| zip.append(filename); |
| |
| uint32_t cd_size = zip.size() - cd_offset; |
| zip.append("PK\x05\x06", 4); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 1); |
| write_le16(zip, 1); |
| write_le32(zip, cd_size); |
| write_le32(zip, cd_offset); |
| write_le16(zip, 0); |
| |
| riegeli::StringReader<> input(zip); |
| EXPECT_THAT(ZipReader::Create(&input), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Local File Header sizes do not match Central " |
| "Directory"))); |
| } |
| |
| TEST(ZipReaderTest, LocalFileHeaderFileNameLengthMismatchRejected) { |
| auto write_le16 = [](std::string& s, uint16_t val) { |
| s.push_back(static_cast<char>(val & 0xff)); |
| s.push_back(static_cast<char>((val >> 8) & 0xff)); |
| }; |
| auto write_le32 = [](std::string& s, uint32_t val) { |
| s.push_back(static_cast<char>(val & 0xff)); |
| s.push_back(static_cast<char>((val >> 8) & 0xff)); |
| s.push_back(static_cast<char>((val >> 16) & 0xff)); |
| s.push_back(static_cast<char>((val >> 24) & 0xff)); |
| }; |
| |
| std::string zip; |
| zip.append("PK\x03\x04", 4); |
| write_le16(zip, 10); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 10); |
| write_le32(zip, 10); |
| std::string filename = "file_1.txt"; |
| write_le16(zip, filename.size() + 5); // LFH filename length mismatched! |
| write_le16(zip, 0); |
| zip.append(filename); |
| zip.append("abcdefghij"); |
| |
| uint32_t cd_offset = zip.size(); |
| zip.append("PK\x01\x02", 4); |
| write_le16(zip, 10); |
| write_le16(zip, 10); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 10); |
| write_le32(zip, 10); |
| write_le16(zip, filename.size()); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 0); |
| zip.append(filename); |
| |
| uint32_t cd_size = zip.size() - cd_offset; |
| zip.append("PK\x05\x06", 4); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 1); |
| write_le16(zip, 1); |
| write_le32(zip, cd_size); |
| write_le32(zip, cd_offset); |
| write_le16(zip, 0); |
| |
| riegeli::StringReader<> input(zip); |
| EXPECT_THAT( |
| ZipReader::Create(&input), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Local File Header file name length does not match " |
| "Central Directory"))); |
| } |
| |
| TEST(ZipReaderTest, LocalFileHeaderBitFlagMismatchRejected) { |
| auto write_le16 = [](std::string& s, uint16_t val) { |
| s.push_back(static_cast<char>(val & 0xff)); |
| s.push_back(static_cast<char>((val >> 8) & 0xff)); |
| }; |
| auto write_le32 = [](std::string& s, uint32_t val) { |
| s.push_back(static_cast<char>(val & 0xff)); |
| s.push_back(static_cast<char>((val >> 8) & 0xff)); |
| s.push_back(static_cast<char>((val >> 16) & 0xff)); |
| s.push_back(static_cast<char>((val >> 24) & 0xff)); |
| }; |
| |
| std::string zip; |
| zip.append("PK\x03\x04", 4); |
| write_le16(zip, 10); |
| write_le16(zip, 0x0800); // LFH has UTF-8 flag set |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 10); |
| write_le32(zip, 10); |
| std::string filename = "file_1.txt"; |
| write_le16(zip, filename.size()); |
| write_le16(zip, 0); |
| zip.append(filename); |
| zip.append("abcdefghij"); |
| |
| uint32_t cd_offset = zip.size(); |
| zip.append("PK\x01\x02", 4); |
| write_le16(zip, 10); |
| write_le16(zip, 10); |
| write_le16(zip, 0); // CD bit flag is 0 |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 10); |
| write_le32(zip, 10); |
| write_le16(zip, filename.size()); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 0); |
| zip.append(filename); |
| |
| uint32_t cd_size = zip.size() - cd_offset; |
| zip.append("PK\x05\x06", 4); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 1); |
| write_le16(zip, 1); |
| write_le32(zip, cd_size); |
| write_le32(zip, cd_offset); |
| write_le16(zip, 0); |
| |
| riegeli::StringReader<> input(zip); |
| EXPECT_THAT( |
| ZipReader::Create(&input), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Local File Header bit flag does not match Central " |
| "Directory"))); |
| } |
| |
| TEST(ZipReaderTest, LocalFileHeaderCompressionMethodMismatchRejected) { |
| auto write_le16 = [](std::string& s, uint16_t val) { |
| s.push_back(static_cast<char>(val & 0xff)); |
| s.push_back(static_cast<char>((val >> 8) & 0xff)); |
| }; |
| auto write_le32 = [](std::string& s, uint32_t val) { |
| s.push_back(static_cast<char>(val & 0xff)); |
| s.push_back(static_cast<char>((val >> 8) & 0xff)); |
| s.push_back(static_cast<char>((val >> 16) & 0xff)); |
| s.push_back(static_cast<char>((val >> 24) & 0xff)); |
| }; |
| |
| std::string zip; |
| zip.append("PK\x03\x04", 4); |
| write_le16(zip, 10); |
| write_le16(zip, 0); |
| write_le16(zip, 8); // LFH compression method = 8 |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 10); |
| write_le32(zip, 10); |
| std::string filename = "file_1.txt"; |
| write_le16(zip, filename.size()); |
| write_le16(zip, 0); |
| zip.append(filename); |
| zip.append("abcdefghij"); |
| |
| uint32_t cd_offset = zip.size(); |
| zip.append("PK\x01\x02", 4); |
| write_le16(zip, 10); |
| write_le16(zip, 10); |
| write_le16(zip, 0); |
| write_le16(zip, 0); // CD compression method = 0 |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 10); |
| write_le32(zip, 10); |
| write_le16(zip, filename.size()); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 0); |
| zip.append(filename); |
| |
| uint32_t cd_size = zip.size() - cd_offset; |
| zip.append("PK\x05\x06", 4); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 1); |
| write_le16(zip, 1); |
| write_le32(zip, cd_size); |
| write_le32(zip, cd_offset); |
| write_le16(zip, 0); |
| |
| riegeli::StringReader<> input(zip); |
| EXPECT_THAT( |
| ZipReader::Create(&input), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Local File Header compression method does not match " |
| "Central Directory"))); |
| } |
| |
| TEST(ZipReaderTest, LocalFileHeaderTruncatedRejected) { |
| auto write_le16 = [](std::string& s, uint16_t val) { |
| s.push_back(static_cast<char>(val & 0xff)); |
| s.push_back(static_cast<char>((val >> 8) & 0xff)); |
| }; |
| auto write_le32 = [](std::string& s, uint32_t val) { |
| s.push_back(static_cast<char>(val & 0xff)); |
| s.push_back(static_cast<char>((val >> 8) & 0xff)); |
| s.push_back(static_cast<char>((val >> 16) & 0xff)); |
| s.push_back(static_cast<char>((val >> 24) & 0xff)); |
| }; |
| |
| std::string zip; |
| uint32_t cd_offset = 0; |
| std::string filename = "file_1.txt"; |
| |
| // CD Record at offset 0 |
| zip.append("PK\x01\x02", 4); |
| write_le16(zip, 10); |
| write_le16(zip, 10); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| write_le32(zip, 10); |
| write_le32(zip, 10); |
| write_le16(zip, filename.size()); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le32(zip, 0); |
| |
| uint32_t lfh_offset = 46 + filename.size() + 12; // Offset 12 inside EOCD |
| write_le32(zip, lfh_offset); |
| zip.append(filename); |
| |
| // EOCD Record |
| zip.append("PK\x05\x06", 4); |
| write_le16(zip, 0); |
| write_le16(zip, 0); |
| write_le16(zip, 1); |
| write_le16(zip, 1); |
| zip.append("PK\x03\x04", 4); // Size of CD field serves as fake LFH signature |
| write_le32(zip, cd_offset); // Offset of CD = 0 (serves as flags = 0) |
| write_le16(zip, 0); // Comment length = 0 (serves as comp method = 0) |
| |
| riegeli::StringReader<> input(zip); |
| EXPECT_THAT( |
| ZipReader::Create(&input), |
| StatusIs(absl::StatusCode::kInvalidArgument, |
| HasSubstr("Failed to skip fields in local file header"))); |
| } |
| |
| } // namespace |
| } // namespace credentio |