blob: 2bbbfa5a3c1e7089a5d5ed9163dea59ac695fa4a [file] [edit]
// 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