| // 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/png/reader.h" | |
| #include <sys/types.h> | |
| #include <cstdint> | |
| #include <string> | |
| #include <utility> | |
| #include "absl/status/status.h" | |
| #include "absl/status/status_macros.h" | |
| #include "absl/status/statusor.h" | |
| #include "absl/strings/ascii.h" | |
| #include "absl/strings/str_cat.h" | |
| #include "formats/png/constants.h" | |
| #include "riegeli/bytes/reader.h" | |
| #include "riegeli/endian/endian_reading.h" | |
| namespace credentio { | |
| namespace { | |
| absl::StatusOr<PngChunk> ReadHeader(riegeli::Reader& reader) { | |
| uint64_t start_offset = reader.pos(); | |
| std::string header; | |
| if (!reader.Read(kPngHeader.size(), header)) { | |
| return reader.StatusOrAnnotate( | |
| absl::InvalidArgumentError("Failed to read header")); | |
| } | |
| if (header != kPngHeader) { | |
| return absl::InvalidArgumentError("input does not start with PNG marker"); | |
| } | |
| return PngChunk{ | |
| .offset = start_offset, | |
| .length = kPngHeader.size(), | |
| .type = kPngChunkTypeHeader.data(), | |
| .data_length = static_cast<uint32_t>(kPngHeader.size()), | |
| }; | |
| } | |
| bool ChunkGoesPastEndOffset(uint64_t chunk_start, uint64_t chunk_length, | |
| uint64_t end_offset) { | |
| if (end_offset < chunk_length) { | |
| // Chunk is larger than the file size | |
| return true; | |
| } | |
| if (chunk_start > (end_offset - chunk_length)) { | |
| // Chunk starts too far into the file to read the entire chunk. | |
| return true; | |
| } | |
| return false; | |
| } | |
| absl::StatusOr<PngChunk> ReadChunk(riegeli::Reader& reader, | |
| int64_t end_offset) { | |
| PngChunk chunk{.offset = reader.pos()}; | |
| if (!riegeli::ReadBigEndian<uint32_t>(reader, chunk.data_length)) { | |
| return reader.StatusOrAnnotate( | |
| absl::DataLossError("Failed to read chunk data length")); | |
| } | |
| chunk.length = sizeof(uint32_t) // Chunk length | |
| + sizeof(uint32_t) // Chunk type | |
| + static_cast<uint64_t>(chunk.data_length) // Chunk data | |
| + sizeof(uint32_t); // CRC | |
| if (ChunkGoesPastEndOffset(chunk.offset, chunk.length, end_offset)) { | |
| return absl::InvalidArgumentError( | |
| "PNG chunk extends beyond the end of the file"); | |
| } | |
| if (!reader.Read(4, chunk.type)) { | |
| return reader.StatusOrAnnotate( | |
| absl::DataLossError("Failed to read chunk type")); | |
| } | |
| for (char c : chunk.type) { | |
| if (!absl::ascii_isalpha(c)) { | |
| return absl::InvalidArgumentError( | |
| absl::StrCat("Invalid PNG chunk type name: ", chunk.type)); | |
| } | |
| } | |
| return std::move(chunk); | |
| } | |
| } // namespace | |
| absl::Status IterateOverPngChunks(riegeli::Reader& reader, | |
| PngChunkProcessor processor) { | |
| if (!reader.SupportsSize() || !reader.Size().has_value()) { | |
| return absl::InvalidArgumentError( | |
| "PNG reader does not support size, cannot iterate"); | |
| } | |
| uint64_t end_offset = *reader.Size(); | |
| if (kPngMinimumAssetSize > end_offset - reader.pos()) { | |
| // Asset size is smaller than the minimum size needed, just return. | |
| return absl::OkStatus(); | |
| } | |
| ABSL_ASSIGN_OR_RETURN(PngChunk header, ReadHeader(reader)); | |
| ABSL_RETURN_IF_ERROR(processor(header)); | |
| uint64_t last_chunk_offset = header.offset; | |
| while (reader.pos() < end_offset) { | |
| ABSL_ASSIGN_OR_RETURN(PngChunk chunk, ReadChunk(reader, end_offset)); | |
| if (chunk.offset <= last_chunk_offset) { | |
| return reader.StatusOrAnnotate( | |
| absl::InvalidArgumentError("PNG chunk offset did not advance")); | |
| } | |
| last_chunk_offset = chunk.offset; | |
| ABSL_RETURN_IF_ERROR(processor(chunk)); | |
| if (!reader.Seek(chunk.offset + chunk.length)) { | |
| return reader.StatusOrAnnotate( | |
| absl::InvalidArgumentError("Failed to seek to next chunk")); | |
| } | |
| if (chunk.type == kPngChunkTypeEnd) { | |
| break; | |
| } | |
| } | |
| if (reader.pos() < end_offset) { | |
| PngChunk chunk{ | |
| .offset = reader.pos(), | |
| .length = end_offset - reader.pos(), | |
| .type = "c2pa.after", | |
| .data_length = 0, | |
| }; | |
| ABSL_RETURN_IF_ERROR(processor(chunk)); | |
| } | |
| return absl::OkStatus(); | |
| } | |
| } // namespace credentio |