diff --git a/core/fpdfapi/parser/cpdf_base_document.h b/core/fpdfapi/parser/cpdf_base_document.h index 6376a4bb7..8f9d4a9b3 100644 --- a/core/fpdfapi/parser/cpdf_base_document.h +++ b/core/fpdfapi/parser/cpdf_base_document.h @@ -47,6 +47,11 @@ class CPDF_BaseDocument final : public CPDF_Document, public Retainable { // full pass over the file — unless the embedder supplied it through // SetKnownRawBaseSha256(). Reads all-zero when the file cannot be read. const std::array& GetRawBaseSha256() const; + // The same hash when it is known already (supplied, or worked out once), + // else null: never hashes the file. + const std::array* GetKnownRawBaseSha256() const { + return raw_base_sha256_ ? &*raw_base_sha256_ : nullptr; + } // A host that already hashed the exact bytes it handed us (a storage layer // verifying a download) need not pay for a second pass. Only an identity // claim: a wrong value makes this host's own artifacts fail to open on the diff --git a/core/fpdfdoc/cpdf_bookmarktree.cpp b/core/fpdfdoc/cpdf_bookmarktree.cpp index ae34e6396..996b38e10 100644 --- a/core/fpdfdoc/cpdf_bookmarktree.cpp +++ b/core/fpdfdoc/cpdf_bookmarktree.cpp @@ -17,11 +17,44 @@ CPDF_BookmarkTree::CPDF_BookmarkTree(const CPDF_Document* doc) CPDF_BookmarkTree::~CPDF_BookmarkTree() = default; +// EmbedPDF(bug-fix): see the declaration. An item has no /Type, and the +// outline's dictionary may say /Outlines; the other keys belong to pages and +// the page tree (/Kids /Contents /MediaBox /Resources), annotations and +// fields (/Subtype /FT) and actions (/S, whose /Next chains actions). +// static +bool CPDF_BookmarkTree::IsOutlineNode(const CPDF_Dictionary* dict) { + if (!dict) { + return false; + } + if (dict->KeyExist("Type") && dict->GetNameFor("Type") != "Outlines") { + return false; + } + for (const char* key : + {"Subtype", "FT", "S", "Kids", "Contents", "MediaBox", "Resources"}) { + if (dict->KeyExist(key)) { + return false; + } + } + return true; +} + +namespace { + +// EmbedPDF(bug-fix): |dict| as a bookmark when IsOutlineNode() takes it. +CPDF_Bookmark OutlineItem(RetainPtr dict) { + return CPDF_BookmarkTree::IsOutlineNode(dict.Get()) + ? CPDF_Bookmark(std::move(dict)) + : CPDF_Bookmark(); +} + +} // namespace + CPDF_Bookmark CPDF_BookmarkTree::GetFirstChild( const CPDF_Bookmark& parent) const { const CPDF_Dictionary* parent_dict = parent.GetDict(); if (parent_dict) { - return CPDF_Bookmark(parent_dict->GetDictFor("First")); + // EmbedPDF(bug-fix): only an item IsOutlineNode() takes. + return OutlineItem(parent_dict->GetDictFor("First")); } const CPDF_Dictionary* root = document_->GetRoot(); @@ -31,8 +64,10 @@ CPDF_Bookmark CPDF_BookmarkTree::GetFirstChild( RetainPtr outlines = root->GetDictFor(pdfium::catalog::kOutlines); - return outlines ? CPDF_Bookmark(outlines->GetDictFor("First")) - : CPDF_Bookmark(); + // EmbedPDF(bug-fix): only an outline and item IsOutlineNode() takes. + return IsOutlineNode(outlines.Get()) + ? OutlineItem(outlines->GetDictFor("First")) + : CPDF_Bookmark(); } CPDF_Bookmark CPDF_BookmarkTree::GetNextSibling( @@ -43,5 +78,6 @@ CPDF_Bookmark CPDF_BookmarkTree::GetNextSibling( } RetainPtr next = dict->GetDictFor("Next"); - return next != dict ? CPDF_Bookmark(std::move(next)) : CPDF_Bookmark(); + // EmbedPDF(bug-fix): only an item IsOutlineNode() takes. + return next != dict ? OutlineItem(std::move(next)) : CPDF_Bookmark(); } diff --git a/core/fpdfdoc/cpdf_bookmarktree.h b/core/fpdfdoc/cpdf_bookmarktree.h index 144879bdf..866a6ac0e 100644 --- a/core/fpdfdoc/cpdf_bookmarktree.h +++ b/core/fpdfdoc/cpdf_bookmarktree.h @@ -10,6 +10,8 @@ #include "core/fpdfdoc/cpdf_bookmark.h" #include "core/fxcrt/unowned_ptr.h" +// EmbedPDF(bug-fix): IsOutlineNode() below reads one. +class CPDF_Dictionary; class CPDF_Document; class CPDF_BookmarkTree { @@ -17,6 +19,13 @@ class CPDF_BookmarkTree { explicit CPDF_BookmarkTree(const CPDF_Document* doc); ~CPDF_BookmarkTree(); + // EmbedPDF(bug-fix): upstream took any dictionary that /Outlines, /First or + // /Next reaches as part of the outline, so a broken file that links a page, + // a field or an action there showed it as a bookmark, and an outline + // writer rewrote its links. Whether |dict| can be the outline's dictionary + // or one of its items: it has no key that gives a dictionary another role. + static bool IsOutlineNode(const CPDF_Dictionary* dict); + CPDF_Bookmark GetFirstChild(const CPDF_Bookmark& parent) const; CPDF_Bookmark GetNextSibling(const CPDF_Bookmark& bookmark) const; diff --git a/fpdfsdk/epdf_c_api_test.c b/fpdfsdk/epdf_c_api_test.c index aecddb9de..99c0ec32c 100644 --- a/fpdfsdk/epdf_c_api_test.c +++ b/fpdfsdk/epdf_c_api_test.c @@ -217,6 +217,10 @@ int CheckEmbedPDFCApi(void) { CHK(EPDFDoc_GetAttachmentKey); // epdf_capture.h + CHK(EPDFAttachment_ExportCanonicalToOwnedBuffer); + CHK(EPDFAttachment_ExportRawToOwnedBuffer); + CHK(EPDFAttachment_ImportRaw); + CHK(EPDFDoc_ExportDictCanonicalToOwnedBuffer); CHK(EPDFDoc_ExportDictRawToOwnedBuffer); CHK(EPDFDoc_ImportDictRaw); CHK(EPDFForm_ExportFieldRawToOwnedBuffer); diff --git a/fpdfsdk/epdf_capture.cpp b/fpdfsdk/epdf_capture.cpp index 467098186..2c549dca5 100644 --- a/fpdfsdk/epdf_capture.cpp +++ b/fpdfsdk/epdf_capture.cpp @@ -14,6 +14,7 @@ #include "core/fpdfapi/edit/cpdf_stringarchivestream.h" #include "core/fpdfapi/parser/cpdf_array.h" +#include "core/fpdfapi/parser/cpdf_base_document.h" #include "core/fpdfapi/parser/cpdf_dictionary.h" #include "core/fpdfapi/parser/cpdf_document_view_scope.h" #include "core/fpdfapi/parser/cpdf_layer_document.h" @@ -21,8 +22,11 @@ #include "core/fpdfapi/parser/cpdf_number.h" #include "core/fpdfapi/parser/cpdf_reference.h" #include "core/fpdfapi/parser/cpdf_stream.h" +#include "core/fpdfapi/parser/cpdf_stream_acc.h" #include "core/fpdfapi/parser/cpdf_string.h" #include "core/fpdfapi/parser/cpdf_syntax_parser.h" +#include "core/fpdfdoc/cpdf_bookmarktree.h" +#include "core/fpdfdoc/cpdf_nametree.h" #include "core/fxcrt/bytestring.h" #include "core/fxcrt/cfx_read_only_span_stream.h" #include "core/fxcrt/compiler_specific.h" @@ -60,6 +64,51 @@ bool IsOutlineLinkKey(ByteStringView key) { key == "SE"; } +// The keys a page inherits from the page tree above it (ISO 32000-2 +// 7.7.3.4). +constexpr const char* kInheritableKeys[] = {"Resources", "MediaBox", "CropBox", + "Rotate"}; + +bool IsInheritableKey(ByteStringView key) { + return std::ranges::any_of(kInheritableKeys, [key](const char* inheritable) { + return key == inheritable; + }); +} + +// The value the nearest /Pages node above |page| gives the inheritable |key|, +// as that node holds it (a reference stays one), or null when none does. +RetainPtr InheritedValueOf(const CPDF_Dictionary* page, + const char* key) { + std::set seen; + for (RetainPtr node = page->GetDictFor("Parent"); + node && seen.insert(node.Get()).second; + node = node->GetDictFor("Parent")) { + if (RetainPtr value = node->GetObjectFor(key)) { + return value; + } + } + return nullptr; +} + +// |value| as PDF syntax, which tells two values apart: a reference reads as +// its object number, anything else as what it holds. +DataVector SyntaxOf(const CPDF_Object* value) { + fxcrt::ostringstream text; + CPDF_StringArchiveStream archive(&text); + if (!value || !value->WriteTo(&archive, nullptr)) { + return {}; + } + const auto bytes = text.str(); + return DataVector(bytes.begin(), bytes.end()); +} + +// Whether |dict| can be an embedded file's file specification: it says so, +// or it holds the file (/EF). +bool IsFileSpec(const CPDF_Dictionary* dict) { + return dict && + (dict->GetNameFor("Type") == "Filespec" || dict->KeyExist("EF")); +} + // Objects a capture never holds: the catalog, the page tree and its pages, // save the page a page capture is of (CaptureWriter::AddPage()). bool IsBoundaryObject(const CPDF_Object* object) { @@ -77,6 +126,39 @@ CPDF_LayerDocument* LayerOf(FPDF_DOCUMENT document) { CPDFDocumentFromFPDFDocument(document)); } +ByteString HexOf(const std::array& sha256) { + static constexpr char kHexDigits[] = "0123456789abcdef"; + ByteString hex; + for (uint8_t byte : sha256) { + hex += kHexDigits[byte >> 4]; + hex += kHexDigits[byte & 0xf]; + } + return hex; +} + +// A capture's manifest: its |kind|, and the file it was taken on (Base: the +// SHA-256 of the layer's base, in hex) when the layer knows it already, so +// taking a capture never costs hashing the file. A host that keeps captures +// beyond a session supplies the hash (EPDF_SetBaseDocumentSha256()). +RetainPtr NewManifest(CPDF_LayerDocument* layer, + const char* kind) { + auto manifest = pdfium::MakeRetain(); + manifest->SetNewFor("Kind", kind); + if (const std::array* sha256 = + layer->GetBaseDocument()->GetKnownRawBaseSha256()) { + manifest->SetNewFor("Base", HexOf(*sha256)); + } + return manifest; +} + +// Whether |manifest| comes from |layer|'s file, as far as it says: one that +// names a file comes back only into a layer of that file. +bool IsOfFile(const CPDF_Dictionary* manifest, CPDF_LayerDocument* layer) { + const ByteString base = manifest->GetByteStringFor("Base"); + return base.IsEmpty() || + base == HexOf(layer->GetBaseDocument()->GetRawBaseSha256()); +} + // The object numbers |array| refers to, in order (0 for a direct entry). std::vector RefsIn(const CPDF_Array* array) { std::vector refs; @@ -111,11 +193,18 @@ void InsertReference(CPDF_Document* doc, objnum); } +// How a capture writes a stream: its bytes as stored, so it comes back byte +// for byte; or as it reads, so a save that only encodes it again changes +// nothing (the canonical bytes a guard compares). +enum class StreamBytes { kAsStored, kAsRead }; + // Collects the layer's versions of what a capture's roots reach, and writes // the capture. class CaptureWriter { public: - explicit CaptureWriter(CPDF_LayerDocument* layer) : layer_(layer) {} + explicit CaptureWriter(CPDF_LayerDocument* layer, + StreamBytes stream_bytes = StreamBytes::kAsStored) + : layer_(layer), stream_bytes_(stream_bytes) {} // Captures the layer's version of |objnum|, if it holds one, and what the // object reaches. @@ -153,7 +242,8 @@ class CaptureWriter { for (const auto& [objnum, object] : objects_) { ok = ok && archive.WriteDWord(objnum) && archive.WriteString(" ") && archive.WriteDWord(object->GetGenNum()) && - archive.WriteString(" obj\n") && WriteBody(&archive, object.Get()) && + archive.WriteString(" obj\n") && + WriteBody(&archive, object.Get(), stream_bytes_) && archive.WriteString("\nendobj\n"); } if (!ok) { @@ -240,20 +330,31 @@ class CaptureWriter { } // An object's body. A stream is written with its bytes as stored, never - // encoded again, so it comes back byte for byte. - static bool WriteBody(IFX_ArchiveStream* archive, const CPDF_Object* object) { + // encoded again, so it comes back byte for byte; or, as it reads, its + // decoded bytes without the keys that say how they were stored. + static bool WriteBody(IFX_ArchiveStream* archive, + const CPDF_Object* object, + StreamBytes stream_bytes) { const CPDF_Stream* stream = object->AsStream(); if (!stream) { return object->WriteTo(archive, nullptr); } + RetainPtr dict = ToDictionary(stream->GetDict()->Clone()); DataVector raw; - if (stream->IsMemoryBased()) { + if (stream_bytes == StreamBytes::kAsRead) { + auto read = + pdfium::MakeRetain(pdfium::WrapRetain(stream)); + read->LoadAllDataFiltered(); + raw = read->DetachData(); + for (const char* key : {"Filter", "DecodeParms", "DL"}) { + dict->RemoveFor(key); + } + } else if (stream->IsMemoryBased()) { pdfium::span data = stream->GetInMemoryRawData(); raw.assign(data.begin(), data.end()); } else { raw = stream->ReadAllRawData(); } - RetainPtr dict = ToDictionary(stream->GetDict()->Clone()); dict->SetNewFor("Length", pdfium::checked_cast(raw.size())); return dict->WriteTo(archive, nullptr) && @@ -262,6 +363,7 @@ class CaptureWriter { } UnownedPtr const layer_; + const StreamBytes stream_bytes_; std::set outline_items_; std::set pages_; std::set visited_; @@ -276,8 +378,10 @@ struct Capture { }; // Reads |data| into objects |layer| holds, or nullopt when it is not a -// whole, well-formed capture of kind |kind|, or names a number above the -// layer's last one. Nothing is written. +// whole, well-formed capture of kind |kind|, names a file other than +// |layer|'s or a number above the layer's last one, or holds an object no +// capture holds (the catalog, the page tree, a page other than the one a +// page capture is of). Nothing is written. std::optional ReadCapture(CPDF_LayerDocument* layer, pdfium::span data, ByteStringView kind) { @@ -290,9 +394,12 @@ std::optional ReadCapture(CPDF_LayerDocument* layer, parser.SetPos(header.GetLength()); Capture capture; capture.manifest = ToDictionary(parser.GetObjectBody(layer)); - if (!capture.manifest || capture.manifest->GetNameFor("Kind") != kind) { + if (!capture.manifest || capture.manifest->GetNameFor("Kind") != kind || + !IsOfFile(capture.manifest.Get(), layer)) { return std::nullopt; } + const int own_page = + kind == "Page" ? capture.manifest->GetIntegerFor("Page") : 0; while (true) { parser.ToNextWord(); if (parser.GetPos() >= parser.GetDocumentSize()) { @@ -305,7 +412,9 @@ std::optional ReadCapture(CPDF_LayerDocument* layer, } const uint32_t objnum = object->GetObjNum(); if (!objnum || objnum > layer->GetLastObjNum() || - capture.objects.contains(objnum)) { + capture.objects.contains(objnum) || + (IsBoundaryObject(object.Get()) && + objnum != static_cast(own_page))) { return std::nullopt; } // A stream read from the capture keeps its bytes in memory, as stored. @@ -412,8 +521,7 @@ EPDFPage_ExportAnnotsRawToOwnedBuffer(FPDF_DOCUMENT document, } const std::vector refs = RefsIn(annots.Get()); - auto manifest = pdfium::MakeRetain(); - manifest->SetNewFor("Kind", "Annots"); + RetainPtr manifest = NewManifest(layer, "Annots"); manifest->SetNewFor( "Page", pdfium::checked_cast(page->GetObjNum())); auto members = manifest->SetNewFor("Members"); @@ -506,8 +614,7 @@ EPDFDoc_ExportDictRawToOwnedBuffer(FPDF_DOCUMENT document, if (!dict) { return nullptr; } - auto manifest = pdfium::MakeRetain(); - manifest->SetNewFor("Kind", "Dict"); + RetainPtr manifest = NewManifest(layer, "Dict"); manifest->SetNewFor("Obj", pdfium::checked_cast(objnum)); manifest->SetNewFor( "Gen", pdfium::checked_cast(dict->GetGenNum())); @@ -525,6 +632,48 @@ EPDFDoc_ExportDictRawToOwnedBuffer(FPDF_DOCUMENT document, return ToOwnedBuffer(writer.Write(manifest.Get()), out_size); } +FPDF_EXPORT void* FPDF_CALLCONV +EPDFDoc_ExportDictCanonicalToOwnedBuffer(FPDF_DOCUMENT document, + uint32_t objnum, + FPDF_BYTESTRING deep_keys, + FPDF_BYTESTRING skip_keys, + unsigned long* out_size) { + if (out_size) { + *out_size = 0; + } + CPDF_LayerDocument* layer = LayerOf(document); + if (!layer || !objnum) { + return nullptr; + } + CPDF_DocumentViewScope view(layer); + RetainPtr dict = + ToDictionary(layer->GetOrParseIndirectObject(objnum)); + if (!dict) { + return nullptr; + } + // No /Base: whether the layer knows its file's hash may change between two + // reads, and the bytes must not. + auto manifest = pdfium::MakeRetain(); + manifest->SetNewFor("Kind", "Dict"); + manifest->SetNewFor("Obj", pdfium::checked_cast(objnum)); + RetainPtr value = ToDictionary(dict->Clone()); + for (const ByteString& key : + fxcrt::Split(ByteString(skip_keys ? skip_keys : ""), ' ')) { + if (!key.IsEmpty()) { + value->RemoveFor(key.AsStringView()); + } + } + manifest->SetFor("Value", value); + CaptureWriter writer(layer, StreamBytes::kAsRead); + for (const ByteString& key : + fxcrt::Split(ByteString(deep_keys ? deep_keys : ""), ' ')) { + if (!key.IsEmpty()) { + writer.AddReachOf(value->GetObjectFor(key.AsStringView()).Get()); + } + } + return ToOwnedBuffer(writer.Write(manifest.Get()), out_size); +} + FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFDoc_ImportDictRaw(FPDF_DOCUMENT document, const void* capture, @@ -614,8 +763,7 @@ EPDFForm_ExportFieldRawToOwnedBuffer(FPDF_DOCUMENT document, } } - auto manifest = pdfium::MakeRetain(); - manifest->SetNewFor("Kind", "Field"); + RetainPtr manifest = NewManifest(layer, "Field"); manifest->SetNewFor("Field", pdfium::checked_cast(field_objnum)); CaptureWriter writer(layer); @@ -795,8 +943,7 @@ EPDFOutline_ExportItemRawToOwnedBuffer(FPDF_DOCUMENT document, // The item, where it was (In: its parent, 0 for the top level; After: the // sibling it followed, 0 when it was the first), and the items under it. - auto manifest = pdfium::MakeRetain(); - manifest->SetNewFor("Kind", "OutlineItem"); + RetainPtr manifest = NewManifest(layer, "OutlineItem"); manifest->SetNewFor("Item", pdfium::checked_cast(objnum)); manifest->SetNewFor( "In", parent == *root ? 0 : pdfium::checked_cast(parent)); @@ -842,10 +989,15 @@ EPDFOutline_ImportItemRaw(FPDF_DOCUMENT document, if (epdf::IsOutlineItem(layer, *root, item)) { return false; } - // It comes back from the capture, or as the version the layer reads now - // (the file's original, when the layer never edited it). - if (!read->objects.contains(item) && - !ToDictionary(layer->GetOrParseIndirectObject(item))) { + // What the number holds once the capture is in, which must be an outline + // item: the layer's own version stays (InstallObjects()), else the + // capture's comes back, else the file's original. + auto captured = read->objects.find(static_cast(item)); + RetainPtr comes_back = + layer->FindLayerVersion(item) || captured == read->objects.end() + ? layer->GetOrParseIndirectObject(item) + : captured->second; + if (!CPDF_BookmarkTree::IsOutlineNode(ToDictionary(comes_back).Get())) { return false; } std::vector children; @@ -902,8 +1054,7 @@ EPDFPage_ExportRawToOwnedBuffer(FPDF_DOCUMENT document, // The page, where it was (After: the page it followed, 0 when it was the // first), and the keys that name it. - auto manifest = pdfium::MakeRetain(); - manifest->SetNewFor("Kind", "Page"); + RetainPtr manifest = NewManifest(layer, "Page"); manifest->SetNewFor("Page", pdfium::checked_cast(page_objnum)); manifest->SetNewFor( @@ -913,6 +1064,19 @@ EPDFPage_ExportRawToOwnedBuffer(FPDF_DOCUMENT document, names->AppendNew(key.AsStringView()); } CaptureWriter writer(layer); + // What it takes from the page tree above it (Inherited): an import may put + // it under another /Pages node, so it brings these back as its own. + RetainPtr page = layer->GetPageDictionary(page_index); + auto inherited = manifest->SetNewFor("Inherited"); + for (const char* key : kInheritableKeys) { + RetainPtr value = page && !page->KeyExist(key) + ? InheritedValueOf(page.Get(), key) + : nullptr; + if (value) { + inherited->SetFor(key, value->Clone()); + writer.AddReachOf(value.Get()); + } + } writer.AddPage(page_objnum); return ToOwnedBuffer(writer.Write(manifest.Get()), out_size); } @@ -949,10 +1113,17 @@ EPDFPage_ImportRaw(FPDF_DOCUMENT document, // file's original, when the layer never edited it). auto captured = read->objects.find(page); const bool in_capture = captured != read->objects.end(); + // What the number holds once the capture is in: the layer's own version + // stays (InstallObjects()), save the null a deleted layer page left, which + // the capture's page replaces; without a version of the layer's, the + // capture's, or the file's original. + RetainPtr held = layer->FindLayerVersion(page); + const bool over_null = held && held->IsNull(); RetainPtr reads_now = ToDictionary(layer->GetOrParseIndirectObject(page)); - const CPDF_Dictionary* comes_back = - in_capture ? captured->second->AsDictionary() : reads_now.Get(); + const CPDF_Dictionary* comes_back = in_capture && (!held || over_null) + ? captured->second->AsDictionary() + : reads_now.Get(); if (!comes_back || comes_back->GetNameFor("Type") != "Page") { return false; } @@ -965,20 +1136,34 @@ EPDFPage_ImportRaw(FPDF_DOCUMENT document, // A page the layer made reads as null once deleted (SetPageToNullObject(), // so nothing that still points at it saves it): it comes back over that // null. - if (in_capture) { - RetainPtr held = layer->FindLayerVersion(page); - if (held && held->IsNull()) { - layer->DeleteIndirectObject(page); - } + if (in_capture && over_null) { + layer->DeleteIndirectObject(page); } if (!InstallObjects(layer, *read)) { return false; } RetainPtr page_dict = ToDictionary(layer->GetMutableIndirectObject(page)); - if (!page_dict || !layer->InsertExistingPage(slot, std::move(page_dict))) { + if (!page_dict || !layer->InsertExistingPage(slot, page_dict)) { return false; } + // What it took from the page tree where it was, as its own key where its + // place now gives otherwise; back under the same node, nothing changes. + if (RetainPtr inherited = + read->manifest->GetDictFor("Inherited")) { + CPDF_DictionaryLocker locker(inherited); + for (const auto& [key, value] : locker) { + if (!IsInheritableKey(key.AsStringView()) || + page_dict->KeyExist(key.AsStringView())) { + continue; + } + RetainPtr now = + InheritedValueOf(page_dict.Get(), key.c_str()); + if (SyntaxOf(now.Get()) != SyntaxOf(value.Get())) { + page_dict->SetFor(key, value->Clone()); + } + } + } // A key another page holds now stays with it. RetainPtr names = read->manifest->GetArrayFor("Names"); for (size_t i = 0; names && i < names->size(); ++i) { @@ -986,3 +1171,138 @@ EPDFPage_ImportRaw(FPDF_DOCUMENT document, } return true; } + +FPDF_EXPORT void* FPDF_CALLCONV +EPDFAttachment_ExportRawToOwnedBuffer(FPDF_DOCUMENT document, + FPDF_WIDESTRING key, + unsigned long* out_size) { + if (out_size) { + *out_size = 0; + } + CPDF_LayerDocument* layer = LayerOf(document); + if (!layer || !key) { + return nullptr; + } + CPDF_DocumentViewScope view(layer); + // SAFETY: required from caller. + const WideString name = UNSAFE_BUFFERS(WideStringFromFPDFWideString(key)); + auto tree = CPDF_NameTree::CreateForReading(layer, "EmbeddedFiles"); + RetainPtr spec = + tree && !name.IsEmpty() ? ToDictionary(tree->LookupValue(name)) : nullptr; + if (!spec) { + return nullptr; + } + + // The file, under its key (Key): its file specification, an object of its + // own (Obj) or written in the tree (Value), and the objects the layer + // holds that it reaches. + RetainPtr manifest = NewManifest(layer, "Attachment"); + manifest->SetNewFor("Key", name.AsStringView()); + CaptureWriter writer(layer); + if (const uint32_t objnum = spec->GetObjNum()) { + manifest->SetNewFor("Obj", pdfium::checked_cast(objnum)); + writer.AddObject(objnum); + } else { + manifest->SetFor("Value", spec->Clone()); + writer.AddReachOf(spec.Get()); + } + return ToOwnedBuffer(writer.Write(manifest.Get()), out_size); +} + +FPDF_EXPORT void* FPDF_CALLCONV +EPDFAttachment_ExportCanonicalToOwnedBuffer(FPDF_DOCUMENT document, + FPDF_WIDESTRING key, + unsigned long* out_size) { + if (out_size) { + *out_size = 0; + } + CPDF_LayerDocument* layer = LayerOf(document); + if (!layer || !key) { + return nullptr; + } + CPDF_DocumentViewScope view(layer); + // SAFETY: required from caller. + const WideString name = UNSAFE_BUFFERS(WideStringFromFPDFWideString(key)); + auto tree = CPDF_NameTree::CreateForReading(layer, "EmbeddedFiles"); + RetainPtr spec = + tree && !name.IsEmpty() ? ToDictionary(tree->LookupValue(name)) : nullptr; + if (!spec) { + return nullptr; + } + // No /Base, as for EPDFDoc_ExportDictCanonicalToOwnedBuffer(). The number + // is part of what it reads as: another file under the key, even with the + // same bytes, is another file. + auto manifest = pdfium::MakeRetain(); + manifest->SetNewFor("Kind", "Attachment"); + manifest->SetNewFor("Key", name.AsStringView()); + if (const uint32_t objnum = spec->GetObjNum()) { + manifest->SetNewFor("Obj", pdfium::checked_cast(objnum)); + } + RetainPtr value = spec->Clone(); + manifest->SetFor("Value", value); + CaptureWriter writer(layer, StreamBytes::kAsRead); + writer.AddReachOf(value.Get()); + return ToOwnedBuffer(writer.Write(manifest.Get()), out_size); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAttachment_ImportRaw(FPDF_DOCUMENT document, + const void* capture, + unsigned long size) { + CPDF_LayerDocument* layer = LayerOf(document); + if (!layer || !layer->InTransaction() || !capture || !size) { + return false; + } + CPDF_DocumentViewScope view(layer); + // SAFETY: required from caller. + std::optional read = ReadCapture( + layer, + UNSAFE_BUFFERS(pdfium::span(static_cast(capture), size)), + "Attachment"); + if (!read) { + return false; + } + const WideString key = read->manifest->GetUnicodeTextFor("Key"); + if (key.IsEmpty()) { + return false; + } + // A key another file holds now stays with it. + if (auto tree = CPDF_NameTree::CreateForReading(layer, "EmbeddedFiles"); + tree && tree->LookupValue(key)) { + return false; + } + + // What goes back under the key: a reference to the file specification, + // which is what its number holds once the capture is in (the layer's own + // version stays, else the capture's, else the file's original); or the + // specification written in the tree. Either must be a file specification. + RetainPtr entry; + if (const int objnum = read->manifest->GetIntegerFor("Obj"); objnum > 0) { + const uint32_t number = static_cast(objnum); + if (number > layer->GetLastObjNum()) { + return false; + } + auto captured = read->objects.find(number); + RetainPtr comes_back = + layer->FindLayerVersion(number) || captured == read->objects.end() + ? layer->GetOrParseIndirectObject(number) + : captured->second; + if (!IsFileSpec(ToDictionary(comes_back).Get())) { + return false; + } + entry = pdfium::MakeRetain(layer, number); + } else { + RetainPtr value = + read->manifest->GetDictFor("Value"); + if (!IsFileSpec(value.Get())) { + return false; + } + entry = value->Clone(); + } + + if (!InstallObjects(layer, *read)) { + return false; + } + auto tree = CPDF_NameTree::CreateWithRootNameArray(layer, "EmbeddedFiles"); + return tree && tree->AddValueAndName(std::move(entry), key); +} diff --git a/fpdfsdk/epdf_capture_embeddertest.cpp b/fpdfsdk/epdf_capture_embeddertest.cpp index 2d2597463..227567e89 100644 --- a/fpdfsdk/epdf_capture_embeddertest.cpp +++ b/fpdfsdk/epdf_capture_embeddertest.cpp @@ -30,16 +30,20 @@ #include "fpdfsdk/epdf_edit_test_util.h" #include "public/cpp/fpdf_scopers.h" #include "public/epdf_annot.h" +#include "public/epdf_attachment.h" #include "public/epdf_capture.h" #include "public/epdf_edit.h" #include "public/epdf_form.h" #include "public/epdf_layer.h" #include "public/epdf_named_pages.h" +#include "public/epdf_outline.h" #include "public/epdf_save.h" #include "public/epdf_view.h" #include "public/fpdf_annot.h" +#include "public/fpdf_attachment.h" #include "public/fpdf_edit.h" #include "public/fpdf_save.h" +#include "public/fpdf_transformpage.h" #include "public/fpdfview.h" #include "testing/embedder_test.h" #include "testing/fx_string_testhelpers.h" @@ -208,6 +212,16 @@ std::vector ExportDict(FPDF_DOCUMENT doc, return Take(buffer, size); } +std::vector CanonicalDict(FPDF_DOCUMENT doc, + uint32_t objnum, + const char* deep_keys, + const char* skip_keys) { + unsigned long size = 0; + void* buffer = EPDFDoc_ExportDictCanonicalToOwnedBuffer( + doc, objnum, deep_keys, skip_keys, &size); + return Take(buffer, size); +} + bool ImportDict(FPDF_DOCUMENT doc, const std::vector& capture) { return EPDFDoc_ImportDictRaw(doc, capture.data(), static_cast(capture.size())); @@ -608,6 +622,67 @@ TEST_F(EPDFCaptureEmbedderTest, ImportRefusesWhatDoesNotFit) { ASSERT_TRUE(EPDFLayer_AbortTransaction(fresh.get())); } +// A capture never holds the catalog, the page tree or a page it isn't of: +// one that does (here an annotation capture with a catalog put in) is +// refused whole, and nothing of it is put in. +TEST_F(EPDFCaptureEmbedderTest, ImportRefusesACaptureHoldingTheCatalog) { + ScopedFPDFDocument layer = OpenLayer(); + ASSERT_TRUE(layer); + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + ASSERT_TRUE(CreateSquare(layer.get(), 1, 20)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + std::vector capture = ExportAnnots(layer.get(), 1, {0}); + ASSERT_FALSE(capture.empty()); + ASSERT_TRUE(EPDFPage_RemoveAnnotRaw(layer.get(), 1, 0)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + + CPDF_Document* pdf = CPDFDocumentFromFPDFDocument(layer.get()); + const std::string catalog = ObjectGraph(pdf).at(1); + const std::string forged = "1 0 obj\n<< /Type /Catalog >>\nendobj\n"; + capture.insert(capture.end(), forged.begin(), forged.end()); + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + EXPECT_FALSE(ImportAnnots(layer.get(), 1, capture)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + EXPECT_EQ(catalog, ObjectGraph(pdf).at(1)); + EXPECT_TRUE(Annots(layer.get(), 1).empty()); +} + +// A dictionary's canonical bytes change only with what it reads as: not +// when a key the caller leaves out changes (the page turned and turned back +// writes /Rotate 0), nor when a save encodes its streams again (a reopen), +// but when what it holds changes (its square recoloured). +TEST_F(EPDFCaptureEmbedderTest, CanonicalBytesChangeOnlyWithWhatReads) { + ScopedFPDFDocument layer = OpenLayer(); + ASSERT_TRUE(layer); + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + ASSERT_TRUE(CreateSquare(layer.get(), 1, 20)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + const std::vector before = + CanonicalDict(layer.get(), 4, "Annots", "Parent Rotate"); + ASSERT_FALSE(before.empty()); + + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + ASSERT_TRUE(EPDFDoc_SetPageRotationByObjectNumber(layer.get(), 4, 1)); + ASSERT_TRUE(EPDFDoc_SetPageRotationByObjectNumber(layer.get(), 4, 0)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + EXPECT_EQ(before, CanonicalDict(layer.get(), 4, "Annots", "Parent Rotate")); + const std::vector stored = ExportDict(layer.get(), 4, "Annots"); + + ScopedFPDFDocument reopened = Reopen(layer.get()); + ASSERT_TRUE(reopened); + // The save encoded the square's appearance: its stored bytes differ. + EXPECT_NE(stored, ExportDict(reopened.get(), 4, "Annots")); + EXPECT_EQ(before, + CanonicalDict(reopened.get(), 4, "Annots", "Parent Rotate")); + + ASSERT_TRUE(EPDFLayer_BeginTransaction(reopened.get())); + ASSERT_TRUE(Recolor(reopened.get(), 20, 1, 0, 0)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(reopened.get())); + EXPECT_NE(before, + CanonicalDict(reopened.get(), 4, "Annots", "Parent Rotate")); +} + // A dictionary captured before an update comes back exactly: every key as it // was, none added, and the appearance under a deep key with its bytes, also // after a save and reopen dropped the old appearance. @@ -1054,13 +1129,186 @@ TEST_F(EPDFPageCaptureEmbedderTest, ImportLeavesAKeyAnotherPageTook) { EXPECT_THAT(NamedPages(layer.get()), ElementsAre(L"Cover=3", L"Intro=5")); } -// A capture holds its page, never another: the cover links to the body, -// which the layer turned, and the page tree is its parent. +// A page comes back with what it took from the page tree, as its own. The +// page tree has two branches of different size, turn and resources; the +// last page of the first branch comes back at its place, which is the start +// of the second branch now, and still reads as the first branch made it. +TEST_F(EPDFPageCaptureEmbedderTest, PageKeepsWhatItInheritedFromItsBranch) { + EPDF_ReleaseBaseDocument(base_); + input_ = MakePdf({ + "<< /Type /Catalog /Pages 2 0 R >>", + "<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 4 >>", + "<< /Type /Pages /Parent 2 0 R /Kids [5 0 R 6 0 R] /Count 2 " + "/MediaBox [0 0 200 100] /Rotate 90 /Resources 9 0 R >>", + "<< /Type /Pages /Parent 2 0 R /Kids [7 0 R 8 0 R] /Count 2 " + "/MediaBox [0 0 500 500] /Resources << >> >>", + "<< /Type /Page /Parent 3 0 R >>", + "<< /Type /Page /Parent 3 0 R >>", + "<< /Type /Page /Parent 4 0 R >>", + "<< /Type /Page /Parent 4 0 R >>", + "<< /ProcSet [/PDF] >>", + }); + base_ = EPDF_LoadMemBaseDocument(input_.data(), input_.size(), nullptr); + ASSERT_TRUE(base_); + for (bool reopen : {false, true}) { + SCOPED_TRACE(reopen ? "reopened between delete and import" + : "same session"); + ScopedFPDFDocument layer = OpenLayer(); + ASSERT_TRUE(layer); + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + const std::vector capture = ExportPage(layer.get(), 6); + ASSERT_FALSE(capture.empty()); + ASSERT_TRUE(EPDFDoc_DeletePageByObjectNumber(layer.get(), 6)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + EXPECT_THAT(PageOrder(layer.get()), ElementsAre(5, 7, 8)); + + ScopedFPDFDocument target = reopen ? Reopen(layer.get()) : std::move(layer); + ASSERT_TRUE(target); + ASSERT_TRUE(EPDFLayer_BeginTransaction(target.get())); + ASSERT_TRUE(ImportPage(target.get(), capture)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(target.get())); + EXPECT_THAT(PageOrder(target.get()), ElementsAre(5, 6, 7, 8)); + EXPECT_EQ(4u, RefOf(target.get(), 6, "Parent")); // the other branch + EXPECT_EQ(9u, RefOf(target.get(), 6, "Resources")); + ScopedFPDFPage page(FPDF_LoadPage(target.get(), 1)); + ASSERT_TRUE(page); + EXPECT_EQ(1, FPDFPage_GetRotation(page.get())); + float left = 0; + float bottom = 0; + float right = 0; + float top = 0; + ASSERT_TRUE(FPDFPage_GetMediaBox(page.get(), &left, &bottom, &right, &top)); + EXPECT_EQ(200, right); + EXPECT_EQ(100, top); + // Turned a quarter, as the first branch turns its pages. + EXPECT_EQ(100, FPDF_GetPageWidthF(page.get())); + EXPECT_EQ(200, FPDF_GetPageHeightF(page.get())); + } +} + +// A capture names the file it was taken on when the layer knows the file's +// hash, and never makes the layer work it out. +TEST_F(EPDFPageCaptureEmbedderTest, CaptureNamesItsFileWhenTheHashIsKnown) { + ScopedFPDFDocument layer = OpenLayer(); + ASSERT_TRUE(layer); + const std::vector unnamed = ExportPage(layer.get(), kIntro); + ASSERT_FALSE(unnamed.empty()); + EXPECT_EQ(std::string::npos, + std::string(unnamed.begin(), unnamed.end()).find("/Base")); + unsigned char sha256[32] = {}; + ASSERT_TRUE(EPDFLayer_GetBaseSha256(layer.get(), sha256)); + const std::vector named = ExportPage(layer.get(), kIntro); + EXPECT_NE(std::string::npos, + std::string(named.begin(), named.end()).find("/Base")); + // Both come back into a layer of this file. + for (const std::vector* capture : {&unnamed, &named}) { + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + ASSERT_TRUE(EPDFDoc_DeletePageByObjectNumber(layer.get(), kIntro)); + EXPECT_TRUE(ImportPage(layer.get(), *capture)); + ASSERT_TRUE(EPDFLayer_AbortTransaction(layer.get())); + } +} + +// A capture that names its file comes back only into a layer of that file: +// in another file the same numbers are other objects. +TEST_F(EPDFPageCaptureEmbedderTest, ImportRefusesACaptureOfAnotherFile) { + ScopedFPDFDocument layer = OpenLayer(); + ASSERT_TRUE(layer); + // The layer knows its file's hash, as a host that keeps captures makes it. + unsigned char sha256[32] = {}; + ASSERT_TRUE(EPDFLayer_GetBaseSha256(layer.get(), sha256)); + const std::vector capture = ExportPage(layer.get(), kIntro); + ASSERT_FALSE(capture.empty()); + + // The same objects, with other bytes after the end: another file. + const std::string other = input_ + "% another file\n"; + EPDF_BASE_DOCUMENT other_base = + EPDF_LoadMemBaseDocument(other.data(), other.size(), nullptr); + ASSERT_TRUE(other_base); + { + ScopedFPDFDocument other_layer( + EPDFLayer_OpenLayer(other_base, nullptr, nullptr, nullptr)); + ASSERT_TRUE(other_layer); + ASSERT_TRUE(EPDFLayer_BeginTransaction(other_layer.get())); + ASSERT_TRUE(EPDFDoc_DeletePageByObjectNumber(other_layer.get(), kIntro)); + EXPECT_FALSE(ImportPage(other_layer.get(), capture)); + ASSERT_TRUE(EPDFLayer_AbortTransaction(other_layer.get())); + } + EPDF_ReleaseBaseDocument(other_base); +} + +// What goes back into the page tree is what the page's number holds once +// the capture is in. A save drops the null a layer page's delete left, so +// its number is free again; an outline item that took it never becomes a +// page. +TEST_F(EPDFPageCaptureEmbedderTest, + ImportRefusesWhenItsNumberHoldsSomethingElse) { + ScopedFPDFDocument layer = OpenLayer(); + ASSERT_TRUE(layer); + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + ASSERT_TRUE(EPDFPage_InsertBlankRaw(layer.get(), 3, 300, 300, 20)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + const std::vector capture = ExportPage(layer.get(), 20); + ASSERT_FALSE(capture.empty()); + ASSERT_TRUE(EPDFDoc_DeletePageByObjectNumber(layer.get(), 20)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + + ScopedFPDFDocument reopened = Reopen(layer.get()); + ASSERT_TRUE(reopened); + ASSERT_TRUE(EPDFLayer_RaiseLastObjectNumber(reopened.get(), kRaised)); + ASSERT_TRUE(EPDFLayer_BeginTransaction(reopened.get())); + ScopedFPDFWideString title = GetFPDFWideString(L"Taken"); + ASSERT_EQ(20u, EPDFOutline_CreateItem(reopened.get(), 20, 0, 0, title.get())); + EXPECT_FALSE(ImportPage(reopened.get(), capture)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(reopened.get())); + EXPECT_THAT(PageOrder(reopened.get()), ElementsAre(kCover, kIntro, kBody)); +} + +// A page that comes back under the node it was under takes nothing as its +// own: its dictionary reads as it did, so what was digested of it still +// matches. +TEST_F(EPDFPageCaptureEmbedderTest, PageBackUnderItsNodeGainsNoKeys) { + EPDF_ReleaseBaseDocument(base_); + input_ = MakePdf({ + "<< /Type /Catalog /Pages 2 0 R >>", + "<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 /MediaBox [0 0 200 100] " + "/Resources << >> >>", + "<< /Type /Page /Parent 2 0 R >>", + "<< /Type /Page /Parent 2 0 R >>", + }); + base_ = EPDF_LoadMemBaseDocument(input_.data(), input_.size(), nullptr); + ASSERT_TRUE(base_); + ScopedFPDFDocument layer = OpenLayer(); + ASSERT_TRUE(layer); + CPDF_Document* pdf = CPDFDocumentFromFPDFDocument(layer.get()); + const std::string before = ObjectGraph(pdf).at(4); + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + const std::vector capture = ExportPage(layer.get(), 4); + ASSERT_FALSE(capture.empty()); + ASSERT_TRUE(EPDFDoc_DeletePageByObjectNumber(layer.get(), 4)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + ASSERT_TRUE(ImportPage(layer.get(), capture)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + EXPECT_THAT(PageOrder(layer.get()), ElementsAre(3, 4)); + EXPECT_EQ(before, ObjectGraph(pdf).at(4)); +} + +// A capture holds its page, never another page, the page tree or the +// catalog: the cover links to the body, which the layer turned, and reaches +// the page tree and the catalog, which the delete changed, through a key a +// capture follows. TEST_F(EPDFPageCaptureEmbedderTest, CaptureHoldsNoOtherPage) { ScopedFPDFDocument layer = OpenLayer(); ASSERT_TRUE(layer); ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); ASSERT_TRUE(EPDFDoc_SetPageRotationByObjectNumber(layer.get(), kCover, 1)); + CPDF_Document* pdf = CPDFDocumentFromFPDFDocument(layer.get()); + RetainPtr reach = + WritableDict(layer.get(), kCover)->SetNewFor("Reach"); + reach->AppendNew(pdf, 1); + reach->AppendNew(pdf, kPageTree); ASSERT_TRUE(EPDFDoc_SetPageRotationByObjectNumber(layer.get(), kBody, 2)); ASSERT_TRUE(EPDFDoc_DeletePageByObjectNumber(layer.get(), kIntro)); ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); @@ -1121,3 +1369,335 @@ TEST_F(EPDFPageCaptureEmbedderTest, CapturesNeedAPageAndALayer) { ASSERT_TRUE(plain); EXPECT_TRUE(ExportPage(plain.get(), kIntro).empty()); } + +namespace { + +// One page and two embedded files: a.txt, whose file specification is an +// object of its own (4, its stream 5), and b.txt, whose specification is +// written in the tree (its stream 6). +std::string MakeAttachmentsDocument() { + return MakePdf({ + "<< /Type /Catalog /Pages 2 0 R /Names << /EmbeddedFiles 7 0 R >> >>", + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] >>", + "<< /Type /Filespec /F (a.txt) /UF (a.txt) /EF << /F 5 0 R >> >>", + "<< /Type /EmbeddedFile /Length 5 >>\nstream\nhello\nendstream", + "<< /Type /EmbeddedFile /Length 3 >>\nstream\nbye\nendstream", + "<< /Names [(a.txt) 4 0 R (b.txt) << /Type /Filespec /F (b.txt) " + "/UF (b.txt) /EF << /F 6 0 R >> >>] >>", + }); +} + +// The same files in a tree with two leaves: a.txt in the first, b.txt in +// the second. +std::string MakeAttachmentsTreeDocument() { + return MakePdf({ + "<< /Type /Catalog /Pages 2 0 R /Names << /EmbeddedFiles 7 0 R >> >>", + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] >>", + "<< /Type /Filespec /F (a.txt) /UF (a.txt) /EF << /F 5 0 R >> >>", + "<< /Type /EmbeddedFile /Length 5 >>\nstream\nhello\nendstream", + "<< /Type /EmbeddedFile /Length 3 >>\nstream\nbye\nendstream", + "<< /Kids [8 0 R 9 0 R] >>", + "<< /Limits [(a.txt) (a.txt)] /Names [(a.txt) 4 0 R] >>", + "<< /Limits [(b.txt) (b.txt)] /Names [(b.txt) 10 0 R] >>", + "<< /Type /Filespec /F (b.txt) /UF (b.txt) /EF << /F 6 0 R >> >>", + }); +} + +// The keys of |doc|'s embedded files, in tree order. +std::vector AttachmentKeys(FPDF_DOCUMENT doc) { + std::vector keys; + for (int i = 0; i < FPDFDoc_GetAttachmentCount(doc); ++i) { + const unsigned long length = EPDFDoc_GetAttachmentKey(doc, i, nullptr, 0); + std::vector buffer = GetFPDFWideStringBuffer(length); + EPDFDoc_GetAttachmentKey(doc, i, buffer.data(), length); + keys.push_back(GetPlatformWString(buffer.data())); + } + return keys; +} + +// The decoded bytes of the file under |key|, or "(none)". +std::string AttachmentContent(FPDF_DOCUMENT doc, const wchar_t* key) { + ScopedFPDFWideString name = GetFPDFWideString(key); + const int index = EPDFDoc_GetAttachmentIndexByKey(doc, name.get()); + FPDF_ATTACHMENT attachment = + index >= 0 ? FPDFDoc_GetAttachment(doc, index) : nullptr; + unsigned long length = 0; + if (!attachment || !FPDFAttachment_GetFile(attachment, nullptr, 0, &length)) { + return "(none)"; + } + std::string bytes(length, '\0'); + FPDFAttachment_GetFile(attachment, bytes.data(), length, &length); + return bytes; +} + +bool DeleteAttachment(FPDF_DOCUMENT doc, const wchar_t* key) { + ScopedFPDFWideString name = GetFPDFWideString(key); + const int index = EPDFDoc_GetAttachmentIndexByKey(doc, name.get()); + return index >= 0 && FPDFDoc_DeleteAttachment(doc, index); +} + +// Adds |key| with |content| and returns its file specification's number. +uint32_t AddAttachment(FPDF_DOCUMENT doc, + const wchar_t* key, + const std::string& content) { + ScopedFPDFWideString name = GetFPDFWideString(key); + FPDF_ATTACHMENT attachment = FPDFDoc_AddAttachment(doc, name.get()); + if (!attachment || !FPDFAttachment_SetFile(attachment, doc, content.data(), + content.size())) { + return 0; + } + return CPDFObjectFromFPDFAttachment(attachment)->GetObjNum(); +} + +std::vector ExportAttachment(FPDF_DOCUMENT doc, const wchar_t* key) { + ScopedFPDFWideString name = GetFPDFWideString(key); + unsigned long size = 0; + void* buffer = EPDFAttachment_ExportRawToOwnedBuffer(doc, name.get(), &size); + return Take(buffer, size); +} + +std::vector CanonicalAttachment(FPDF_DOCUMENT doc, + const wchar_t* key) { + ScopedFPDFWideString name = GetFPDFWideString(key); + unsigned long size = 0; + void* buffer = + EPDFAttachment_ExportCanonicalToOwnedBuffer(doc, name.get(), &size); + return Take(buffer, size); +} + +bool ImportAttachment(FPDF_DOCUMENT doc, const std::vector& capture) { + return EPDFAttachment_ImportRaw(doc, capture.data(), + static_cast(capture.size())); +} + +} // namespace + +class EPDFAttachmentCaptureEmbedderTest : public EmbedderTest { + protected: + void TearDown() override { + if (base_) { + EPDF_ReleaseBaseDocument(base_); + } + EmbedderTest::TearDown(); + } + + ScopedFPDFDocument OpenLayer(std::string bytes = MakeAttachmentsDocument()) { + input_ = std::move(bytes); + base_ = EPDF_LoadMemBaseDocument(input_.data(), input_.size(), nullptr); + if (!base_) { + return nullptr; + } + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base_, nullptr, nullptr, nullptr)); + if (layer && !EPDFLayer_RaiseLastObjectNumber(layer.get(), kRaised)) { + return nullptr; + } + return layer; + } + + // The layer saved and opened again: what nothing reaches is gone. + ScopedFPDFDocument Reopen(FPDF_DOCUMENT layer) { + ClearString(); + EXPECT_TRUE(EPDFLayer_SaveLayerArtifact(layer, this, nullptr)); + artifact_ = GetString(); + FPDF_FILEACCESS access = {}; + access.m_FileLen = artifact_.size(); + access.m_GetBlock = GetBlockFromString; + access.m_Param = &artifact_; + EPDFLayerOpenStatus status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument reopened( + EPDFLayer_OpenLayerArtifact(base_, &access, nullptr, &status)); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, status); + return reopened; + } + + std::string input_; + std::string artifact_; + EPDF_BASE_DOCUMENT base_ = nullptr; +}; + +// A file of the uploaded file comes back under its key, with its bytes: +// one whose specification is an object of its own, and one written in the +// tree. Neither was edited, so the capture holds no copy of them. +TEST_F(EPDFAttachmentCaptureEmbedderTest, FileAttachmentsComeBack) { + for (const wchar_t* key : {L"a.txt", L"b.txt"}) { + SCOPED_TRACE(GetPlatformString(GetFPDFWideString(key).get())); + ScopedFPDFDocument layer = OpenLayer(); + ASSERT_TRUE(layer); + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + const std::vector capture = ExportAttachment(layer.get(), key); + ASSERT_FALSE(capture.empty()); + ASSERT_TRUE(DeleteAttachment(layer.get(), key)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + EXPECT_EQ(1u, AttachmentKeys(layer.get()).size()); + + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + ASSERT_TRUE(ImportAttachment(layer.get(), capture)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + EXPECT_THAT(AttachmentKeys(layer.get()), ElementsAre(L"a.txt", L"b.txt")); + EXPECT_EQ("hello", AttachmentContent(layer.get(), L"a.txt")); + EXPECT_EQ("bye", AttachmentContent(layer.get(), L"b.txt")); + } +} + +// A file the layer added comes back with its bytes, also when a save and +// reopen dropped it, which nothing reached once it was deleted. +TEST_F(EPDFAttachmentCaptureEmbedderTest, LayerAttachmentComesBack) { + for (bool reopen : {false, true}) { + SCOPED_TRACE(reopen ? "reopened between delete and import" + : "same session"); + ScopedFPDFDocument layer = OpenLayer(); + ASSERT_TRUE(layer); + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + ASSERT_TRUE(AddAttachment(layer.get(), L"c.txt", "made here")); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + const std::vector capture = + ExportAttachment(layer.get(), L"c.txt"); + ASSERT_FALSE(capture.empty()); + ASSERT_TRUE(DeleteAttachment(layer.get(), L"c.txt")); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + + ScopedFPDFDocument target = reopen ? Reopen(layer.get()) : std::move(layer); + ASSERT_TRUE(target); + EXPECT_EQ("(none)", AttachmentContent(target.get(), L"c.txt")); + ASSERT_TRUE(EPDFLayer_BeginTransaction(target.get())); + ASSERT_TRUE(ImportAttachment(target.get(), capture)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(target.get())); + EXPECT_THAT(AttachmentKeys(target.get()), + ElementsAre(L"a.txt", L"b.txt", L"c.txt")); + EXPECT_EQ("made here", AttachmentContent(target.get(), L"c.txt")); + } +} + +// A file comes back into the leaf of a tree with kids where its key goes. +TEST_F(EPDFAttachmentCaptureEmbedderTest, ComesBackIntoATreeWithKids) { + ScopedFPDFDocument layer = OpenLayer(MakeAttachmentsTreeDocument()); + ASSERT_TRUE(layer); + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + const std::vector capture = ExportAttachment(layer.get(), L"b.txt"); + ASSERT_FALSE(capture.empty()); + ASSERT_TRUE(DeleteAttachment(layer.get(), L"b.txt")); + ASSERT_TRUE(ImportAttachment(layer.get(), capture)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + EXPECT_THAT(AttachmentKeys(layer.get()), ElementsAre(L"a.txt", L"b.txt")); + EXPECT_EQ("bye", AttachmentContent(layer.get(), L"b.txt")); +} + +// A key another file took since stays with it. +TEST_F(EPDFAttachmentCaptureEmbedderTest, ImportLeavesAKeyAnotherFileTook) { + ScopedFPDFDocument layer = OpenLayer(); + ASSERT_TRUE(layer); + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + const std::vector capture = ExportAttachment(layer.get(), L"a.txt"); + ASSERT_TRUE(DeleteAttachment(layer.get(), L"a.txt")); + ASSERT_TRUE(AddAttachment(layer.get(), L"a.txt", "someone else's")); + EXPECT_FALSE(ImportAttachment(layer.get(), capture)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + EXPECT_EQ("someone else's", AttachmentContent(layer.get(), L"a.txt")); +} + +// What goes back under the key is what the file specification's number +// holds once the capture is in. A save drops a deleted layer file, so its +// number is free again; an outline item that took it never becomes a file. +TEST_F(EPDFAttachmentCaptureEmbedderTest, + ImportRefusesWhenItsNumberHoldsSomethingElse) { + ScopedFPDFDocument layer = OpenLayer(); + ASSERT_TRUE(layer); + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + const uint32_t spec = AddAttachment(layer.get(), L"c.txt", "made here"); + ASSERT_TRUE(spec); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + const std::vector capture = ExportAttachment(layer.get(), L"c.txt"); + ASSERT_TRUE(DeleteAttachment(layer.get(), L"c.txt")); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + + ScopedFPDFDocument reopened = Reopen(layer.get()); + ASSERT_TRUE(reopened); + ASSERT_TRUE(EPDFLayer_RaiseLastObjectNumber(reopened.get(), kRaised)); + ASSERT_TRUE(EPDFLayer_BeginTransaction(reopened.get())); + ScopedFPDFWideString title = GetFPDFWideString(L"Taken"); + ASSERT_EQ(spec, + EPDFOutline_CreateItem(reopened.get(), spec, 0, 0, title.get())); + EXPECT_FALSE(ImportAttachment(reopened.get(), capture)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(reopened.get())); + EXPECT_THAT(AttachmentKeys(reopened.get()), ElementsAre(L"a.txt", L"b.txt")); +} + +// A file reads the same while it is the same file: across a save and reopen, +// and when its capture brought it back. Another file under its key, with the +// same bytes, reads differently. +TEST_F(EPDFAttachmentCaptureEmbedderTest, CanonicalBytesTellTheSameFile) { + ScopedFPDFDocument layer = OpenLayer(); + ASSERT_TRUE(layer); + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + ASSERT_TRUE(AddAttachment(layer.get(), L"c.txt", "made here")); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + const std::vector made = CanonicalAttachment(layer.get(), L"c.txt"); + const std::vector inline_file = + CanonicalAttachment(layer.get(), L"b.txt"); + ASSERT_FALSE(made.empty()); + ASSERT_FALSE(inline_file.empty()); + EXPECT_NE(made, CanonicalAttachment(layer.get(), L"a.txt")); + + ScopedFPDFDocument reopened = Reopen(layer.get()); + ASSERT_TRUE(reopened); + EXPECT_EQ(made, CanonicalAttachment(reopened.get(), L"c.txt")); + EXPECT_EQ(inline_file, CanonicalAttachment(reopened.get(), L"b.txt")); + + ASSERT_TRUE(EPDFLayer_RaiseLastObjectNumber(reopened.get(), kRaised)); + for (const wchar_t* key : {L"c.txt", L"b.txt"}) { + SCOPED_TRACE(GetPlatformString(GetFPDFWideString(key).get())); + const std::vector before = + CanonicalAttachment(reopened.get(), key); + ASSERT_TRUE(EPDFLayer_BeginTransaction(reopened.get())); + const std::vector capture = ExportAttachment(reopened.get(), key); + ASSERT_TRUE(DeleteAttachment(reopened.get(), key)); + EXPECT_TRUE(CanonicalAttachment(reopened.get(), key).empty()); + ASSERT_TRUE(ImportAttachment(reopened.get(), capture)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(reopened.get())); + EXPECT_EQ(before, CanonicalAttachment(reopened.get(), key)); + } + + ASSERT_TRUE(EPDFLayer_BeginTransaction(reopened.get())); + ASSERT_TRUE(DeleteAttachment(reopened.get(), L"c.txt")); + ASSERT_TRUE(AddAttachment(reopened.get(), L"c.txt", "made here")); + ASSERT_TRUE(EPDFLayer_CommitTransaction(reopened.get())); + EXPECT_NE(made, CanonicalAttachment(reopened.get(), L"c.txt")); + + EXPECT_TRUE(CanonicalAttachment(reopened.get(), L"none.txt").empty()); + ScopedFPDFDocument plain( + FPDF_LoadMemDocument(input_.data(), input_.size(), nullptr)); + ASSERT_TRUE(plain); + EXPECT_TRUE(CanonicalAttachment(plain.get(), L"a.txt").empty()); +} + +TEST_F(EPDFAttachmentCaptureEmbedderTest, CapturesNeedAFileAndALayer) { + ScopedFPDFDocument layer = OpenLayer(); + ASSERT_TRUE(layer); + EXPECT_TRUE(ExportAttachment(layer.get(), L"none.txt").empty()); + EXPECT_TRUE(ExportAttachment(layer.get(), L"").empty()); + const std::vector capture = ExportAttachment(layer.get(), L"a.txt"); + ASSERT_FALSE(capture.empty()); + ASSERT_TRUE(DeleteAttachment(layer.get(), L"a.txt")); + EXPECT_FALSE( + ImportAttachment(layer.get(), capture)); // outside a transaction + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + EXPECT_FALSE(ImportAttachment(layer.get(), {})); + const std::vector cut(capture.begin(), capture.end() - 4); + EXPECT_FALSE(ImportAttachment(layer.get(), cut)); + // A capture of another kind, and a file capture as another kind. + EXPECT_FALSE(EPDFPage_ImportRaw(layer.get(), capture.data(), + static_cast(capture.size()))); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + EXPECT_THAT(AttachmentKeys(layer.get()), ElementsAre(L"b.txt")); + + ScopedFPDFDocument plain( + FPDF_LoadMemDocument(input_.data(), input_.size(), nullptr)); + ASSERT_TRUE(plain); + EXPECT_TRUE(ExportAttachment(plain.get(), L"a.txt").empty()); +} diff --git a/fpdfsdk/epdf_outline_embeddertest.cpp b/fpdfsdk/epdf_outline_embeddertest.cpp index 58ed2ad4c..9ca4b3cc2 100644 --- a/fpdfsdk/epdf_outline_embeddertest.cpp +++ b/fpdfsdk/epdf_outline_embeddertest.cpp @@ -25,6 +25,7 @@ #include "public/cpp/fpdf_scopers.h" #include "public/epdf_capture.h" #include "public/epdf_doc.h" +#include "public/epdf_edit.h" #include "public/epdf_layer.h" #include "public/epdf_outline.h" #include "public/epdf_save.h" @@ -331,10 +332,15 @@ class EPDFOutlineEmbedderTest : public EmbedderTest { return reopened; } + void RandomWrites(FPDF_DOCUMENT doc, bool layer); + std::string plain_; std::string input_; std::string artifact_; EPDF_BASE_DOCUMENT base_ = nullptr; + // The next number a layer write creates an item at (OpenLayer() hands out + // numbers up to 40). + uint32_t next_number = 41; }; TEST_F(EPDFOutlineEmbedderTest, ItemsAreNamedByNumber) { @@ -549,11 +555,15 @@ TEST_F(EPDFOutlineEmbedderTest, SetActionReplacesTheDestination) { EXPECT_TRUE(RefOf(a.Get(), "A")); EXPECT_TRUE(FPDFBookmark_GetAction(EPDFOutline_GetItem(doc.get(), kA))); - // Another document's action is refused; none removes the target. - ScopedFPDFDocument other = OpenLayer(); + // Another document's action is refused, also when its number names an + // object here (the action set above); none removes the target. + ScopedFPDFDocument other( + FPDF_LoadMemDocument(plain_.data(), plain_.size(), nullptr)); ASSERT_TRUE(other); FPDF_ACTION foreign = GoTo(other.get(), kFirstPage); ASSERT_TRUE(foreign); + ASSERT_TRUE( + DictOf(doc.get(), CPDFDictionaryFromFPDFAction(foreign)->GetObjNum())); EXPECT_FALSE(EPDFOutline_SetAction(doc.get(), kB, foreign)); ASSERT_TRUE(EPDFOutline_SetAction(doc.get(), kA, nullptr)); a = DictOf(doc.get(), kA); @@ -664,11 +674,119 @@ TEST_F(EPDFOutlineEmbedderTest, ADirectOutlineDictionaryIsNeverWritten) { EXPECT_FALSE(Create(doc.get(), 0, 0, L"x")); } -// Random writes, checked after each one: the links and the counts are what -// the tree says. -TEST_F(EPDFOutlineEmbedderTest, RandomWritesKeepTheOutlineRight) { - ScopedFPDFDocument doc = OpenPlain(); +// A dictionary that isn't an item (a page, an action, a field, an +// annotation, a page tree) ends the list a broken file links it into: the +// readers don't show it, and no write takes it for an item or changes it. +TEST_F(EPDFOutlineEmbedderTest, ADictionaryThatIsntAnItemEndsTheList) { + for (const char* other : { + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] >>", + "<< /MediaBox [0 0 300 300] /Resources << >> >>", + "<< /S /URI /URI (https://example.com) /Next 7 0 R >>", + "<< /FT /Tx /T (name) /Parent 3 0 R >>", + "<< /Subtype /Text /Rect [0 0 10 10] >>", + "<< /Type /Pages /Kids [4 0 R] /Count 1 >>", + }) { + SCOPED_TRACE(other); + ScopedFPDFDocument doc = OpenPlain(MakeDocumentWith({ + "<< /Type /Outlines /First 5 0 R /Last 5 0 R /Count 1 >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] >>", + "<< /Title (One) /Parent 3 0 R /Next 6 0 R >>", + other, + })); + ASSERT_TRUE(doc); + CPDF_Document* pdf = CPDFDocumentFromFPDFDocument(doc.get()); + const std::string before = ObjectGraph(pdf)[6]; + EXPECT_EQ("1:One", Tree(doc.get())); + EXPECT_FALSE(EPDFOutline_GetItem(doc.get(), 6)); + EXPECT_FALSE(EPDFOutline_DeleteItem(doc.get(), 6)); + EXPECT_FALSE(EPDFOutline_MoveItem(doc.get(), 6, 0, 0)); + EXPECT_FALSE(Create(doc.get(), 6, 0, L"under it")); + ASSERT_TRUE(Create(doc.get(), 0, 5, L"Two")); + EXPECT_EQ(before, ObjectGraph(pdf)[6]); + EXPECT_EQ("2:One Two", Tree(doc.get())); + EXPECT_EQ("", Problems(doc.get())); + EXPECT_EQ(1, FPDF_GetPageCount(doc.get())); + } +} + +// An /Outlines that isn't an outline's dictionary (here the page tree) is +// no outline: the readers show none, and a create makes a new outline in +// its place without touching what was linked there. +TEST_F(EPDFOutlineEmbedderTest, AnOutlinesThatIsntAnOutlineIsNone) { + ScopedFPDFDocument doc = OpenPlain(MakePdf({ + "<< /Type /Catalog /Pages 2 0 R /Outlines 2 0 R >>", + "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] >>", + })); ASSERT_TRUE(doc); + CPDF_Document* pdf = CPDFDocumentFromFPDFDocument(doc.get()); + const std::string pages = ObjectGraph(pdf)[2]; + EXPECT_FALSE(FPDFBookmark_GetFirstChild(doc.get(), nullptr)); + ASSERT_TRUE(Create(doc.get(), 0, 0, L"One")); + EXPECT_EQ(pages, ObjectGraph(pdf)[2]); + EXPECT_NE(2u, OutlinesOf(doc.get())); + EXPECT_EQ("1:One", Tree(doc.get())); + EXPECT_EQ(1, FPDF_GetPageCount(doc.get())); +} + +// Lists a broken file shares are counted once: in an outline whose every +// item lists the items before it as its children, each item counts once, +// and counting costs a look at each (not one per list it is in). +TEST_F(EPDFOutlineEmbedderTest, SharedListsAreCountedOnce) { + constexpr uint32_t kItems = 3000; + const uint32_t last = 4 + kItems; + std::vector outline = { + "<< /Type /Outlines /First 5 0 R /Last " + std::to_string(last) + + " 0 R >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] >>", + }; + for (uint32_t objnum = 5; objnum <= last; ++objnum) { + std::string item = "<< /Title (x) /Parent 3 0 R /First 5 0 R /Count 1"; + if (objnum < last) { + item += " /Next " + std::to_string(objnum + 1) + " 0 R"; + } + outline.push_back(item + " >>"); + } + ScopedFPDFDocument doc = OpenPlain(MakeDocumentWith(outline)); + ASSERT_TRUE(doc); + ASSERT_TRUE(Create(doc.get(), 0, last, L"After")); + EXPECT_EQ(static_cast(kItems) + 1, + DictOf(doc.get(), 3)->GetIntegerFor("Count")); +} + +// Reads of the outline go 128 levels deep; an item deeper than that is +// neither found nor written by its number, and the one at the last level +// still is. +TEST_F(EPDFOutlineEmbedderTest, ItemsDeeperThanReadsGoAreLeftAlone) { + constexpr uint32_t kLevels = 130; + std::vector outline = { + "<< /Type /Outlines /First 5 0 R /Last 5 0 R >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] >>", + }; + for (uint32_t level = 0; level < kLevels; ++level) { + const uint32_t objnum = 5 + level; + std::string item = "<< /Title (x) /Parent " + + std::to_string(level ? objnum - 1 : 3) + " 0 R"; + if (level + 1 < kLevels) { + const std::string child = std::to_string(objnum + 1); + item += " /First " + child + " 0 R /Last " + child + " 0 R"; + } + outline.push_back(item + " >>"); + } + ScopedFPDFDocument doc = OpenPlain(MakeDocumentWith(outline)); + ASSERT_TRUE(doc); + ScopedFPDFWideString title = GetFPDFWideString(L"y"); + EXPECT_TRUE(EPDFOutline_GetItem(doc.get(), 5 + 127)); + EXPECT_TRUE(EPDFOutline_SetTitle(doc.get(), 5 + 127, title.get())); + EXPECT_FALSE(EPDFOutline_GetItem(doc.get(), 5 + 128)); + EXPECT_FALSE(EPDFOutline_SetTitle(doc.get(), 5 + 128, title.get())); + EXPECT_FALSE(EPDFOutline_DeleteItem(doc.get(), 5 + 128)); +} + +// Random writes, checked after each one: the links and the counts are what +// the tree says. On a layer each write is a transaction, and one in five is +// aborted, which must leave the outline as it was. +void EPDFOutlineEmbedderTest::RandomWrites(FPDF_DOCUMENT doc, bool layer) { std::mt19937 random(20261018); auto pick = [&](const std::vector& from) -> uint32_t { if (from.empty()) { @@ -679,44 +797,67 @@ TEST_F(EPDFOutlineEmbedderTest, RandomWritesKeepTheOutlineRight) { }; for (int step = 0; step < 300; ++step) { SCOPED_TRACE(step); - const std::vector items = Items(doc.get()); + const std::vector items = Items(doc); const uint32_t item = pick(items); // A parent and a neighbour that may or may not fit: refusals must also // leave the outline right. const uint32_t parent = random() % 4 ? pick(items) : 0; std::vector siblings; for (FPDF_BOOKMARK child = FPDFBookmark_GetFirstChild( - doc.get(), - parent ? EPDFOutline_GetItem(doc.get(), parent) : nullptr); - child; child = FPDFBookmark_GetNextSibling(doc.get(), child)) { + doc, parent ? EPDFOutline_GetItem(doc, parent) : nullptr); + child; child = FPDFBookmark_GetNextSibling(doc, child)) { siblings.push_back(EPDFOutline_GetObjectNumber(child)); } const uint32_t after = random() % 3 ? pick(siblings) : 0; + const std::string before = Tree(doc); + if (layer) { + ASSERT_TRUE(EPDFLayer_BeginTransaction(doc)); + } switch (random() % 5) { case 0: case 1: - Create(doc.get(), parent, after, L"new"); + // A layer creates at numbers it holds; a plain document picks one. + Create(doc, parent, after, L"new", layer ? next_number++ : 0); break; case 2: if (item) { - EPDFOutline_MoveItem(doc.get(), item, parent, after); + EPDFOutline_MoveItem(doc, item, parent, after); } break; case 3: if (item && items.size() > 3) { - EPDFOutline_DeleteItem(doc.get(), item); + EPDFOutline_DeleteItem(doc, item); } break; case 4: if (item) { - EPDFOutline_SetOpen(doc.get(), item, random() % 2); + EPDFOutline_SetOpen(doc, item, random() % 2); } break; } - ASSERT_EQ("", Problems(doc.get())); + if (layer && random() % 5 == 0) { + ASSERT_TRUE(EPDFLayer_AbortTransaction(doc)); + ASSERT_EQ(before, Tree(doc)); + } else if (layer) { + ASSERT_TRUE(EPDFLayer_CommitTransaction(doc)); + } + ASSERT_EQ("", Problems(doc)); } } +TEST_F(EPDFOutlineEmbedderTest, RandomWritesKeepTheOutlineRight) { + ScopedFPDFDocument doc = OpenPlain(); + ASSERT_TRUE(doc); + RandomWrites(doc.get(), /*layer=*/false); +} + +TEST_F(EPDFOutlineEmbedderTest, RandomWritesKeepALayersOutlineRight) { + ScopedFPDFDocument layer = OpenLayer(); + ASSERT_TRUE(layer); + ASSERT_TRUE(EPDFLayer_RaiseLastObjectNumber(layer.get(), 400)); + RandomWrites(layer.get(), /*layer=*/true); +} + // A write changes only what it must: appending an item touches the item // before it and the outline, never the items further up the list. TEST_F(EPDFOutlineEmbedderTest, WritesCopyOnlyWhatChanges) { @@ -839,6 +980,31 @@ TEST_F(EPDFOutlineEmbedderTest, ImportFindsItsPlace) { EXPECT_EQ("", Problems(layer.get())); } +// What goes back into the outline is what the item's number holds once the +// capture is in: a page that took the number after a save dropped the +// deleted item never becomes an item. +TEST_F(EPDFOutlineEmbedderTest, ImportRefusesWhenItsNumberHoldsSomethingElse) { + ScopedFPDFDocument layer = OpenLayer(); + ASSERT_TRUE(layer); + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + ASSERT_EQ(20u, Create(layer.get(), 0, kC, L"New", 20)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + const std::vector capture = ExportItem(layer.get(), 20); + ASSERT_FALSE(capture.empty()); + ASSERT_TRUE(EPDFOutline_DeleteItem(layer.get(), 20)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + + ScopedFPDFDocument reopened = Reopen(layer.get()); + ASSERT_TRUE(reopened); + ASSERT_TRUE(EPDFLayer_RaiseLastObjectNumber(reopened.get(), 40)); + ASSERT_TRUE(EPDFLayer_BeginTransaction(reopened.get())); + ASSERT_TRUE(EPDFPage_InsertBlankRaw(reopened.get(), 0, 100, 100, 20)); + EXPECT_FALSE(ImportItem(reopened.get(), capture)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(reopened.get())); + EXPECT_EQ("4:A+1[A1] B C-1[C1]", Tree(reopened.get())); +} + TEST_F(EPDFOutlineEmbedderTest, CapturesNeedAnItemAndALayer) { ScopedFPDFDocument layer = OpenLayer(); ASSERT_TRUE(layer); diff --git a/fpdfsdk/epdf_outline_helpers.cpp b/fpdfsdk/epdf_outline_helpers.cpp index 5c65d51a6..0af703577 100644 --- a/fpdfsdk/epdf_outline_helpers.cpp +++ b/fpdfsdk/epdf_outline_helpers.cpp @@ -13,11 +13,18 @@ #include "core/fpdfapi/parser/cpdf_name.h" #include "core/fpdfapi/parser/cpdf_number.h" #include "core/fpdfapi/parser/cpdf_reference.h" +#include "core/fpdfdoc/cpdf_bookmarktree.h" namespace epdf { namespace { +// How deep an item may sit: the steps up its /Parent chain to the outline's +// dictionary, 1 for a top-level item. Reads of the outline go no deeper (the +// engine's OUTLINE_READ_LIMITS.depth), so no write reaches an item a read +// can't show, and no walk up a chain goes on for long. +constexpr size_t kMaxOutlineDepth = 128; + RetainPtr DictOf(CPDF_Document* doc, uint32_t objnum) { return objnum ? ToDictionary(doc->GetOrParseIndirectObject(objnum)) : nullptr; } @@ -34,9 +41,14 @@ bool IsOpen(const CPDF_Dictionary* item) { } // The children of |dict|, whose own number is |self| (0 when it has none). +// With |taken|, the list also ends at an item another list gave, and its +// own items join |taken|: in a well-formed outline every item is in one +// list, so a walk that shares |taken| looks at each item once, however a +// broken file links its lists. std::vector ChildrenOfDict(CPDF_Document* doc, const CPDF_Dictionary* dict, - uint32_t self) { + uint32_t self, + std::set* taken = nullptr) { std::vector children; if (!dict) { return children; @@ -45,20 +57,28 @@ std::vector ChildrenOfDict(CPDF_Document* doc, if (self) { seen.insert(self); } + // The outline's own dictionary is never an item. + if (const uint32_t root = OutlineRootOf(doc).value_or(0)) { + seen.insert(root); + } RetainPtr link = dict->GetObjectFor("First"); while (link) { OutlineChild child; if (const CPDF_Reference* ref = link->AsReference()) { child.objnum = ref->GetRefObjNum(); - if (!seen.insert(child.objnum).second) { - break; // a loop + if (!seen.insert(child.objnum).second || + (taken && taken->contains(child.objnum))) { + break; // a loop, or an item another list has } child.dict = DictOf(doc, child.objnum); } else { child.dict = ToDictionary(link); } - if (!child.dict) { - break; // a reference to nothing, or not a dictionary + if (!CPDF_BookmarkTree::IsOutlineNode(child.dict.Get())) { + break; // a reference to nothing, or not an item: a page, an action + } + if (taken && child.objnum) { + taken->insert(child.objnum); } link = child.dict->GetObjectFor("Next"); children.push_back(std::move(child)); @@ -69,13 +89,15 @@ std::vector ChildrenOfDict(CPDF_Document* doc, // How many items show under |node| when it is open: each child, and what // shows under each open child, read from the tree. |known| holds what was // worked out already, by object number. +// Each item is counted once, so the count costs one look at each item under +// |node|, whatever a broken file shares between lists. int VisibleUnder(CPDF_Document* doc, uint32_t node, const std::map& known) { int total = 0; - std::set expanded = {node}; + std::set taken = {node}; std::vector pending = - ChildrenOfDict(doc, DictOf(doc, node).Get(), node); + ChildrenOfDict(doc, DictOf(doc, node).Get(), node, &taken); while (!pending.empty()) { OutlineChild child = std::move(pending.back()); pending.pop_back(); @@ -88,12 +110,9 @@ int VisibleUnder(CPDF_Document* doc, total += it->second; continue; } - if (!expanded.insert(child.objnum).second) { - continue; // a loop - } } for (OutlineChild& grandchild : - ChildrenOfDict(doc, child.dict.Get(), child.objnum)) { + ChildrenOfDict(doc, child.dict.Get(), child.objnum, &taken)) { pending.push_back(std::move(grandchild)); } } @@ -158,14 +177,14 @@ std::optional OutlineRootOf(CPDF_Document* doc) { const CPDF_Dictionary* catalog = doc->GetRoot(); RetainPtr value = catalog ? catalog->GetObjectFor("Outlines") : nullptr; - if (!value) { + // What isn't an outline's dictionary (a page tree a broken file links + // there) is no outline: a create makes a new one in its place. + if (!value || !CPDF_BookmarkTree::IsOutlineNode( + ToDictionary(value->GetDirect()).Get())) { return 0; } const CPDF_Reference* ref = value->AsReference(); - if (!ref) { - return value->IsDictionary() ? std::nullopt : std::optional(0); - } - return DictOf(doc, ref->GetRefObjNum()) ? ref->GetRefObjNum() : 0; + return ref ? std::optional(ref->GetRefObjNum()) : std::nullopt; } uint32_t CreateOutlineRoot(CPDF_Document* doc) { @@ -201,8 +220,8 @@ bool IsOutlineItem(CPDF_Document* doc, uint32_t root, uint32_t objnum) { } std::set seen; for (uint32_t node = objnum; node != root;) { - if (!seen.insert(node).second) { - return false; // a loop in the /Parent chain + if (seen.size() >= kMaxOutlineDepth || !seen.insert(node).second) { + return false; // deeper than reads go, or a loop in the /Parent chain } const uint32_t parent = OutlineParentOf(doc, node); if (!parent) { @@ -225,12 +244,12 @@ uint32_t OutlineParentOf(CPDF_Document* doc, uint32_t objnum) { std::vector OutlineSubtreeOf(CPDF_Document* doc, uint32_t objnum) { std::vector items = {objnum}; - std::set seen = {objnum}; + std::set taken = {objnum}; for (size_t i = 0; i < items.size(); ++i) { const uint32_t parent = items[i]; - for (const OutlineChild& child : OutlineChildrenOf(doc, parent)) { - if (child.objnum && LinkOf(child.dict.Get(), "Parent") == parent && - seen.insert(child.objnum).second) { + for (const OutlineChild& child : + ChildrenOfDict(doc, DictOf(doc, parent).Get(), parent, &taken)) { + if (child.objnum && LinkOf(child.dict.Get(), "Parent") == parent) { items.push_back(child.objnum); } } @@ -258,6 +277,9 @@ bool RecountOutline(CPDF_Document* doc, uint32_t root, uint32_t start) { std::map known; std::set seen; for (uint32_t node = start; node && seen.insert(node).second;) { + if (seen.size() > kMaxOutlineDepth + 1) { + return false; // deeper than an item may sit + } RetainPtr dict = DictOf(doc, node); if (!dict) { return false; diff --git a/fpdfsdk/epdf_outline_helpers.h b/fpdfsdk/epdf_outline_helpers.h index cc0c12c3b..3142ec281 100644 --- a/fpdfsdk/epdf_outline_helpers.h +++ b/fpdfsdk/epdf_outline_helpers.h @@ -29,16 +29,18 @@ struct OutlineChild { }; // The outline's own dictionary, /Root /Outlines: its object number, 0 when -// the document has none, or nullopt when it is a direct dictionary, which no -// item's /Parent can refer to. +// the document has none (or links something that isn't an outline there, see +// CPDF_BookmarkTree::IsOutlineNode()), or nullopt when it is a direct +// dictionary, which no item's /Parent can refer to. std::optional OutlineRootOf(CPDF_Document* doc); // Makes a new /Root /Outlines and returns its object number, or 0. uint32_t CreateOutlineRoot(CPDF_Document* doc); // The children of |node| (the outline's dictionary or an item): its /First -// and the /Next that follow. A repeat, or a reference to nothing, ends the -// list. +// and the /Next that follow. A repeat, a reference to nothing, the outline's +// own dictionary, or a dictionary that isn't an item (a page, an action: +// CPDF_BookmarkTree::IsOutlineNode()) ends the list. std::vector OutlineChildrenOf(CPDF_Document* doc, uint32_t node); // The object numbers of |node|'s children, or nullopt when one of them isn't @@ -48,7 +50,7 @@ std::optional> OutlineChildNumbers(CPDF_Document* doc, // Whether |objnum| is an item of the outline whose dictionary is |root|: // each step up its /Parent chain is among the parent's children, up to -// |root|. +// |root|, in at most 128 steps (the depth reads of the outline go to). bool IsOutlineItem(CPDF_Document* doc, uint32_t root, uint32_t objnum); // The object number the item's /Parent refers to, or 0. @@ -56,7 +58,8 @@ uint32_t OutlineParentOf(CPDF_Document* doc, uint32_t objnum); // The item |objnum| and the items under it, the item first. A child goes // with its parent only when its /Parent says so, so a list that wandered -// into another part of the tree never takes that part along. +// into another part of the tree never takes that part along, and each item +// is looked at once. std::vector OutlineSubtreeOf(CPDF_Document* doc, uint32_t objnum); // Makes |children| the children of |node|, in order: their /Parent, /Prev diff --git a/public/epdf_capture.h b/public/epdf_capture.h index c144a1fbb..5ef254eca 100644 --- a/public/epdf_capture.h +++ b/public/epdf_capture.h @@ -19,18 +19,25 @@ extern "C" { // A capture holds the layer's own versions of the objects involved and of // what they reach: what the layer made, and what it edited. It never holds // the uploaded file's originals, which always stay. It is PDF syntax: the -// line "%EPDF-CAPTURE 1", a manifest dictionary naming what was captured and -// where it was, then each object as "N G obj ... endobj", a stream with its -// bytes as stored. Following references stops at the catalog, the page tree -// and its pages, and at the keys /P, /Parent, /IRT and /OC. +// line "%EPDF-CAPTURE 1", a manifest dictionary naming what was captured, +// where it was and, when the layer knows its base's SHA-256, the file it was +// taken on (/Base: supplied with EPDF_SetBaseDocumentSha256() or worked out +// once, so taking a capture never hashes the file), then each object as +// "N G obj ... endobj", a stream with its bytes as stored. Following references +// stops at the catalog, the page tree and its pages, and at the keys /P, +// /Parent, /IRT and /OC. // // An import runs inside a layer transaction (EPDFLayer_BeginTransaction()). // It puts an object back only where the layer holds no version of its number // now: a version the layer holds may be newer (a shared font grows its -// subset), and numbers are never reused, so it is the same object. A capture -// that names a number above the layer's last one is refused before anything -// is written. Which objects to capture, and when to put them back, is the -// caller's decision. +// subset). What it puts back in its place (an annotation into /Annots, a page +// into the page tree) is what the number holds once the objects are in, and +// an import that would place something of another kind is refused: a save can +// free a deleted object's number for another object. A capture that names +// another file, one that names a number above the layer's last one, and one +// that holds the catalog, the page tree or a page it isn't of are refused +// before anything is written. Which objects to capture, and when to put them +// back, is the caller's decision. // // Exports return a buffer the caller releases with EPDF_FreeBuffer(), its // size in |out_size|, or NULL. All of these work on layer documents only, and @@ -71,6 +78,22 @@ EPDFDoc_ExportDictRawToOwnedBuffer(FPDF_DOCUMENT document, FPDF_BYTESTRING deep_keys, unsigned long* out_size); +// Experimental EmbedPDF Extension API. +// What EPDFDoc_ExportDictRawToOwnedBuffer() holds of the dictionary |objnum|, +// as bytes a guard compares to tell whether it reads the same: the +// dictionary without the keys named in |skip_keys| (separated by spaces; +// NULL for none), and under |deep_keys| every object the layer holds a +// version of that they reach, each stream as it reads (its decoded bytes, +// without /Filter, /DecodeParms and /DL). So a save that only encodes a +// stream again changes nothing. Never imported. NULL when |objnum| is not a +// dictionary. +FPDF_EXPORT void* FPDF_CALLCONV +EPDFDoc_ExportDictCanonicalToOwnedBuffer(FPDF_DOCUMENT document, + uint32_t objnum, + FPDF_BYTESTRING deep_keys, + FPDF_BYTESTRING skip_keys, + unsigned long* out_size); + // Experimental EmbedPDF Extension API. // Puts the dictionary of |capture| back as it was: the objects under its deep // keys where the layer holds no version, then exactly the keys it held, with @@ -125,7 +148,8 @@ EPDFOutline_ExportItemRawToOwnedBuffer(FPDF_DOCUMENT document, // children, right after the sibling it followed while that is still one of // them, else first. Counts are kept, as every outline write keeps them. // False outside a transaction, for a malformed capture, when its parent is no -// longer an item in the outline, or when the item is in the outline already. +// longer an item in the outline, when the item is in the outline already, or +// when what its number holds isn't an outline item. FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFOutline_ImportItemRaw(FPDF_DOCUMENT document, const void* capture, @@ -136,8 +160,9 @@ EPDFOutline_ImportItemRaw(FPDF_DOCUMENT document, // the page and every object the layer holds a version of that it reaches // (its contents, resources and annotations, with their appearances; never // another page or the form's fields), the /Names /Pages keys that name it, -// and its place: the page it followed. NULL when |page_objnum| is not a page -// in the page tree. +// its place (the page it followed), and what it takes from the page tree +// above it: the /Resources, /MediaBox, /CropBox and /Rotate it doesn't hold +// itself. NULL when |page_objnum| is not a page in the page tree. FPDF_EXPORT void* FPDF_CALLCONV EPDFPage_ExportRawToOwnedBuffer(FPDF_DOCUMENT document, uint32_t page_objnum, @@ -147,14 +172,51 @@ EPDFPage_ExportRawToOwnedBuffer(FPDF_DOCUMENT document, // Puts the page of |capture| back: its objects where the layer holds no // version, the page itself also over the null a delete leaves for a page the // layer made; then the page into the page tree right after the page it -// followed while that is still in it, else first; then each of its keys that -// no registration holds now. False outside a transaction, for a malformed -// capture, or when the page is in the page tree already. +// followed while that is still in it, else first, holding what it took from +// the page tree as its own keys where its place now gives otherwise; then each +// of its keys that no registration holds now. False outside a transaction, for +// a malformed capture, when the page is in the page tree already, or when what +// its number holds is no page. FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFPage_ImportRaw(FPDF_DOCUMENT document, const void* capture, unsigned long size); +// Experimental EmbedPDF Extension API. +// The embedded file under |key| in /Root /Names /EmbeddedFiles, as +// FPDFDoc_DeleteAttachment() removes it: its key, its file specification +// (an object of its own, or written in the tree) and every object the layer +// holds a version of that it reaches (its /EF streams). NULL when no entry +// has |key|. +FPDF_EXPORT void* FPDF_CALLCONV +EPDFAttachment_ExportRawToOwnedBuffer(FPDF_DOCUMENT document, + FPDF_WIDESTRING key, + unsigned long* out_size); + +// Experimental EmbedPDF Extension API. +// What EPDFAttachment_ExportRawToOwnedBuffer() holds of the file under |key|, +// as bytes a guard compares to tell whether it is still the same file, as it +// was: its key, the number of its file specification (none when it is +// written in the tree), the specification, and every object the layer holds +// a version of that it reaches, each stream as it reads (as +// EPDFDoc_ExportDictCanonicalToOwnedBuffer() writes them). Never imported. +// NULL when no entry has |key|. +FPDF_EXPORT void* FPDF_CALLCONV +EPDFAttachment_ExportCanonicalToOwnedBuffer(FPDF_DOCUMENT document, + FPDF_WIDESTRING key, + unsigned long* out_size); + +// Experimental EmbedPDF Extension API. +// Puts the file of |capture| back under its key: its objects where the layer +// holds no version, then the entry, in key order (the tree is made when the +// document has none). False outside a transaction, for a malformed capture, +// when another file holds the key now, or when what its number holds isn't a +// file specification. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAttachment_ImportRaw(FPDF_DOCUMENT document, + const void* capture, + unsigned long size); + #ifdef __cplusplus } // extern "C" #endif diff --git a/public/epdf_outline.h b/public/epdf_outline.h index c727ca619..7c9cfc037 100644 --- a/public/epdf_outline.h +++ b/public/epdf_outline.h @@ -22,11 +22,16 @@ extern "C" { // number to anything else. A |parent| is an item's number, or 0 for the // outline itself, whose children are the top-level items. An item is in the // outline when each step up its /Parent chain is among the parent's children, -// up to /Root /Outlines: a deleted item is not, even where its dictionary can -// still be read. +// up to /Root /Outlines, in at most 128 steps (the depth reads of the outline +// go to): a deleted item is not, even where its dictionary can still be read. // // A parent's children are its /First and the /Next that follow, a loop ending -// the list. Every write keeps the outline well formed: +// the list. A dictionary that isn't an item also ends it, for the readers of +// fpdf_doc.h as for these functions: one with a /Type other than /Outlines, +// or a key of a page, the page tree, an annotation, a field or an action +// (/Kids, /Contents, /MediaBox, /Resources, /Subtype, /FT, /S). An /Outlines +// that isn't an outline dictionary is no outline: a create makes a new one in +// its place. Every write keeps the outline well formed: // * It rewrites /Parent, /Prev, /Next, /First and /Last of every list it // changes, from that walk, so a wrong /Last or /Prev never misleads it. It // writes only the keys whose value changes.