blob: cd9cb71123ec5d76c902c704cedcae66bbf8e2cb [file]
// 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 <string>
#include <utility>
#include <vector>
#include "absl/container/flat_hash_map.h"
#include "absl/status/status.h"
#include "absl/status/status_matchers.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_replace.h"
#include "absl/strings/string_view.h"
#include "formats/pdf/objects.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "riegeli/bytes/reader.h"
#include "riegeli/bytes/string_reader.h"
namespace credentio {
namespace {
using ::absl_testing::IsOkAndHolds;
using ::absl_testing::StatusIs;
using ::testing::ElementsAre;
using ::testing::HasSubstr;
using ::testing::Pair;
using ::testing::UnorderedElementsAre;
Object CreateName(absl::string_view value) {
return Object{.value = Object::Name{.value = std::string(value)}};
}
Object CreateInteger(int64_t value) {
return Object{.value = Object::Integer{.value = value}};
}
Object CreateRealNumber(absl::string_view value) {
return Object{.value = Object::RealNumber{.raw_value = std::string(value)}};
}
Object CreateIndirectRef(uint32_t object_number, uint32_t generation_number) {
return Object{.value = Object::IndirectReference{
.object_number = object_number,
.generation_number = generation_number}};
}
Object CreateArray(std::vector<Object> values) {
return Object{.value = Object::Array{.objects = std::move(values)}};
}
Object CreateStream(absl::string_view raw_value) {
return Object{.value = Object::Stream{.raw_value = std::string(raw_value)}};
}
Object CreateDictionary(absl::flat_hash_map<std::string, Object> entries) {
return Object{.value = Object::Dictionary{.entries = std::move(entries)}};
}
std::string GetTestPdfContent() {
std::string content = R"(%PDF-1.7
%%%%%
1 0 obj
/test_name
endobj
2 0 obj
123.456
endobj
3 0 obj
<</Type /Catalog>>
endobj
xref
0 4
0000000000 65535 f@
0000000015 00000 n@
0000000041 00000 n@
0000000064 00000 n@
trailer
<</Root 3 0 R /Size 3>>
startxref
98
%%EOF)";
return absl::StrReplaceAll(content, {{"@", " "}});
}
TEST(PdfReaderTest, ReadsAllObjects) {
std::string content = GetTestPdfContent();
riegeli::StringReader<> input(content);
absl::StatusOr<std::unique_ptr<PdfReader>> reader = PdfReader::Create(&input);
ABSL_ASSERT_OK(reader);
std::vector<IndirectObject> objects;
while ((*reader)->HasNext()) {
absl::StatusOr<IndirectObject> obj = (*reader)->Next();
ABSL_ASSERT_OK(obj);
objects.push_back(*std::move(obj));
}
EXPECT_THAT(
objects,
ElementsAre(IndirectObject{.object_number = 1,
.generation_number = 0,
.object = CreateName("test_name")},
IndirectObject{.object_number = 2,
.generation_number = 0,
.object = CreateRealNumber("123.456")},
IndirectObject{.object_number = 3,
.generation_number = 0,
.object = CreateDictionary(
{{"Type", CreateName("Catalog")}})}));
}
TEST(PdfReaderTest, FailsWhenReadingPastAllObjects) {
std::string content = GetTestPdfContent();
riegeli::StringReader<> input(content);
absl::StatusOr<std::unique_ptr<PdfReader>> reader = PdfReader::Create(&input);
ABSL_ASSERT_OK(reader);
int64_t count = 0;
while ((*reader)->HasNext()) {
ABSL_ASSERT_OK((*reader)->Next());
++count;
}
EXPECT_EQ(count, 3);
EXPECT_THAT((*reader)->Next(),
StatusIs(absl::StatusCode::kOutOfRange,
HasSubstr("No more objects to read")));
}
TEST(PdfReaderTest, GetFileTrailerDictionary) {
std::string content = GetTestPdfContent();
riegeli::StringReader<> input(content);
absl::StatusOr<std::unique_ptr<PdfReader>> reader = PdfReader::Create(&input);
ABSL_ASSERT_OK(reader);
auto file_trailer_dict = (*reader)->file_trailer_dictionary();
EXPECT_THAT(file_trailer_dict.entries,
UnorderedElementsAre(Pair("Root", CreateIndirectRef(3, 0)),
Pair("Size", CreateInteger(3))));
}
TEST(PdfReaderTest, FreeEntries) {
std::string content = R"(%PDF-1.7
xref
0 8
0000000003 65535 f@
0000000015 00000 n@
0000000081 00000 n@
0000000005 00000 f@
0000000331 00000 n@
0000000000 00000 f@
0000000409 00000 n@
0000000555 00000 n@
trailer
<<>>
startxref
9
%%EOF)";
content = absl::StrReplaceAll(content, {{"@", " "}});
riegeli::StringReader<> input(content);
absl::StatusOr<std::unique_ptr<PdfReader>> reader = PdfReader::Create(&input);
ABSL_ASSERT_OK(reader);
EXPECT_THAT((*reader)->free_entries(),
ElementsAre(Object::IndirectReference{.object_number = 0,
.generation_number = 65535},
Object::IndirectReference{.object_number = 3,
.generation_number = 0},
Object::IndirectReference{.object_number = 5,
.generation_number = 0}));
}
TEST(PdfReaderTest, GetObjectOffset) {
std::string content = GetTestPdfContent();
riegeli::StringReader<> input(content);
absl::StatusOr<std::unique_ptr<PdfReader>> reader = PdfReader::Create(&input);
ABSL_ASSERT_OK(reader);
EXPECT_THAT(
(*reader)->GetObjectOffset(/*object_number=*/2, /*generation_number=*/0),
IsOkAndHolds(41));
}
TEST(PdfReaderTest, GetNonExistentObjectOffsetFailure) {
std::string content = GetTestPdfContent();
riegeli::StringReader<> input(content);
absl::StatusOr<std::unique_ptr<PdfReader>> reader = PdfReader::Create(&input);
ABSL_ASSERT_OK(reader);
EXPECT_THAT(
(*reader)->GetObjectOffset(/*object_number=*/10, /*generation_number=*/0),
StatusIs(absl::StatusCode::kNotFound, HasSubstr("No offset found")));
}
TEST(PdfReaderTest, GetSpecificObject) {
std::string content = GetTestPdfContent();
riegeli::StringReader<> input(content);
absl::StatusOr<std::unique_ptr<PdfReader>> reader = PdfReader::Create(&input);
ABSL_ASSERT_OK(reader);
EXPECT_THAT(
(*reader)->GetObject(2, 0),
IsOkAndHolds(IndirectObject{.object_number = 2,
.generation_number = 0,
.object = CreateRealNumber("123.456")}));
}
TEST(PdfReaderTest, GetNonExistentObjectFailure) {
std::string content = GetTestPdfContent();
riegeli::StringReader<> input(content);
absl::StatusOr<std::unique_ptr<PdfReader>> reader = PdfReader::Create(&input);
ABSL_ASSERT_OK(reader);
EXPECT_THAT(
(*reader)->GetObject(10, 0),
StatusIs(absl::StatusCode::kNotFound, HasSubstr("No offset found")));
}
TEST(PdfReaderTest, ShortFileCausesInitializationFailure) {
std::string content = "file is too short";
riegeli::StringReader<> input(content);
EXPECT_THAT(PdfReader::Create(&input),
StatusIs(absl::StatusCode::kDataLoss,
HasSubstr("Failed to seek to offset")));
}
TEST(PdfReaderTest, MissingXrefOffsetCausesInitializationFailure) {
std::string content = R"(%PDF-1.7
xref
0 1
0000000000 65536 f
startxref
%%EOF)";
riegeli::StringReader<> input(content);
EXPECT_THAT(PdfReader::Create(&input),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("Failed to find cross-reference table offset "
"in the following payload")));
}
TEST(PdfReaderTest, InvalidXrefOffsetCausesInitializationFailure) {
std::string content = R"(%PDF-1.7
xref
0 1
0000000000 65536 f
startxref
2
%%EOF)";
riegeli::StringReader<> input(content);
EXPECT_THAT(
PdfReader::Create(&input),
StatusIs(
absl::StatusCode::kInvalidArgument,
HasSubstr(
"Malformed cross-reference table. Expected 'xref' but got")));
}
TEST(PdfReaderTest, MalformedXrefTableCausesInitializationFailure) {
std::string content = R"(%PDF-1.7
xref
0 2
0000000000 65536 f
0000000 00001 n
startxref
9
%%EOF)";
riegeli::StringReader<> input(content);
EXPECT_THAT(
PdfReader::Create(&input),
StatusIs(
absl::StatusCode::kInvalidArgument,
HasSubstr(
"Failed to parse cross-reference table entry. Invalid value")));
}
TEST(PdfReaderTest, InvalidPrevXrefOffsetTypeCausesInitializationFailure) {
std::string content = R"(%PDF-1.7
xref
0 1
0000000000 65535 f@
trailer
<</Prev /NotAnInteger /Size 1>>
startxref
9
%%EOF)";
content = absl::StrReplaceAll(content, {{"@", " "}});
riegeli::StringReader<> input(content);
EXPECT_THAT(
PdfReader::Create(&input),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("Object is not a credentio::Object::Integer")));
}
TEST(PdfReaderTest, ContainingPreviousXrefTable) {
std::string content = R"(%PDF-1.7
1 0 obj
<< /Type /Catalog
/Pages 2 0 R
>>
endobj
2 0 obj
<< /Type /Pages
/Kids [3 0 R]
/Count 1
>>
endobj
3 0 obj
<< /Type /Page
/Parent 2 0 R
/MediaBox [0 0 100 100]
/Contents 4 0 R
>>
endobj
4 0 obj
<< /Length 35 >>
stream
BT /F1 12 Tf 35 50 Td (Hello) Tj ET
endstream
endobj
xref
0 5
0000000000 65535 f@
0000000009 00000 n@
0000000058 00000 n@
0000000115 00000 n@
0000000202 00000 n@
trailer
<< /Size 5
/Root 1 0 R
>>
startxref
287
%%EOF
5 0 obj
<< /Type /Catalog
/Pages 6 0 R
>>
endobj
6 0 obj
<< /Type /Pages
/Kids [7 0 R 3 0 R]
/Count 2
>>
endobj
7 0 obj
<< /Type /Page
/Parent 6 0 R
/MediaBox [0 0 50 50]
/Contents 8 0 R
>>
endobj
8 0 obj
<< /Length 35 >>
stream
BT /F1 12 Tf 10 20 Td (Hello) Tj ET
endstream
endobj
xref
5 4
0000000450 00000 n@
0000000499 00000 n@
0000000563 00000 n@
0000000648 00000 n@
trailer
<< /Size 9
/Root 5 0 R
/Prev 287
>>
startxref
733
%%EOF)";
// Replace all @ with space to avoid presubmit failing trailing whitespace.
content = absl::StrReplaceAll(content, {{"@", " "}});
riegeli::StringReader<> input(content);
absl::StatusOr<std::unique_ptr<PdfReader>> reader = PdfReader::Create(&input);
ABSL_ASSERT_OK(reader);
std::vector<IndirectObject> objects;
while ((*reader)->HasNext()) {
absl::StatusOr<IndirectObject> obj = (*reader)->Next();
ABSL_ASSERT_OK(obj);
objects.push_back(*std::move(obj));
}
ASSERT_THAT(
objects,
ElementsAre(
IndirectObject{.object_number = 1,
.generation_number = 0,
.object = CreateDictionary({
{"Type", CreateName("Catalog")},
{"Pages", CreateIndirectRef(2, 0)},
})},
IndirectObject{.object_number = 2,
.generation_number = 0,
.object = CreateDictionary({
{"Kids", CreateArray({CreateIndirectRef(3, 0)})},
{"Type", CreateName("Pages")},
{"Count", CreateInteger(1)},
})},
IndirectObject{.object_number = 3,
.generation_number = 0,
.object = CreateDictionary({
{"Parent", CreateIndirectRef(2, 0)},
{"Type", CreateName("Page")},
{"MediaBox", CreateArray({
CreateInteger(0),
CreateInteger(0),
CreateInteger(100),
CreateInteger(100),
})},
{"Contents", CreateIndirectRef(4, 0)},
})},
IndirectObject{
.object_number = 4,
.generation_number = 0,
.stream_dictionary =
Object::Dictionary{
.entries = {{"Length", CreateInteger(35)}}},
.object = CreateStream("BT /F1 12 Tf 35 50 Td (Hello) Tj ET")},
IndirectObject{.object_number = 5,
.generation_number = 0,
.object = CreateDictionary({
{"Type", CreateName("Catalog")},
{"Pages", CreateIndirectRef(6, 0)},
})},
IndirectObject{.object_number = 6,
.generation_number = 0,
.object = CreateDictionary({
{"Kids", CreateArray({CreateIndirectRef(7, 0),
CreateIndirectRef(3, 0)})},
{"Type", CreateName("Pages")},
{"Count", CreateInteger(2)},
})},
IndirectObject{.object_number = 7,
.generation_number = 0,
.object = CreateDictionary({
{"Parent", CreateIndirectRef(6, 0)},
{"Type", CreateName("Page")},
{"MediaBox", CreateArray({
CreateInteger(0),
CreateInteger(0),
CreateInteger(50),
CreateInteger(50),
})},
{"Contents", CreateIndirectRef(8, 0)},
})},
IndirectObject{
.object_number = 8,
.generation_number = 0,
.stream_dictionary =
Object::Dictionary{
.entries = {{"Length", CreateInteger(35)}}},
.object = CreateStream("BT /F1 12 Tf 10 20 Td (Hello) Tj ET")}));
}
riegeli::Reader* GetNullReader() { return nullptr; }
TEST(PdfReaderTest, NullInputFailure)
__attribute__((no_sanitize("nullability"))) {
EXPECT_THAT(PdfReader::Create(GetNullReader()),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("input cannot be null")));
}
TEST(PdfReaderTest, CircularXrefTableFailure) {
std::string content = R"(%PDF-1.7
1 0 obj
<< /Type /Catalog
/Pages 2 0 R
>>
endobj
2 0 obj
<< /Type /Pages
/Kids [3 0 R]
/Count 1
>>
endobj
3 0 obj
<< /Type /Page
/Parent 2 0 R
/MediaBox [0 0 100 100]
/Contents 4 0 R
>>
endobj
4 0 obj
<< /Length 35 >>
stream
BT /F1 12 Tf 35 50 Td (Hello) Tj ET
endstream
endobj
xref
0 5
0000000000 65535 f@
0000000009 00000 n@
0000000058 00000 n@
0000000115 00000 n@
0000000202 00000 n@
trailer
<< /Size 5
/Root 1 0 R
/Prev 743
>>
startxref
287
%%EOF
5 0 obj
<< /Type /Catalog
/Pages 6 0 R
>>
endobj
6 0 obj
<< /Type /Pages
/Kids [7 0 R 3 0 R]
/Count 2
>>
endobj
7 0 obj
<< /Type /Page
/Parent 6 0 R
/MediaBox [0 0 50 50]
/Contents 8 0 R
>>
endobj
8 0 obj
<< /Length 35 >>
stream
BT /F1 12 Tf 10 20 Td (Hello) Tj ET
endstream
endobj
xref
5 4
0000000460 00000 n@
0000000509 00000 n@
0000000573 00000 n@
0000000658 00000 n@
trailer
<< /Size 9
/Root 5 0 R
/Prev 287
>>
startxref
743
%%EOF)";
content = absl::StrReplaceAll(content, {{"@", " "}});
riegeli::StringReader<> input(content);
EXPECT_THAT(PdfReader::Create(&input),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("Circular cross-reference table detected")));
}
TEST(PdfReaderTest, IncrementalUpdateOverlappingXrefEntriesSuccess) {
std::string content = R"(%PDF-1.7
1 0 obj
<< /Type /Catalog
/Pages 2 0 R
>>
endobj
2 0 obj
<< /Type /Pages
/Kids [3 0 R]
/Count 1
>>
endobj
3 0 obj
<< /Type /Page
/Parent 2 0 R
/MediaBox [0 0 100 100]
/Contents 4 0 R
>>
endobj
4 0 obj
<< /Length 35 >>
stream
BT /F1 12 Tf 35 50 Td (Hello) Tj ET
endstream
endobj
xref
0 5
0000000000 65535 f@
0000000009 00000 n@
0000000058 00000 n@
0000000115 00000 n@
0000000202 00000 n@
trailer
<< /Size 5
/Root 1 0 R
>>
startxref
287
%%EOF
5 0 obj
<< /Type /Catalog
/Pages 6 0 R
>>
endobj
6 0 obj
<< /Type /Pages
/Kids [7 0 R 3 0 R]
/Count 2
>>
endobj
7 0 obj
<< /Type /Page
/Parent 6 0 R
/MediaBox [0 0 50 50]
/Contents 8 0 R
>>
endobj
8 0 obj
<< /Length 35 >>
stream
BT /F1 12 Tf 10 20 Td (Hello) Tj ET
endstream
endobj
xref
1 1
0000000450 00000 n@
5 4
0000000450 00000 n@
0000000499 00000 n@
0000000563 00000 n@
0000000648 00000 n@
trailer
<< /Size 9
/Root 5 0 R
/Prev 287
>>
startxref
733
%%EOF)";
content = absl::StrReplaceAll(content, {{"@", " "}});
riegeli::StringReader<> input(content);
absl::StatusOr<std::unique_ptr<PdfReader>> reader = PdfReader::Create(&input);
ABSL_ASSERT_OK(reader);
// Object 1 was redefined in the second xref section, so it should have the
// newer offset.
EXPECT_THAT((*reader)->GetObjectOffset(1, 0), IsOkAndHolds(450));
// Object 2 was only defined in the first xref section.
EXPECT_THAT((*reader)->GetObjectOffset(2, 0), IsOkAndHolds(58));
}
TEST(PdfReaderTest, IncrementalUpdateReDeclaresObject) {
// This test uses exact offsets generated by a custom script to ensure
// GetObject can actually read the objects.
std::string content = "%PDF-1.7\n%\xe2\xe3\xcf\xd3\n" + std::string(R"(1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>
endobj
xref
0 4
0000000000 65535 f@
0000000015 00000 n@
0000000064 00000 n@
0000000121 00000 n@
trailer
<< /Size 4 /Root 1 0 R >>
startxref
192
%%EOF
1 0 obj
<< /Type /Catalog /Pages 2 0 R /Note (updated) >>
endobj
xref
1 1
0000000335 00000 n@
trailer
<< /Size 4 /Root 1 0 R /Prev 192 >>
startxref
400
%%EOF)");
content = absl::StrReplaceAll(content, {{"@", " "}});
riegeli::StringReader<> input(content);
absl::StatusOr<std::unique_ptr<PdfReader>> reader = PdfReader::Create(&input);
ABSL_ASSERT_OK(reader);
// Object 1 should be the updated one.
absl::StatusOr<IndirectObject> obj1 = (*reader)->GetObject(1, 0);
ABSL_ASSERT_OK(obj1);
// Verify it is a dictionary and contains the Note key.
absl::StatusOr<Object::Dictionary> dict =
Object::As<Object::Dictionary>(obj1->object);
ABSL_ASSERT_OK(dict);
EXPECT_TRUE(dict->entries.contains("Note"));
// Verify the value of Note.
auto note_val = dict->entries.at("Note");
absl::StatusOr<Object::LiteralString> note_str =
Object::As<Object::LiteralString>(note_val);
ABSL_ASSERT_OK(note_str);
EXPECT_EQ(note_str->raw_value, "updated");
// Object 3 should be the original one from base.
absl::StatusOr<IndirectObject> obj3 = (*reader)->GetObject(3, 0);
ABSL_ASSERT_OK(obj3);
absl::StatusOr<Object::Dictionary> dict3 =
Object::As<Object::Dictionary>(obj3->object);
ABSL_ASSERT_OK(dict3);
EXPECT_TRUE(dict3->entries.contains("MediaBox"));
}
TEST(PdfReaderTest, CyclicStreamLengthFailure) {
std::string content = R"(%PDF-1.7
%%%%%
1 0 obj
<< /Length 1 0 R >>
stream
abc
endstream
endobj
xref
0 2
0000000000 65535 f@
0000000015 00000 n@
trailer
<< /Size 2 >>
startxref
71
%%EOF)";
content = absl::StrReplaceAll(content, {{"@", " "}});
riegeli::StringReader<> input(content);
absl::StatusOr<std::unique_ptr<PdfReader>> reader = PdfReader::Create(&input);
ABSL_ASSERT_OK(reader);
EXPECT_THAT((*reader)->GetObject(1, 0),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("Circular reference detected")));
}
} // namespace
} // namespace credentio