From 8e2c3fd5ff22ec08559c5edb3a7eccfc5609217f Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Sun, 15 Nov 2026 11:40:24 +0200 Subject: [PATCH] Read and write the document security store Long-term validation of a signature needs the certificates, OCSP answers and CRLs that prove its chain, and PAdES keeps them in the catalog's /DSS. Nothing in the fork reached it. EPDFDss_GetCount and EPDFDss_GetData read the store's three kinds of item, decoded: what /Certs, /OCSPs and /CRLs list, then what only a /VRI entry names, each stream once. EPDFDss_Add writes one item as a stream of its own and names it in the store's list. It makes the store and the list when they are missing and extends them where they are kept, as an object of their own or inside the catalog. It returns the stream's object number, and refuses a store or a list of the wrong type without changing anything. /VRI is read and never written. EPDFDoc_GetLoadedObjectValue reads the value of any object as the document's loaded bytes hold it, in the text and with the read status of EPDFObjectDiff_GetValue. The revision diff lists only the objects a revision wrote. Acrobat points a page at a new /Annots list and leaves the old one unwritten, and the engine needs the old list to judge what was added. --- docs/embedpdf/fork-ledger.md | 9 +- fpdfsdk/BUILD.gn | 4 + fpdfsdk/epdf_c_api_test.c | 4 + fpdfsdk/epdf_dss.cpp | 233 ++++++++++ fpdfsdk/epdf_dss_embeddertest.cpp | 550 ++++++++++++++++++++++++ fpdfsdk/epdf_revision_diff.cpp | 52 +++ fpdfsdk/epdf_signature_embeddertest.cpp | 154 +++++++ public/epdf_signature.h | 86 ++++ 8 files changed, 1088 insertions(+), 4 deletions(-) create mode 100644 fpdfsdk/epdf_dss.cpp create mode 100644 fpdfsdk/epdf_dss_embeddertest.cpp diff --git a/docs/embedpdf/fork-ledger.md b/docs/embedpdf/fork-ledger.md index e50c001d08..c2a1c6909b 100644 --- a/docs/embedpdf/fork-ledger.md +++ b/docs/embedpdf/fork-ledger.md @@ -439,10 +439,11 @@ a field breaks its lines where upstream's would. Keep `font_index_` declared aft **When upstream touches this.** Annotation drawing. ### signatures -**Rule.** Revisions, signature reading and signing support. -**Why.** Digital signatures. -**Where.** `epdf_signature.cpp`, `epdf_revision_*` (ours), hooks in the parser and creator. -**Guarded by.** `epdf_signature_embeddertest.cpp`. +**Rule.** Revisions, signature reading and signing support, and the document security store. +**Why.** Digital signatures, and keeping them checkable for years. +**Where.** `epdf_signature.cpp`, `epdf_revision_*`, `epdf_dss.cpp` (ours), hooks in the parser and +creator. +**Guarded by.** `epdf_signature_embeddertest.cpp`, `epdf_dss_embeddertest.cpp`. **When upstream touches this.** Parser revision handling and the creator. ### embed-metadata diff --git a/fpdfsdk/BUILD.gn b/fpdfsdk/BUILD.gn index 5c4403acd5..1ab04ad4d4 100644 --- a/fpdfsdk/BUILD.gn +++ b/fpdfsdk/BUILD.gn @@ -53,6 +53,8 @@ source_set("fpdfsdk") { "epdf_hasher.h", # EmbedPDF: digital signatures - revisions, the signature model. "epdf_signature.cpp", + # EmbedPDF: digital signatures - the document security store (/DSS). + "epdf_dss.cpp", # EmbedPDF: digital signatures - the cross-revision object diff. "epdf_revision_diff.cpp", # EmbedPDF: digital signatures - the immutable loaded-bytes view that @@ -298,6 +300,8 @@ pdfium_embeddertest_source_set("embeddertests") { "epdf_redact_embeddertest.cpp", # EmbedPDF: covers EPDF_DigestBuffer. "epdf_digest_embeddertest.cpp", + # EmbedPDF: covers the document security store (EPDFDss_*). + "epdf_dss_embeddertest.cpp", # EmbedPDF: covers the digital-signature APIs. "epdf_signature_embeddertest.cpp", # EmbedPDF: covers generic document/page /PieceInfo metadata APIs. diff --git a/fpdfsdk/epdf_c_api_test.c b/fpdfsdk/epdf_c_api_test.c index deb0909969..28f5d75a4d 100644 --- a/fpdfsdk/epdf_c_api_test.c +++ b/fpdfsdk/epdf_c_api_test.c @@ -595,6 +595,7 @@ int CheckEmbedPDFCApi(void) { CHK(EPDFDoc_CompareRevisions); CHK(EPDFDoc_GetBaseBytesSize); CHK(EPDFDoc_GetLoadedBytesSize); + CHK(EPDFDoc_GetLoadedObjectValue); CHK(EPDFDoc_GetRevision); CHK(EPDFDoc_GetRevisionCount); CHK(EPDFDoc_GetStructureObjectNumbers); @@ -602,6 +603,9 @@ int CheckEmbedPDFCApi(void) { CHK(EPDFDoc_OpenBaseOverlayFromPath); CHK(EPDFDoc_OpenRevision); CHK(EPDFDoc_ReadLoadedBytes); + CHK(EPDFDss_Add); + CHK(EPDFDss_GetCount); + CHK(EPDFDss_GetData); CHK(EPDFObjectDiff_Close); CHK(EPDFObjectDiff_GetCount); CHK(EPDFObjectDiff_GetEntry); diff --git a/fpdfsdk/epdf_dss.cpp b/fpdfsdk/epdf_dss.cpp new file mode 100644 index 0000000000..1a88328ee3 --- /dev/null +++ b/fpdfsdk/epdf_dss.cpp @@ -0,0 +1,233 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 + +// The document security store functions declared in public/epdf_signature.h. +// +// The store is a dictionary in the catalog with three lists of streams and an +// optional index whose entries hold lists of the same three kinds. Reading +// flattens those into one list per kind; adding appends one stream to a +// kind's list. Neither looks inside a stream's bytes. + +#include + +#include +#include +#include + +#include "core/fpdfapi/parser/cpdf_array.h" +#include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fpdfapi/parser/cpdf_document_view_scope.h" +#include "core/fpdfapi/parser/cpdf_object.h" +#include "core/fpdfapi/parser/cpdf_reference.h" +#include "core/fpdfapi/parser/cpdf_stream.h" +#include "core/fxcrt/bytestring.h" +#include "core/fxcrt/compiler_specific.h" +#include "core/fxcrt/retain_ptr.h" +#include "core/fxcrt/span.h" +#include "fpdfsdk/cpdfsdk_helpers.h" +#include "public/epdf_signature.h" + +namespace { + +// Where one kind lives: the key of its list in the store, and the key of +// the same kind inside an index entry. +struct KindKeys { + const char* list; + const char* index_entry; +}; + +const KindKeys* KeysOf(int kind) { + static constexpr KindKeys kCertificates = {"Certs", "Cert"}; + static constexpr KindKeys kOcspResponses = {"OCSPs", "OCSP"}; + static constexpr KindKeys kCrls = {"CRLs", "CRL"}; + switch (kind) { + case EPDF_DSS_CERTIFICATES: + return &kCertificates; + case EPDF_DSS_OCSP_RESPONSES: + return &kOcspResponses; + case EPDF_DSS_CRLS: + return &kCrls; + default: + return nullptr; + } +} + +// Walks the streams of one kind in the order the API counts them: the +// store's list, then what only index entries name, each stream once. The walk +// stops as soon as |wanted| is reached, so reading an item of the list never +// walks the index. +class StoreWalk { + public: + // |wanted| is the index to stop at, or -1 to count every stream. + StoreWalk(const CPDF_Document* doc, const KindKeys& keys, int wanted) + : wanted_(wanted) { + const CPDF_Dictionary* catalog = doc->GetRoot(); + RetainPtr store = + catalog ? catalog->GetDictFor("DSS") : nullptr; + if (!store) { + return; + } + if (Visit(store->GetArrayFor(keys.list).Get())) { + return; + } + RetainPtr index = store->GetDictFor("VRI"); + if (!index) { + return; + } + // CPDF_Dictionary iterates its std::map: the entries come in key order. + for (const ByteString& key : index->GetKeys()) { + RetainPtr entry = + index->GetDictFor(key.AsStringView()); + if (entry && Visit(entry->GetArrayFor(keys.index_entry).Get())) { + return; + } + } + } + + int count() const { return count_; } + RetainPtr found() const { return found_; } + + private: + // Returns true when the wanted stream was reached. + bool Visit(const CPDF_Array* array) { + if (!array) { + return false; + } + for (size_t i = 0; i < array->size(); ++i) { + // A stream is always an indirect object, and its number is what makes + // two references the same stream. + RetainPtr reference = + ToReference(array->GetObjectAt(i)); + if (!reference) { + continue; + } + RetainPtr stream = array->GetStreamAt(i); + if (!stream || !seen_.insert(reference->GetRefObjNum()).second) { + continue; + } + if (count_ == wanted_) { + found_ = std::move(stream); + return true; + } + ++count_; + } + return false; + } + + const int wanted_; + int count_ = 0; + std::set seen_; + RetainPtr found_; +}; + +} // namespace + +FPDF_EXPORT int FPDF_CALLCONV EPDFDss_GetCount(FPDF_DOCUMENT document, + int kind) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + const KindKeys* keys = KeysOf(kind); + if (!doc || !keys) { + return -1; + } + // References a base document's objects hold resolve through the layer. + CPDF_DocumentViewScope document_view(doc); + return StoreWalk(doc, *keys, -1).count(); +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV EPDFDss_GetData(FPDF_DOCUMENT document, + int kind, + int index, + void* buffer, + unsigned long length) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + const KindKeys* keys = KeysOf(kind); + if (!doc || !keys || index < 0) { + return 0; + } + CPDF_DocumentViewScope document_view(doc); + RetainPtr stream = StoreWalk(doc, *keys, index).found(); + if (!stream) { + return 0; + } + if (!buffer) { + length = 0; + } + // SAFETY: required from caller. + return DecodeStreamMaybeCopyAndReturnLength( + std::move(stream), + UNSAFE_BUFFERS(pdfium::span(static_cast(buffer), + static_cast(length)))); +} + +FPDF_EXPORT uint32_t FPDF_CALLCONV EPDFDss_Add(FPDF_DOCUMENT document, + int kind, + const void* data, + unsigned long size) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + const KindKeys* keys = KeysOf(kind); + if (!doc || !keys || !data || size == 0) { + return 0; + } + CPDF_DocumentViewScope document_view(doc); + const CPDF_Dictionary* catalog = doc->GetRoot(); + if (!catalog) { + return 0; + } + + // ---- Validate. ---- + // A store or a list that is there and is not what it should be is left as + // it is: writing over it would drop whatever it holds. + RetainPtr store_entry = catalog->GetObjectFor("DSS"); + RetainPtr store = catalog->GetDictFor("DSS"); + if (store_entry && !store) { + return 0; + } + RetainPtr list_entry = + store ? store->GetObjectFor(keys->list) : nullptr; + if (list_entry && !store->GetArrayFor(keys->list)) { + return 0; + } + + // ---- Apply. ---- + // Objects of their own are written through their numbers, so on a layer + // the write lands in the layer's copy and never in the base's. + RetainPtr writable_store; + if (const CPDF_Reference* reference = ToReference(store_entry.Get())) { + writable_store = + ToDictionary(doc->GetMutableIndirectObject(reference->GetRefObjNum())); + } else { + RetainPtr root = doc->GetMutableRoot(); + if (!root) { + return 0; + } + if (store_entry) { + writable_store = root->GetMutableDictFor("DSS"); + } else { + writable_store = doc->NewIndirect(); + root->SetNewFor("DSS", doc, writable_store->GetObjNum()); + } + } + if (!writable_store) { + return 0; + } + RetainPtr writable_list; + if (const CPDF_Reference* reference = ToReference(list_entry.Get())) { + writable_list = + ToArray(doc->GetMutableIndirectObject(reference->GetRefObjNum())); + } else if (list_entry) { + writable_list = writable_store->GetMutableArrayFor(keys->list); + } else { + writable_list = writable_store->SetNewFor(keys->list); + } + if (!writable_list) { + return 0; + } + + // SAFETY: required from caller. + RetainPtr stream = + doc->NewIndirect(UNSAFE_BUFFERS(pdfium::span( + static_cast(data), static_cast(size)))); + writable_list->AppendNew(doc, stream->GetObjNum()); + return stream->GetObjNum(); +} diff --git a/fpdfsdk/epdf_dss_embeddertest.cpp b/fpdfsdk/epdf_dss_embeddertest.cpp new file mode 100644 index 0000000000..5bb27488a5 --- /dev/null +++ b/fpdfsdk/epdf_dss_embeddertest.cpp @@ -0,0 +1,550 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 + +// The document security store (EPDFDss_* in public/epdf_signature.h): what +// it reads from the store's lists and its index, and what adding an item +// writes, in a plain document and in a layer. + +#include "public/epdf_signature.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include "core/fpdfapi/parser/cpdf_array.h" +#include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fpdfapi/parser/cpdf_document_view_scope.h" +#include "core/fpdfapi/parser/cpdf_reference.h" +#include "core/fpdfapi/parser/cpdf_stream.h" +#include "fpdfsdk/cpdfsdk_helpers.h" +#include "fpdfsdk/epdf_edit_test_util.h" +#include "public/cpp/fpdf_scopers.h" +#include "public/epdf_layer.h" +#include "public/epdf_save.h" +#include "public/epdf_view.h" +#include "public/fpdf_save.h" +#include "public/fpdfview.h" +#include "testing/embedder_test.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +constexpr int kKinds[] = {EPDF_DSS_CERTIFICATES, EPDF_DSS_OCSP_RESPONSES, + EPDF_DSS_CRLS}; + +// Bytes as a store holds them: binary, with zero bytes and every value a +// PDF string would have to escape. +std::string Der(const std::string& label) { + return std::string("\x30\x82\x00\x0d\x00)(\\\r\n>", 11) + label; +} + +std::string HexStream(const std::string& data) { + static constexpr char kDigits[] = "0123456789ABCDEF"; + std::string hex; + for (unsigned char byte : data) { + hex += kDigits[byte >> 4]; + hex += kDigits[byte & 0xf]; + } + return Stream("/Filter /ASCIIHexDecode", hex + ">"); +} + +std::vector PageObjects(const std::string& catalog_extra) { + return { + "<< /Type /Catalog /Pages 2 0 R " + catalog_extra + " >>", + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] >>", + }; +} + +std::string PdfWithoutStore() { + return MakePdf(PageObjects("")); +} + +// A store as Acrobat lays one out: an object of its own (4) whose lists are +// objects of their own (5, 6, 7), with an index. +// certificates: 8, 9 OCSP responses: 10 CRLs: 11 +// index: one entry naming 8 and 10 +std::string PdfWithStoreOfObjects() { + std::vector objects = PageObjects("/DSS 4 0 R"); + objects.push_back( + "<< /Certs 5 0 R /OCSPs 6 0 R /CRLs 7 0 R /VRI << /AB12 << /Cert [8 0 R] " + "/OCSP [10 0 R] >> >> >>"); + objects.push_back("[8 0 R 9 0 R]"); + objects.push_back("[10 0 R]"); + objects.push_back("[11 0 R]"); + objects.push_back(Stream(Der("certificate one"))); + objects.push_back(HexStream(Der("certificate two"))); + objects.push_back(Stream(Der("answer one"))); + objects.push_back(Stream(Der("list one"))); + return MakePdf(objects); +} + +// A store written inside the catalog, with its lists inside it. +std::string PdfWithStoreInTheCatalog() { + std::vector objects = + PageObjects("/DSS << /Certs [4 0 R] /CRLs [5 0 R] >>"); + objects.push_back(Stream(Der("certificate one"))); + objects.push_back(Stream(Der("list one"))); + return MakePdf(objects); +} + +// A PDF file opened as a document, or as a layer over it. Both read +// `input`, which must outlive them. +struct OpenedPdf { + std::unique_ptr, + decltype(&EPDF_ReleaseBaseDocument)> + base{nullptr, EPDF_ReleaseBaseDocument}; + ScopedFPDFDocument doc; +}; + +OpenedPdf OpenPdf(const std::string& input, bool layer) { + OpenedPdf opened; + if (layer) { + opened.base.reset( + EPDF_LoadMemBaseDocument64(input.data(), input.size(), nullptr)); + if (opened.base) { + opened.doc.reset( + EPDFLayer_OpenLayer(opened.base.get(), nullptr, nullptr, nullptr)); + } + } else { + // Plain in layer mode too: the layer is the other case. + opened.doc.reset( + FPDF_LoadMemDocument64(input.data(), input.size(), nullptr)); + } + return opened; +} + +// Every item of one kind, read with the two-call protocol. +std::vector ItemsOf(FPDF_DOCUMENT doc, int kind) { + std::vector items; + const int count = EPDFDss_GetCount(doc, kind); + for (int i = 0; i < count; ++i) { + const unsigned long size = EPDFDss_GetData(doc, kind, i, nullptr, 0); + std::string bytes(size, '\0'); + if (size > 0) { + EXPECT_EQ(size, EPDFDss_GetData(doc, kind, i, bytes.data(), size)); + } + items.push_back(bytes); + } + return items; +} + +// Adds an item; on a layer in a transaction, as the engine runs every write. +uint32_t Add(FPDF_DOCUMENT doc, bool layer, int kind, const std::string& data) { + if (layer) { + EXPECT_TRUE(EPDFLayer_BeginTransaction(doc)); + } + const uint32_t number = EPDFDss_Add(doc, kind, data.data(), data.size()); + if (layer) { + EXPECT_TRUE(EPDFLayer_CommitTransaction(doc)); + } + return number; +} + +// The object numbers a revision of |saved| wrote after its first +// |base_size| bytes, the trailer left out. +std::set WrittenAfter(const std::string& saved, + size_t base_size) { + std::set written; + // Plain in layer mode too: revisions are read from a file's own bytes. + ScopedFPDFDocument newer( + FPDF_LoadMemDocument64(saved.data(), saved.size(), nullptr)); + EXPECT_TRUE(newer); + ScopedFPDFDocument older(EPDFDoc_OpenRevision(newer.get(), base_size)); + EXPECT_TRUE(older); + EPDF_OBJECT_DIFF diff = EPDFDoc_CompareRevisions(older.get(), newer.get()); + EXPECT_TRUE(diff); + const int count = EPDFObjectDiff_GetCount(diff); + for (int i = 0; i < count; ++i) { + unsigned int number = 0; + EXPECT_TRUE(EPDFObjectDiff_GetEntry(diff, i, &number, nullptr, nullptr, + nullptr, nullptr, nullptr)); + if (number != 0) { + written.insert(number); + } + } + EPDFObjectDiff_Close(diff); + return written; +} + +} // namespace + +class EPDFDssEmbedderTest : public EmbedderTest {}; + +TEST_F(EPDFDssEmbedderTest, BadArguments) { + const std::string input = PdfWithStoreOfObjects(); + OpenedPdf opened = OpenPdf(input, false); + ASSERT_TRUE(opened.doc); + const std::string data = Der("x"); + + EXPECT_EQ(-1, EPDFDss_GetCount(nullptr, EPDF_DSS_CERTIFICATES)); + EXPECT_EQ(-1, EPDFDss_GetCount(opened.doc.get(), 3)); + EXPECT_EQ(-1, EPDFDss_GetCount(opened.doc.get(), -1)); + + EXPECT_EQ(0u, EPDFDss_GetData(nullptr, EPDF_DSS_CERTIFICATES, 0, nullptr, 0)); + EXPECT_EQ(0u, EPDFDss_GetData(opened.doc.get(), 3, 0, nullptr, 0)); + EXPECT_EQ(0u, EPDFDss_GetData(opened.doc.get(), EPDF_DSS_CERTIFICATES, -1, + nullptr, 0)); + EXPECT_EQ(0u, EPDFDss_GetData(opened.doc.get(), EPDF_DSS_CERTIFICATES, 2, + nullptr, 0)); + + EXPECT_EQ(0u, EPDFDss_Add(nullptr, EPDF_DSS_CRLS, data.data(), data.size())); + EXPECT_EQ(0u, EPDFDss_Add(opened.doc.get(), 3, data.data(), data.size())); + EXPECT_EQ(0u, EPDFDss_Add(opened.doc.get(), EPDF_DSS_CRLS, nullptr, 4)); + EXPECT_EQ(0u, EPDFDss_Add(opened.doc.get(), EPDF_DSS_CRLS, data.data(), 0)); + EXPECT_EQ(1, EPDFDss_GetCount(opened.doc.get(), EPDF_DSS_CRLS)); +} + +TEST_F(EPDFDssEmbedderTest, ADocumentWithoutAStoreReadsEmpty) { + const std::string input = PdfWithoutStore(); + for (bool layer : {false, true}) { + SCOPED_TRACE(layer ? "layer" : "document"); + OpenedPdf opened = OpenPdf(input, layer); + ASSERT_TRUE(opened.doc); + for (int kind : kKinds) { + EXPECT_EQ(0, EPDFDss_GetCount(opened.doc.get(), kind)); + EXPECT_EQ(0u, EPDFDss_GetData(opened.doc.get(), kind, 0, nullptr, 0)); + } + } +} + +TEST_F(EPDFDssEmbedderTest, ReadsEachListDecoded) { + const std::string input = PdfWithStoreOfObjects(); + for (bool layer : {false, true}) { + SCOPED_TRACE(layer ? "layer" : "document"); + OpenedPdf opened = OpenPdf(input, layer); + ASSERT_TRUE(opened.doc); + FPDF_DOCUMENT doc = opened.doc.get(); + + // The second certificate is stored through a filter. + EXPECT_EQ(std::vector( + {Der("certificate one"), Der("certificate two")}), + ItemsOf(doc, EPDF_DSS_CERTIFICATES)); + EXPECT_EQ(std::vector({Der("answer one")}), + ItemsOf(doc, EPDF_DSS_OCSP_RESPONSES)); + EXPECT_EQ(std::vector({Der("list one")}), + ItemsOf(doc, EPDF_DSS_CRLS)); + + // A buffer that can't hold every byte gets none. + const std::string expected = Der("certificate one"); + std::string small(expected.size() - 1, '-'); + EXPECT_EQ(expected.size(), EPDFDss_GetData(doc, EPDF_DSS_CERTIFICATES, 0, + small.data(), small.size())); + EXPECT_EQ(std::string(expected.size() - 1, '-'), small); + } +} + +// The index names streams of the same three kinds. What only it names +// follows the list, in the order of the entries' keys; what a list or an +// earlier entry already named is not read twice; what is not a reference to a +// stream is skipped. +TEST_F(EPDFDssEmbedderTest, ReadsWhatOnlyTheIndexNamesAndEachStreamOnce) { + std::vector objects = PageObjects("/DSS 4 0 R"); + objects.push_back( + "<< /Certs [5 0 R 6 0 R 5 0 R] /CRLs [8 0 R (text) 2 0 R 99 0 R] " + "/VRI << /B << /Cert [6 0 R 9 0 R] /OCSP [7 0 R] >> /A 10 0 R " + "/C (not an entry) >> >>"); + objects.push_back(Stream(Der("certificate one"))); // 5 + objects.push_back(Stream(Der("certificate two"))); // 6 + objects.push_back(Stream(Der("answer one"))); // 7 + objects.push_back(Stream(Der("list one"))); // 8 + objects.push_back(Stream(Der("certificate three"))); // 9 + objects.push_back("<< /Cert [11 0 R] /CRL [8 0 R 12 0 R] /OCSP 7 0 R >>"); + objects.push_back(Stream(Der("certificate four"))); // 11 + objects.push_back(Stream(Der("list two"))); // 12 + const std::string input = MakePdf(objects); + + for (bool layer : {false, true}) { + SCOPED_TRACE(layer ? "layer" : "document"); + OpenedPdf opened = OpenPdf(input, layer); + ASSERT_TRUE(opened.doc); + FPDF_DOCUMENT doc = opened.doc.get(); + + // The list, then entry A (an object of its own), then entry B. + EXPECT_EQ(std::vector( + {Der("certificate one"), Der("certificate two"), + Der("certificate four"), Der("certificate three")}), + ItemsOf(doc, EPDF_DSS_CERTIFICATES)); + // No list of answers at all: the index alone names one. Entry A's + // /OCSP is a stream where an array belongs, and is skipped. + EXPECT_EQ(std::vector({Der("answer one")}), + ItemsOf(doc, EPDF_DSS_OCSP_RESPONSES)); + // A string, a dictionary and a missing object in the list are skipped. + EXPECT_EQ(std::vector({Der("list one"), Der("list two")}), + ItemsOf(doc, EPDF_DSS_CRLS)); + } +} + +TEST_F(EPDFDssEmbedderTest, AddMakesAStoreAsAnObjectOfItsOwn) { + const std::string input = PdfWithoutStore(); + for (bool layer : {false, true}) { + for (FPDF_DWORD flags : {FPDF_NO_INCREMENTAL, FPDF_INCREMENTAL}) { + SCOPED_TRACE(layer ? "layer" : "document"); + SCOPED_TRACE(flags == FPDF_INCREMENTAL ? "incremental" : "rewrite"); + OpenedPdf opened = OpenPdf(input, layer); + ASSERT_TRUE(opened.doc); + FPDF_DOCUMENT doc = opened.doc.get(); + + const uint32_t first = + Add(doc, layer, EPDF_DSS_CERTIFICATES, Der("certificate one")); + const uint32_t second = + Add(doc, layer, EPDF_DSS_OCSP_RESPONSES, Der("answer one")); + const uint32_t third = Add(doc, layer, EPDF_DSS_CRLS, Der("list one")); + const uint32_t fourth = + Add(doc, layer, EPDF_DSS_CERTIFICATES, Der("certificate two")); + // Objects 1 to 3 are the file's; the store is the first new one. + EXPECT_EQ(5u, first); + EXPECT_EQ(6u, second); + EXPECT_EQ(7u, third); + EXPECT_EQ(8u, fourth); + + // Read back before any save. + const std::vector certificates = {Der("certificate one"), + Der("certificate two")}; + EXPECT_EQ(certificates, ItemsOf(doc, EPDF_DSS_CERTIFICATES)); + EXPECT_EQ(std::vector({Der("answer one")}), + ItemsOf(doc, EPDF_DSS_OCSP_RESPONSES)); + EXPECT_EQ(std::vector({Der("list one")}), + ItemsOf(doc, EPDF_DSS_CRLS)); + + { + CPDF_Document* pdf = CPDFDocumentFromFPDFDocument(doc); + CPDF_DocumentViewScope document_view(pdf); + const CPDF_Dictionary* catalog = pdf->GetRoot(); + // The store is an object of its own, its lists are inside it. + RetainPtr reference = + ToReference(catalog->GetObjectFor("DSS")); + ASSERT_TRUE(reference); + EXPECT_EQ(4u, reference->GetRefObjNum()); + RetainPtr store = catalog->GetDictFor("DSS"); + ASSERT_TRUE(store); + EXPECT_EQ(std::vector({"CRLs", "Certs", "OCSPs"}), + store->GetKeys()); + for (const char* key : {"Certs", "OCSPs", "CRLs"}) { + EXPECT_TRUE(ToArray(store->GetObjectFor(key))) << key; + } + // No index, and nothing announced in the catalog. + EXPECT_FALSE(catalog->KeyExist("Extensions")); + // A stream holds the bytes as they were given, and says nothing more. + RetainPtr stream = + store->GetArrayFor("Certs")->GetStreamAt(1); + ASSERT_TRUE(stream); + EXPECT_EQ(fourth, stream->GetObjNum()); + EXPECT_FALSE(stream->HasFilter()); + EXPECT_FALSE(stream->GetDict()->KeyExist("Type")); + } + + ClearString(); + ASSERT_TRUE(FPDF_SaveAsCopy(doc, this, flags)); + const std::string saved = GetString(); + if (flags == FPDF_INCREMENTAL) { + // The catalog, the store and the four streams, and nothing else. + EXPECT_EQ(std::set({1, 4, 5, 6, 7, 8}), + WrittenAfter(saved, input.size())); + } + ScopedSavedDoc reopened = OpenScopedSavedDocument(); + ASSERT_TRUE(reopened); + EXPECT_EQ(certificates, ItemsOf(reopened.get(), EPDF_DSS_CERTIFICATES)); + EXPECT_EQ(std::vector({Der("answer one")}), + ItemsOf(reopened.get(), EPDF_DSS_OCSP_RESPONSES)); + EXPECT_EQ(std::vector({Der("list one")}), + ItemsOf(reopened.get(), EPDF_DSS_CRLS)); + } + } +} + +// Acrobat's layout: the store and each list are objects of their own. An +// item is added to the list's own object; the store and the catalog are not +// written again. +TEST_F(EPDFDssEmbedderTest, AddExtendsAListThatIsAnObjectOfItsOwn) { + const std::string input = PdfWithStoreOfObjects(); + for (bool layer : {false, true}) { + SCOPED_TRACE(layer ? "layer" : "document"); + OpenedPdf opened = OpenPdf(input, layer); + ASSERT_TRUE(opened.doc); + FPDF_DOCUMENT doc = opened.doc.get(); + + const uint32_t added = Add(doc, layer, EPDF_DSS_CRLS, Der("list two")); + EXPECT_EQ(12u, added); + EXPECT_EQ(std::vector({Der("list one"), Der("list two")}), + ItemsOf(doc, EPDF_DSS_CRLS)); + // The other kinds are as they were. + EXPECT_EQ(2, EPDFDss_GetCount(doc, EPDF_DSS_CERTIFICATES)); + EXPECT_EQ(1, EPDFDss_GetCount(doc, EPDF_DSS_OCSP_RESPONSES)); + + { + CPDF_Document* pdf = CPDFDocumentFromFPDFDocument(doc); + CPDF_DocumentViewScope document_view(pdf); + RetainPtr store = + pdf->GetRoot()->GetDictFor("DSS"); + ASSERT_TRUE(store); + RetainPtr list = + ToReference(store->GetObjectFor("CRLs")); + ASSERT_TRUE(list); + EXPECT_EQ(7u, list->GetRefObjNum()); + // The index is untouched: adding writes no entry. + EXPECT_EQ(std::vector({"AB12"}), + store->GetDictFor("VRI")->GetKeys()); + } + + ClearString(); + ASSERT_TRUE(FPDF_SaveAsCopy(doc, this, FPDF_INCREMENTAL)); + EXPECT_EQ(std::set({7, 12}), + WrittenAfter(GetString(), input.size())); + ScopedSavedDoc reopened = OpenScopedSavedDocument(); + ASSERT_TRUE(reopened); + EXPECT_EQ(std::vector({Der("list one"), Der("list two")}), + ItemsOf(reopened.get(), EPDF_DSS_CRLS)); + } +} + +// A store written inside the catalog stays there: the catalog is the one +// object that changes, and a kind it had no list for gets one inside it. +TEST_F(EPDFDssEmbedderTest, AddExtendsAStoreInsideTheCatalog) { + const std::string input = PdfWithStoreInTheCatalog(); + for (bool layer : {false, true}) { + SCOPED_TRACE(layer ? "layer" : "document"); + OpenedPdf opened = OpenPdf(input, layer); + ASSERT_TRUE(opened.doc); + FPDF_DOCUMENT doc = opened.doc.get(); + + EXPECT_EQ(6u, + Add(doc, layer, EPDF_DSS_CERTIFICATES, Der("certificate two"))); + EXPECT_EQ(7u, Add(doc, layer, EPDF_DSS_OCSP_RESPONSES, Der("answer one"))); + EXPECT_EQ(std::vector( + {Der("certificate one"), Der("certificate two")}), + ItemsOf(doc, EPDF_DSS_CERTIFICATES)); + EXPECT_EQ(std::vector({Der("answer one")}), + ItemsOf(doc, EPDF_DSS_OCSP_RESPONSES)); + EXPECT_EQ(std::vector({Der("list one")}), + ItemsOf(doc, EPDF_DSS_CRLS)); + { + CPDF_Document* pdf = CPDFDocumentFromFPDFDocument(doc); + CPDF_DocumentViewScope document_view(pdf); + EXPECT_TRUE(ToDictionary(pdf->GetRoot()->GetObjectFor("DSS"))); + } + + ClearString(); + ASSERT_TRUE(FPDF_SaveAsCopy(doc, this, FPDF_INCREMENTAL)); + EXPECT_EQ(std::set({1, 6, 7}), + WrittenAfter(GetString(), input.size())); + ScopedSavedDoc reopened = OpenScopedSavedDocument(); + ASSERT_TRUE(reopened); + EXPECT_EQ(std::vector( + {Der("certificate one"), Der("certificate two")}), + ItemsOf(reopened.get(), EPDF_DSS_CERTIFICATES)); + } +} + +// A store or a list that is there and is something else is left alone, and +// so is the rest of the document. +TEST_F(EPDFDssEmbedderTest, AddRefusesAStoreItCannotExtend) { + const std::string not_a_store = MakePdf(PageObjects("/DSS [ /Certs 2 0 R ]")); + std::vector objects = PageObjects("/DSS 4 0 R"); + objects.push_back("<< /Certs << /Not /AnArray >> /OCSPs 5 0 R >>"); + objects.push_back("(a string where a list belongs)"); + const std::string bad_lists = MakePdf(objects); + const std::string data = Der("x"); + + for (bool layer : {false, true}) { + SCOPED_TRACE(layer ? "layer" : "document"); + { + OpenedPdf opened = OpenPdf(not_a_store, layer); + ASSERT_TRUE(opened.doc); + CPDF_Document* pdf = CPDFDocumentFromFPDFDocument(opened.doc.get()); + const std::map before = ObjectGraph(pdf); + for (int kind : kKinds) { + EXPECT_EQ(0u, Add(opened.doc.get(), layer, kind, data)); + EXPECT_EQ(0, EPDFDss_GetCount(opened.doc.get(), kind)); + } + EXPECT_EQ(before, ObjectGraph(pdf)); + } + { + OpenedPdf opened = OpenPdf(bad_lists, layer); + ASSERT_TRUE(opened.doc); + CPDF_Document* pdf = CPDFDocumentFromFPDFDocument(opened.doc.get()); + const std::map before = ObjectGraph(pdf); + EXPECT_EQ(0u, Add(opened.doc.get(), layer, EPDF_DSS_CERTIFICATES, data)); + EXPECT_EQ(0u, + Add(opened.doc.get(), layer, EPDF_DSS_OCSP_RESPONSES, data)); + EXPECT_EQ(before, ObjectGraph(pdf)); + // The kind whose list is simply missing is still served. + EXPECT_EQ(6u, Add(opened.doc.get(), layer, EPDF_DSS_CRLS, data)); + EXPECT_EQ(std::vector({data}), + ItemsOf(opened.doc.get(), EPDF_DSS_CRLS)); + } + } +} + +TEST_F(EPDFDssEmbedderTest, AddOnALayerLeavesItsBaseAlone) { + const std::string input = PdfWithStoreOfObjects(); + std::unique_ptr, + decltype(&EPDF_ReleaseBaseDocument)> + base(EPDF_LoadMemBaseDocument64(input.data(), input.size(), nullptr), + EPDF_ReleaseBaseDocument); + ASSERT_TRUE(base); + ScopedFPDFDocument edited( + EPDFLayer_OpenLayer(base.get(), nullptr, nullptr, nullptr)); + ScopedFPDFDocument sibling( + EPDFLayer_OpenLayer(base.get(), nullptr, nullptr, nullptr)); + ASSERT_TRUE(edited); + ASSERT_TRUE(sibling); + EXPECT_NE(0u, Add(edited.get(), true, EPDF_DSS_CERTIFICATES, + Der("certificate three"))); + EXPECT_EQ(3, EPDFDss_GetCount(edited.get(), EPDF_DSS_CERTIFICATES)); + EXPECT_EQ(2, EPDFDss_GetCount(sibling.get(), EPDF_DSS_CERTIFICATES)); + + // The delta carries the new item back onto the base. + ClearString(); + EPDFLayerSaveStatus status = EPDFLayerSaveStatus_kSaveFailed; + ASSERT_TRUE(EPDFLayer_SaveDelta(edited.get(), this, &status)); + std::string delta = GetString(); + FPDF_FILEACCESS delta_access = {}; + delta_access.m_FileLen = delta.size(); + delta_access.m_GetBlock = GetBlockFromString; + delta_access.m_Param = δ + ScopedFPDFDocument replayed( + EPDFLayer_OpenLayer(base.get(), &delta_access, nullptr, nullptr)); + ASSERT_TRUE(replayed); + EXPECT_EQ( + std::vector({Der("certificate one"), Der("certificate two"), + Der("certificate three")}), + ItemsOf(replayed.get(), EPDF_DSS_CERTIFICATES)); +} + +// A save of an encrypted file encrypts the store's streams with everything +// else, and a reader decrypts them back. +TEST_F(EPDFDssEmbedderTest, AnEncryptedSaveKeepsTheStoreReadable) { + const std::string input = PdfWithoutStore(); + std::string encrypted; + { + OpenedPdf opened = OpenPdf(input, false); + ASSERT_TRUE(opened.doc); + ASSERT_TRUE( + EPDF_SetEncryption(opened.doc.get(), "", "owner", EPDF_PERM_PRINT)); + ClearString(); + ASSERT_TRUE(FPDF_SaveAsCopy(opened.doc.get(), this, FPDF_NO_INCREMENTAL)); + encrypted = GetString(); + } + for (bool layer : {false, true}) { + SCOPED_TRACE(layer ? "layer" : "document"); + OpenedPdf opened = OpenPdf(encrypted, layer); + ASSERT_TRUE(opened.doc); + ASSERT_TRUE(EPDF_IsEncrypted(opened.doc.get())); + EXPECT_NE(0u, Add(opened.doc.get(), layer, EPDF_DSS_OCSP_RESPONSES, + Der("answer one"))); + ClearString(); + ASSERT_TRUE(FPDF_SaveAsCopy(opened.doc.get(), this, FPDF_INCREMENTAL)); + EXPECT_EQ(std::string::npos, GetString().find("answer one")); + ScopedSavedDoc reopened = OpenScopedSavedDocument(); + ASSERT_TRUE(reopened); + EXPECT_EQ(std::vector({Der("answer one")}), + ItemsOf(reopened.get(), EPDF_DSS_OCSP_RESPONSES)); + } +} diff --git a/fpdfsdk/epdf_revision_diff.cpp b/fpdfsdk/epdf_revision_diff.cpp index 328853de66..05ae8d6a9a 100644 --- a/fpdfsdk/epdf_revision_diff.cpp +++ b/fpdfsdk/epdf_revision_diff.cpp @@ -1040,3 +1040,55 @@ EPDFObjectDiff_GetReferrer(EPDF_OBJECT_DIFF diff, return NulTerminateMaybeCopyAndReturnLength( edge.label, UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); } + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetLoadedObjectValue(FPDF_DOCUMENT document, + unsigned int obj_num, + char* buffer, + unsigned long buflen, + int* out_read_status, + FPDF_BOOL* out_truncated) { + if (out_read_status) { + *out_read_status = -1; + } + if (out_truncated) { + *out_truncated = false; + } + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + const epdf::RevisionView* view = doc ? epdf::RevisionView::For(doc) : nullptr; + // A table rebuilt by scanning maps numbers to whatever the scan found: a + // value read through it says nothing about a revision. + if (!view || view->parser()->xref_table_rebuilt()) { + return 0; + } + // The same reading and the same serialisation as a diff entry's side, so a + // value from here and a value from a diff compare as text. + RevisionSide side; + if (obj_num == 0) { + RetainPtr trailer = view->parser()->GetCombinedTrailer(); + FillValue(trailer.Get(), nullptr, &side); + } else { + const auto& info = view->parser()->GetCrossRefTable()->objects_info(); + const auto it = info.find(obj_num); + if (it != info.end() && IsLive(it->second)) { + RetainPtr object = view->ParseObject(obj_num); + std::optional signature; + if (object && object->IsStream()) { + signature = StreamSignature(object->AsStream()); + } + FillValue(object.Get(), &signature, &side); + } + } + if (out_read_status) { + *out_read_status = side.read; + } + if (out_truncated) { + *out_truncated = side.value_truncated; + } + if (!side.present || side.value_truncated) { + return 0; + } + // SAFETY: required from caller. + return NulTerminateMaybeCopyAndReturnLength( + side.value, UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} diff --git a/fpdfsdk/epdf_signature_embeddertest.cpp b/fpdfsdk/epdf_signature_embeddertest.cpp index 34f9e412f9..721ffe441f 100644 --- a/fpdfsdk/epdf_signature_embeddertest.cpp +++ b/fpdfsdk/epdf_signature_embeddertest.cpp @@ -15,6 +15,7 @@ #include "core/fpdfapi/parser/cpdf_array.h" #include "core/fpdfapi/parser/cpdf_dictionary.h" #include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fpdfapi/parser/cpdf_number.h" #include "core/fpdfapi/parser/cpdf_string.h" #include "core/fxcrt/data_vector.h" #include "fpdfsdk/cpdfsdk_helpers.h" @@ -2600,3 +2601,156 @@ TEST_F(EPDFSignatureEmbedderTest, FileCandidateSaveSealSpanAndFileDigestAgree) { unsigned long long bad[4] = {0, 10, static_cast(patched.size()), 10}; EXPECT_FALSE(EPDFSig_DigestFileRange(&access, bad, EPDF_DIGEST_SHA256, fdigest, &fdlen)); } + +// --------------------------------------------------------------------------- +// Reading an object an update did not write. +// --------------------------------------------------------------------------- + +namespace { + +struct LoadedValue { + std::string text; + int status = -2; + bool truncated = false; +}; + +LoadedValue ReadLoadedValue(FPDF_DOCUMENT doc, unsigned int obj_num) { + LoadedValue value; + FPDF_BOOL truncated = false; + const unsigned long len = EPDFDoc_GetLoadedObjectValue( + doc, obj_num, nullptr, 0, &value.status, &truncated); + value.truncated = !!truncated; + if (len == 0) { + return value; + } + std::vector buffer(len); + EXPECT_EQ(len, EPDFDoc_GetLoadedObjectValue(doc, obj_num, buffer.data(), len, + nullptr, nullptr)); + value.text = std::string(buffer.data()); + return value; +} + +} // namespace + +// What Acrobat does when it adds a signature field to a page whose /Annots +// is an object of its own: it writes a NEW list and points the page at it. +// The old list stays in the file and no update writes it, so no diff entry +// holds what it listed. +TEST_F(EPDFSignatureEmbedderTest, LoadedObjectValueReadsWhatNoUpdateWrote) { + std::vector b = BasicObjects(); + b[2] = "<>"; + b.push_back("[5 0 R]"); + b.push_back("<>"); + b.push_back("<>\nstream\nabc\nendstream"); + RawPdf old = MakeRawPdf(b); + RawPdf n = AppendRawUpdate(old, 7, "[5 0 R 8 0 R]", 8); + n = AppendRawUpdate(n, 8, "<>", + 9); + n = AppendRawUpdate( + n, 3, "<>", + 9); + + embedpdf_test::ScopedTestDocument newer = OpenRaw(n); + ASSERT_TRUE(newer); + ScopedFPDFDocument older(EPDFDoc_OpenRevision(newer.get(), old.bytes.size())); + ASSERT_TRUE(older); + EPDF_OBJECT_DIFF diff = EPDFDoc_CompareRevisions(older.get(), newer.get()); + ASSERT_TRUE(diff); + const std::vector rows = ReadDiffRows(diff); + + // The old list is not in the diff, and it reads the same from both sides. + EXPECT_EQ(-1, FindRow(rows, 4)); + LoadedValue list = ReadLoadedValue(older.get(), 4); + EXPECT_EQ(EPDF_DIFF_READ_OK, list.status); + EXPECT_FALSE(list.truncated); + EXPECT_EQ("[5 0 R]", list.text); + EXPECT_EQ("[5 0 R]", ReadLoadedValue(newer.get(), 4).text); + + // An object the update added is absent before it and there after it. + LoadedValue added = ReadLoadedValue(older.get(), 7); + EXPECT_EQ(EPDF_DIFF_READ_ABSENT, added.status); + EXPECT_EQ("", added.text); + EXPECT_EQ("[5 0 R 8 0 R]", ReadLoadedValue(newer.get(), 7).text); + EXPECT_EQ(EPDF_DIFF_READ_ABSENT, ReadLoadedValue(newer.get(), 999).status); + + // The same text as the diff's, for an object, a stream and the trailer. + const int page = FindRow(rows, 3); + ASSERT_GE(page, 0); + EXPECT_EQ(ReadDiffValue(diff, page, EPDF_DIFF_OLD), + ReadLoadedValue(older.get(), 3).text); + EXPECT_EQ(ReadDiffValue(diff, page, EPDF_DIFF_NEW), + ReadLoadedValue(newer.get(), 3).text); + const int trailer = FindRow(rows, 0); + ASSERT_GE(trailer, 0); + EXPECT_EQ(ReadDiffValue(diff, trailer, EPDF_DIFF_OLD), + ReadLoadedValue(older.get(), 0).text); + EXPECT_EQ(ReadDiffValue(diff, trailer, EPDF_DIFF_NEW), + ReadLoadedValue(newer.get(), 0).text); + EXPECT_EQ( + "stream(3," + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad)" + "<>", + ReadLoadedValue(newer.get(), 6).text); + EPDFObjectDiff_Close(diff); + + // A buffer that can't hold the value gets none of it. + char small[4] = {'-', '-', '-', '-'}; + EXPECT_EQ(8u, EPDFDoc_GetLoadedObjectValue(older.get(), 4, small, + sizeof(small), nullptr, nullptr)); + EXPECT_EQ('-', small[0]); + + // NULL document: no value, and a status that is none of the three. + int status = 0; + FPDF_BOOL truncated = true; + EXPECT_EQ(0u, EPDFDoc_GetLoadedObjectValue(nullptr, 4, nullptr, 0, &status, + &truncated)); + EXPECT_EQ(-1, status); + EXPECT_FALSE(truncated); +} + +TEST_F(EPDFSignatureEmbedderTest, LoadedObjectValueComesFromBytesNotCache) { + std::vector b = BasicObjects(); + b.push_back("[5 0 R]"); + b.push_back("<>"); + RawPdf p = MakeRawPdf(b); + ScopedFPDFDocument doc = OpenPlainBytes(p.bytes); + ASSERT_TRUE(doc); + { + CPDF_Document* native = CPDFDocumentFromFPDFDocument(doc.get()); + RetainPtr list = ToArray(native->GetMutableIndirectObject(4)); + ASSERT_TRUE(list); + list->AppendNew(7); + native->NewIndirect(); + } + // An unsaved edit is not part of the loaded bytes, and neither is an + // object made in memory. + EXPECT_EQ("[5 0 R]", ReadLoadedValue(doc.get(), 4).text); + EXPECT_EQ(EPDF_DIFF_READ_ABSENT, ReadLoadedValue(doc.get(), 6).status); +} + +TEST_F(EPDFSignatureEmbedderTest, LoadedObjectValueSeparatesFailureFromNull) { + // As in DiffReadStatusSeparatesFailureFromNull: object 4's entry points two + // bytes into the object, so its mapping is live and its bytes do not parse. + std::vector b = BasicObjects("/Extra 4 0 R"); + b.push_back("<>"); + RawPdf old = MakeRawPdf(b); + RawPdf broken = AppendRawUpdate(old, 4, "<>", 5); + { + const size_t row = broken.bytes.find("xref\n4 1\n", broken.xref) + 9; + ASSERT_NE(std::string::npos, row); + const size_t offset = std::stoul(broken.bytes.substr(row, 10)); + char fixed[16]; + snprintf(fixed, sizeof(fixed), "%010zu", offset + 2); + broken.bytes.replace(row, 10, fixed); + } + embedpdf_test::ScopedTestDocument dnew = OpenRaw(broken); + ASSERT_TRUE(dnew); + ScopedFPDFDocument dold(EPDFDoc_OpenRevision(dnew.get(), old.bytes.size())); + ASSERT_TRUE(dold); + LoadedValue before = ReadLoadedValue(dold.get(), 4); + EXPECT_EQ(EPDF_DIFF_READ_OK, before.status); + EXPECT_EQ("<>", before.text); + LoadedValue after = ReadLoadedValue(dnew.get(), 4); + EXPECT_EQ(EPDF_DIFF_READ_FAILED, after.status); + EXPECT_EQ("null", after.text); +} diff --git a/public/epdf_signature.h b/public/epdf_signature.h index 8228d58225..fde8b4f90a 100644 --- a/public/epdf_signature.h +++ b/public/epdf_signature.h @@ -491,6 +491,32 @@ EPDFObjectDiff_GetRevisionHealth(EPDF_OBJECT_DIFF diff, unsigned int* out_bare_reference_count, FPDF_BOOL* out_referrers_complete); +// Experimental EmbedPDF Extension API. +// The shallow serialisation of object |obj_num| as |document|'s loaded bytes +// hold it, in the form EPDFObjectDiff_GetValue() returns, for ANY object +// number. A diff carries the values of the objects an update wrote; a rule +// sometimes has to read one it did not write, such as the /Annots array a +// page pointed at before an update pointed it at a new array and left the old +// one in the file. |document| is a revision opened with +// EPDFDoc_OpenRevision(), or the document itself for its last revision; +// |obj_num| 0 reads the trailer. NUL-terminated, two-call protocol. +// +// |out_read_status| receives EPDF_DIFF_READ_*: ABSENT when the number is +// free or unknown in those bytes (the call returns 0), FAILED when its +// mapping is live but the bytes do not parse (the value is "null" and is not +// evidence, as in the diff), or -1 when the document is NULL or has no +// parser. |out_truncated| is set when the serialisation exceeded the cap +// (1 MiB); the call then returns 0 and carries no value evidence. Either +// out-param may be NULL. Like the diff, this parses the loaded bytes and +// never reads the document's object cache. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetLoadedObjectValue(FPDF_DOCUMENT document, + unsigned int obj_num, + char* buffer, + unsigned long buflen, + int* out_read_status, + FPDF_BOOL* out_truncated); + // --------------------------------------------------------------------------- // Digests. // --------------------------------------------------------------------------- @@ -730,6 +756,66 @@ EPDFSig_WriteContents(unsigned char* buffer, const unsigned char* der, unsigned long der_len); +// --------------------------------------------------------------------------- +// Document security store (/DSS, ISO 32000-2 12.8.4.3). +// +// The catalog's /DSS holds what a later reader needs to check the document's +// signatures without asking anyone: certificates (/Certs), OCSP responses +// (/OCSPs) and CRLs (/CRLs), each a stream holding one DER object. Its +// optional index (/VRI) points at streams of the same three kinds, one entry +// per signature. +// +// These functions move bytes and nothing more. They parse none of them, look +// for no duplicates and never remove anything: what is worth storing, and +// what a stored item proves, is the caller's decision. +// +// Unlike the revision functions above, they read and write the document as +// it is now: an item added and not yet saved is read back. +// --------------------------------------------------------------------------- + +#define EPDF_DSS_CERTIFICATES 0 // /Certs, and /Cert of an index entry +#define EPDF_DSS_OCSP_RESPONSES 1 // /OCSPs, and /OCSP of an index entry +#define EPDF_DSS_CRLS 2 // /CRLs, and /CRL of an index entry + +// Experimental EmbedPDF Extension API. +// Number of streams of |kind| (EPDF_DSS_*) the store references: those of +// its list, in the list's order, followed by those only an index entry names, +// in the order of the entries' keys. A stream referenced more than once +// counts once. An entry that is not a reference to a stream is skipped. +// 0 when the document has no store; -1 for a NULL document or an unknown +// kind. Reads the catalog and the store, and loads no page. +FPDF_EXPORT int FPDF_CALLCONV EPDFDss_GetCount(FPDF_DOCUMENT document, + int kind); + +// Experimental EmbedPDF Extension API. +// The decoded data of stream |index| of |kind|, in the order +// EPDFDss_GetCount() counts them. Two-call protocol: returns the data's +// length; nothing is copied unless |buffer| holds at least that many bytes. +// 0 when |index| is out of range or the stream's data cannot be decoded. +// Each call walks the store up to |index|. +FPDF_EXPORT unsigned long FPDF_CALLCONV EPDFDss_GetData(FPDF_DOCUMENT document, + int kind, + int index, + void* buffer, + unsigned long length); + +// Experimental EmbedPDF Extension API. +// Store |size| bytes of |data| as a new stream and append it to the list of +// |kind|. A store the document lacks is created as an object of its own, and +// a list the store lacks as an array inside it; a store or a list that is +// already an object of its own is extended where it is. The stream holds the +// bytes as given, with no filter and no /Type. Nothing else is written: no +// index entry and no /Extensions. Returns the new stream's object number. +// +// Validate-then-apply: returns 0 and leaves the document untouched when the +// document or |data| is NULL, |size| is 0, the kind is unknown, the document +// has no catalog, or the catalog's /DSS or the kind's list exists and is not +// a dictionary or an array. +FPDF_EXPORT uint32_t FPDF_CALLCONV EPDFDss_Add(FPDF_DOCUMENT document, + int kind, + const void* data, + unsigned long size); + #ifdef __cplusplus } // extern "C" #endif // __cplusplus