Commit 8720065c61799978103f2441987f2ac2522c86c6
1 parent
4927e049
Fix logic around cleartext metadata (fixes #1368)
Only top-level XMP metadata is supposed to be left unencrypted. All other metadata is not treated specially.
Showing
13 changed files
with
164 additions
and
31 deletions
include/qpdf/QPDF.hh
| @@ -835,6 +835,7 @@ class QPDF | @@ -835,6 +835,7 @@ class QPDF | ||
| 835 | qpdf_offset_t offset, | 835 | qpdf_offset_t offset, |
| 836 | size_t length, | 836 | size_t length, |
| 837 | QPDFObjectHandle dict, | 837 | QPDFObjectHandle dict, |
| 838 | + bool is_root_metadata, | ||
| 838 | Pipeline* pipeline, | 839 | Pipeline* pipeline, |
| 839 | bool suppress_warnings, | 840 | bool suppress_warnings, |
| 840 | bool will_retry); | 841 | bool will_retry); |
| @@ -848,6 +849,7 @@ class QPDF | @@ -848,6 +849,7 @@ class QPDF | ||
| 848 | qpdf_offset_t offset, | 849 | qpdf_offset_t offset, |
| 849 | size_t length, | 850 | size_t length, |
| 850 | QPDFObjectHandle dict, | 851 | QPDFObjectHandle dict, |
| 852 | + bool is_root_metadata, | ||
| 851 | Pipeline* pipeline, | 853 | Pipeline* pipeline, |
| 852 | bool suppress_warnings, | 854 | bool suppress_warnings, |
| 853 | bool will_retry); | 855 | bool will_retry); |
| @@ -920,6 +922,7 @@ class QPDF | @@ -920,6 +922,7 @@ class QPDF | ||
| 920 | Pipeline*& pipeline, | 922 | Pipeline*& pipeline, |
| 921 | QPDFObjGen og, | 923 | QPDFObjGen og, |
| 922 | QPDFObjectHandle& stream_dict, | 924 | QPDFObjectHandle& stream_dict, |
| 925 | + bool is_root_metadata, | ||
| 923 | std::unique_ptr<Pipeline>& heap); | 926 | std::unique_ptr<Pipeline>& heap); |
| 924 | 927 | ||
| 925 | // Methods to support object copying | 928 | // Methods to support object copying |
include/qpdf/QPDFObjectHandle.hh
| @@ -1063,6 +1063,10 @@ class QPDFObjectHandle: public qpdf::BaseHandle | @@ -1063,6 +1063,10 @@ class QPDFObjectHandle: public qpdf::BaseHandle | ||
| 1063 | QPDF_DLL | 1063 | QPDF_DLL |
| 1064 | void replaceDict(QPDFObjectHandle const&); | 1064 | void replaceDict(QPDFObjectHandle const&); |
| 1065 | 1065 | ||
| 1066 | + // Test whether a stream is the root XMP /Metadata object of its owning QPDF. | ||
| 1067 | + QPDF_DLL | ||
| 1068 | + bool isRootMetadata() const; | ||
| 1069 | + | ||
| 1066 | // REPLACING STREAM DATA | 1070 | // REPLACING STREAM DATA |
| 1067 | 1071 | ||
| 1068 | // Note about all replaceStreamData methods: whatever values are passed as filter and | 1072 | // Note about all replaceStreamData methods: whatever values are passed as filter and |
libqpdf/QPDF.cc
| @@ -115,13 +115,15 @@ QPDF::ForeignStreamData::ForeignStreamData( | @@ -115,13 +115,15 @@ QPDF::ForeignStreamData::ForeignStreamData( | ||
| 115 | QPDFObjGen foreign_og, | 115 | QPDFObjGen foreign_og, |
| 116 | qpdf_offset_t offset, | 116 | qpdf_offset_t offset, |
| 117 | size_t length, | 117 | size_t length, |
| 118 | - QPDFObjectHandle local_dict) : | 118 | + QPDFObjectHandle local_dict, |
| 119 | + bool is_root_metadata) : | ||
| 119 | encp(encp), | 120 | encp(encp), |
| 120 | file(file), | 121 | file(file), |
| 121 | foreign_og(foreign_og), | 122 | foreign_og(foreign_og), |
| 122 | offset(offset), | 123 | offset(offset), |
| 123 | length(length), | 124 | length(length), |
| 124 | - local_dict(local_dict) | 125 | + local_dict(local_dict), |
| 126 | + is_root_metadata(is_root_metadata) | ||
| 125 | { | 127 | { |
| 126 | } | 128 | } |
| 127 | 129 | ||
| @@ -740,7 +742,8 @@ QPDF::copyStreamData(QPDFObjectHandle result, QPDFObjectHandle foreign) | @@ -740,7 +742,8 @@ QPDF::copyStreamData(QPDFObjectHandle result, QPDFObjectHandle foreign) | ||
| 740 | foreign, | 742 | foreign, |
| 741 | foreign.getParsedOffset(), | 743 | foreign.getParsedOffset(), |
| 742 | stream.getLength(), | 744 | stream.getLength(), |
| 743 | - dict); | 745 | + dict, |
| 746 | + stream.isRootMetadata()); | ||
| 744 | m->copied_stream_data_provider->registerForeignStream(local_og, foreign_stream_data); | 747 | m->copied_stream_data_provider->registerForeignStream(local_og, foreign_stream_data); |
| 745 | result.replaceStreamData( | 748 | result.replaceStreamData( |
| 746 | m->copied_streams, dict.getKey("/Filter"), dict.getKey("/DecodeParms")); | 749 | m->copied_streams, dict.getKey("/Filter"), dict.getKey("/DecodeParms")); |
| @@ -849,13 +852,15 @@ QPDF::pipeStreamData( | @@ -849,13 +852,15 @@ QPDF::pipeStreamData( | ||
| 849 | qpdf_offset_t offset, | 852 | qpdf_offset_t offset, |
| 850 | size_t length, | 853 | size_t length, |
| 851 | QPDFObjectHandle stream_dict, | 854 | QPDFObjectHandle stream_dict, |
| 855 | + bool is_root_metadata, | ||
| 852 | Pipeline* pipeline, | 856 | Pipeline* pipeline, |
| 853 | bool suppress_warnings, | 857 | bool suppress_warnings, |
| 854 | bool will_retry) | 858 | bool will_retry) |
| 855 | { | 859 | { |
| 856 | std::unique_ptr<Pipeline> to_delete; | 860 | std::unique_ptr<Pipeline> to_delete; |
| 857 | if (encp->encrypted) { | 861 | if (encp->encrypted) { |
| 858 | - decryptStream(encp, file, qpdf_for_warning, pipeline, og, stream_dict, to_delete); | 862 | + decryptStream( |
| 863 | + encp, file, qpdf_for_warning, pipeline, og, stream_dict, is_root_metadata, to_delete); | ||
| 859 | } | 864 | } |
| 860 | 865 | ||
| 861 | bool attempted_finish = false; | 866 | bool attempted_finish = false; |
| @@ -911,6 +916,7 @@ QPDF::pipeStreamData( | @@ -911,6 +916,7 @@ QPDF::pipeStreamData( | ||
| 911 | qpdf_offset_t offset, | 916 | qpdf_offset_t offset, |
| 912 | size_t length, | 917 | size_t length, |
| 913 | QPDFObjectHandle stream_dict, | 918 | QPDFObjectHandle stream_dict, |
| 919 | + bool is_root_metadata, | ||
| 914 | Pipeline* pipeline, | 920 | Pipeline* pipeline, |
| 915 | bool suppress_warnings, | 921 | bool suppress_warnings, |
| 916 | bool will_retry) | 922 | bool will_retry) |
| @@ -923,6 +929,7 @@ QPDF::pipeStreamData( | @@ -923,6 +929,7 @@ QPDF::pipeStreamData( | ||
| 923 | offset, | 929 | offset, |
| 924 | length, | 930 | length, |
| 925 | stream_dict, | 931 | stream_dict, |
| 932 | + is_root_metadata, | ||
| 926 | pipeline, | 933 | pipeline, |
| 927 | suppress_warnings, | 934 | suppress_warnings, |
| 928 | will_retry); | 935 | will_retry); |
| @@ -946,6 +953,7 @@ QPDF::pipeForeignStreamData( | @@ -946,6 +953,7 @@ QPDF::pipeForeignStreamData( | ||
| 946 | foreign->offset, | 953 | foreign->offset, |
| 947 | foreign->length, | 954 | foreign->length, |
| 948 | foreign->local_dict, | 955 | foreign->local_dict, |
| 956 | + foreign->is_root_metadata, | ||
| 949 | pipeline, | 957 | pipeline, |
| 950 | suppress_warnings, | 958 | suppress_warnings, |
| 951 | will_retry); | 959 | will_retry); |
libqpdf/QPDFWriter.cc
| @@ -1247,18 +1247,18 @@ QPDFWriter::writeTrailer( | @@ -1247,18 +1247,18 @@ QPDFWriter::writeTrailer( | ||
| 1247 | bool | 1247 | bool |
| 1248 | QPDFWriter::willFilterStream( | 1248 | QPDFWriter::willFilterStream( |
| 1249 | QPDFObjectHandle stream, | 1249 | QPDFObjectHandle stream, |
| 1250 | - bool& compress_stream, // out only | ||
| 1251 | - bool& is_metadata, // out only | 1250 | + bool& compress_stream, // out only |
| 1251 | + bool& is_root_metadata, // out only | ||
| 1252 | std::string* stream_data) | 1252 | std::string* stream_data) |
| 1253 | { | 1253 | { |
| 1254 | compress_stream = false; | 1254 | compress_stream = false; |
| 1255 | - is_metadata = false; | 1255 | + is_root_metadata = false; |
| 1256 | 1256 | ||
| 1257 | QPDFObjGen old_og = stream.getObjGen(); | 1257 | QPDFObjGen old_og = stream.getObjGen(); |
| 1258 | QPDFObjectHandle stream_dict = stream.getDict(); | 1258 | QPDFObjectHandle stream_dict = stream.getDict(); |
| 1259 | 1259 | ||
| 1260 | - if (stream_dict.isDictionaryOfType("/Metadata")) { | ||
| 1261 | - is_metadata = true; | 1260 | + if (stream.isRootMetadata()) { |
| 1261 | + is_root_metadata = true; | ||
| 1262 | } | 1262 | } |
| 1263 | bool filter = stream.isDataModified() || m->compress_streams || m->stream_decode_level; | 1263 | bool filter = stream.isDataModified() || m->compress_streams || m->stream_decode_level; |
| 1264 | bool filter_on_write = stream.getFilterOnWrite(); | 1264 | bool filter_on_write = stream.getFilterOnWrite(); |
| @@ -1280,7 +1280,7 @@ QPDFWriter::willFilterStream( | @@ -1280,7 +1280,7 @@ QPDFWriter::willFilterStream( | ||
| 1280 | } | 1280 | } |
| 1281 | bool normalize = false; | 1281 | bool normalize = false; |
| 1282 | bool uncompress = false; | 1282 | bool uncompress = false; |
| 1283 | - if (filter_on_write && is_metadata && (!m->encrypted || !m->encrypt_metadata)) { | 1283 | + if (filter_on_write && is_root_metadata && (!m->encrypted || !m->encrypt_metadata)) { |
| 1284 | QTC::TC("qpdf", "QPDFWriter not compressing metadata"); | 1284 | QTC::TC("qpdf", "QPDFWriter not compressing metadata"); |
| 1285 | filter = true; | 1285 | filter = true; |
| 1286 | compress_stream = false; | 1286 | compress_stream = false; |
libqpdf/QPDF_Stream.cc
| @@ -281,6 +281,16 @@ Stream::getRawStreamData() | @@ -281,6 +281,16 @@ Stream::getRawStreamData() | ||
| 281 | } | 281 | } |
| 282 | 282 | ||
| 283 | bool | 283 | bool |
| 284 | +Stream::isRootMetadata() const | ||
| 285 | +{ | ||
| 286 | + if (!getDict().isDictionaryOfType("/Metadata", "/XML")) { | ||
| 287 | + return false; | ||
| 288 | + } | ||
| 289 | + auto root_metadata = qpdf()->getRoot().getKey("/Metadata"); | ||
| 290 | + return root_metadata.isSameObjectAs(obj); | ||
| 291 | +} | ||
| 292 | + | ||
| 293 | +bool | ||
| 284 | Stream::filterable( | 294 | Stream::filterable( |
| 285 | std::vector<std::shared_ptr<QPDFStreamFilter>>& filters, | 295 | std::vector<std::shared_ptr<QPDFStreamFilter>>& filters, |
| 286 | bool& specialized_compression, | 296 | bool& specialized_compression, |
| @@ -520,6 +530,7 @@ Stream::pipeStreamData( | @@ -520,6 +530,7 @@ Stream::pipeStreamData( | ||
| 520 | obj->getParsedOffset(), | 530 | obj->getParsedOffset(), |
| 521 | s->length, | 531 | s->length, |
| 522 | s->stream_dict, | 532 | s->stream_dict, |
| 533 | + isRootMetadata(), | ||
| 523 | pipeline, | 534 | pipeline, |
| 524 | suppress_warnings, | 535 | suppress_warnings, |
| 525 | will_retry)) { | 536 | will_retry)) { |
| @@ -626,6 +637,12 @@ QPDFObjectHandle::replaceDict(QPDFObjectHandle const& new_dict) | @@ -626,6 +637,12 @@ QPDFObjectHandle::replaceDict(QPDFObjectHandle const& new_dict) | ||
| 626 | as_stream(error).replaceDict(new_dict); | 637 | as_stream(error).replaceDict(new_dict); |
| 627 | } | 638 | } |
| 628 | 639 | ||
| 640 | +bool | ||
| 641 | +QPDFObjectHandle::isRootMetadata() const | ||
| 642 | +{ | ||
| 643 | + return as_stream(error).isRootMetadata(); | ||
| 644 | +} | ||
| 645 | + | ||
| 629 | std::shared_ptr<Buffer> | 646 | std::shared_ptr<Buffer> |
| 630 | QPDFObjectHandle::getStreamData(qpdf_stream_decode_level_e level) | 647 | QPDFObjectHandle::getStreamData(qpdf_stream_decode_level_e level) |
| 631 | { | 648 | { |
libqpdf/QPDF_encryption.cc
| @@ -1049,6 +1049,7 @@ QPDF::decryptStream( | @@ -1049,6 +1049,7 @@ QPDF::decryptStream( | ||
| 1049 | Pipeline*& pipeline, | 1049 | Pipeline*& pipeline, |
| 1050 | QPDFObjGen og, | 1050 | QPDFObjGen og, |
| 1051 | QPDFObjectHandle& stream_dict, | 1051 | QPDFObjectHandle& stream_dict, |
| 1052 | + bool is_root_metadata, | ||
| 1052 | std::unique_ptr<Pipeline>& decrypt_pipeline) | 1053 | std::unique_ptr<Pipeline>& decrypt_pipeline) |
| 1053 | { | 1054 | { |
| 1054 | std::string type; | 1055 | std::string type; |
| @@ -1094,7 +1095,7 @@ QPDF::decryptStream( | @@ -1094,7 +1095,7 @@ QPDF::decryptStream( | ||
| 1094 | } | 1095 | } |
| 1095 | 1096 | ||
| 1096 | if (method == e_unknown) { | 1097 | if (method == e_unknown) { |
| 1097 | - if ((!encp->encrypt_metadata) && (type == "/Metadata")) { | 1098 | + if ((!encp->encrypt_metadata) && is_root_metadata) { |
| 1098 | QTC::TC("qpdf", "QPDF_encryption cleartext metadata"); | 1099 | QTC::TC("qpdf", "QPDF_encryption cleartext metadata"); |
| 1099 | method = e_none; | 1100 | method = e_none; |
| 1100 | } else { | 1101 | } else { |
libqpdf/qpdf/QPDFObjectHandle_private.hh
| @@ -290,6 +290,7 @@ namespace qpdf | @@ -290,6 +290,7 @@ namespace qpdf | ||
| 290 | s->stream_dict = new_dict; | 290 | s->stream_dict = new_dict; |
| 291 | setDictDescription(); | 291 | setDictDescription(); |
| 292 | } | 292 | } |
| 293 | + bool isRootMetadata() const; | ||
| 293 | 294 | ||
| 294 | void setDictDescription(); | 295 | void setDictDescription(); |
| 295 | 296 |
libqpdf/qpdf/QPDF_private.hh
| @@ -143,12 +143,13 @@ class QPDF::Pipe | @@ -143,12 +143,13 @@ class QPDF::Pipe | ||
| 143 | qpdf_offset_t offset, | 143 | qpdf_offset_t offset, |
| 144 | size_t length, | 144 | size_t length, |
| 145 | QPDFObjectHandle dict, | 145 | QPDFObjectHandle dict, |
| 146 | + bool is_root_metadata, | ||
| 146 | Pipeline* pipeline, | 147 | Pipeline* pipeline, |
| 147 | bool suppress_warnings, | 148 | bool suppress_warnings, |
| 148 | bool will_retry) | 149 | bool will_retry) |
| 149 | { | 150 | { |
| 150 | return qpdf->pipeStreamData( | 151 | return qpdf->pipeStreamData( |
| 151 | - og, offset, length, dict, pipeline, suppress_warnings, will_retry); | 152 | + og, offset, length, dict, is_root_metadata, pipeline, suppress_warnings, will_retry); |
| 152 | } | 153 | } |
| 153 | }; | 154 | }; |
| 154 | 155 | ||
| @@ -216,7 +217,8 @@ class QPDF::ForeignStreamData | @@ -216,7 +217,8 @@ class QPDF::ForeignStreamData | ||
| 216 | QPDFObjGen foreign_og, | 217 | QPDFObjGen foreign_og, |
| 217 | qpdf_offset_t offset, | 218 | qpdf_offset_t offset, |
| 218 | size_t length, | 219 | size_t length, |
| 219 | - QPDFObjectHandle local_dict); | 220 | + QPDFObjectHandle local_dict, |
| 221 | + bool is_root_metadata); | ||
| 220 | 222 | ||
| 221 | private: | 223 | private: |
| 222 | std::shared_ptr<EncryptionParameters> encp; | 224 | std::shared_ptr<EncryptionParameters> encp; |
| @@ -225,6 +227,7 @@ class QPDF::ForeignStreamData | @@ -225,6 +227,7 @@ class QPDF::ForeignStreamData | ||
| 225 | qpdf_offset_t offset; | 227 | qpdf_offset_t offset; |
| 226 | size_t length; | 228 | size_t length; |
| 227 | QPDFObjectHandle local_dict; | 229 | QPDFObjectHandle local_dict; |
| 230 | + bool is_root_metadata{false}; | ||
| 228 | }; | 231 | }; |
| 229 | 232 | ||
| 230 | class QPDF::CopiedStreamDataProvider: public QPDFObjectHandle::StreamDataProvider | 233 | class QPDF::CopiedStreamDataProvider: public QPDFObjectHandle::StreamDataProvider |
manual/release-notes.rst
| @@ -32,6 +32,12 @@ more detail. | @@ -32,6 +32,12 @@ more detail. | ||
| 32 | - Accept an array for ``rotate`` in qpdf job JSON since it is a | 32 | - Accept an array for ``rotate`` in qpdf job JSON since it is a |
| 33 | repeatable option. | 33 | repeatable option. |
| 34 | 34 | ||
| 35 | + - When reading an encrypted PDF with cleartext metadata, only | ||
| 36 | + expect top-level /Metadata to be clear-text. When writing an | ||
| 37 | + encrypted PDF with cleartext metadata, only leave top-level | ||
| 38 | + unencrypted. qpdf has always incorrectly handled all | ||
| 39 | + ``/Metadata`` streams as special with cleartext metadata. | ||
| 40 | + | ||
| 35 | - CLI Enhancements | 41 | - CLI Enhancements |
| 36 | 42 | ||
| 37 | - New :qpdf:ref:`--remove-structure` option to exclude the document | 43 | - New :qpdf:ref:`--remove-structure` option to exclude the document |
qpdf/qtest/cleartext-metadata.test
| @@ -14,7 +14,7 @@ cleanup(); | @@ -14,7 +14,7 @@ cleanup(); | ||
| 14 | 14 | ||
| 15 | my $td = new TestDriver('cleartext-metadata'); | 15 | my $td = new TestDriver('cleartext-metadata'); |
| 16 | 16 | ||
| 17 | -my $n_tests = 58; | 17 | +my $n_tests = 61; |
| 18 | 18 | ||
| 19 | # args: file, exp_encrypted, exp_cleartext | 19 | # args: file, exp_encrypted, exp_cleartext |
| 20 | check_metadata($td, "compressed-metadata.pdf", 0, 0); | 20 | check_metadata($td, "compressed-metadata.pdf", 0, 0); |
| @@ -68,5 +68,16 @@ foreach my $f (qw(compressed-metadata.pdf enc-base.pdf)) | @@ -68,5 +68,16 @@ foreach my $f (qw(compressed-metadata.pdf enc-base.pdf)) | ||
| 68 | } | 68 | } |
| 69 | } | 69 | } |
| 70 | 70 | ||
| 71 | +$td->runtest("only root metadata is compressed", | ||
| 72 | + {$td->COMMAND => "qpdf --deterministic-id compressed-metadata.pdf a.pdf"}, | ||
| 73 | + {$td->STRING => "", $td->EXIT_STATUS => 0}); | ||
| 74 | +$td->runtest("check file", | ||
| 75 | + {$td->COMMAND => "qpdf-test-compare a.pdf compressed-metadata-out-normal.pdf"}, | ||
| 76 | + {$td->FILE => "compressed-metadata-out-normal.pdf", $td->EXIT_STATUS => 0}); | ||
| 77 | +$td->runtest("additional cleartext metadata tests", | ||
| 78 | + {$td->COMMAND => "test_driver 100 compressed-metadata.pdf"}, | ||
| 79 | + {$td->STRING => "test 100 done\n", $td->EXIT_STATUS => 0}, | ||
| 80 | + $td->NORMALIZE_NEWLINES); | ||
| 81 | + | ||
| 71 | cleanup(); | 82 | cleanup(); |
| 72 | $td->report($n_tests); | 83 | $td->report($n_tests); |
qpdf/qtest/qpdf/compressed-metadata-out-normal.pdf
0 โ 100644
No preview for this file type
qpdf/qtest/qpdf/compressed-metadata.pdf
No preview for this file type
qpdf/test_driver.cc
| @@ -3482,6 +3482,85 @@ test_99(QPDF& pdf, char const* arg2) | @@ -3482,6 +3482,85 @@ test_99(QPDF& pdf, char const* arg2) | ||
| 3482 | } | 3482 | } |
| 3483 | } | 3483 | } |
| 3484 | 3484 | ||
| 3485 | +static void | ||
| 3486 | +test_100(QPDF& pdf, char const* arg2) | ||
| 3487 | +{ | ||
| 3488 | + // Designed for compressed-metadata.pdf | ||
| 3489 | + auto is_cleartext = [](QPDFObjectHandle& metadata, bool raw) { | ||
| 3490 | + std::string buf; | ||
| 3491 | + Pl_String bufpl("buffer", nullptr, buf); | ||
| 3492 | + metadata.pipeStreamData(&bufpl, 0, raw ? qpdf_dl_none : qpdf_dl_generalized); | ||
| 3493 | + return buf.substr(0, 9) == "<?xpacket"; | ||
| 3494 | + }; | ||
| 3495 | + auto is_cleartext_in_file = [](std::string const& raw_bytes, | ||
| 3496 | + QPDFObjectHandle& metadata) -> bool { | ||
| 3497 | + auto buf = metadata.getRawStreamData(); | ||
| 3498 | + auto offset = metadata.getParsedOffset(); | ||
| 3499 | + auto from_file = raw_bytes.substr(QIntC::to_size(offset), 10); | ||
| 3500 | + assert(buf->getSize() > 10); | ||
| 3501 | + auto from_buf = std::string(reinterpret_cast<char*>(buf->getBuffer()), from_file.size()); | ||
| 3502 | + return from_buf == from_file; | ||
| 3503 | + }; | ||
| 3504 | + | ||
| 3505 | + { | ||
| 3506 | + QPDFWriter w(pdf, "a.pdf"); | ||
| 3507 | + w.setR6EncryptionParameters( | ||
| 3508 | + "", "", true, true, true, true, true, true, qpdf_r3p_full, false); | ||
| 3509 | + w.write(); | ||
| 3510 | + } | ||
| 3511 | + { | ||
| 3512 | + QPDF encrypted; | ||
| 3513 | + encrypted.processFile("a.pdf"); | ||
| 3514 | + auto raw_bytes = QUtil::read_file_into_string("a.pdf"); | ||
| 3515 | + auto root = encrypted.getRoot(); | ||
| 3516 | + auto root_metadata = root.getKey("/Metadata"); | ||
| 3517 | + if (!root_metadata.isStream()) { | ||
| 3518 | + throw std::logic_error("test 100 run on file with no metadata"); | ||
| 3519 | + } | ||
| 3520 | + assert(is_cleartext_in_file(raw_bytes, root_metadata)); | ||
| 3521 | + assert(is_cleartext(root_metadata, true)); | ||
| 3522 | + auto n_pages = QIntC::to_size(root.getKey("/Pages").getKey("/Count").getIntValue()); | ||
| 3523 | + auto page = encrypted.getAllPages().at(n_pages - 1); | ||
| 3524 | + auto page_metadata = page.getKey("/Metadata"); | ||
| 3525 | + if (!page_metadata.isStream()) { | ||
| 3526 | + throw std::logic_error("test 100 run on file with no metadata on last page"); | ||
| 3527 | + } | ||
| 3528 | + // It's encrypted in the file, but you can recover the data, so it is properly decrypted. | ||
| 3529 | + assert(!is_cleartext_in_file(raw_bytes, page_metadata)); | ||
| 3530 | + assert(!is_cleartext(page_metadata, true)); | ||
| 3531 | + assert(is_cleartext(page_metadata, false)); | ||
| 3532 | + | ||
| 3533 | + // Now copy these metadata objects to the file and write it out again. | ||
| 3534 | + auto copied_root_metadata = pdf.copyForeignObject(root_metadata); | ||
| 3535 | + auto copied_page_metadata = pdf.copyForeignObject(page_metadata); | ||
| 3536 | + pdf.getRoot().replaceKey("/CopiedRootMetadata", copied_root_metadata); | ||
| 3537 | + pdf.getRoot().replaceKey("/CopiedPageMetadata", copied_root_metadata); | ||
| 3538 | + QPDFWriter w(pdf, "b.pdf"); | ||
| 3539 | + w.setR6EncryptionParameters( | ||
| 3540 | + "", "", true, true, true, true, true, true, qpdf_r3p_full, false); | ||
| 3541 | + w.write(); | ||
| 3542 | + } | ||
| 3543 | + auto raw_bytes = QUtil::read_file_into_string("b.pdf"); | ||
| 3544 | + QPDF updated; | ||
| 3545 | + updated.processFile("b.pdf"); | ||
| 3546 | + auto root = updated.getRoot(); | ||
| 3547 | + auto root_metadata = root.getKey("/Metadata"); | ||
| 3548 | + auto n_pages = QIntC::to_size(root.getKey("/Pages").getKey("/Count").getIntValue()); | ||
| 3549 | + auto page = updated.getAllPages().at(n_pages - 1); | ||
| 3550 | + auto page_metadata = page.getKey("/Metadata"); | ||
| 3551 | + auto copied_root = root.getKey("/CopiedRootMetadata"); | ||
| 3552 | + auto copied_page = root.getKey("/CopiedPageMetadata"); | ||
| 3553 | + // The ultimate root is still clear-text in file | ||
| 3554 | + assert(is_cleartext_in_file(raw_bytes, root_metadata)); | ||
| 3555 | + assert(is_cleartext(root_metadata, true)); | ||
| 3556 | + // Everything else is compressed and encrypted in the file (not handled as special case). | ||
| 3557 | + for (auto o: {page_metadata, copied_root, copied_page}) { | ||
| 3558 | + assert(!is_cleartext_in_file(raw_bytes, o)); | ||
| 3559 | + assert(!is_cleartext(o, true)); | ||
| 3560 | + assert(is_cleartext(o, false)); | ||
| 3561 | + } | ||
| 3562 | +} | ||
| 3563 | + | ||
| 3485 | void | 3564 | void |
| 3486 | runtest(int n, char const* filename1, char const* arg2) | 3565 | runtest(int n, char const* filename1, char const* arg2) |
| 3487 | { | 3566 | { |
| @@ -3563,23 +3642,23 @@ runtest(int n, char const* filename1, char const* arg2) | @@ -3563,23 +3642,23 @@ runtest(int n, char const* filename1, char const* arg2) | ||
| 3563 | } | 3642 | } |
| 3564 | 3643 | ||
| 3565 | std::map<int, void (*)(QPDF&, char const*)> test_functions = { | 3644 | std::map<int, void (*)(QPDF&, char const*)> test_functions = { |
| 3566 | - {0, test_0_1}, {1, test_0_1}, {2, test_2}, {3, test_3}, {4, test_4}, {5, test_5}, | ||
| 3567 | - {6, test_6}, {7, test_7}, {8, test_8}, {9, test_9}, {10, test_10}, {11, test_11}, | ||
| 3568 | - {12, test_12}, {13, test_13}, {14, test_14}, {15, test_15}, {16, test_16}, {17, test_17}, | ||
| 3569 | - {18, test_18}, {19, test_19}, {20, test_20}, {21, test_21}, {22, test_22}, {23, test_23}, | ||
| 3570 | - {24, test_24}, {25, test_25}, {26, test_26}, {27, test_27}, {28, test_28}, {29, test_29}, | ||
| 3571 | - {30, test_30}, {31, test_31}, {32, test_32}, {33, test_33}, {34, test_34}, {35, test_35}, | ||
| 3572 | - {36, test_36}, {37, test_37}, {38, test_38}, {39, test_39}, {40, test_40}, {41, test_41}, | ||
| 3573 | - {42, test_42}, {43, test_43}, {44, test_44}, {45, test_45}, {46, test_46}, {47, test_47}, | ||
| 3574 | - {48, test_48}, {49, test_49}, {50, test_50}, {51, test_51}, {52, test_52}, {53, test_53}, | ||
| 3575 | - {54, test_54}, {55, test_55}, {56, test_56}, {57, test_57}, {58, test_58}, {59, test_59}, | ||
| 3576 | - {60, test_60}, {61, test_61}, {62, test_62}, {63, test_63}, {64, test_64}, {65, test_65}, | ||
| 3577 | - {66, test_66}, {67, test_67}, {68, test_68}, {69, test_69}, {70, test_70}, {71, test_71}, | ||
| 3578 | - {72, test_72}, {73, test_73}, {74, test_74}, {75, test_75}, {76, test_76}, {77, test_77}, | ||
| 3579 | - {78, test_78}, {79, test_79}, {80, test_80}, {81, test_81}, {82, test_82}, {83, test_83}, | ||
| 3580 | - {84, test_84}, {85, test_85}, {86, test_86}, {87, test_87}, {88, test_88}, {89, test_89}, | ||
| 3581 | - {90, test_90}, {91, test_91}, {92, test_92}, {93, test_93}, {94, test_94}, {95, test_95}, | ||
| 3582 | - {96, test_96}, {97, test_97}, {98, test_98}, {99, test_99}}; | 3645 | + {0, test_0_1}, {1, test_0_1}, {2, test_2}, {3, test_3}, {4, test_4}, {5, test_5}, |
| 3646 | + {6, test_6}, {7, test_7}, {8, test_8}, {9, test_9}, {10, test_10}, {11, test_11}, | ||
| 3647 | + {12, test_12}, {13, test_13}, {14, test_14}, {15, test_15}, {16, test_16}, {17, test_17}, | ||
| 3648 | + {18, test_18}, {19, test_19}, {20, test_20}, {21, test_21}, {22, test_22}, {23, test_23}, | ||
| 3649 | + {24, test_24}, {25, test_25}, {26, test_26}, {27, test_27}, {28, test_28}, {29, test_29}, | ||
| 3650 | + {30, test_30}, {31, test_31}, {32, test_32}, {33, test_33}, {34, test_34}, {35, test_35}, | ||
| 3651 | + {36, test_36}, {37, test_37}, {38, test_38}, {39, test_39}, {40, test_40}, {41, test_41}, | ||
| 3652 | + {42, test_42}, {43, test_43}, {44, test_44}, {45, test_45}, {46, test_46}, {47, test_47}, | ||
| 3653 | + {48, test_48}, {49, test_49}, {50, test_50}, {51, test_51}, {52, test_52}, {53, test_53}, | ||
| 3654 | + {54, test_54}, {55, test_55}, {56, test_56}, {57, test_57}, {58, test_58}, {59, test_59}, | ||
| 3655 | + {60, test_60}, {61, test_61}, {62, test_62}, {63, test_63}, {64, test_64}, {65, test_65}, | ||
| 3656 | + {66, test_66}, {67, test_67}, {68, test_68}, {69, test_69}, {70, test_70}, {71, test_71}, | ||
| 3657 | + {72, test_72}, {73, test_73}, {74, test_74}, {75, test_75}, {76, test_76}, {77, test_77}, | ||
| 3658 | + {78, test_78}, {79, test_79}, {80, test_80}, {81, test_81}, {82, test_82}, {83, test_83}, | ||
| 3659 | + {84, test_84}, {85, test_85}, {86, test_86}, {87, test_87}, {88, test_88}, {89, test_89}, | ||
| 3660 | + {90, test_90}, {91, test_91}, {92, test_92}, {93, test_93}, {94, test_94}, {95, test_95}, | ||
| 3661 | + {96, test_96}, {97, test_97}, {98, test_98}, {99, test_99}, {100, test_100}}; | ||
| 3583 | 3662 | ||
| 3584 | auto fn = test_functions.find(n); | 3663 | auto fn = test_functions.find(n); |
| 3585 | if (fn == test_functions.end()) { | 3664 | if (fn == test_functions.end()) { |