blob: dbe3973f810a19bfe17e06c8d1fa6838bf7ae69c [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/pdf/reader.h"
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include "absl/base/nullability.h"
#include "absl/container/btree_map.h"
#include "absl/container/btree_set.h"
#include "absl/container/flat_hash_set.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/str_cat.h"
#include "absl/strings/substitute.h"
#include "formats/pdf/object_reader.h"
#include "formats/pdf/objects.h"
#include "re2/re2.h"
#include "riegeli/bytes/reader.h"
namespace credentio {
namespace {
// Matches the cross-reference table offset in the trailer. See ISO 32000-2
// Section 7.5.5
static constexpr LazyRE2 kXrefOffsetPattern = {R"(startxref\s(\d+)\s%%EOF)"};
// Matches a single cross-reference entry in the cross-reference table. See ISO
// 32000-2 Section 7.5.4
static constexpr LazyRE2 kXrefEntryPattern = {
R"(^(\d{10}) (\d{5}) ([nf])(?: \r| \n|\r\n))"};
// Reads the first cross-reference table offset from the file trailer.
absl::StatusOr<uint64_t> GetFirstXrefTableOffset(riegeli::Reader& input) {
// Scan the last 30 bytes for the cross-reference table offset. This allows
// for the "startxref" keyword and an offset value large enough for a
// multi-terabyte file.
if (!input.Seek(input.Size().value_or(30) - 30)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to seek to offset"));
}
std::string buffer;
if (!input.Read(30, buffer)) {
return input.StatusOrAnnotate(absl::DataLossError("Failed to read buffer"));
}
uint64_t offset;
if (!RE2::PartialMatch(buffer, *kXrefOffsetPattern, &offset)) {
return absl::InvalidArgumentError(
absl::StrCat("Failed to find cross-reference table offset in the "
"following payload: ",
buffer));
}
return offset;
}
// Reads the offset to the next cross-reference table from the file trailer.
// Returns `std::nullopt` if the file trailer dictionary does not contain a
// "Prev" entry.
absl::StatusOr<std::optional<uint64_t>> GetPrevXrefTableOffset(
const Object::Dictionary& file_trailer_dict) {
if (const auto it = file_trailer_dict.entries.find("Prev");
it != file_trailer_dict.entries.end()) {
ABSL_ASSIGN_OR_RETURN(const auto prev_xref_offset,
Object::As<Object::Integer>(it->second));
return prev_xref_offset.value;
}
return std::nullopt;
}
// Reads the standard cross-reference table and the file trailer from the input
// stream at the given offset.
absl::Status ReadXrefTableAndTrailer(
riegeli::Reader& input, uint64_t xref_offset,
absl::btree_map<Object::IndirectReference, uint64_t>& xref_table,
absl::btree_set<Object::IndirectReference>& free_entries,
Object::Dictionary& file_trailer_dict) {
if (!input.Seek(xref_offset)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to seek to offset"));
}
std::string buffer;
if (!input.Read(4, buffer)) {
return input.StatusOrAnnotate(absl::DataLossError("Failed to read buffer"));
}
if (buffer != "xref") {
return absl::InvalidArgumentError(
absl::StrCat("Malformed cross-reference table. Expected 'xref' but got "
"'",
buffer, "'"));
}
while (true) {
ABSL_RETURN_IF_ERROR(SkipOverWhiteSpaces(input));
if (!input.Read(7, buffer)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to read buffer"));
}
if (buffer == "trailer") {
break;
}
if (!input.Seek(input.pos() - 7)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to seek to offset"));
}
Object int_obj;
ABSL_RETURN_IF_ERROR(ReadObject(input, input.pos(), int_obj));
ABSL_ASSIGN_OR_RETURN(const auto obj_num,
Object::As<Object::Integer>(int_obj));
ABSL_RETURN_IF_ERROR(ReadObject(input, input.pos(), int_obj));
ABSL_ASSIGN_OR_RETURN(const auto obj_count,
Object::As<Object::Integer>(int_obj));
ABSL_RETURN_IF_ERROR(SkipOverWhiteSpaces(input));
for (uint32_t i = 0; i < obj_count.value; ++i) {
// Each cross-reference entry is expected to be exactly 20 bytes long.
if (!input.Read(20, buffer)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to read buffer"));
}
uint64_t obj_offset;
uint32_t gen_num;
std::string type;
if (RE2::PartialMatch(buffer, *kXrefEntryPattern, &obj_offset, &gen_num,
&type)) {
auto xref_entry_key = Object::IndirectReference{
.object_number = static_cast<uint32_t>(obj_num.value + i),
.generation_number = gen_num};
if (type == "n") { // 'n' represents an in-use entry.
xref_table.insert({xref_entry_key, obj_offset});
} else { // 'f' represents a free entry.
free_entries.insert(xref_entry_key);
}
} else {
return absl::InvalidArgumentError(
absl::StrCat("Failed to parse cross-reference table entry. Invalid "
"value: ",
buffer));
}
}
}
Object dict_obj;
ABSL_RETURN_IF_ERROR(ReadObject(input, input.pos(), dict_obj));
ABSL_ASSIGN_OR_RETURN(file_trailer_dict,
Object::As<Object::Dictionary>(dict_obj));
return absl::OkStatus();
}
} // namespace
absl::StatusOr<std::unique_ptr<PdfReader>> PdfReader::Create(
riegeli::Reader* absl_nonnull input) {
if (input == nullptr) {
return absl::InvalidArgumentError("input cannot be null");
}
absl::flat_hash_set<uint64_t> xref_offsets_visited;
ABSL_ASSIGN_OR_RETURN(uint64_t xref_offset, GetFirstXrefTableOffset(*input));
xref_offsets_visited.insert(xref_offset);
absl::btree_map<Object::IndirectReference, uint64_t> xref_table;
absl::btree_set<Object::IndirectReference> free_entries;
Object::Dictionary file_trailer_dict;
ABSL_RETURN_IF_ERROR(ReadXrefTableAndTrailer(
*input, xref_offset, xref_table, free_entries, file_trailer_dict));
ABSL_ASSIGN_OR_RETURN(std::optional<uint64_t> prev_xref_offset,
GetPrevXrefTableOffset(file_trailer_dict));
while (prev_xref_offset.has_value()) {
if (xref_offsets_visited.contains(*prev_xref_offset)) {
return absl::InvalidArgumentError(
"Initialization failed: Circular cross-reference table detected.");
}
xref_offsets_visited.insert(*prev_xref_offset);
absl::btree_map<Object::IndirectReference, uint64_t> prev_xref_table;
absl::btree_set<Object::IndirectReference> prev_free_entries;
Object::Dictionary prev_file_trailer_dict;
ABSL_RETURN_IF_ERROR(
ReadXrefTableAndTrailer(*input, *prev_xref_offset, prev_xref_table,
prev_free_entries, prev_file_trailer_dict));
ABSL_ASSIGN_OR_RETURN(prev_xref_offset,
GetPrevXrefTableOffset(prev_file_trailer_dict));
xref_table.merge(prev_xref_table);
if (!prev_xref_table.empty()) {
return absl::InvalidArgumentError(
"Initialization failed: Collision detected in cross-reference "
"table for in-use entries.");
}
free_entries.merge(prev_free_entries);
if (!prev_free_entries.empty()) {
return absl::InvalidArgumentError(
"Initialization failed: Collision detected in cross-reference "
"table for free entries.");
}
}
auto reader = absl::WrapUnique(new PdfReader(input, std::move(xref_table),
std::move(free_entries),
std::move(file_trailer_dict)));
return reader;
}
bool PdfReader::HasNext() { return current_it_ != xref_table_.end(); }
absl::StatusOr<IndirectObject> PdfReader::Next() {
if (!HasNext()) {
return absl::OutOfRangeError("No more objects to read.");
}
IndirectObject obj;
if (absl::Status status = ReadIndirectObject(
input_, current_it_->second,
[this](const Object::IndirectReference& ref) {
return this->GetObject(ref.object_number, ref.generation_number);
},
obj);
!status.ok()) {
return status;
}
++current_it_;
return obj;
}
absl::StatusOr<uint64_t> PdfReader::GetObjectOffset(
uint32_t object_number, uint32_t generation_number) {
auto it = xref_table_.find(Object::IndirectReference{
.object_number = object_number, .generation_number = generation_number});
if (it == xref_table_.end()) {
return absl::NotFoundError(absl::Substitute(
"No offset found for object number: $0, generation number: $1",
object_number, generation_number));
}
return it->second;
}
absl::StatusOr<IndirectObject> PdfReader::GetObject(
uint32_t object_number, uint32_t generation_number) {
Object::IndirectReference ref{.object_number = object_number,
.generation_number = generation_number};
if (active_objects_.contains(ref)) {
return absl::InvalidArgumentError(
absl::StrCat("Circular reference detected for object: ", object_number,
" ", generation_number));
}
struct ActiveObjectGuard {
absl::btree_set<Object::IndirectReference>& active_objects;
Object::IndirectReference ref;
~ActiveObjectGuard() { active_objects.erase(ref); }
};
active_objects_.insert(ref);
ActiveObjectGuard guard{active_objects_, ref};
ABSL_ASSIGN_OR_RETURN(uint64_t offset,
GetObjectOffset(object_number, generation_number));
IndirectObject obj;
if (absl::Status status = ReadIndirectObject(
input_, offset,
[this](const Object::IndirectReference& ref) {
return this->GetObject(ref.object_number, ref.generation_number);
},
obj);
!status.ok()) {
return status;
}
return obj;
}
} // namespace credentio