| // 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 |