blob: 75dbbb73116e32a6a642f8004e4fb0bed96b496b [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 <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/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;
#ifndef ASSERT_OK_AND_ASSIGN
#define ASSERT_OK_AND_ASSIGN_CONCAT2(x, y) x##y
#define ASSERT_OK_AND_ASSIGN_CONCAT(x, y) ASSERT_OK_AND_ASSIGN_CONCAT2(x, y)
#define ASSERT_OK_AND_ASSIGN(lhs, rexpr) \
ASSERT_OK_AND_ASSIGN_IMPL(lhs, rexpr, __COUNTER__)
#define ASSERT_OK_AND_ASSIGN_IMPL(lhs, rexpr, id) \
auto ASSERT_OK_AND_ASSIGN_CONCAT(status_or_, id) = (rexpr); \
ASSERT_THAT(ASSERT_OK_AND_ASSIGN_CONCAT(status_or_, id), \
::absl_testing::IsOk()); \
lhs = std::move(*ASSERT_OK_AND_ASSIGN_CONCAT(status_or_, id))
#endif
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);
ASSERT_OK_AND_ASSIGN(auto reader, PdfReader::Create(&input));
std::vector<IndirectObject> objects;
while (reader->HasNext()) {
ASSERT_OK_AND_ASSIGN(auto obj, reader->Next());
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);
ASSERT_OK_AND_ASSIGN(auto reader, PdfReader::Create(&input));
int64_t count = 0;
while (reader->HasNext()) {
ASSERT_OK_AND_ASSIGN(auto obj, 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);
ASSERT_OK_AND_ASSIGN(auto reader, PdfReader::Create(&input));
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);
ASSERT_OK_AND_ASSIGN(auto reader, PdfReader::Create(&input));
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);
ASSERT_OK_AND_ASSIGN(auto reader, PdfReader::Create(&input));
EXPECT_THAT(
reader->GetObjectOffset(/*object_number=*/2, /*generation_number=*/0),
IsOkAndHolds(41));
}
TEST(PdfReaderTest, GetNonExistentObjectOffsetFailure) {
std::string content = GetTestPdfContent();
riegeli::StringReader<> input(content);
ASSERT_OK_AND_ASSIGN(auto reader, PdfReader::Create(&input));
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);
ASSERT_OK_AND_ASSIGN(auto reader, PdfReader::Create(&input));
ASSERT_OK_AND_ASSIGN(auto obj, reader->GetObject(2, 0));
EXPECT_THAT(obj, (IndirectObject{.object_number = 2,
.generation_number = 0,
.object = CreateRealNumber("123.456")}));
}
TEST(PdfReaderTest, GetNonExistentObjectFailure) {
std::string content = GetTestPdfContent();
riegeli::StringReader<> input(content);
ASSERT_OK_AND_ASSIGN(auto reader, PdfReader::Create(&input));
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);
ASSERT_OK_AND_ASSIGN(auto reader, PdfReader::Create(&input));
std::vector<IndirectObject> objects;
while (reader->HasNext()) {
ASSERT_OK_AND_ASSIGN(auto obj, reader->Next());
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, CollisionInUseEntriesFailure) {
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);
EXPECT_THAT(PdfReader::Create(&input),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("Collision detected in cross-reference table "
"for in-use entries")));
}
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);
ASSERT_OK_AND_ASSIGN(auto reader, PdfReader::Create(&input));
EXPECT_THAT(reader->GetObject(1, 0),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("Circular reference detected")));
}
} // namespace
} // namespace credentio