| // 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 <algorithm> |
| #include <memory> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "core/fxcrt/bytestring.h" |
| #include "core/fxcrt/span_util.h" |
| #include "public/fpdf_doc.h" |
| #include "public/fpdfview.h" |
| #include "testing/embedder_test.h" |
| #include "testing/fx_string_testhelpers.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "testing/range_set.h" |
| #include "testing/utils/file_util.h" |
| #include "testing/utils/path_service.h" |
| #include "third_party/base/numerics/safe_conversions.h" |
| |
| namespace { |
| |
| class MockDownloadHints final : public FX_DOWNLOADHINTS { |
| public: |
| static void SAddSegment(FX_DOWNLOADHINTS* pThis, size_t offset, size_t size) { |
| } |
| |
| MockDownloadHints() { |
| FX_DOWNLOADHINTS::version = 1; |
| FX_DOWNLOADHINTS::AddSegment = SAddSegment; |
| } |
| |
| ~MockDownloadHints() = default; |
| }; |
| |
| class TestAsyncLoader final : public FX_DOWNLOADHINTS, FX_FILEAVAIL { |
| public: |
| explicit TestAsyncLoader(const std::string& file_name) { |
| std::string file_path = PathService::GetTestFilePath(file_name); |
| if (file_path.empty()) { |
| return; |
| } |
| file_contents_ = GetFileContents(file_path.c_str()); |
| if (file_contents_.empty()) { |
| return; |
| } |
| |
| file_access_.m_FileLen = |
| pdfium::base::checked_cast<unsigned long>(file_contents_.size()); |
| file_access_.m_GetBlock = SGetBlock; |
| file_access_.m_Param = this; |
| |
| FX_DOWNLOADHINTS::version = 1; |
| FX_DOWNLOADHINTS::AddSegment = SAddSegment; |
| |
| FX_FILEAVAIL::version = 1; |
| FX_FILEAVAIL::IsDataAvail = SIsDataAvail; |
| } |
| |
| bool IsOpened() const { return !file_contents_.empty(); } |
| |
| FPDF_FILEACCESS* file_access() { return &file_access_; } |
| FX_DOWNLOADHINTS* hints() { return this; } |
| FX_FILEAVAIL* file_avail() { return this; } |
| |
| const std::vector<std::pair<size_t, size_t>>& requested_segments() const { |
| return requested_segments_; |
| } |
| |
| size_t max_requested_bound() const { return max_requested_bound_; } |
| |
| void ClearRequestedSegments() { |
| requested_segments_.clear(); |
| max_requested_bound_ = 0; |
| } |
| |
| bool is_new_data_available() const { return is_new_data_available_; } |
| void set_is_new_data_available(bool is_new_data_available) { |
| is_new_data_available_ = is_new_data_available; |
| } |
| |
| size_t max_already_available_bound() const { |
| return available_ranges_.IsEmpty() |
| ? 0 |
| : available_ranges_.ranges().rbegin()->second; |
| } |
| |
| void FlushRequestedData() { |
| for (const auto& it : requested_segments_) { |
| SetDataAvailable(it.first, it.second); |
| } |
| ClearRequestedSegments(); |
| } |
| |
| pdfium::span<const uint8_t> file_contents() const { return file_contents_; } |
| pdfium::span<uint8_t> mutable_file_contents() { return file_contents_; } |
| |
| private: |
| void SetDataAvailable(size_t start, size_t size) { |
| available_ranges_.Union(RangeSet::Range(start, start + size)); |
| } |
| |
| bool CheckDataAlreadyAvailable(size_t start, size_t size) const { |
| return available_ranges_.Contains(RangeSet::Range(start, start + size)); |
| } |
| |
| int GetBlockImpl(unsigned long pos, unsigned char* pBuf, unsigned long size) { |
| if (!IsDataAvailImpl(pos, size)) |
| return 0; |
| const unsigned long end = std::min( |
| pdfium::base::checked_cast<unsigned long>(file_contents_.size()), |
| pos + size); |
| if (end <= pos) |
| return 0; |
| const unsigned long bytes_to_copy = end - pos; |
| fxcrt::spancpy(pdfium::make_span(pBuf, size), |
| file_contents().subspan(pos, bytes_to_copy)); |
| SetDataAvailable(pos, bytes_to_copy); |
| return static_cast<int>(bytes_to_copy); |
| } |
| |
| void AddSegmentImpl(size_t offset, size_t size) { |
| requested_segments_.emplace_back(offset, size); |
| max_requested_bound_ = std::max(max_requested_bound_, offset + size); |
| } |
| |
| bool IsDataAvailImpl(size_t offset, size_t size) { |
| if (offset + size > file_contents_.size()) { |
| return false; |
| } |
| if (is_new_data_available_) { |
| SetDataAvailable(offset, size); |
| return true; |
| } |
| return CheckDataAlreadyAvailable(offset, size); |
| } |
| |
| static int SGetBlock(void* param, |
| unsigned long pos, |
| unsigned char* pBuf, |
| unsigned long size) { |
| return static_cast<TestAsyncLoader*>(param)->GetBlockImpl(pos, pBuf, size); |
| } |
| |
| static void SAddSegment(FX_DOWNLOADHINTS* pThis, size_t offset, size_t size) { |
| return static_cast<TestAsyncLoader*>(pThis)->AddSegmentImpl(offset, size); |
| } |
| |
| static FPDF_BOOL SIsDataAvail(FX_FILEAVAIL* pThis, |
| size_t offset, |
| size_t size) { |
| return static_cast<TestAsyncLoader*>(pThis)->IsDataAvailImpl(offset, size); |
| } |
| |
| FPDF_FILEACCESS file_access_; |
| |
| std::vector<uint8_t> file_contents_; |
| std::vector<std::pair<size_t, size_t>> requested_segments_; |
| size_t max_requested_bound_ = 0; |
| bool is_new_data_available_ = true; |
| |
| RangeSet available_ranges_; |
| }; |
| |
| } // namespace |
| |
| class FPDFDataAvailEmbedderTest : public EmbedderTest {}; |
| |
| TEST_F(FPDFDataAvailEmbedderTest, TrailerUnterminated) { |
| // Document must load without crashing but is too malformed to be available. |
| EXPECT_FALSE(OpenDocument("trailer_unterminated.pdf")); |
| MockDownloadHints hints; |
| EXPECT_FALSE(FPDFAvail_IsDocAvail(avail(), &hints)); |
| } |
| |
| TEST_F(FPDFDataAvailEmbedderTest, TrailerAsHexstring) { |
| // Document must load without crashing but is too malformed to be available. |
| EXPECT_FALSE(OpenDocument("trailer_as_hexstring.pdf")); |
| MockDownloadHints hints; |
| EXPECT_FALSE(FPDFAvail_IsDocAvail(avail(), &hints)); |
| } |
| |
| TEST_F(FPDFDataAvailEmbedderTest, LoadUsingHintTables) { |
| TestAsyncLoader loader("feature_linearized_loading.pdf"); |
| CreateAvail(loader.file_avail(), loader.file_access()); |
| ASSERT_EQ(PDF_DATA_AVAIL, FPDFAvail_IsDocAvail(avail(), loader.hints())); |
| SetDocumentFromAvail(); |
| ASSERT_TRUE(document()); |
| ASSERT_EQ(PDF_DATA_AVAIL, FPDFAvail_IsPageAvail(avail(), 1, loader.hints())); |
| |
| // No new data available, to prevent load "Pages" node. |
| loader.set_is_new_data_available(false); |
| ScopedFPDFPage page(FPDF_LoadPage(document(), 1)); |
| EXPECT_TRUE(page); |
| } |
| |
| TEST_F(FPDFDataAvailEmbedderTest, CheckFormAvailIfLinearized) { |
| TestAsyncLoader loader("feature_linearized_loading.pdf"); |
| CreateAvail(loader.file_avail(), loader.file_access()); |
| ASSERT_EQ(PDF_DATA_AVAIL, FPDFAvail_IsDocAvail(avail(), loader.hints())); |
| SetDocumentFromAvail(); |
| ASSERT_TRUE(document()); |
| |
| // Prevent access to non-requested data to coerce the parser to send new |
| // request for non available (non-requested before) data. |
| loader.set_is_new_data_available(false); |
| loader.ClearRequestedSegments(); |
| |
| int status = PDF_FORM_NOTAVAIL; |
| while (status == PDF_FORM_NOTAVAIL) { |
| loader.FlushRequestedData(); |
| status = FPDFAvail_IsFormAvail(avail(), loader.hints()); |
| } |
| EXPECT_NE(PDF_FORM_ERROR, status); |
| } |
| |
| TEST_F(FPDFDataAvailEmbedderTest, |
| DoNotLoadMainCrossRefForFirstPageIfLinearized) { |
| TestAsyncLoader loader("feature_linearized_loading.pdf"); |
| CreateAvail(loader.file_avail(), loader.file_access()); |
| ASSERT_EQ(PDF_DATA_AVAIL, FPDFAvail_IsDocAvail(avail(), loader.hints())); |
| SetDocumentFromAvail(); |
| ASSERT_TRUE(document()); |
| const int first_page_num = FPDFAvail_GetFirstPageNum(document()); |
| |
| // The main cross ref table should not be processed. |
| // (It is always at file end) |
| EXPECT_GT(loader.file_access()->m_FileLen, |
| loader.max_already_available_bound()); |
| |
| // Prevent access to non-requested data to coerce the parser to send new |
| // request for non available (non-requested before) data. |
| loader.set_is_new_data_available(false); |
| FPDFAvail_IsPageAvail(avail(), first_page_num, loader.hints()); |
| |
| // The main cross ref table should not be requested. |
| // (It is always at file end) |
| EXPECT_GT(loader.file_access()->m_FileLen, loader.max_requested_bound()); |
| |
| // Allow parse page. |
| loader.set_is_new_data_available(true); |
| ASSERT_EQ(PDF_DATA_AVAIL, |
| FPDFAvail_IsPageAvail(avail(), first_page_num, loader.hints())); |
| |
| // The main cross ref table should not be processed. |
| // (It is always at file end) |
| EXPECT_GT(loader.file_access()->m_FileLen, |
| loader.max_already_available_bound()); |
| |
| // Prevent loading data, while page loading. |
| loader.set_is_new_data_available(false); |
| ScopedFPDFPage page(FPDF_LoadPage(document(), first_page_num)); |
| EXPECT_TRUE(page); |
| } |
| |
| TEST_F(FPDFDataAvailEmbedderTest, LoadSecondPageIfLinearizedWithHints) { |
| TestAsyncLoader loader("feature_linearized_loading.pdf"); |
| CreateAvail(loader.file_avail(), loader.file_access()); |
| ASSERT_EQ(PDF_DATA_AVAIL, FPDFAvail_IsDocAvail(avail(), loader.hints())); |
| SetDocumentFromAvail(); |
| ASSERT_TRUE(document()); |
| |
| static constexpr uint32_t kSecondPageNum = 1; |
| |
| // Prevent access to non-requested data to coerce the parser to send new |
| // request for non available (non-requested before) data. |
| loader.set_is_new_data_available(false); |
| loader.ClearRequestedSegments(); |
| |
| int status = PDF_DATA_NOTAVAIL; |
| while (status == PDF_DATA_NOTAVAIL) { |
| loader.FlushRequestedData(); |
| status = FPDFAvail_IsPageAvail(avail(), kSecondPageNum, loader.hints()); |
| } |
| EXPECT_EQ(PDF_DATA_AVAIL, status); |
| |
| // Prevent loading data, while page loading. |
| loader.set_is_new_data_available(false); |
| ScopedFPDFPage page(FPDF_LoadPage(document(), kSecondPageNum)); |
| EXPECT_TRUE(page); |
| } |
| |
| TEST_F(FPDFDataAvailEmbedderTest, LoadInfoAfterReceivingWholeDocument) { |
| TestAsyncLoader loader("linearized.pdf"); |
| loader.set_is_new_data_available(false); |
| CreateAvail(loader.file_avail(), loader.file_access()); |
| while (PDF_DATA_AVAIL != FPDFAvail_IsDocAvail(avail(), loader.hints())) { |
| loader.FlushRequestedData(); |
| } |
| |
| SetDocumentFromAvail(); |
| ASSERT_TRUE(document()); |
| |
| // The "info" dictionary should still be unavailable. |
| EXPECT_FALSE(FPDF_GetMetaText(document(), "CreationDate", nullptr, 0)); |
| |
| // Simulate receiving whole file. |
| loader.set_is_new_data_available(true); |
| // Load second page, to parse additional crossref sections. |
| EXPECT_EQ(PDF_DATA_AVAIL, FPDFAvail_IsPageAvail(avail(), 1, loader.hints())); |
| |
| EXPECT_TRUE(FPDF_GetMetaText(document(), "CreationDate", nullptr, 0)); |
| } |
| |
| TEST_F(FPDFDataAvailEmbedderTest, LoadInfoAfterReceivingFirstPage) { |
| TestAsyncLoader loader("linearized.pdf"); |
| // Map "Info" to an object within the first section without breaking |
| // linearization. |
| ByteString data(ByteStringView(loader.file_contents())); |
| absl::optional<size_t> index = data.Find("/Info 27 0 R"); |
| ASSERT_TRUE(index.has_value()); |
| auto span = loader.mutable_file_contents().subspan(index.value()).subspan(7); |
| ASSERT_FALSE(span.empty()); |
| EXPECT_EQ('7', span[0]); |
| span[0] = '9'; |
| |
| loader.set_is_new_data_available(false); |
| CreateAvail(loader.file_avail(), loader.file_access()); |
| while (PDF_DATA_AVAIL != FPDFAvail_IsDocAvail(avail(), loader.hints())) { |
| loader.FlushRequestedData(); |
| } |
| |
| SetDocumentFromAvail(); |
| ASSERT_TRUE(document()); |
| |
| // The "Info" dictionary should be available for the linearized document, if |
| // it is located in the first page section. |
| // Info was remapped to a dictionary with Type "Catalog" |
| unsigned short buffer[100] = {0}; |
| EXPECT_TRUE(FPDF_GetMetaText(document(), "Type", buffer, sizeof(buffer))); |
| EXPECT_EQ(L"Catalog", GetPlatformWString(buffer)); |
| } |
| |
| TEST_F(FPDFDataAvailEmbedderTest, TryLoadInvalidInfo) { |
| TestAsyncLoader loader("linearized.pdf"); |
| // Map "Info" to an invalid object without breaking linearization. |
| ByteString data(ByteStringView(loader.file_contents())); |
| absl::optional<size_t> index = data.Find("/Info 27 0 R"); |
| ASSERT_TRUE(index.has_value()); |
| auto span = loader.mutable_file_contents().subspan(index.value()).subspan(6); |
| ASSERT_GE(span.size(), 2u); |
| EXPECT_EQ('2', span[0]); |
| EXPECT_EQ('7', span[1]); |
| span[0] = '9'; |
| span[1] = '9'; |
| |
| loader.set_is_new_data_available(false); |
| CreateAvail(loader.file_avail(), loader.file_access()); |
| while (PDF_DATA_AVAIL != FPDFAvail_IsDocAvail(avail(), loader.hints())) { |
| loader.FlushRequestedData(); |
| } |
| |
| SetDocumentFromAvail(); |
| ASSERT_TRUE(document()); |
| |
| // Set all data available. |
| loader.set_is_new_data_available(true); |
| // Check second page, to load additional crossrefs. |
| ASSERT_EQ(PDF_DATA_AVAIL, FPDFAvail_IsPageAvail(avail(), 0, loader.hints())); |
| |
| // Test that api is robust enough to handle the bad case. |
| EXPECT_FALSE(FPDF_GetMetaText(document(), "Type", nullptr, 0)); |
| } |
| |
| TEST_F(FPDFDataAvailEmbedderTest, TryLoadNonExistsInfo) { |
| TestAsyncLoader loader("linearized.pdf"); |
| // Break the "Info" parameter without breaking linearization. |
| ByteString data(ByteStringView(loader.file_contents())); |
| absl::optional<size_t> index = data.Find("/Info 27 0 R"); |
| ASSERT_TRUE(index.has_value()); |
| auto span = loader.mutable_file_contents().subspan(index.value()).subspan(2); |
| ASSERT_FALSE(span.empty()); |
| EXPECT_EQ('n', span[0]); |
| span[0] = '_'; |
| |
| loader.set_is_new_data_available(false); |
| CreateAvail(loader.file_avail(), loader.file_access()); |
| while (PDF_DATA_AVAIL != FPDFAvail_IsDocAvail(avail(), loader.hints())) { |
| loader.FlushRequestedData(); |
| } |
| |
| SetDocumentFromAvail(); |
| ASSERT_TRUE(document()); |
| |
| // Set all data available. |
| loader.set_is_new_data_available(true); |
| // Check second page, to load additional crossrefs. |
| ASSERT_EQ(PDF_DATA_AVAIL, FPDFAvail_IsPageAvail(avail(), 0, loader.hints())); |
| |
| // Test that api is robust enough to handle the bad case. |
| EXPECT_FALSE(FPDF_GetMetaText(document(), "Type", nullptr, 0)); |
| } |
| |
| TEST_F(FPDFDataAvailEmbedderTest, BadInputsToAPIs) { |
| EXPECT_EQ(PDF_DATA_ERROR, FPDFAvail_IsDocAvail(nullptr, nullptr)); |
| EXPECT_FALSE(FPDFAvail_GetDocument(nullptr, nullptr)); |
| EXPECT_EQ(0, FPDFAvail_GetFirstPageNum(nullptr)); |
| EXPECT_EQ(PDF_DATA_ERROR, FPDFAvail_IsPageAvail(nullptr, 0, nullptr)); |
| EXPECT_EQ(PDF_FORM_ERROR, FPDFAvail_IsFormAvail(nullptr, nullptr)); |
| EXPECT_EQ(PDF_LINEARIZATION_UNKNOWN, FPDFAvail_IsLinearized(nullptr)); |
| } |
| |
| TEST_F(FPDFDataAvailEmbedderTest, NegativePageIndex) { |
| TestAsyncLoader loader("linearized.pdf"); |
| CreateAvail(loader.file_avail(), loader.file_access()); |
| ASSERT_EQ(PDF_DATA_AVAIL, FPDFAvail_IsDocAvail(avail(), loader.hints())); |
| EXPECT_EQ(PDF_DATA_NOTAVAIL, |
| FPDFAvail_IsPageAvail(avail(), -1, loader.hints())); |
| } |
| |
| TEST_F(FPDFDataAvailEmbedderTest, Bug_1324189) { |
| // Test passes if it doesn't crash. |
| TestAsyncLoader loader("bug_1324189.pdf"); |
| CreateAvail(loader.file_avail(), loader.file_access()); |
| ASSERT_EQ(PDF_DATA_NOTAVAIL, FPDFAvail_IsDocAvail(avail(), loader.hints())); |
| } |
| |
| TEST_F(FPDFDataAvailEmbedderTest, Bug_1324503) { |
| // Test passes if it doesn't crash. |
| TestAsyncLoader loader("bug_1324503.pdf"); |
| CreateAvail(loader.file_avail(), loader.file_access()); |
| ASSERT_EQ(PDF_DATA_NOTAVAIL, FPDFAvail_IsDocAvail(avail(), loader.hints())); |
| } |