| // 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/escaping.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::IsOk; | |
| 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"))); | |
| } | |
| // Returns the test PDF with the cross-reference entry for object 2 pointing at | |
| // the header of object 1. | |
| std::string GetTestPdfContentWithMismatchedObjectNumber() { | |
| return absl::StrReplaceAll(GetTestPdfContent(), | |
| {{"0000000041 00000 n ", "0000000015 00000 n "}}); | |
| } | |
| // Returns the test PDF with object 2 declared as `2 1 obj` while the | |
| // cross-reference table lists it as `2 0`. | |
| std::string GetTestPdfContentWithMismatchedGenerationNumber() { | |
| return absl::StrReplaceAll(GetTestPdfContent(), {{"2 0 obj", "2 1 obj"}}); | |
| } | |
| TEST(PdfReaderTest, GetObjectWithMismatchedObjectNumberFailure) { | |
| std::string content = GetTestPdfContentWithMismatchedObjectNumber(); | |
| riegeli::StringReader<> input(content); | |
| absl::StatusOr<std::unique_ptr<PdfReader>> reader = PdfReader::Create(&input); | |
| ABSL_ASSERT_OK(reader); | |
| EXPECT_THAT((*reader)->GetObject(2, 0), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("expects object 2 0 but found object 1 0"))); | |
| // Entries that point at the right header still resolve. | |
| EXPECT_THAT((*reader)->GetObject(1, 0), | |
| IsOkAndHolds(IndirectObject{.object_number = 1, | |
| .generation_number = 0, | |
| .object = CreateName("test_name")})); | |
| EXPECT_THAT( | |
| (*reader)->GetObject(3, 0), | |
| IsOkAndHolds(IndirectObject{ | |
| .object_number = 3, | |
| .generation_number = 0, | |
| .object = CreateDictionary({{"Type", CreateName("Catalog")}})})); | |
| } | |
| TEST(PdfReaderTest, GetObjectWithMismatchedGenerationNumberFailure) { | |
| std::string content = GetTestPdfContentWithMismatchedGenerationNumber(); | |
| riegeli::StringReader<> input(content); | |
| absl::StatusOr<std::unique_ptr<PdfReader>> reader = PdfReader::Create(&input); | |
| ABSL_ASSERT_OK(reader); | |
| EXPECT_THAT((*reader)->GetObject(2, 0), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("expects object 2 0 but found object 2 1"))); | |
| } | |
| TEST(PdfReaderTest, NextWithMismatchedObjectNumberFailure) { | |
| std::string content = GetTestPdfContentWithMismatchedObjectNumber(); | |
| riegeli::StringReader<> input(content); | |
| absl::StatusOr<std::unique_ptr<PdfReader>> reader = PdfReader::Create(&input); | |
| ABSL_ASSERT_OK(reader); | |
| ABSL_ASSERT_OK((*reader)->Next()); // Object 1. | |
| EXPECT_THAT((*reader)->Next(), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("expects object 2 0 but found object 1 0"))); | |
| } | |
| 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"))); | |
| } | |
| // Returns the test PDF with its "startxref" trailer replaced by `trailer`. | |
| std::string GetTestPdfContentWithTrailer(absl::string_view trailer) { | |
| return absl::StrReplaceAll(GetTestPdfContent(), | |
| {{"startxref\n98\n%%EOF", trailer}}); | |
| } | |
| TEST(PdfReaderTest, ReadsTrailerWithAnyEndOfLineMarkers) { | |
| for (absl::string_view trailer : { | |
| "startxref\n98\n%%EOF", | |
| "startxref\r\n98\r\n%%EOF", | |
| "startxref\r98\r%%EOF", | |
| "startxref\n98\n%%EOF\n", | |
| "startxref\r\n98\r\n%%EOF\r\n", | |
| "startxref \n\n98 \r\n\r\n%%EOF", | |
| }) { | |
| std::string content = GetTestPdfContentWithTrailer(trailer); | |
| riegeli::StringReader<> input(content); | |
| absl::StatusOr<std::unique_ptr<PdfReader>> reader = | |
| PdfReader::Create(&input); | |
| EXPECT_THAT(reader, IsOk()) << "trailer: " << absl::CEscape(trailer); | |
| if (!reader.ok()) continue; | |
| EXPECT_THAT( | |
| (*reader)->GetObject(2, 0), | |
| IsOkAndHolds(IndirectObject{.object_number = 2, | |
| .generation_number = 0, | |
| .object = CreateRealNumber("123.456")})) | |
| << "trailer: " << absl::CEscape(trailer); | |
| } | |
| } | |
| TEST(PdfReaderTest, CrlfTrailerWithTenDigitOffsetFitsTailScan) { | |
| // "startxref\r\n" (11) + 10 digits + "\r\n%%EOF\r\n" (9) = 30 bytes, which | |
| // is exactly the tail scan. The offset is found, then seeking to it fails. | |
| std::string content = | |
| GetTestPdfContentWithTrailer("startxref\r\n9999999999\r\n%%EOF\r\n"); | |
| riegeli::StringReader<> input(content); | |
| EXPECT_THAT(PdfReader::Create(&input), | |
| StatusIs(absl::StatusCode::kDataLoss, | |
| HasSubstr("Failed to seek to offset"))); | |
| } | |
| TEST(PdfReaderTest, CrlfTrailerWithElevenDigitOffsetExceedsTailScanFailure) { | |
| // 31 bytes: "startxref" no longer fits in the 30-byte tail scan. | |
| std::string content = | |
| GetTestPdfContentWithTrailer("startxref\r\n99999999999\r\n%%EOF\r\n"); | |
| riegeli::StringReader<> input(content); | |
| EXPECT_THAT( | |
| PdfReader::Create(&input), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("Failed to find cross-reference table offset"))); | |
| } | |
| 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 |