diff --git a/docs/embedpdf/fork-ledger.md b/docs/embedpdf/fork-ledger.md index e50c001d0..c2a1c6909 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 5c4403acd..1ab04ad4d 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 deb090996..28f5d75a4 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 000000000..1a88328ee --- /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 000000000..5bb27488a --- /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 328853de6..05ae8d6a9 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 34f9e412f..721ffe441 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 8228d5822..fde8b4f90 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