// Copyright 2015 The PDFium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "core/fpdfapi/parser/cpdf_parser.h" #include #include #include #include #include #include #include "core/fpdfapi/parser/cpdf_dictionary.h" #include "core/fpdfapi/parser/cpdf_linearized_header.h" #include "core/fpdfapi/parser/cpdf_object.h" #include "core/fpdfapi/parser/cpdf_syntax_parser.h" #include "core/fxcrt/cfx_read_only_span_stream.h" #include "core/fxcrt/fx_extension.h" #include "core/fxcrt/fx_stream.h" #include "core/fxcrt/retain_ptr.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" #include "testing/utils/path_service.h" using testing::ElementsAre; using testing::Pair; using testing::Return; namespace { CPDF_Parser::ObjectInfo GetObjInfo(const CPDF_Parser& parser, uint32_t obj_num) { const auto* info = parser.GetCrossRefTable()->GetObjectInfo(obj_num); return info ? *info : CPDF_Parser::ObjectInfo(); } class TestObjectsHolder final : public CPDF_Parser::ParsedObjectsHolder { public: TestObjectsHolder() = default; ~TestObjectsHolder() override = default; // CPDF_Parser::ParsedObjectsHolder: bool TryInit() override { return true; } MOCK_METHOD1(ParseIndirectObject, RetainPtr(uint32_t objnum)); }; } // namespace // Test-only helper to support Gmock. Cannot be in an anonymous namespace. bool operator==(const CPDF_Parser::ObjectInfo& lhs, const CPDF_Parser::ObjectInfo& rhs) { if (lhs.type != rhs.type) { return false; } if (lhs.gennum != rhs.gennum) { return false; } switch (lhs.type) { case CPDF_Parser::ObjectType::kFree: return true; case CPDF_Parser::ObjectType::kNormal: return lhs.pos == rhs.pos; case CPDF_Parser::ObjectType::kCompressed: return lhs.archive.obj_num == rhs.archive.obj_num && lhs.archive.obj_index == rhs.archive.obj_index; case CPDF_Parser::ObjectType::kObjStream: return false; } } // Test-only helper to let Gmock pretty-print `info`. Cannot be in an anonymous // namespace. std::ostream& operator<<(std::ostream& os, const CPDF_Parser::ObjectInfo& info) { os << "("; switch (info.type) { case CPDF_Parser::ObjectType::kFree: os << "Free object"; break; case CPDF_Parser::ObjectType::kNormal: os << "Normal object, pos: " << info.pos; break; case CPDF_Parser::ObjectType::kCompressed: os << "Compressed object, archive obj_num: " << info.archive.obj_num << ", archive obj_index: " << info.archive.obj_index; break; case CPDF_Parser::ObjectType::kObjStream: os << "ObjectStream object"; break; } os << ", gennum: " << info.gennum << ")"; return os; } // A wrapper class to help test member functions of CPDF_Parser. class CPDF_TestParser final : public CPDF_Parser { public: CPDF_TestParser() : CPDF_Parser(&object_holder_) {} ~CPDF_TestParser() = default; // Setup reading from a file and initial states. bool InitTestFromFile(const char* path) { RetainPtr pFileAccess = IFX_SeekableReadStream::CreateFromFilename(path); if (!pFileAccess) return false; // For the test file, the header is set at the beginning. SetSyntaxParserForTesting( std::make_unique(std::move(pFileAccess))); return true; } // Setup reading from a buffer and initial states. bool InitTestFromBufferWithOffset(pdfium::span buffer, FX_FILESIZE header_offset) { SetSyntaxParserForTesting(CPDF_SyntaxParser::CreateForTesting( pdfium::MakeRetain(buffer), header_offset)); return true; } bool InitTestFromBuffer(pdfium::span buffer) { return InitTestFromBufferWithOffset(buffer, 0 /*header_offset*/); } // Expose protected CPDF_Parser methods for testing. using CPDF_Parser::LoadCrossRefV4; using CPDF_Parser::ParseLinearizedHeader; using CPDF_Parser::ParseStartXRef; using CPDF_Parser::RebuildCrossRef; using CPDF_Parser::StartParseInternal; TestObjectsHolder& object_holder() { return object_holder_; } private: TestObjectsHolder object_holder_; }; TEST(ParserTest, RebuildCrossRefCorrectly) { CPDF_TestParser parser; std::string test_file; ASSERT_TRUE(PathService::GetTestFilePath("parser_rebuildxref_correct.pdf", &test_file)); ASSERT_TRUE(parser.InitTestFromFile(test_file.c_str())) << test_file; ASSERT_TRUE(parser.RebuildCrossRef()); const FX_FILESIZE offsets[] = {0, 15, 61, 154, 296, 374, 450}; const uint16_t versions[] = {0, 0, 2, 4, 6, 8, 0}; for (size_t i = 0; i < std::size(offsets); ++i) EXPECT_EQ(offsets[i], GetObjInfo(parser, i).pos); for (size_t i = 0; i < std::size(versions); ++i) EXPECT_EQ(versions[i], GetObjInfo(parser, i).gennum); const CPDF_CrossRefTable* cross_ref_table = parser.GetCrossRefTable(); ASSERT_TRUE(cross_ref_table); EXPECT_EQ(0u, cross_ref_table->trailer_object_number()); } TEST(ParserTest, RebuildCrossRefFailed) { CPDF_TestParser parser; std::string test_file; ASSERT_TRUE(PathService::GetTestFilePath( "parser_rebuildxref_error_notrailer.pdf", &test_file)); ASSERT_TRUE(parser.InitTestFromFile(test_file.c_str())) << test_file; ASSERT_FALSE(parser.RebuildCrossRef()); } TEST(ParserTest, LoadCrossRefV4) { { static const unsigned char kXrefTable[] = "xref \n" "0 6 \n" "0000000003 65535 f \n" "0000000017 00000 n \n" "0000000081 00000 n \n" "0000000000 00007 f \n" "0000000331 00000 n \n" "0000000409 00000 n \n" "trail"; // Needed to end cross ref table reading. CPDF_TestParser parser; ASSERT_TRUE(parser.InitTestFromBuffer(kXrefTable)); ASSERT_TRUE(parser.LoadCrossRefV4(0, false)); static const FX_FILESIZE kOffsets[] = {0, 17, 81, 0, 331, 409}; static const CPDF_TestParser::ObjectType kTypes[] = { CPDF_TestParser::ObjectType::kFree, CPDF_TestParser::ObjectType::kNotCompressed, CPDF_TestParser::ObjectType::kNotCompressed, CPDF_TestParser::ObjectType::kFree, CPDF_TestParser::ObjectType::kNotCompressed, CPDF_TestParser::ObjectType::kNotCompressed}; static_assert(std::size(kOffsets) == std::size(kTypes), "kOffsets / kTypes size mismatch"); for (size_t i = 0; i < std::size(kOffsets); ++i) { EXPECT_EQ(kOffsets[i], GetObjInfo(parser, i).pos); EXPECT_EQ(kTypes[i], GetObjInfo(parser, i).type); } } { static const unsigned char kXrefTable[] = "xref \n" "0 1 \n" "0000000000 65535 f \n" "3 1 \n" "0000025325 00000 n \n" "8 2 \n" "0000025518 00002 n \n" "0000025635 00000 n \n" "12 1 \n" "0000025777 00000 n \n" "trail"; // Needed to end cross ref table reading. CPDF_TestParser parser; ASSERT_TRUE(parser.InitTestFromBuffer(kXrefTable)); ASSERT_TRUE(parser.LoadCrossRefV4(0, false)); static const FX_FILESIZE kOffsets[] = {0, 0, 0, 25325, 0, 0, 0, 0, 25518, 25635, 0, 0, 25777}; static const CPDF_TestParser::ObjectType kTypes[] = { CPDF_TestParser::ObjectType::kFree, CPDF_TestParser::ObjectType::kFree, CPDF_TestParser::ObjectType::kFree, CPDF_TestParser::ObjectType::kNotCompressed, CPDF_TestParser::ObjectType::kFree, CPDF_TestParser::ObjectType::kFree, CPDF_TestParser::ObjectType::kFree, CPDF_TestParser::ObjectType::kFree, CPDF_TestParser::ObjectType::kNotCompressed, CPDF_TestParser::ObjectType::kNotCompressed, CPDF_TestParser::ObjectType::kFree, CPDF_TestParser::ObjectType::kFree, CPDF_TestParser::ObjectType::kNotCompressed}; static_assert(std::size(kOffsets) == std::size(kTypes), "kOffsets / kTypes size mismatch"); for (size_t i = 0; i < std::size(kOffsets); ++i) { EXPECT_EQ(kOffsets[i], GetObjInfo(parser, i).pos); EXPECT_EQ(kTypes[i], GetObjInfo(parser, i).type); } } { static const unsigned char kXrefTable[] = "xref \n" "0 1 \n" "0000000000 65535 f \n" "3 1 \n" "0000025325 00000 n \n" "8 2 \n" "0000000000 65535 f \n" "0000025635 00000 n \n" "12 1 \n" "0000025777 00000 n \n" "trail"; // Needed to end cross ref table reading. CPDF_TestParser parser; ASSERT_TRUE(parser.InitTestFromBuffer(kXrefTable)); ASSERT_TRUE(parser.LoadCrossRefV4(0, false)); static const FX_FILESIZE kOffsets[] = {0, 0, 0, 25325, 0, 0, 0, 0, 0, 25635, 0, 0, 25777}; static const CPDF_TestParser::ObjectType kTypes[] = { CPDF_TestParser::ObjectType::kFree, CPDF_TestParser::ObjectType::kFree, CPDF_TestParser::ObjectType::kFree, CPDF_TestParser::ObjectType::kNotCompressed, CPDF_TestParser::ObjectType::kFree, CPDF_TestParser::ObjectType::kFree, CPDF_TestParser::ObjectType::kFree, CPDF_TestParser::ObjectType::kFree, CPDF_TestParser::ObjectType::kFree, CPDF_TestParser::ObjectType::kNotCompressed, CPDF_TestParser::ObjectType::kFree, CPDF_TestParser::ObjectType::kFree, CPDF_TestParser::ObjectType::kNotCompressed}; static_assert(std::size(kOffsets) == std::size(kTypes), "kOffsets / kTypes size mismatch"); for (size_t i = 0; i < std::size(kOffsets); ++i) { EXPECT_EQ(kOffsets[i], GetObjInfo(parser, i).pos); EXPECT_EQ(kTypes[i], GetObjInfo(parser, i).type); } } { static const unsigned char kXrefTable[] = "xref \n" "0 7 \n" "0000000002 65535 f \n" "0000000023 00000 n \n" "0000000003 65535 f \n" "0000000004 65535 f \n" "0000000000 65535 f \n" "0000000045 00000 n \n" "0000000179 00000 n \n" "trail"; // Needed to end cross ref table reading. CPDF_TestParser parser; ASSERT_TRUE(parser.InitTestFromBuffer(kXrefTable)); ASSERT_TRUE(parser.LoadCrossRefV4(0, false)); static const FX_FILESIZE kOffsets[] = {0, 23, 0, 0, 0, 45, 179}; static const CPDF_TestParser::ObjectType kTypes[] = { CPDF_TestParser::ObjectType::kFree, CPDF_TestParser::ObjectType::kNotCompressed, CPDF_TestParser::ObjectType::kFree, CPDF_TestParser::ObjectType::kFree, CPDF_TestParser::ObjectType::kFree, CPDF_TestParser::ObjectType::kNotCompressed, CPDF_TestParser::ObjectType::kNotCompressed}; static_assert(std::size(kOffsets) == std::size(kTypes), "kOffsets / kTypes size mismatch"); for (size_t i = 0; i < std::size(kOffsets); ++i) { EXPECT_EQ(kOffsets[i], GetObjInfo(parser, i).pos); EXPECT_EQ(kTypes[i], GetObjInfo(parser, i).type); } } { // Regression test for https://crbug.com/945624 - Make sure the parser // can correctly handle table sizes that are multiples of the read size, // which is 1024. std::string xref_table = "xref \n 0 2048 \n"; xref_table.reserve(41000); for (int i = 0; i < 2048; ++i) { char buffer[21]; snprintf(buffer, sizeof(buffer), "%010d 00000 n \n", i + 1); xref_table += buffer; } xref_table += "trail"; // Needed to end cross ref table reading. CPDF_TestParser parser; ASSERT_TRUE(parser.InitTestFromBuffer( pdfium::make_span(reinterpret_cast(xref_table.c_str()), xref_table.size()))); ASSERT_TRUE(parser.LoadCrossRefV4(0, false)); for (size_t i = 0; i < 2048; ++i) { EXPECT_EQ(static_cast(i) + 1, GetObjInfo(parser, i).pos); EXPECT_EQ(CPDF_TestParser::ObjectType::kNotCompressed, GetObjInfo(parser, i).type); } } } TEST(ParserTest, ParseStartXRef) { CPDF_TestParser parser; std::string test_file; ASSERT_TRUE( PathService::GetTestFilePath("annotation_stamp_with_ap.pdf", &test_file)); ASSERT_TRUE(parser.InitTestFromFile(test_file.c_str())) << test_file; EXPECT_EQ(100940, parser.ParseStartXRef()); RetainPtr cross_ref_v5_obj = parser.ParseIndirectObjectAt(100940, 0); ASSERT_TRUE(cross_ref_v5_obj); EXPECT_EQ(75u, cross_ref_v5_obj->GetObjNum()); } TEST(ParserTest, ParseStartXRefWithHeaderOffset) { static constexpr FX_FILESIZE kTestHeaderOffset = 765; std::string test_file; ASSERT_TRUE( PathService::GetTestFilePath("annotation_stamp_with_ap.pdf", &test_file)); RetainPtr pFileAccess = IFX_SeekableReadStream::CreateFromFilename(test_file.c_str()); ASSERT_TRUE(pFileAccess); std::vector data(pFileAccess->GetSize() + kTestHeaderOffset); ASSERT_TRUE(pFileAccess->ReadBlockAtOffset( pdfium::make_span(data).subspan(kTestHeaderOffset), 0)); CPDF_TestParser parser; parser.InitTestFromBufferWithOffset(data, kTestHeaderOffset); EXPECT_EQ(100940, parser.ParseStartXRef()); RetainPtr cross_ref_v5_obj = parser.ParseIndirectObjectAt(100940, 0); ASSERT_TRUE(cross_ref_v5_obj); EXPECT_EQ(75u, cross_ref_v5_obj->GetObjNum()); } TEST(ParserTest, ParseLinearizedWithHeaderOffset) { static constexpr FX_FILESIZE kTestHeaderOffset = 765; std::string test_file; ASSERT_TRUE(PathService::GetTestFilePath("linearized.pdf", &test_file)); RetainPtr pFileAccess = IFX_SeekableReadStream::CreateFromFilename(test_file.c_str()); ASSERT_TRUE(pFileAccess); std::vector data(pFileAccess->GetSize() + kTestHeaderOffset); ASSERT_TRUE(pFileAccess->ReadBlockAtOffset( pdfium::make_span(data).subspan(kTestHeaderOffset), 0)); CPDF_TestParser parser; parser.InitTestFromBufferWithOffset(data, kTestHeaderOffset); EXPECT_TRUE(parser.ParseLinearizedHeader()); const CPDF_CrossRefTable* cross_ref_table = parser.GetCrossRefTable(); ASSERT_TRUE(cross_ref_table); EXPECT_EQ(0u, cross_ref_table->trailer_object_number()); } TEST(ParserTest, BadStartXrefShouldNotBuildCrossRefTable) { const unsigned char kData[] = "%PDF1-7 0 obj <>\n" "stream\n" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" "endstream\n" "endobj\n" "startxref\n" "6\n" "%%EOF\n"; CPDF_TestParser parser; ASSERT_TRUE(parser.InitTestFromBuffer(kData)); EXPECT_EQ(CPDF_Parser::FORMAT_ERROR, parser.StartParseInternal()); ASSERT_TRUE(parser.GetCrossRefTable()); EXPECT_EQ(0u, parser.GetCrossRefTable()->objects_info().size()); } class ParserXRefTest : public testing::Test { public: ParserXRefTest() = default; ~ParserXRefTest() override = default; // testing::Test: void SetUp() override { // Satisfy CPDF_Parser's checks, so the test data below can concentrate on // the /XRef stream and avoid also providing other valid dictionaries. dummy_root_ = pdfium::MakeRetain(); EXPECT_CALL(parser().object_holder(), ParseIndirectObject) .WillRepeatedly(Return(dummy_root_)); } CPDF_TestParser& parser() { return parser_; } private: RetainPtr dummy_root_; CPDF_TestParser parser_; }; TEST_F(ParserXRefTest, XrefObjectIndicesTooBig) { // Since /Index starts at 4194303, the object number will go past // `kMaxObjectNumber`. static_assert(CPDF_Parser::kMaxObjectNumber == 4194304, "Unexpected kMaxObjectNumber"); const unsigned char kData[] = "%PDF1-7\n%\xa0\xf2\xa4\xf4\n" "7 0 obj <<\n" " /Filter /ASCIIHexDecode\n" " /Index [4194303 3]\n" " /Root 1 0 R\n" " /Size 4194306\n" " /W [1 1 1]\n" ">>\n" "stream\n" "01 00 00\n" "01 0F 00\n" "01 12 00\n" "endstream\n" "endobj\n" "startxref\n" "14\n" "%%EOF\n"; ASSERT_TRUE(parser().InitTestFromBuffer(kData)); EXPECT_EQ(CPDF_Parser::SUCCESS, parser().StartParseInternal()); EXPECT_FALSE(parser().xref_table_rebuilt()); ASSERT_TRUE(parser().GetCrossRefTable()); const auto& objects_info = parser().GetCrossRefTable()->objects_info(); // This should be the only object from table. Subsequent objects have object // numbers that are too big. CPDF_Parser::ObjectInfo only_valid_object; only_valid_object.type = CPDF_Parser::ObjectType::kNormal; only_valid_object.pos = 0; // TODO(thestig): Should the xref table contain object 4194305? // Consider reworking CPDF_Parser's object representation to avoid having to // store this placeholder object. CPDF_Parser::ObjectInfo placeholder_object; placeholder_object.type = CPDF_Parser::ObjectType::kFree; placeholder_object.pos = 0; EXPECT_THAT(objects_info, ElementsAre(Pair(4194303, only_valid_object), Pair(4194305, placeholder_object))); } TEST_F(ParserXRefTest, XrefHasInvalidArchiveObjectNumber) { // 0xFF in the first object in the xref object stream is invalid. const unsigned char kData[] = "%PDF1-7\n%\xa0\xf2\xa4\xf4\n" "7 0 obj <<\n" " /Filter /ASCIIHexDecode\n" " /Root 1 0 R\n" " /Size 3\n" " /W [1 1 1]\n" ">>\n" "stream\n" "02 FF 00\n" "01 0F 00\n" "01 12 00\n" "endstream\n" "endobj\n" "startxref\n" "14\n" "%%EOF\n"; ASSERT_TRUE(parser().InitTestFromBuffer(kData)); EXPECT_EQ(CPDF_Parser::SUCCESS, parser().StartParseInternal()); EXPECT_FALSE(parser().xref_table_rebuilt()); const CPDF_CrossRefTable* cross_ref_table = parser().GetCrossRefTable(); ASSERT_TRUE(cross_ref_table); EXPECT_EQ(7u, cross_ref_table->trailer_object_number()); const auto& objects_info = cross_ref_table->objects_info(); // The expectation is for the parser to skip over the first object, and // continue parsing the remaining objects. So these are the second and third // objects. CPDF_Parser::ObjectInfo expected_objects[2]; expected_objects[0].type = CPDF_Parser::ObjectType::kNormal; expected_objects[0].pos = 15; expected_objects[1].type = CPDF_Parser::ObjectType::kNormal; expected_objects[1].pos = 18; EXPECT_THAT(objects_info, ElementsAre(Pair(1, expected_objects[0]), Pair(2, expected_objects[1]))); } TEST_F(ParserXRefTest, XrefHasInvalidObjectType) { // The XRef object is a dictionary and not a stream. const unsigned char kData[] = "%PDF1-7\n%\xa0\xf2\xa4\xf4\n" "7 0 obj <<\n" " /Filter /ASCIIHexDecode\n" " /Root 1 0 R\n" " /Size 3\n" " /W [1 1 1]\n" ">>\n" "endobj\n" "startxref\n" "14\n" "%%EOF\n"; ASSERT_TRUE(parser().InitTestFromBuffer(kData)); EXPECT_EQ(CPDF_Parser::FORMAT_ERROR, parser().StartParseInternal()); } TEST_F(ParserXRefTest, XrefHasInvalidPrevValue) { // The /Prev value is an absolute offset, so it should never be negative. const unsigned char kData[] = "%PDF1-7\n%\xa0\xf2\xa4\xf4\n" "7 0 obj <<\n" " /Filter /ASCIIHexDecode\n" " /Root 1 0 R\n" " /Size 3\n" " /W [1 1 1]\n" " /Prev -1\n" ">>\n" "stream\n" "02 FF 00\n" "01 0F 00\n" "01 12 00\n" "endstream\n" "endobj\n" "startxref\n" "14\n" "%%EOF\n"; ASSERT_TRUE(parser().InitTestFromBuffer(kData)); EXPECT_EQ(CPDF_Parser::FORMAT_ERROR, parser().StartParseInternal()); } TEST_F(ParserXRefTest, XrefHasInvalidSizeValue) { // The /Size value should never be negative. const unsigned char kData[] = "%PDF1-7\n%\xa0\xf2\xa4\xf4\n" "7 0 obj <<\n" " /Filter /ASCIIHexDecode\n" " /Root 1 0 R\n" " /Size 3\n" " /W [1 1 1]\n" " /Size -1\n" ">>\n" "stream\n" "02 FF 00\n" "01 0F 00\n" "01 12 00\n" "endstream\n" "endobj\n" "startxref\n" "14\n" "%%EOF\n"; ASSERT_TRUE(parser().InitTestFromBuffer(kData)); EXPECT_EQ(CPDF_Parser::FORMAT_ERROR, parser().StartParseInternal()); } TEST_F(ParserXRefTest, XrefHasInvalidWidth) { // The /W array needs to have at least 3 values. const unsigned char kData[] = "%PDF1-7\n%\xa0\xf2\xa4\xf4\n" "7 0 obj <<\n" " /Filter /ASCIIHexDecode\n" " /Root 1 0 R\n" " /Size 3\n" " /W [1 1]\n" ">>\n" "stream\n" "02 FF 00\n" "01 0F 00\n" "01 12 00\n" "endstream\n" "endobj\n" "startxref\n" "14\n" "%%EOF\n"; ASSERT_TRUE(parser().InitTestFromBuffer(kData)); // StartParseInternal() succeeded not because XRef parsing succeeded, but // because RebuildCrossRef() got lucky with the data stream. Therefore, don't // bother checking the garbage output. EXPECT_EQ(CPDF_Parser::SUCCESS, parser().StartParseInternal()); EXPECT_TRUE(parser().xref_table_rebuilt()); } TEST_F(ParserXRefTest, XrefFirstWidthEntryIsZero) { // When the first /W array entry is 0, it implies the objects are all of the // normal type. const unsigned char kData[] = "%PDF1-7\n%\xa0\xf2\xa4\xf4\n" "7 0 obj <<\n" " /Filter /ASCIIHexDecode\n" " /Root 1 0 R\n" " /Size 2\n" " /W [0 1 1]\n" ">>\n" "stream\n" "0F 00\n" "12 00\n" "endstream\n" "endobj\n" "startxref\n" "14\n" "%%EOF\n"; ASSERT_TRUE(parser().InitTestFromBuffer(kData)); EXPECT_EQ(CPDF_Parser::SUCCESS, parser().StartParseInternal()); EXPECT_FALSE(parser().xref_table_rebuilt()); ASSERT_TRUE(parser().GetCrossRefTable()); const auto& objects_info = parser().GetCrossRefTable()->objects_info(); CPDF_Parser::ObjectInfo expected_result[2]; expected_result[0].type = CPDF_Parser::ObjectType::kNormal; expected_result[0].pos = 15; expected_result[1].type = CPDF_Parser::ObjectType::kNormal; expected_result[1].pos = 18; EXPECT_THAT(objects_info, ElementsAre(Pair(0, expected_result[0]), Pair(1, expected_result[1]))); } TEST_F(ParserXRefTest, XrefWithValidIndex) { // The /Index specifies objects (2), (4, 5), (80, 81, 82). const unsigned char kData[] = "%PDF1-7\n%\xa0\xf2\xa4\xf4\n" "7 0 obj <<\n" " /Filter /ASCIIHexDecode\n" " /Root 1 0 R\n" " /Size 83\n" " /Index [2 1 4 2 80 3]\n" " /W [1 1 1]\n" ">>\n" "stream\n" "01 00 00\n" "01 0F 00\n" "01 12 00\n" "01 20 00\n" "01 22 00\n" "01 25 00\n" "endstream\n" "endobj\n" "startxref\n" "14\n" "%%EOF\n"; ASSERT_TRUE(parser().InitTestFromBuffer(kData)); EXPECT_EQ(CPDF_Parser::SUCCESS, parser().StartParseInternal()); EXPECT_FALSE(parser().xref_table_rebuilt()); ASSERT_TRUE(parser().GetCrossRefTable()); const auto& objects_info = parser().GetCrossRefTable()->objects_info(); CPDF_Parser::ObjectInfo expected_result[6]; expected_result[0].type = CPDF_Parser::ObjectType::kNormal; expected_result[0].pos = 0; expected_result[1].type = CPDF_Parser::ObjectType::kNormal; expected_result[1].pos = 15; expected_result[2].type = CPDF_Parser::ObjectType::kNormal; expected_result[2].pos = 18; expected_result[3].type = CPDF_Parser::ObjectType::kNormal; expected_result[3].pos = 32; expected_result[4].type = CPDF_Parser::ObjectType::kNormal; expected_result[4].pos = 34; expected_result[5].type = CPDF_Parser::ObjectType::kNormal; expected_result[5].pos = 37; EXPECT_THAT( objects_info, ElementsAre(Pair(2, expected_result[0]), Pair(4, expected_result[1]), Pair(5, expected_result[2]), Pair(80, expected_result[3]), Pair(81, expected_result[4]), Pair(82, expected_result[5]))); } TEST_F(ParserXRefTest, XrefIndexWithRepeatedObject) { // The /Index specifies objects (2, 3), (3). AKA the sub-sections overlap. const unsigned char kData[] = "%PDF1-7\n%\xa0\xf2\xa4\xf4\n" "7 0 obj <<\n" " /Filter /ASCIIHexDecode\n" " /Root 1 0 R\n" " /Size 4\n" " /Index [2 2 3 1]\n" " /W [1 1 1]\n" ">>\n" "stream\n" "01 00 00\n" "01 0F 00\n" "01 12 00\n" "endstream\n" "endobj\n" "startxref\n" "14\n" "%%EOF\n"; ASSERT_TRUE(parser().InitTestFromBuffer(kData)); EXPECT_EQ(CPDF_Parser::SUCCESS, parser().StartParseInternal()); EXPECT_FALSE(parser().xref_table_rebuilt()); ASSERT_TRUE(parser().GetCrossRefTable()); const auto& objects_info = parser().GetCrossRefTable()->objects_info(); CPDF_Parser::ObjectInfo expected_result[2]; expected_result[0].type = CPDF_Parser::ObjectType::kNormal; expected_result[0].pos = 0; expected_result[1].type = CPDF_Parser::ObjectType::kNormal; // Since the /Index does not follow the spec, this is one of the 2 possible // values that a parser can come up with. expected_result[1].pos = 15; EXPECT_THAT(objects_info, ElementsAre(Pair(2, expected_result[0]), Pair(3, expected_result[1]))); } TEST_F(ParserXRefTest, XrefIndexWithOutOfOrderObjects) { // The /Index specifies objects (3, 4), (2), which is not in ascending order. const unsigned char kData[] = "%PDF1-7\n%\xa0\xf2\xa4\xf4\n" "7 0 obj <<\n" " /Filter /ASCIIHexDecode\n" " /Root 1 0 R\n" " /Size 5\n" " /Index [3 2 2 1]\n" " /W [1 1 1]\n" ">>\n" "stream\n" "01 00 00\n" "01 0F 00\n" "01 12 00\n" "endstream\n" "endobj\n" "startxref\n" "14\n" "%%EOF\n"; ASSERT_TRUE(parser().InitTestFromBuffer(kData)); EXPECT_EQ(CPDF_Parser::SUCCESS, parser().StartParseInternal()); EXPECT_FALSE(parser().xref_table_rebuilt()); ASSERT_TRUE(parser().GetCrossRefTable()); const auto& objects_info = parser().GetCrossRefTable()->objects_info(); // Although the /Index does not follow the spec, the parser tolerates it. CPDF_Parser::ObjectInfo expected_result[3]; expected_result[0].type = CPDF_Parser::ObjectType::kNormal; expected_result[0].pos = 18; expected_result[1].type = CPDF_Parser::ObjectType::kNormal; expected_result[1].pos = 0; expected_result[2].type = CPDF_Parser::ObjectType::kNormal; expected_result[2].pos = 15; EXPECT_THAT(objects_info, ElementsAre(Pair(2, expected_result[0]), Pair(3, expected_result[1]), Pair(4, expected_result[2]))); } TEST_F(ParserXRefTest, XrefWithIndexAndWrongSize) { // The /Index specifies objects (2), (80, 81), so the /Size should be 82, // but is actually 81. const unsigned char kData[] = "%PDF1-7\n%\xa0\xf2\xa4\xf4\n" "7 0 obj <<\n" " /Filter /ASCIIHexDecode\n" " /Root 1 0 R\n" " /Size 81\n" " /Index [2 1 80 2]\n" " /W [1 1 1]\n" ">>\n" "stream\n" "01 00 00\n" "01 0F 00\n" "01 12 00\n" "endstream\n" "endobj\n" "startxref\n" "14\n" "%%EOF\n"; ASSERT_TRUE(parser().InitTestFromBuffer(kData)); EXPECT_EQ(CPDF_Parser::SUCCESS, parser().StartParseInternal()); EXPECT_FALSE(parser().xref_table_rebuilt()); ASSERT_TRUE(parser().GetCrossRefTable()); const auto& objects_info = parser().GetCrossRefTable()->objects_info(); CPDF_Parser::ObjectInfo expected_result[3]; expected_result[0].type = CPDF_Parser::ObjectType::kNormal; expected_result[0].pos = 0; expected_result[1].type = CPDF_Parser::ObjectType::kNormal; expected_result[1].pos = 15; expected_result[2].type = CPDF_Parser::ObjectType::kNormal; expected_result[2].pos = 18; EXPECT_THAT(objects_info, ElementsAre(Pair(2, expected_result[0]), Pair(80, expected_result[1]), Pair(81, expected_result[2]))); }