blob: a92ec79456f98869037e2a5f7bb3df7a9bfc3416 [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/object_reader.h"
#include <cstdint>
#include <string>
#include <utility>
#include <variant>
#include "absl/functional/any_invocable.h"
#include "absl/status/status.h"
#include "absl/status/status_macros.h"
#include "absl/status/statusor.h"
#include "absl/strings/ascii.h"
#include "absl/strings/match.h"
#include "absl/strings/numbers.h"
#include "absl/strings/str_cat.h"
#include "formats/pdf/objects.h"
#include "re2/re2.h"
#include "riegeli/bytes/reader.h"
namespace credentio {
namespace {
constexpr uint64_t kMaxPayloadSize = 1024 * 1024 * 10; // 10 MiB
constexpr uint64_t kMaxStringTokenLength = 1024 * 1024 * 10; // 10 MiB
constexpr uint64_t kMaxNameTokenLength = 4096;
constexpr uint64_t kMaxNumberTokenLength = 128;
constexpr int kMaxObjectRecursionDepth = 100;
// PDF White-space characters. See ISO 32000-2 Section 7.2.3 Table 1.
constexpr bool IsPdfWhiteSpace(uint8_t c) {
return c == 0x00 || c == 0x09 || c == 0x0a || c == 0x0c || c == 0x0d ||
c == 0x20;
}
// PDF Delimiter characters. See ISO 32000-2 Section 7.2.3 Table 2.
constexpr bool IsPdfDelimiter(uint8_t c) {
return c == 0x28 || c == 0x29 || c == 0x3c || c == 0x3e || c == 0x5b ||
c == 0x5d || c == 0x7b || c == 0x7d || c == 0x2f || c == 0x25;
}
static constexpr LazyRE2 kIndirectReferencePattern = {
R"(^[+-]?(\d+)\s+(\d+)\s+R[^a-zA-Z])"};
// Reads either an integer or a real number from the given input stream at the
// current position.
absl::Status ReadNumber(riegeli::Reader& input, Object& obj) {
uint8_t token = 0x00;
std::string buffer;
while (true) {
if (buffer.size() >= kMaxNumberTokenLength) {
return absl::InvalidArgumentError(
absl::StrCat("Number token exceeds maximum allowed length: ",
kMaxNumberTokenLength));
}
if (!input.ReadByte(token)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to read beginning delimiter"));
}
// A number can be terminated by any white-space character or delimiter.
if (IsPdfWhiteSpace(token) || IsPdfDelimiter(token)) {
if (!input.Seek(input.pos() - 1)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to seek to offset"));
}
break;
}
buffer += token;
}
if (absl::StrContains(buffer, '.')) {
obj.value = Object::RealNumber{.raw_value = std::move(buffer)};
} else {
int64_t value;
if (!absl::SimpleAtoi(buffer, &value)) {
return absl::InvalidArgumentError(
absl::StrCat("Failed to parse integer. Invalid value: ", buffer));
}
obj.value = Object::Integer{.value = std::move(value)};
}
return absl::OkStatus();
}
absl::Status ReadBoolean(riegeli::Reader& input, Object& obj) {
std::string buffer;
if (!input.Read(5, buffer)) {
return input.StatusOrAnnotate(absl::DataLossError("Failed to read buffer"));
}
if (buffer.substr(0, 4) == "true") {
obj.value = Object::Boolean{.value = true};
if (!input.Seek(input.pos() - 1)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to seek to offset"));
}
} else if (buffer == "false") {
obj.value = Object::Boolean{.value = false};
} else {
return absl::InvalidArgumentError(
absl::StrCat("Failed to parse boolean. Invalid value: ", buffer));
}
return absl::OkStatus();
}
absl::Status ReadLiteralString(riegeli::Reader& input, Object& obj) {
uint8_t token = 0x00;
std::string buffer;
int32_t parentheses_count = 1;
// Consume the beginning delimiter.
if (!input.ReadByte(token)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to read beginning delimiter"));
}
if (token != '(') {
return absl::InvalidArgumentError(
absl::StrCat("Invalid delimiter. Expected '(' but got 0x",
absl::Hex(token), " at offset ", input.pos() - 1));
}
while (true) {
if (buffer.size() >= kMaxStringTokenLength) {
return absl::InvalidArgumentError(
absl::StrCat("Literal string exceeds maximum allowed length: ",
kMaxStringTokenLength));
}
if (!input.ReadByte(token)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to read beginning delimiter"));
}
if (token == '(') {
parentheses_count++;
}
if (token == ')' && --parentheses_count == 0) {
break;
}
buffer += token;
}
obj.value = Object::LiteralString{.raw_value = std::move(buffer)};
return absl::OkStatus();
}
absl::Status ReadHexadecimalString(riegeli::Reader& input, Object& obj) {
uint8_t token = 0x00;
std::string buffer;
// Consume the beginning delimiter.
if (!input.ReadByte(token)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to read beginning delimiter"));
}
if (token != '<') {
return absl::InvalidArgumentError(
absl::StrCat("Invalid delimiter. Expected '<' but got 0x",
absl::Hex(token), " at offset ", input.pos() - 1));
}
while (true) {
if (buffer.size() >= kMaxStringTokenLength) {
return absl::InvalidArgumentError(
absl::StrCat("Hexadecimal string exceeds maximum allowed length: ",
kMaxStringTokenLength));
}
if (!input.ReadByte(token)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to read beginning delimiter"));
}
if (token == '>') {
break;
}
buffer += token;
}
obj.value = Object::HexadecimalString{.raw_value = std::move(buffer)};
return absl::OkStatus();
}
absl::Status ReadName(riegeli::Reader& input, Object& obj) {
uint8_t token = 0x00;
std::string buffer;
// Consume the beginning delimiter.
if (!input.ReadByte(token)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to read beginning delimiter"));
}
if (token != '/') {
return absl::InvalidArgumentError(
absl::StrCat("Invalid delimiter. Expected '/' but got 0x",
absl::Hex(token), " at offset ", input.pos() - 1));
}
while (true) {
if (buffer.size() >= kMaxNameTokenLength) {
return absl::InvalidArgumentError(absl::StrCat(
"Name token exceeds maximum allowed length: ", kMaxNameTokenLength));
}
if (!input.ReadByte(token)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to read beginning delimiter"));
}
// A name can be terminated by any white-space character or delimiter.
if (IsPdfWhiteSpace(token) || IsPdfDelimiter(token)) {
if (!input.Seek(input.pos() - 1)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to seek to offset"));
}
break;
}
if (token == '#') {
std::string hex_str(2, '\0');
if (!input.Read(2, hex_str)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to read hex string"));
}
if (!absl::ascii_isxdigit(hex_str[0]) ||
!absl::ascii_isxdigit(hex_str[1])) {
return absl::InvalidArgumentError("Invalid hex string");
}
int hex_val = 0;
if (!absl::SimpleHexAtoi(hex_str, &hex_val)) {
return absl::InvalidArgumentError("Invalid hex string");
}
token = hex_val;
}
buffer += token;
}
obj.value = Object::Name{.value = std::move(buffer)};
return absl::OkStatus();
}
absl::Status ReadArray(riegeli::Reader& input, Object& obj, int depth) {
uint8_t token = 0x00;
Object::Array array;
// Consume the beginning delimiter.
if (!input.ReadByte(token)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to read beginning delimiter"));
}
if (token != '[') {
return absl::InvalidArgumentError(
absl::StrCat("Invalid delimiter. Expected '[' but got '",
absl::Hex(token), "' at offset ", input.pos() - 1));
}
while (true) {
ABSL_RETURN_IF_ERROR(SkipOverWhiteSpaces(input));
if (!input.ReadByte(token)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to read beginning delimiter"));
}
if (token == ']') {
break;
}
Object sub_obj;
ABSL_RETURN_IF_ERROR(ReadObject(input, input.pos() - 1, sub_obj, depth));
array.objects.push_back(std::move(sub_obj));
}
obj.value = std::move(array);
return absl::OkStatus();
}
absl::Status ReadNull(riegeli::Reader& input, Object& obj) {
std::string buffer;
if (!input.Read(4, buffer)) {
return input.StatusOrAnnotate(absl::DataLossError("Failed to read buffer"));
}
if (buffer == "null") {
obj.value = Object::Null{};
} else {
return absl::InvalidArgumentError(absl::StrCat("Invalid value: ", buffer));
}
return absl::OkStatus();
}
absl::Status ReadDictionary(riegeli::Reader& input, Object& obj, int depth) {
std::string buffer;
Object::Dictionary dictionary;
if (!input.Read(2, buffer)) {
return input.StatusOrAnnotate(absl::DataLossError("Failed to read buffer"));
}
if (buffer != "<<") {
return absl::InvalidArgumentError(absl::StrCat(
"Invalid delimiter. Expected '<<' but got '", buffer, "'"));
}
while (true) {
ABSL_RETURN_IF_ERROR(SkipOverWhiteSpaces(input));
if (!input.Read(2, buffer)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to read buffer"));
}
if (buffer == ">>") {
break;
}
if (!input.Seek(input.pos() - 2)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to seek to offset"));
}
Object name_obj;
ABSL_RETURN_IF_ERROR(ReadName(input, name_obj));
ABSL_RETURN_IF_ERROR(SkipOverWhiteSpaces(input));
Object value_obj;
ABSL_RETURN_IF_ERROR(ReadObject(input, input.pos(), value_obj, depth));
ABSL_ASSIGN_OR_RETURN(auto name, Object::As<Object::Name>(name_obj));
dictionary.entries.insert({name.value, std::move(value_obj)});
}
obj.value = std::move(dictionary);
return absl::OkStatus();
}
absl::Status ReadStream(riegeli::Reader& input, int64_t length, Object& obj) {
std::string buffer;
Object::Stream stream;
if (!input.Read(6, buffer)) {
return input.StatusOrAnnotate(absl::DataLossError("Failed to read buffer"));
}
if (buffer != "stream") {
return absl::InvalidArgumentError(absl::StrCat(
"Malformed keyword. Expected 'stream' but got '", buffer, "'"));
}
if (!input.Read(2, buffer)) {
return input.StatusOrAnnotate(absl::DataLossError("Failed to read buffer"));
}
// The spec requires "stream" keyword to be followed by a CARRIAGE RETURN and
// a LINE FEED, or just a LINE FEED.
if (buffer == "\r\n") { // CARRIAGE RETURN + LINE FEED
// Do nothing.
} else if (buffer[0] == '\n') { // LINE FEED
// Seek back to the end of the LINE FEED.
if (!input.Seek(input.pos() - 1)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to seek to offset"));
}
} else {
return absl::InvalidArgumentError(absl::StrCat(
"Malformed stream. Expected '\r\n' or '\n' but got '", buffer, "'"));
}
if (length > kMaxPayloadSize) {
return absl::InvalidArgumentError(
absl::StrCat("Stream length exceeds maximum allowed size: ", length,
" > ", kMaxPayloadSize));
}
if (!input.Read(length, stream.raw_value)) {
return input.StatusOrAnnotate(absl::DataLossError("Failed to read buffer"));
}
obj.value = std::move(stream);
// Skip over the end-of-line marker.
ABSL_RETURN_IF_ERROR(SkipOverWhiteSpaces(input));
if (!input.Read(9, buffer)) {
return input.StatusOrAnnotate(absl::DataLossError("Failed to read buffer"));
}
if (buffer != "endstream") {
return absl::InvalidArgumentError(absl::StrCat(
"Malformed keyword. Expected 'endstream' but got '", buffer, "'"));
}
return absl::OkStatus();
}
} // namespace
absl::Status ReadObject(riegeli::Reader& input, int64_t obj_offset, Object& obj,
int depth) {
if (depth > kMaxObjectRecursionDepth) {
return absl::InvalidArgumentError("Max nesting depth exceeded.");
}
if (!input.Seek(obj_offset)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to seek to offset"));
}
uint8_t token = 0x00;
ABSL_RETURN_IF_ERROR(SkipOverWhiteSpaces(input));
if (!input.ReadByte(token)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to read beginning delimiter"));
}
if (!input.Seek(input.pos() - 1)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to seek to offset"));
}
switch (token) {
case '+':
case '-':
case '.':
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9': { // Numbers or indirect reference
std::string buffer(20, '\0');
// Read more bytes to check if this is an indirect reference.
if (!input.Read(20, buffer)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to read buffer"));
}
if (!input.Seek(input.pos() - 20)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to seek to offset"));
}
if (uint32_t obj_num, gen_num; RE2::PartialMatch(
buffer, *kIndirectReferencePattern, &obj_num, &gen_num)) {
obj.value = Object::IndirectReference{.object_number = obj_num,
.generation_number = gen_num};
auto pos = buffer.find('R');
// This should never happen.
if (pos == std::string::npos) {
return absl::InternalError(
"Failed to locate keyword 'R' in indirect reference buffer.");
}
if (!input.Seek(input.pos() + pos + 1)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to seek to offset"));
}
break;
}
ABSL_RETURN_IF_ERROR(ReadNumber(input, obj));
break;
}
case 't':
case 'f': { // Boolean
ABSL_RETURN_IF_ERROR(ReadBoolean(input, obj));
break;
}
case '(': { // Literal string
ABSL_RETURN_IF_ERROR(ReadLiteralString(input, obj));
break;
}
case '<': { // Hexadecimal string or Dictionary
// Read one more byte to determine the object type.
std::string buffer(2, '\0');
if (!input.Read(2, buffer)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to read buffer"));
}
if (!input.Seek(input.pos() - 2)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to seek to offset"));
}
if (buffer == "<<") { // Dictionary
ABSL_RETURN_IF_ERROR(ReadDictionary(input, obj, depth + 1));
} else {
// Hexadecimal string
ABSL_RETURN_IF_ERROR(ReadHexadecimalString(input, obj));
}
break;
}
case '/': { // Name
ABSL_RETURN_IF_ERROR(ReadName(input, obj));
break;
}
case '[': { // Array
ABSL_RETURN_IF_ERROR(ReadArray(input, obj, depth + 1));
break;
}
case 'n': { // Null
ABSL_RETURN_IF_ERROR(ReadNull(input, obj));
break;
}
case 's': { // Stream
std::string buffer(6, '\0');
if (!input.Read(6, buffer)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to read buffer"));
}
if (!input.Seek(input.pos() - 6)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to seek to offset"));
}
if (buffer == "stream") {
return absl::InvalidArgumentError(
"Stream must reside in an indirect object. Call "
"ReadIndirectObject() instead.");
} else {
return absl::InvalidArgumentError(absl::StrCat(
"Malformed keyword. Expected 'stream' but got '", buffer, "'"));
}
break;
}
default:
return absl::InvalidArgumentError(
absl::StrCat("Unsupported delimiter: 0x", absl::Hex(token),
" at offset ", input.pos()));
}
return absl::OkStatus();
}
absl::Status ReadIndirectObject(
riegeli::Reader& input, int64_t obj_offset,
absl::AnyInvocable<
absl::StatusOr<IndirectObject>(const Object::IndirectReference&)>
indirect_reference_resolver,
IndirectObject& obj) {
if (!input.Seek(obj_offset)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to seek to offset"));
}
uint8_t token = 0x00;
std::string buffer;
// Read the object number.
while (true) {
if (buffer.size() >= kMaxNumberTokenLength) {
return absl::InvalidArgumentError(
absl::StrCat("Object number token exceeds maximum allowed length: ",
kMaxNumberTokenLength));
}
if (!input.ReadByte(token)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to read beginning delimiter"));
}
if (IsPdfWhiteSpace(token)) {
break;
}
absl::StrAppend(&buffer, std::string(1, token));
}
bool success = absl::SimpleAtoi(buffer, &obj.object_number);
if (!success) {
return absl::InvalidArgumentError(
absl::StrCat("Failed to parse object number. Invalid value: ", buffer));
}
// Read the generation number.
buffer.clear();
while (true) {
if (buffer.size() >= kMaxNumberTokenLength) {
return absl::InvalidArgumentError(absl::StrCat(
"Generation number token exceeds maximum allowed length: ",
kMaxNumberTokenLength));
}
if (!input.ReadByte(token)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to read beginning delimiter"));
}
if (IsPdfWhiteSpace(token)) {
break;
}
absl::StrAppend(&buffer, std::string(1, token));
}
success = absl::SimpleAtoi(buffer, &obj.generation_number);
if (!success) {
return absl::InvalidArgumentError(absl::StrCat(
"Failed to parse generation number. Invalid value: ", buffer));
}
// Read the enclosed object.
ABSL_RETURN_IF_ERROR(SkipOverWhiteSpaces(input));
if (!input.Read(3, buffer)) {
return input.StatusOrAnnotate(absl::DataLossError("Failed to read buffer"));
}
if (buffer != "obj") {
return absl::InvalidArgumentError(absl::StrCat(
"Malformed keyword. Expected 'obj' but got '", buffer, "'"));
}
Object temp_obj;
ABSL_RETURN_IF_ERROR(ReadObject(input, input.pos(), temp_obj));
ABSL_RETURN_IF_ERROR(SkipOverWhiteSpaces(input));
if (!input.Read(6, buffer)) {
return input.StatusOrAnnotate(absl::DataLossError("Failed to read buffer"));
}
if (!input.Seek(input.pos() - 6)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to seek to offset"));
}
if (buffer == "stream") {
// Maker indicates that this is a stream object, hence the object preceding
// it must be the stream dictionary.
ABSL_ASSIGN_OR_RETURN(obj.stream_dictionary,
Object::As<Object::Dictionary>(temp_obj));
ABSL_RETURN_IF_ERROR(SkipOverWhiteSpaces(input));
// Determine the length of the stream.
int64_t length = -1;
if (const auto it = obj.stream_dictionary->entries.find("Length");
it != obj.stream_dictionary->entries.end()) {
if (std::holds_alternative<Object::IndirectReference>(it->second.value)) {
ABSL_ASSIGN_OR_RETURN(
auto indirect_ref,
Object::As<Object::IndirectReference>(it->second));
// Save the current input stream position, as the
// `indirect_reference_resolver` might change it.
auto pos = input.pos();
ABSL_ASSIGN_OR_RETURN(auto indirect_length_obj,
indirect_reference_resolver(indirect_ref));
ABSL_ASSIGN_OR_RETURN(auto length_obj, Object::As<Object::Integer>(
indirect_length_obj.object));
length = length_obj.value;
// Seek back to the saved position to continue reading the stream.
if (!input.Seek(pos)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to seek to offset"));
}
} else {
ABSL_ASSIGN_OR_RETURN(auto length_obj,
Object::As<Object::Integer>(it->second));
length = length_obj.value;
}
} else {
return absl::DataLossError(
"Stream dictionary does not contain a 'Length' entry.");
}
ABSL_RETURN_IF_ERROR(ReadStream(input, length, obj.object));
// Skip over the end-of-object marker.
ABSL_RETURN_IF_ERROR(SkipOverWhiteSpaces(input));
if (!input.Read(6, buffer)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to read buffer"));
}
} else {
obj.object = std::move(temp_obj);
}
if (buffer != "endobj") {
return absl::InvalidArgumentError(absl::StrCat(
"Malformed keyword. Expected 'endobj' but got '", buffer, "'"));
}
return absl::OkStatus();
}
absl::Status SkipOverWhiteSpaces(riegeli::Reader& input) {
uint8_t token = 0x00;
while (true) {
if (!input.ReadByte(token)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to read beginning delimiter"));
}
if (token == '%') {
// It's a comment, skip until EOL or EOF.
while (true) {
if (!input.ReadByte(token)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to read beginning delimiter"));
}
if (token == 0x0a || token == 0x0d) { // LF or CR
break;
}
}
continue;
}
if (!IsPdfWhiteSpace(token)) {
// Seek back to the last non-whitespace character.
if (!input.Seek(input.pos() - 1)) {
return input.StatusOrAnnotate(
absl::DataLossError("Failed to seek to offset"));
}
break;
}
}
return absl::OkStatus();
}
} // namespace credentio