2
1
mirror of https://github.com/qpdf/qpdf.git synced 2024-12-22 10:58:58 +00:00

Create a special "destroyed" type rather than using null

When a QPDF is destroyed, changing indirect objects to direct nulls
makes them effectively disappear silently when they sneak into other
places. Instead, we should treat this as an error. Adding a destroyed
object type makes this possible.
This commit is contained in:
Jay Berkenbilt 2022-09-08 08:03:57 -04:00
parent 264e25f391
commit dba61da1bf
16 changed files with 164 additions and 47 deletions

View File

@ -1,3 +1,8 @@
2022-09-08 Jay Berkenbilt <ejb@ql.org>
* Add QPDFObjectHandle::isDestroyed() to test whether an indirect
object was from a QPDF that has been destroyed.
2022-09-07 Jay Berkenbilt <ejb@ql.org> 2022-09-07 Jay Berkenbilt <ejb@ql.org>
* Add QPDFObjectHandle::getQPDF(), which returns a reference, as * Add QPDFObjectHandle::getQPDF(), which returns a reference, as

14
TODO
View File

@ -812,9 +812,11 @@ Rejected Ideas
QPDFObjectHandle because of indirect objects. This only pertains to QPDFObjectHandle because of indirect objects. This only pertains to
direct objects, which are always "resolved" in QPDFObjectHandle. direct objects, which are always "resolved" in QPDFObjectHandle.
If this is addressed, read comments in QPDFWriter.cc::enqueueObject If this is addressed, read comments in the following places:
near the call to getOwningQPDF, comments in QPDFValueProxy::reset, * QPDFWriter.cc::enqueueObject near the call to getOwningQPDF
and comments in QPDF::~QPDF() near the line that assigns to null. * QPDFValueProxy::reset and QPDFValueProxy::destroy
This will also affect test 92 in test_driver.cc. All these * QPDF::~QPDF()
references were from the release of qpdf 11 (in case they have moved * test 92 in test_driver.cc
by such time as this might be resurrected). * QPDFObjectHandle.hh near isDestroyed
All these references were from the release of qpdf 11 (in case they
have moved by such time as this might be resurrected).

View File

@ -123,6 +123,7 @@ enum qpdf_object_type_e {
ot_inlineimage, ot_inlineimage,
/* Object types internal to qpdf */ /* Object types internal to qpdf */
ot_unresolved, ot_unresolved,
ot_destroyed,
}; };
/* Write Parameters. See QPDFWriter.hh for details. */ /* Write Parameters. See QPDFWriter.hh for details. */

View File

@ -391,6 +391,11 @@ class QPDFObjectHandle
QPDF_DLL QPDF_DLL
inline bool isIndirect() const; inline bool isIndirect() const;
// This returns true for indirect objects from a QPDF that has
// been destroyed.
QPDF_DLL
bool isDestroyed();
// True for everything except array, dictionary, stream, word, and // True for everything except array, dictionary, stream, word, and
// inline image. // inline image.
QPDF_DLL QPDF_DLL

View File

@ -90,6 +90,7 @@ set(libqpdf_SOURCES
QPDFXRefEntry.cc QPDFXRefEntry.cc
QPDF_Array.cc QPDF_Array.cc
QPDF_Bool.cc QPDF_Bool.cc
QPDF_Destroyed.cc
QPDF_Dictionary.cc QPDF_Dictionary.cc
QPDF_InlineImage.cc QPDF_InlineImage.cc
QPDF_Integer.cc QPDF_Integer.cc

View File

@ -249,22 +249,23 @@ QPDF::~QPDF()
// std::shared_ptr objects will prevent the objects from being // std::shared_ptr objects will prevent the objects from being
// deleted. Walk through all objects in the object cache, which is // deleted. Walk through all objects in the object cache, which is
// those objects that we read from the file, and break all // those objects that we read from the file, and break all
// resolved indirect references by replacing them with direct null // resolved indirect references by replacing them with an internal
// objects. At this point, obviously no one is still using the // object type representing that they have been destroyed. Note
// QPDF object, but we'll explicitly clear the xref table anyway
// just to prevent any possibility of resolve() succeeding. Note
// that we can't break references like this at any time when the // that we can't break references like this at any time when the
// QPDF object is active. This also causes all QPDFObjectHandle // QPDF object is active. The call to reset also causes all
// objects that are reachable from this object to become nulls and // QPDFObjectHandle objects that are reachable from this object to
// release their association with this QPDF. // release their association with this QPDF.
// At this point, obviously no one is still using the QPDF object,
// but we'll explicitly clear the xref table anyway just to
// prevent any possibility of resolve() succeeding.
this->m->xref_table.clear(); this->m->xref_table.clear();
auto null_obj = QPDF_Null::create(); auto null_obj = QPDF_Null::create();
for (auto const& iter: this->m->obj_cache) { for (auto const& iter: this->m->obj_cache) {
iter.second.object->reset(); iter.second.object->reset();
// If the issue discussed in QPDFValueProxy::reset were // It would be better if reset() could call destroy(), but it
// resolved, then this assignment to null_obj could be // can't -- see comments in QPDFValueProxy::reset().
// removed. iter.second.object->destroy();
iter.second.object->assign(null_obj);
} }
} }

