blob: d208981ff970eae2dde7ce92baca859c6038ef4b [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 <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "absl/base/nullability.h"
#include "absl/memory/memory.h"
#include "absl/status/status.h"
#include "absl/status/status_macros.h"
#include "absl/status/statusor.h"
#include "absl/strings/string_view.h"
#include "absl/strings/substitute.h"
#include "absl/types/span.h"
#include "formats/zip/constants.h"
#include "riegeli/base/types.h"
#include "riegeli/bytes/reader.h"
#include "riegeli/endian/endian_reading.h"
namespace credentio {
namespace {
// Holds information from the End of Central Directory Record (EOCD) needed to
// locate and read the central directory.
struct CentralDirectoryLocator {
// Offset of the start of the central directory.
uint32_t offset;
// Total number of entries in the central directory.
uint16_t entry_count;
};
// Reads the End of Central Directory (EOCD) record from the end of the input
// to locate the central directory.
absl::StatusOr<CentralDirectoryLocator> FindCentralDirectory(
riegeli::Reader& input) {
if (!input.SupportsSize() || !input.Size().has_value()) {
return absl::InvalidArgumentError(
"Input does not support size or size is unknown.");
}
int64_t file_size = *input.Size();
if (file_size < kEocdRecordSize) {
return absl::InvalidArgumentError("Input is too small to be a ZIP file.");
}
if (!input.Seek(file_size - kEocdRecordSize)) {
return absl::InvalidArgumentError(
"Failed to seek to the end of the input.");
}
CentralDirectoryLocator locator;
std::string eocd_signature;
if (!input.Read(kZipEndOfCentralDirectorySignature.size(), eocd_signature) ||
eocd_signature != kZipEndOfCentralDirectorySignature) {
return absl::InvalidArgumentError(absl::Substitute(
"Invalid End of Central Directory record signature: $0",
eocd_signature));
}
// Skip the following fields since they are not needed for locating the
// central directory:
// - Number of this disk (2 bytes)
// - Number of the disk with the start of the central directory (2 bytes)
// - Total number of entries in the central directory on this disk (2 bytes)
if (!input.Skip(6)) {
return absl::InvalidArgumentError(
"Failed to skip fields in the End of Central Directory record.");
}
// Total number of entries in the central directory (2 bytes, little-endian)
if (!riegeli::ReadLittleEndian<uint16_t>(input, locator.entry_count)) {
return absl::InvalidArgumentError(
"Failed to read the total number of entries in the central directory.");
}
if (locator.entry_count == 0xffff) {
return absl::UnimplementedError(
"ZIP64 format is not supported. Central directory entry "
"count must "
"not be 0xffff.");
}
// Skip the size of the central directory (4 bytes)
if (!input.Skip(4)) {
return absl::InvalidArgumentError(
"Failed to skip the size of the central directory.");
}
// Offset of the start of the central directory (4 bytes, little-endian)
if (!riegeli::ReadLittleEndian<uint32_t>(input, locator.offset)) {
return absl::InvalidArgumentError(
"Failed to read the offset of the start of the central directory.");
}
if (locator.offset == 0xffffffff) {
return absl::UnimplementedError(
"ZIP64 format is not supported. Central Directory offset "
"must not be "
"0xffffffff.");
}
return locator;
}
// Reads the central directory entries, and extracts file header information.
absl::StatusOr<std::vector<ZipReader::FileEntry>>
ExtractFileEntriesFromCentralDirectory(riegeli::Reader& input,
const CentralDirectoryLocator& locator) {
if (!input.Seek(locator.offset) || input.pos() != locator.offset) {
return absl::InvalidArgumentError(
"Failed to seek to the start of the central directory.");
}
std::vector<ZipReader::FileEntry> file_entries;
file_entries.reserve(locator.entry_count);
for (int i = 0; i < locator.entry_count; ++i) {
ZipReader::FileEntry entry;
entry.central_directory_header_offset = input.pos();
// Central directory signature (4 bytes)
std::string central_directory_signature;
if (!input.Read(kZipCentralDirectorySignature.size(),
central_directory_signature) ||
central_directory_signature != kZipCentralDirectorySignature) {
return absl::InvalidArgumentError(
"Central directory signature not found.");
}
// Skip the following fields since they are not needed:
// - Version made by (2 bytes)
// - Version needed to extract (2 bytes)
if (!input.Skip(4)) {
return absl::InvalidArgumentError(
"Failed to skip version fields in the central directory entry.");
}
// General purpose bit flag (2 bytes, little-endian)
uint16_t general_purpose_bit_flag;
if (!riegeli::ReadLittleEndian<uint16_t>(input, general_purpose_bit_flag)) {
return absl::InvalidArgumentError(
"Failed to read the general purpose bit flag.");
}
// Bit 3 in the general purpose bit flag indicates whether the file has a
// data descriptor.
entry.has_data_descriptor = general_purpose_bit_flag & 0x0008;
entry.general_purpose_bit_flag = general_purpose_bit_flag;
// Compression method (2 bytes, little-endian)
if (!riegeli::ReadLittleEndian<uint16_t>(input, entry.compression_method)) {
return absl::InvalidArgumentError(
"Failed to read the compression method.");
}
// Skip the following fields since they are not needed:
// - Last mod file time (2 bytes)
// - Last mod file date (2 bytes)
// - CRC-32 (4 bytes)
if (!input.Skip(8)) {
return absl::InvalidArgumentError(
"Failed to skip fields in the central directory entry.");
}
// Compressed size (4 bytes, little-endian)
uint32_t compressed_size;
if (!riegeli::ReadLittleEndian<uint32_t>(input, compressed_size)) {
return absl::InvalidArgumentError("Failed to read the compressed size.");
}
if (compressed_size == 0xffffffff) {
return absl::UnimplementedError(
"ZIP64 format is not supported. Compressed size must "
"not be "
"0xffffffff.");
}
std::optional<riegeli::Position> input_size = input.Size();
if (input_size.has_value() && compressed_size > *input_size) {
return absl::InvalidArgumentError("Compressed size exceeds file limits.");
}
entry.file_range.length = compressed_size;
// Uncompressed size (4 bytes, little-endian)
if (!riegeli::ReadLittleEndian<uint32_t>(input, entry.uncompressed_size)) {
return absl::InvalidArgumentError(
"Failed to read the uncompressed size.");
}
// File name length (2 bytes, little-endian)
uint16_t file_name_length;
if (!riegeli::ReadLittleEndian<uint16_t>(input, file_name_length)) {
return absl::InvalidArgumentError("Failed to read the file name length.");
}
// Extra field length (2 bytes, little-endian)
uint16_t extra_field_length;
if (!riegeli::ReadLittleEndian<uint16_t>(input, extra_field_length)) {
return absl::InvalidArgumentError(
"Failed to read the extra field length.");
}
entry.extra_field_length = extra_field_length;
// File comment length (2 bytes, little-endian)
uint16_t file_comment_length;
if (!riegeli::ReadLittleEndian<uint16_t>(input, file_comment_length)) {
return absl::InvalidArgumentError(
"Failed to read the file comment length.");
}
entry.file_comment_length = file_comment_length;
// Skip the following fields since they are not needed:
// - Disk number (2 bytes)
// - Internal file attributes (2 bytes)
// - External file attributes (4 bytes)
if (!input.Skip(8)) {
return absl::InvalidArgumentError(
"Failed to skip fields in the central directory entry.");
}
// Local file header offset (4 bytes, little-endian)
uint32_t local_file_header_offset;
if (!riegeli::ReadLittleEndian<uint32_t>(input, local_file_header_offset)) {
return absl::InvalidArgumentError(
"Failed to read the local file header offset.");
}
entry.local_file_header_offset = local_file_header_offset;
// File name (length is determined by `file_name_length`)
if (!input.Read(file_name_length, entry.file_name)) {
return absl::InvalidArgumentError("Failed to read the file name.");
}
// Skip extra field and file comment in central directory
if (!input.Skip(extra_field_length + file_comment_length)) {
return absl::InvalidArgumentError(
"Failed to skip extra field and file comment in central directory.");
}
// The file data starts immediately after the local file header, which
// consists of:
// - Fixed length fields (in total of 30 bytes)
// - File name (variable length)
// - Extra field (variable length)
entry.file_range.offset = entry.local_file_header_offset +
kLocalFileHeaderFixedSize +
entry.file_name.size() + entry.extra_field_length;
file_entries.push_back(entry);
}
// Reads and validates LFH signatures and ensures that the LFH
// extra_field_length matches the Central Directory requirement.
const int64_t cd_end_pos = input.pos();
ABSL_ASSIGN_OR_RETURN(auto sorted_entries,
ZipReader::SortFileEntries(file_entries));
for (const auto& entry : sorted_entries) {
if (!input.Seek(entry.local_file_header_offset) ||
input.pos() != entry.local_file_header_offset) {
return absl::InvalidArgumentError(
"Failed to seek to local file header offset.");
}
std::string lfh_sig;
if (!input.Read(kZipLocalFileHeaderSignature.size(), lfh_sig) ||
lfh_sig != kZipLocalFileHeaderSignature) {
return absl::InvalidArgumentError("Invalid local file header signature.");
}
if (!input.Skip(2)) {
return absl::InvalidArgumentError(
"Failed to skip fields in local file header.");
}
uint16_t lfh_general_purpose_bit_flag;
if (!riegeli::ReadLittleEndian<uint16_t>(input,
lfh_general_purpose_bit_flag)) {
return absl::InvalidArgumentError(
"Failed to read general purpose bit flag from local file header.");
}
if ((entry.general_purpose_bit_flag & 0x0808) !=
(lfh_general_purpose_bit_flag & 0x0808)) {
return absl::InvalidArgumentError(
"Local File Header bit flag does not match Central Directory.");
}
uint16_t lfh_compression_method;
if (!riegeli::ReadLittleEndian<uint16_t>(input, lfh_compression_method)) {
return absl::InvalidArgumentError(
"Failed to read compression method from local file header.");
}
if (lfh_compression_method != entry.compression_method) {
return absl::InvalidArgumentError(
"Local File Header compression method does not match Central "
"Directory.");
}
if (!input.Skip(8)) {
return absl::InvalidArgumentError(
"Failed to skip fields in local file header.");
}
uint32_t lfh_compressed_size;
if (!riegeli::ReadLittleEndian<uint32_t>(input, lfh_compressed_size)) {
return absl::InvalidArgumentError(
"Failed to read compressed size from local file header.");
}
uint32_t lfh_uncompressed_size;
if (!riegeli::ReadLittleEndian<uint32_t>(input, lfh_uncompressed_size)) {
return absl::InvalidArgumentError(
"Failed to read uncompressed size from local file header.");
}
if (!entry.has_data_descriptor) {
if (lfh_compressed_size != entry.file_range.length ||
lfh_uncompressed_size != entry.uncompressed_size) {
return absl::InvalidArgumentError(
"Local File Header sizes do not match Central Directory.");
}
} else {
if ((lfh_compressed_size != 0 &&
lfh_compressed_size != entry.file_range.length) ||
(lfh_uncompressed_size != 0 &&
lfh_uncompressed_size != entry.uncompressed_size)) {
return absl::InvalidArgumentError(
"Local File Header sizes do not match Central Directory.");
}
}
uint16_t lfh_file_name_length;
if (!riegeli::ReadLittleEndian<uint16_t>(input, lfh_file_name_length)) {
return absl::InvalidArgumentError(
"Failed to read file name length from local file header.");
}
if (lfh_file_name_length != entry.file_name.size()) {
return absl::InvalidArgumentError(
"Local File Header file name length does not match Central "
"Directory.");
}
uint16_t lfh_extra_field_length;
if (!riegeli::ReadLittleEndian<uint16_t>(input, lfh_extra_field_length)) {
return absl::InvalidArgumentError(
"Failed to read extra field length from local file header.");
}
if (lfh_extra_field_length != entry.extra_field_length) {
return absl::InvalidArgumentError(
"Local File Header extra field length does not match Central "
"Directory.");
}
}
if (!input.Seek(cd_end_pos) || input.pos() != cd_end_pos) {
return absl::InvalidArgumentError(
"Failed to seek back to central directory position.");
}
return sorted_entries;
}
} // namespace
// Sorts ZIP file entries by header offset and checks that they do not have
// duplicate offsets or overlapping ranges to prevent ZIP bomb conditions.
absl::StatusOr<std::vector<ZipReader::FileEntry>> ZipReader::SortFileEntries(
absl::Span<const ZipReader::FileEntry> file_entries) {
std::vector<ZipReader::FileEntry> sorted_entries(file_entries.begin(),
file_entries.end());
std::sort(sorted_entries.begin(), sorted_entries.end(),
[](const ZipReader::FileEntry& a, const ZipReader::FileEntry& b) {
return a.local_file_header_offset < b.local_file_header_offset;
});
int64_t previous_entry_end = 0;
for (const auto& entry : sorted_entries) {
if (entry.local_file_header_offset < previous_entry_end) {
return absl::InvalidArgumentError(
"Overlapping or duplicate ZIP entries detected.");
}
previous_entry_end = entry.file_range.offset + entry.file_range.length;
}
return sorted_entries;
}
absl::StatusOr<std::unique_ptr<ZipReader>> ZipReader::Create(
riegeli::Reader* absl_nonnull input) {
ABSL_ASSIGN_OR_RETURN(CentralDirectoryLocator locator,
FindCentralDirectory(*input));
ABSL_ASSIGN_OR_RETURN(
std::vector<FileEntry> file_entries,
ExtractFileEntriesFromCentralDirectory(*input, locator));
auto reader =
absl::WrapUnique(new ZipReader(std::move(file_entries), locator.offset));
return std::move(reader);
}
absl::StatusOr<ZipReader::FileEntry> ZipReader::Next() {
if (!HasNext()) {
return absl::OutOfRangeError("No more file entries to read.");
}
return *current_it_++;
}
} // namespace credentio