| // 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::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); | |
| ABSL_ASSERT_OK(reader_or); | |
| 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); | |
| ABSL_ASSERT_OK(input.status()); | |
| 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); | |
| ABSL_ASSERT_OK(reader_or); | |
| auto reader = std::move(*reader_or); | |
| ASSERT_TRUE(reader->HasNext()); | |
| auto entry_or = reader->Next(); | |
| ABSL_ASSERT_OK(entry_or); | |
| 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); | |
| ABSL_ASSERT_OK(input.status()); | |
| auto reader_or = ZipReader::Create(&input); | |
| ABSL_ASSERT_OK(reader_or); | |
| auto reader = std::move(*reader_or); | |
| std::vector<ZipReader::FileEntry> entries; | |
| while (reader->HasNext()) { | |
| auto entry_or = reader->Next(); | |
| ABSL_ASSERT_OK(entry_or); | |
| 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); | |
| ABSL_ASSERT_OK(input.status()); | |
| auto reader_or = ZipReader::Create(&input); | |
| ABSL_ASSERT_OK(reader_or); | |
| auto reader = std::move(*reader_or); | |
| std::vector<ZipReader::FileEntry> entries; | |
| while (reader->HasNext()) { | |
| auto entry_or = reader->Next(); | |
| ABSL_ASSERT_OK(entry_or); | |
| 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); | |
| ABSL_ASSERT_OK(reader_or); | |
| 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); | |
| ABSL_ASSERT_OK(input.status()); | |
| auto reader_or = ZipReader::Create(&input); | |
| ABSL_ASSERT_OK(reader_or); | |
| 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); | |
| ABSL_ASSERT_OK(reader_or); | |
| auto reader = std::move(*reader_or); | |
| std::vector<ZipReader::FileEntry> entries; | |
| while (reader->HasNext()) { | |
| auto entry_or = reader->Next(); | |
| ABSL_ASSERT_OK(entry_or); | |
| 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 |