View File

@ -252,8 +252,10 @@ void
QPDFObjectHandle::reset() QPDFObjectHandle::reset()
{ {
// Recursively remove association with any QPDF object. This // Recursively remove association with any QPDF object. This
// method may only be called during final destruction. See // method may only be called during final destruction.
// comments in QPDF::~QPDF(). // QPDF::~QPDF() calls it for indirect objects using the object
// pointer itself, so we don't do that here. Other objects call it
// through this method.
if (!isIndirect()) { if (!isIndirect()) {
this->obj->reset(); this->obj->reset();
} }
@ -351,6 +353,12 @@ QPDFObjectHandle::asString()
return dereference() ? obj->as<QPDF_String>() : nullptr; return dereference() ? obj->as<QPDF_String>() : nullptr;
} }
bool
QPDFObjectHandle::isDestroyed()
{
return dereference() && (obj->getTypeCode() == ::ot_destroyed);
}
bool bool
QPDFObjectHandle::isBool() QPDFObjectHandle::isBool()
{ {

View File

@ -1,6 +1,7 @@
#include <qpdf/QPDFValueProxy.hh> #include <qpdf/QPDFValueProxy.hh>
#include <qpdf/QPDF.hh> #include <qpdf/QPDF.hh>
#include <qpdf/QPDF_Destroyed.hh>
void void
QPDFValueProxy::doResolve() QPDFValueProxy::doResolve()
@ -8,3 +9,10 @@ QPDFValueProxy::doResolve()
auto og = value->og; auto og = value->og;
QPDF::Resolver::resolve(value->qpdf, og); QPDF::Resolver::resolve(value->qpdf, og);
} }
void
QPDFValueProxy::destroy()
{
// See comments in reset() for why this isn't part of reset.
value = QPDF_Destroyed::getInstance();
}

39
libqpdf/QPDF_Destroyed.cc Normal file
View File

@ -0,0 +1,39 @@
#include <qpdf/QPDF_Destroyed.hh>
#include <stdexcept>
QPDF_Destroyed::QPDF_Destroyed() :
QPDFValue(::ot_destroyed, "destroyed")
{
}
std::shared_ptr<QPDFValue>
QPDF_Destroyed::getInstance()
{
static std::shared_ptr<QPDFValue> instance(new QPDF_Destroyed());
return instance;
}
std::shared_ptr<QPDFValueProxy>
QPDF_Destroyed::shallowCopy()
{
throw std::logic_error(
"attempted to shallow copy QPDFObjectHandle from destroyed QPDF");
return nullptr;
}
std::string
QPDF_Destroyed::unparse()
{
throw std::logic_error(
"attempted to unparse a QPDFObjectHandle from a destroyed QPDF");
return "";
}
JSON
QPDF_Destroyed::getJSON(int json_version)
{
throw std::logic_error(
"attempted to get JSON from a QPDFObjectHandle from a destroyed QPDF");
return JSON::makeNull();
}

View File

@ -31,6 +31,6 @@ JSON
QPDF_Reserved::getJSON(int json_version) QPDF_Reserved::getJSON(int json_version)
{ {
throw std::logic_error( throw std::logic_error(
"QPDFObjectHandle: attempting to unparse a reserved object"); "QPDFObjectHandle: attempting to get JSON from a reserved object");
return JSON::makeNull(); return JSON::makeNull();
} }

View File

@ -17,7 +17,7 @@ std::shared_ptr<QPDFValueProxy>
QPDF_Unresolved::shallowCopy() QPDF_Unresolved::shallowCopy()
{ {
throw std::logic_error( throw std::logic_error(
"attempted to shallow copy unresolved QPDFObjectHandle"); "attempted to shallow copy an unresolved QPDFObjectHandle");
return nullptr; return nullptr;
} }
@ -32,5 +32,7 @@ QPDF_Unresolved::unparse()
JSON JSON
QPDF_Unresolved::getJSON(int json_version) QPDF_Unresolved::getJSON(int json_version)
{ {
throw std::logic_error(
"attempted to get JSON from an unresolved QPDFObjectHandle");
return JSON::makeNull(); return JSON::makeNull();
} }

View File

@ -114,21 +114,23 @@ class QPDFValueProxy
{ {
value->reset(); value->reset();
// It would be better if, rather than clearing value->qpdf and // It would be better if, rather than clearing value->qpdf and
// value->og, we completely replaced value with a null object. // value->og, we completely replaced value with
// However, at the time of the release of qpdf 11, this causes // QPDF_Destroyed. However, at the time of the release of qpdf
// test failures and would likely break a lot of code since it // 11, this causes test failures and would likely break a lot
// possible for a direct object that recursively contains no // of code since it possible for a direct object that
// indirect objects to be copied into multiple QPDF objects. // recursively contains no indirect objects to be copied into
// For that reason, we have to break the association with the // multiple QPDF objects. For that reason, we have to break
// owning QPDF but not otherwise mutate the object. For // the association with the owning QPDF but not otherwise
// indirect objects, QPDF::~QPDF replaces the object with // mutate the object. For indirect objects, QPDF::~QPDF
// null, which clears circular references. If this code were // replaces indirect objects with QPDF_Destroyed, which clears
// able to do the null replacement, that code would not have // circular references. If this code were able to do that,
// to. // that code would not have to.
value->qpdf = nullptr; value->qpdf = nullptr;
value->og = QPDFObjGen(); value->og = QPDFObjGen();
} }
void destroy();
bool bool
isUnresolved() const isUnresolved() const
{ {

View File

@ -0,0 +1,19 @@
#ifndef QPDF_DESTROYED_HH
#define QPDF_DESTROYED_HH
#include <qpdf/QPDFValue.hh>
class QPDF_Destroyed: public QPDFValue
{
public:
virtual ~QPDF_Destroyed() = default;
virtual std::shared_ptr<QPDFValueProxy> shallowCopy();
virtual std::string unparse();
virtual JSON getJSON(int json_version);
static std::shared_ptr<QPDFValue> getInstance();
private:
QPDF_Destroyed();
};
#endif // QPDF_DESTROYED_HH

View File

@ -202,11 +202,13 @@ For a detailed list of changes, please see the file
``replaceKeyAndGetOld``, a ``null`` object if the object was not ``replaceKeyAndGetOld``, a ``null`` object if the object was not
previously there. previously there.
- The method ``QPDFObjectHandle::getOwningQPDF`` now returns a - It is now possible to detect when a ``QPDFObjectHandle`` is an
null pointer (rather than an invalid pointer) if the owning indirect object that belongs to a ``QPDF`` that has been
``QPDF`` object has been destroyed. This situation should destroyed. Any attempt to unparse this type of
generally not happen for correct code, but at least the ``QPDFObjectHandle`` will throw a logic error. You can detect
situation is detectible now. this by calling the new ``QPDFObjectHandle::isDestroyed``
method. Also the ``QPDFObjectHandle::getOwningQPDF`` method now
returns a null pointer rather than an invalid pointer.
- The method ``QPDFObjectHandle::getQPDF`` returns a ``QPDF&`` - The method ``QPDFObjectHandle::getQPDF`` returns a ``QPDF&``
(rather than a ``QPDF*``) and is an alternative to (rather than a ``QPDF*``) and is an alternative to

View File

@ -1,3 +1,4 @@
logic error: QPDFObjectHandle from different QPDF found while writing. Use QPDF::copyForeignObject to add objects from another file. logic error: QPDFObjectHandle from different QPDF found while writing. Use QPDF::copyForeignObject to add objects from another file.
logic error: attempted to unparse a QPDFObjectHandle from a destroyed QPDF
logic error: Attempting to add an object from a different QPDF. Use QPDF::copyForeignObject to add objects from another file. logic error: Attempting to add an object from a different QPDF. Use QPDF::copyForeignObject to add objects from another file.
test 29 done test 29 done

View File

@ -1135,8 +1135,8 @@ test_29(QPDF& pdf, char const* arg2)
{ {
// Detect mixed objects in QPDFWriter // Detect mixed objects in QPDFWriter
assert(arg2 != 0); assert(arg2 != 0);
QPDF other; auto other = QPDF::create();
other.processFile(arg2); other->processFile(arg2);
// We need to create a QPDF with mixed ownership to exercise // We need to create a QPDF with mixed ownership to exercise
// QPDFWriter's ownership check. To do this, we have to sneak the // QPDFWriter's ownership check. To do this, we have to sneak the
// foreign object inside an ownerless direct object to avoid // foreign object inside an ownerless direct object to avoid
@ -1146,10 +1146,25 @@ test_29(QPDF& pdf, char const* arg2)
// explicitly change the ownership to the wrong value. // explicitly change the ownership to the wrong value.
auto dict = QPDFObjectHandle::newDictionary(); auto dict = QPDFObjectHandle::newDictionary();
dict.replaceKey("/QTest", pdf.getTrailer().getKey("/QTest")); dict.replaceKey("/QTest", pdf.getTrailer().getKey("/QTest"));
other.getTrailer().replaceKey("/QTest", dict); other->getTrailer().replaceKey("/QTest", dict);
try { try {
QPDFWriter w(other, "a.pdf"); QPDFWriter w(*other, "a.pdf");
w.write();
std::cout << "oops -- didn't throw" << std::endl;
} catch (std::logic_error const& e) {
std::cout << "logic error: " << e.what() << std::endl;
}
// Make sure deleting the other source doesn't prevent detection.
auto other2 = QPDF::create();
other2->emptyPDF();
dict = QPDFObjectHandle::newDictionary();
dict.replaceKey("/QTest", other2->getRoot());
other->getTrailer().replaceKey("/QTest", dict);
other2 = nullptr;
try {
QPDFWriter w(*other, "a.pdf");
w.write(); w.write();
std::cout << "oops -- didn't throw" << std::endl; std::cout << "oops -- didn't throw" << std::endl;
} catch (std::logic_error const& e) { } catch (std::logic_error const& e) {
@ -1158,7 +1173,7 @@ test_29(QPDF& pdf, char const* arg2)
// Detect adding a foreign object // Detect adding a foreign object
auto root1 = pdf.getRoot(); auto root1 = pdf.getRoot();
auto root2 = other.getRoot(); auto root2 = other->getRoot();
try { try {
root1.replaceKey("/Oops", root2); root1.replaceKey("/Oops", root2);
} catch (std::logic_error const& e) { } catch (std::logic_error const& e) {
@ -3300,14 +3315,20 @@ test_92(QPDF& pdf, char const* arg2)
check(resources); check(resources);
check(contents); check(contents);
check(contents_dict); check(contents_dict);
// Objects that were originally indirect should be null. // Objects that were originally indirect should be destroyed.
// Otherwise, they should have retained their old values. See // Otherwise, they should have retained their old values. See
// comments in QPDFValueProxy::reset for why this is the case. // comments in QPDFValueProxy::reset for why this is the case.
assert(root.isNull()); assert(root.isDestroyed());
assert(page1.isNull()); assert(page1.isDestroyed());
assert(contents.isNull()); assert(contents.isDestroyed());
assert(!resources.isNull()); assert(resources.isDictionary());
assert(!contents_dict.isNull()); assert(contents_dict.isDictionary());
try {
root.unparse();
assert(false);
} catch (std::logic_error&) {
// Expected
}
} }
static void static void