2
1
mirror of https://github.com/qpdf/qpdf.git synced 2024-10-31 19:02:30 +00:00
qpdf/libqpdf/NNTree.cc
2021-04-05 07:59:02 -04:00

1202 lines
34 KiB
C++

#include <qpdf/NNTree.hh>
#include <qpdf/QTC.hh>
#include <qpdf/QUtil.hh>
#include <exception>
static std::string
get_description(QPDFObjectHandle& node)
{
std::string result("Name/Number tree node");
if (node.isIndirect())
{
result += " (object " + QUtil::int_to_string(node.getObjectID()) + ")";
}
return result;
}
static void
warn(QPDF* qpdf, QPDFObjectHandle& node, std::string const& msg)
{
// ABI: in qpdf 11, change to a reference.
if (qpdf)
{
qpdf->warn(QPDFExc(
qpdf_e_damaged_pdf,
qpdf->getFilename(), get_description(node), 0, msg));
}
}
static void
error(QPDF* qpdf, QPDFObjectHandle& node, std::string const& msg)
{
// ABI: in qpdf 11, change to a reference.
if (qpdf)
{
throw QPDFExc(qpdf_e_damaged_pdf,
qpdf->getFilename(), get_description(node), 0, msg);
}
else
{
throw std::runtime_error(get_description(node) + ": " + msg);
}
}
NNTreeIterator::NNTreeIterator(NNTreeImpl& impl) :
impl(impl),
item_number(-1)
{
}
void
NNTreeIterator::updateIValue(bool allow_invalid)
{
// ivalue should never be used inside the class since we return a
// pointer/reference to it. Every bit of code that ever changes
// what object the iterator points to should take care to call
// updateIValue. Failure to do this means that any old references
// to *iter will point to incorrect objects, though the next
// dereference of the iterator will fix it. This isn't necessarily
// catastrophic, but it would be confusing. The test suite
// attempts to exercise various cases to ensure we don't introduce
// that bug in the future, but sadly it's tricky to verify by
// reasoning about the code that this constraint is always
// satisfied. Whenever we update what the iterator points to, we
// should call setItemNumber, which calls this. If we change what
// the iterator in some other way, such as replacing a value or
// removing an item and making the iterator point at a different
// item in potentially the same position, we must call
// updateIValue as well. These cases are handled, and for good
// measure, we also call updateIValue in operator* and operator->.
bool okay = false;
if ((item_number >= 0) &&
this->node.isInitialized() &&
this->node.isDictionary())
{
auto items = this->node.getKey(impl.details.itemsKey());
if (this->item_number + 1 < items.getArrayNItems())
{
okay = true;
this->ivalue.first = items.getArrayItem(this->item_number);
this->ivalue.second = items.getArrayItem(1+this->item_number);
}
else
{
error(impl.qpdf, node, "update ivalue: items array is too short");
}
}
if (! okay)
{
if (! allow_invalid)
{
throw std::logic_error(
"attempt made to dereference an invalid"
" name/number tree iterator");
}
this->ivalue.first = QPDFObjectHandle();
this->ivalue.second = QPDFObjectHandle();
}
}
NNTreeIterator::PathElement::PathElement(
QPDFObjectHandle const& node, int kid_number) :
node(node),
kid_number(kid_number)
{
}
QPDFObjectHandle
NNTreeIterator::getNextKid(PathElement& pe, bool backward)
{
QPDFObjectHandle result;
bool found = false;
while (! found)
{
pe.kid_number += backward ? -1 : 1;
auto kids = pe.node.getKey("/Kids");
if ((pe.kid_number >= 0) && (pe.kid_number < kids.getArrayNItems()))
{
result = kids.getArrayItem(pe.kid_number);
if (result.isDictionary() &&
(result.hasKey("/Kids") ||
result.hasKey(impl.details.itemsKey())))
{
found = true;
}
else
{
QTC::TC("qpdf", "NNTree skip invalid kid");
warn(impl.qpdf, pe.node,
"skipping over invalid kid at index " +
QUtil::int_to_string(pe.kid_number));
}
}
else
{
result = QPDFObjectHandle::newNull();
found = true;
}
}
return result;
}
bool
NNTreeIterator::valid() const
{
return this->item_number >= 0;
}
void
NNTreeIterator::increment(bool backward)
{
if (this->item_number < 0)
{
QTC::TC("qpdf", "NNTree increment end()");
deepen(impl.oh, ! backward, true);
return;
}
bool found_valid_key = false;
while (valid() && (! found_valid_key))
{
this->item_number += backward ? -2 : 2;
auto items = this->node.getKey(impl.details.itemsKey());
if ((this->item_number < 0) ||
(this->item_number >= items.getArrayNItems()))
{
bool found = false;
setItemNumber(QPDFObjectHandle(), -1);
while (! (found || this->path.empty()))
{
auto& element = this->path.back();
auto pe_node = getNextKid(element, backward);
if (pe_node.isNull())
{
this->path.pop_back();
}
else
{
found = deepen(pe_node, ! backward, false);
}
}
}
if (this->item_number >= 0)
{
items = this->node.getKey(impl.details.itemsKey());
if (this->item_number + 1 >= items.getArrayNItems())
{
QTC::TC("qpdf", "NNTree skip item at end of short items");
warn(impl.qpdf, this->node,
"items array doesn't have enough elements");
}
else if (! impl.details.keyValid(
items.getArrayItem(this->item_number)))
{
QTC::TC("qpdf", "NNTree skip invalid key");
warn(impl.qpdf, this->node,
"item " + QUtil::int_to_string(this->item_number) +
" has the wrong type");
}
else
{
found_valid_key = true;
}
}
}
}
void
NNTreeIterator::resetLimits(QPDFObjectHandle node,
std::list<PathElement>::iterator parent)
{
bool done = false;
while (! done)
{
if (parent == this->path.end())
{
QTC::TC("qpdf", "NNTree remove limits from root");
node.removeKey("/Limits");
done = true;
break;
}
auto kids = node.getKey("/Kids");
int nkids = kids.isArray() ? kids.getArrayNItems() : 0;
auto items = node.getKey(impl.details.itemsKey());
int nitems = items.isArray() ? items.getArrayNItems() : 0;
bool changed = true;
QPDFObjectHandle first;
QPDFObjectHandle last;
if (nitems >= 2)
{
first = items.getArrayItem(0);
last = items.getArrayItem((nitems - 1) & ~1);
}
else if (nkids > 0)
{
auto first_kid = kids.getArrayItem(0);
auto last_kid = kids.getArrayItem(nkids - 1);
if (first_kid.isDictionary() && last_kid.isDictionary())
{
auto first_limits = first_kid.getKey("/Limits");
auto last_limits = last_kid.getKey("/Limits");
if (first_limits.isArray() &&
(first_limits.getArrayNItems() >= 2) &&
last_limits.isArray() &&
(last_limits.getArrayNItems() >= 2))
{
first = first_limits.getArrayItem(0);
last = last_limits.getArrayItem(1);
}
}
}
if (first.isInitialized() && last.isInitialized())
{
auto limits = QPDFObjectHandle::newArray();
limits.appendItem(first);
limits.appendItem(last);
auto olimits = node.getKey("/Limits");
if (olimits.isArray() && (olimits.getArrayNItems() == 2))
{
auto ofirst = olimits.getArrayItem(0);
auto olast = olimits.getArrayItem(1);
if (impl.details.keyValid(ofirst) &&
impl.details.keyValid(olast) &&
(impl.details.compareKeys(first, ofirst) == 0) &&
(impl.details.compareKeys(last, olast) == 0))
{
QTC::TC("qpdf", "NNTree limits didn't change");
changed = false;
}
}
if (changed)
{
node.replaceKey("/Limits", limits);
}
}
else
{
QTC::TC("qpdf", "NNTree unable to determine limits");
warn(impl.qpdf, node, "unable to determine limits");
}
if ((! changed) || (parent == this->path.begin()))
{
done = true;
}
else
{
node = parent->node;
--parent;
}
}
}
void
NNTreeIterator::split(QPDFObjectHandle to_split,
std::list<PathElement>::iterator parent)
{
// Split some node along the path to the item pointed to by this
// iterator, and adjust the iterator so it points to the same
// item.
// In examples, for simplicity, /Nums is show to just contain
// numbers instead of pairs. Imagine this tree:
//
// root: << /Kids [ A B C D ] >>
// A: << /Nums [ 1 2 3 4 ] >>
// B: << /Nums [ 5 6 7 8 ] >>
// C: << /Nums [ 9 10 11 12 ] >>
// D: << /Kids [ E F ]
// E: << /Nums [ 13 14 15 16 ] >>
// F: << /Nums [ 17 18 19 20 ] >>
// iter1 (points to 19)
// path:
// - { node: root: kid_number: 3 }
// - { node: D, kid_number: 1 }
// node: F
// item_number: 2
// iter2 (points to 1)
// path:
// - { node: root, kid_number: 0}
// node: A
// item_number: 0
if (! this->impl.qpdf)
{
throw std::logic_error(
"NNTreeIterator::split called with null qpdf");
}
if (! valid())
{
throw std::logic_error(
"NNTreeIterator::split called an invalid iterator");
}
// Find the array we actually need to split, which is either this
// node's kids or items.
auto kids = to_split.getKey("/Kids");
int nkids = kids.isArray() ? kids.getArrayNItems() : 0;
auto items = to_split.getKey(impl.details.itemsKey());
int nitems = items.isArray() ? items.getArrayNItems() : 0;
QPDFObjectHandle first_half;
int n = 0;
std::string key;
int threshold = 0;
if (nkids > 0)
{
QTC::TC("qpdf", "NNTree split kids");
first_half = kids;
n = nkids;
threshold = impl.split_threshold;
key = "/Kids";
}
else if (nitems > 0)
{
QTC::TC("qpdf", "NNTree split items");
first_half = items;
n = nitems;
threshold = 2 * impl.split_threshold;
key = impl.details.itemsKey();
}
else
{
throw std::logic_error("NNTreeIterator::split called on invalid node");
}
if (n <= threshold)
{
return;
}
bool is_root = (parent == this->path.end());
bool is_leaf = (nitems > 0);
// CURRENT STATE: tree is in original state; iterator is valid and
// unchanged.
if (is_root)
{
// What we want to do is to create a new node for the second
// half of the items and put it in the parent's /Kids array
// right after the element that points to the current to_split
// node, but if we're splitting root, there is no parent, so
// handle that first.
// In the non-root case, parent points to the path element
// whose /Kids contains the first half node, and the first
// half node is to_split. If we are splitting the root, we
// need to push everything down a level, but we want to keep
// the actual root object the same so that indirect references
// to it remain intact (and also in case it might be a direct
// object, which it shouldn't be but that case probably exists
// in the wild). To achieve this, we create a new node for the
// first half and then replace /Kids in the root to contain
// it. Then we adjust the path so that the first element is
// root and the second element, if any, is the new first half.
// In this way, we make the root case identical to the
// non-root case so remaining logic can handle them in the
// same way.
auto first_node = impl.qpdf->makeIndirectObject(
QPDFObjectHandle::newDictionary());
first_node.replaceKey(key, first_half);
QPDFObjectHandle new_kids = QPDFObjectHandle::newArray();
new_kids.appendItem(first_node);
to_split.removeKey("/Limits"); // already shouldn't be there for root
to_split.removeKey(impl.details.itemsKey());
to_split.replaceKey("/Kids", new_kids);
if (is_leaf)
{
QTC::TC("qpdf", "NNTree split root + leaf");
this->node = first_node;
}
else
{
QTC::TC("qpdf", "NNTree split root + !leaf");
auto next = this->path.begin();
next->node = first_node;
}
this->path.push_front(PathElement(to_split, 0));
parent = this->path.begin();
to_split = first_node;
}
// CURRENT STATE: parent is guaranteed to be defined, and we have
// the invariants that parent[/Kids][kid_number] == to_split and
// (++parent).node == to_split.
// Create a second half array, and transfer the second half of the
// items into the second half array.
QPDFObjectHandle second_half = QPDFObjectHandle::newArray();
int start_idx = ((n / 2) & ~1);
while (first_half.getArrayNItems() > start_idx)
{
second_half.appendItem(first_half.getArrayItem(start_idx));
first_half.eraseItem(start_idx);
}
resetLimits(to_split, parent);
// Create a new node to contain the second half
QPDFObjectHandle second_node = impl.qpdf->makeIndirectObject(
QPDFObjectHandle::newDictionary());
second_node.replaceKey(key, second_half);
resetLimits(second_node, parent);
// CURRENT STATE: half the items from the kids or items array in
// the node being split have been moved into a new node. The new
// node is not yet attached to the tree. The iterator may have a
// path element or leaf node that is out of bounds.
// We need to adjust the parent to add the second node to /Kids
// and, if needed, update kid_number to traverse through it. We
// need to update to_split's path element, or the node if this is
// a leaf, so that the kid/item number points to the right place.
auto parent_kids = parent->node.getKey("/Kids");
parent_kids.insertItem(parent->kid_number + 1, second_node);
auto cur_elem = parent;
++cur_elem; // points to end() for leaf nodes
int old_idx = (is_leaf ? this->item_number : cur_elem->kid_number);
if (old_idx >= start_idx)
{
++parent->kid_number;
if (is_leaf)
{
QTC::TC("qpdf", "NNTree split second half item");
setItemNumber(second_node, this->item_number - start_idx);
}
else
{
QTC::TC("qpdf", "NNTree split second half kid");
cur_elem->node = second_node;
cur_elem->kid_number -= start_idx;
}
}
if (! is_root)
{
QTC::TC("qpdf", "NNTree split parent");
auto next = parent->node;
resetLimits(next, parent);
--parent;
split(next, parent);
}
}
std::list<NNTreeIterator::PathElement>::iterator
NNTreeIterator::lastPathElement()
{
auto result = this->path.end();
if (! this->path.empty())
{
--result;
}
return result;
}
void
NNTreeIterator::insertAfter(QPDFObjectHandle key, QPDFObjectHandle value)
{
if (! valid())
{
QTC::TC("qpdf", "NNTree insertAfter inserts first");
impl.insertFirst(key, value);
deepen(impl.oh, true, false);
return;
}
auto items = this->node.getKey(impl.details.itemsKey());
if (! items.isArray())
{
error(impl.qpdf, node, "node contains no items array");
}
if (items.getArrayNItems() < this->item_number + 2)
{
error(impl.qpdf, node, "insert: items array is too short");
}
items.insertItem(this->item_number + 2, key);
items.insertItem(this->item_number + 3, value);
resetLimits(this->node, lastPathElement());
split(this->node, lastPathElement());
increment(false);
}
void
NNTreeIterator::remove()
{
// Remove this item, leaving the tree valid and this iterator
// pointing to the next item.
if (! valid())
{
throw std::logic_error("attempt made to remove an invalid iterator");
}
auto items = this->node.getKey(impl.details.itemsKey());
int nitems = items.getArrayNItems();
if (this->item_number + 2 > nitems)
{
error(impl.qpdf, this->node,
"found short items array while removing an item");
}
items.eraseItem(this->item_number);
items.eraseItem(this->item_number);
nitems -= 2;
if (nitems > 0)
{
// There are still items left
if ((this->item_number == 0) || (this->item_number == nitems))
{
// We removed either the first or last item of an items array
// that remains non-empty, so we have to adjust limits.
QTC::TC("qpdf", "NNTree remove reset limits");
resetLimits(this->node, lastPathElement());
}
if (this->item_number == nitems)
{
// We removed the last item of a non-empty items array, so
// advance to the successor of the previous item.
QTC::TC("qpdf", "NNTree erased last item");
this->item_number -= 2;
increment(false);
}
else if (this->item_number < nitems)
{
// We don't have to do anything since the removed item's
// successor now occupies its former location.
QTC::TC("qpdf", "NNTree erased non-last item");
updateIValue();
}
else
{
// We already checked to ensure this condition would not
// happen.
throw std::logic_error(
"NNTreeIterator::remove: item_number > nitems after erase");
}
return;
}
if (this->path.empty())
{
// Special case: if this is the root node, we can leave it
// empty.
QTC::TC("qpdf", "NNTree erased all items on leaf/root");
setItemNumber(impl.oh, -1);
return;
}
QTC::TC("qpdf", "NNTree items is empty after remove");
// We removed the last item from this items array, so we need to
// remove this node from the parent on up the tree. Then we need
// to position ourselves at the removed item's successor.
bool done = false;
while (! done)
{
auto element = lastPathElement();
auto parent = element;
--parent;
auto kids = element->node.getKey("/Kids");
kids.eraseItem(element->kid_number);
auto nkids = kids.getArrayNItems();
if (nkids > 0)
{
// The logic here is similar to the items case.
if ((element->kid_number == 0) || (element->kid_number == nkids))
{
QTC::TC("qpdf", "NNTree erased first or last kid");
resetLimits(element->node, parent);
}
if (element->kid_number == nkids)
{
// Move to the successor of the last child of the
// previous kid.
setItemNumber(QPDFObjectHandle(), -1);
--element->kid_number;
deepen(kids.getArrayItem(element->kid_number), false, true);
if (valid())
{
increment(false);
if (! valid())
{
QTC::TC("qpdf", "NNTree erased last item in tree");
}
else
{
QTC::TC("qpdf", "NNTree erased last kid");
}
}
}
else
{
// Next kid is in deleted kid's position
QTC::TC("qpdf", "NNTree erased non-last kid");
deepen(kids.getArrayItem(element->kid_number), true, true);
}
done = true;
}
else if (parent == this->path.end())
{
// We erased the very last item. Convert the root to an
// empty items array.
QTC::TC("qpdf", "NNTree non-flat tree is empty after remove");
element->node.removeKey("/Kids");
element->node.replaceKey(impl.details.itemsKey(),
QPDFObjectHandle::newArray());
this->path.clear();
setItemNumber(impl.oh, -1);
done = true;
}
else
{
// Walk up the tree and continue
QTC::TC("qpdf", "NNTree remove walking up tree");
this->path.pop_back();
}
}
}
NNTreeIterator&
NNTreeIterator::operator++()
{
increment(false);
return *this;
}
NNTreeIterator&
NNTreeIterator::operator--()
{
increment(true);
return *this;
}
NNTreeIterator::reference
NNTreeIterator::operator*()
{
updateIValue(false);
return this->ivalue;
}
NNTreeIterator::pointer
NNTreeIterator::operator->()
{
updateIValue(false);
return &(this->ivalue);
}
bool
NNTreeIterator::operator==(NNTreeIterator const& other) const
{
if ((this->item_number == -1) && (other.item_number == -1))
{
return true;
}
if (this->path.size() != other.path.size())
{
return false;
}
auto tpi = this->path.begin();
auto opi = other.path.begin();
while (tpi != this->path.end())
{
if (tpi->kid_number != opi->kid_number)
{
return false;
}
++tpi;
++opi;
}
if (this->item_number != other.item_number)
{
return false;
}
return true;
}
void
NNTreeIterator::setItemNumber(QPDFObjectHandle const& node, int n)
{
this->node = node;
this->item_number = n;
updateIValue();
}
void
NNTreeIterator::addPathElement(QPDFObjectHandle const& node,
int kid_number)
{
this->path.push_back(PathElement(node, kid_number));
}
bool
NNTreeIterator::deepen(QPDFObjectHandle node, bool first, bool allow_empty)
{
// Starting at this node, descend through the first or last kid
// until we reach a node with items. If we succeed, return true;
// otherwise return false and leave path alone.
auto opath = this->path;
bool failed = false;
std::set<QPDFObjGen> seen;
for (auto i: this->path)
{
if (i.node.isIndirect())
{
seen.insert(i.node.getObjGen());
}
}
while (! failed)
{
if (node.isIndirect())
{
auto og = node.getObjGen();
if (seen.count(og))
{
QTC::TC("qpdf", "NNTree deepen: loop");
warn(impl.qpdf, node,
"loop detected while traversing name/number tree");
failed = true;
break;
}
seen.insert(og);
}
if (! node.isDictionary())
{
QTC::TC("qpdf", "NNTree node is not a dictionary");
warn(impl.qpdf, node,
"non-dictionary node while traversing name/number tree");
failed = true;
break;
}
auto kids = node.getKey("/Kids");
int nkids = kids.isArray() ? kids.getArrayNItems() : 0;
auto items = node.getKey(impl.details.itemsKey());
int nitems = items.isArray() ? items.getArrayNItems() : 0;
if (nitems > 0)
{
setItemNumber(node, first ? 0 : nitems - 2);
break;
}
else if (nkids > 0)
{
int kid_number = first ? 0 : nkids - 1;
addPathElement(node, kid_number);
auto next = kids.getArrayItem(kid_number);
if (! next.isIndirect())
{
if (impl.qpdf && impl.auto_repair)
{
QTC::TC("qpdf", "NNTree fix indirect kid");
warn(impl.qpdf, node,
"converting kid number " +
QUtil::int_to_string(kid_number) +
" to an indirect object");
next = impl.qpdf->makeIndirectObject(next);
kids.setArrayItem(kid_number, next);
}
else
{
QTC::TC("qpdf", "NNTree warn indirect kid");
warn(impl.qpdf, node,
"kid number " + QUtil::int_to_string(kid_number) +
" is not an indirect object");
}
}
node = next;
}
else if (allow_empty && items.isArray())
{
QTC::TC("qpdf", "NNTree deepen found empty");
setItemNumber(node, -1);
break;
}
else
{
QTC::TC("qpdf", "NNTree deepen: invalid node");
warn(impl.qpdf, node,
"name/number tree node has neither non-empty " +
impl.details.itemsKey() + " nor /Kids");
failed = true;
break;
}
}
if (failed)
{
this->path = opath;
return false;
}
return true;
}
NNTreeImpl::NNTreeImpl(NNTreeDetails const& details,
QPDF* qpdf,
QPDFObjectHandle& oh,
bool auto_repair) :
details(details),
qpdf(qpdf),
split_threshold(32),
oh(oh),
auto_repair(auto_repair)
{
}
void
NNTreeImpl::setSplitThreshold(int split_threshold)
{
this->split_threshold = split_threshold;
}
NNTreeImpl::iterator
NNTreeImpl::begin()
{
iterator result(*this);
result.deepen(this->oh, true, true);
return result;
}
NNTreeImpl::iterator
NNTreeImpl::end()
{
return iterator(*this);
}
NNTreeImpl::iterator
NNTreeImpl::last()
{
iterator result(*this);
result.deepen(this->oh, false, true);
return result;
}
int
NNTreeImpl::withinLimits(QPDFObjectHandle key, QPDFObjectHandle node)
{
int result = 0;
auto limits = node.getKey("/Limits");
if (limits.isArray() && (limits.getArrayNItems() >= 2) &&
details.keyValid(limits.getArrayItem(0)) &&
details.keyValid(limits.getArrayItem(1)))
{
if (details.compareKeys(key, limits.getArrayItem(0)) < 0)
{
result = -1;
}
else if (details.compareKeys(key, limits.getArrayItem(1)) > 0)
{
result = 1;
}
}
else
{
QTC::TC("qpdf", "NNTree missing limits");
error(qpdf, node, "node is missing /Limits");
}
return result;
}
int
NNTreeImpl::binarySearch(
QPDFObjectHandle key, QPDFObjectHandle items,
int num_items, bool return_prev_if_not_found,
int (NNTreeImpl::*compare)(QPDFObjectHandle& key,
QPDFObjectHandle& arr,
int item))
{
int max_idx = 1;
while (max_idx < num_items)
{
max_idx <<= 1;
}
int step = max_idx / 2;
int checks = max_idx;
int idx = step;
int found_idx = -1;
bool found = false;
bool found_leq = false;
int status = 0;
while ((! found) && (checks > 0))
{
if (idx < num_items)
{
status = (this->*compare)(key, items, idx);
if (status >= 0)
{
found_leq = true;
found_idx = idx;
}
}
else
{
// consider item to be below anything after the top
status = -1;
}
if (status == 0)
{
found = true;
}
else
{
checks >>= 1;
if (checks > 0)
{
step >>= 1;
if (step == 0)
{
step = 1;
}
if (status < 0)
{
idx -= step;
}
else
{
idx += step;
}
}
}
}
if (found || (found_leq && return_prev_if_not_found))
{
return found_idx;
}
else
{
return -1;
}
}
int
NNTreeImpl::compareKeyItem(
QPDFObjectHandle& key, QPDFObjectHandle& items, int idx)
{
if (! ((items.isArray() && (items.getArrayNItems() > (2 * idx)) &&
details.keyValid(items.getArrayItem(2 * idx)))))
{
QTC::TC("qpdf", "NNTree item is wrong type");
error(qpdf, this->oh,
"item at index " + QUtil::int_to_string(2 * idx) +
" is not the right type");
}
return details.compareKeys(key, items.getArrayItem(2 * idx));
}
int
NNTreeImpl::compareKeyKid(
QPDFObjectHandle& key, QPDFObjectHandle& kids, int idx)
{
if (! (kids.isArray() && (idx < kids.getArrayNItems()) &&
kids.getArrayItem(idx).isDictionary()))
{
QTC::TC("qpdf", "NNTree kid is invalid");
error(qpdf, this->oh,
"invalid kid at index " + QUtil::int_to_string(idx));
}
return withinLimits(key, kids.getArrayItem(idx));
}
void
NNTreeImpl::repair()
{
auto new_node = QPDFObjectHandle::newDictionary();
new_node.replaceKey(details.itemsKey(), QPDFObjectHandle::newArray());
NNTreeImpl repl(details, qpdf, new_node, false);
for (auto const& i: *this)
{
repl.insert(i.first, i.second);
}
this->oh.replaceKey("/Kids", new_node.getKey("/Kids"));
this->oh.replaceKey(
details.itemsKey(), new_node.getKey(details.itemsKey()));
}
NNTreeImpl::iterator
NNTreeImpl::find(QPDFObjectHandle key, bool return_prev_if_not_found)
{
try
{
return findInternal(key, return_prev_if_not_found);
}
catch (QPDFExc& e)
{
if (this->auto_repair)
{
QTC::TC("qpdf", "NNTree repair");
warn(qpdf, this->oh,
std::string("attempting to repair after error: ") + e.what());
repair();
return findInternal(key, return_prev_if_not_found);
}
else
{
throw e;
}
}
}
NNTreeImpl::iterator
NNTreeImpl::findInternal(QPDFObjectHandle key, bool return_prev_if_not_found)
{
auto first_item = begin();
auto last_item = end();
if (first_item == end())
{
// Empty
return end();
}
else if (first_item.valid() &&
details.keyValid(first_item->first) &&
details.compareKeys(key, first_item->first) < 0)
{
// Before the first key
return end();
}
else if (last_item.valid() &&
details.keyValid(last_item->first) &&
details.compareKeys(key, last_item->first) > 0)
{
// After the last key
if (return_prev_if_not_found)
{
return last_item;
}
else
{
return end();
}
}
std::set<QPDFObjGen> seen;
auto node = this->oh;
iterator result(*this);
while (true)
{
auto og = node.getObjGen();
if (seen.count(og))
{
QTC::TC("qpdf", "NNTree loop in find");
error(qpdf, node, "loop detected in find");
}
seen.insert(og);
auto kids = node.getKey("/Kids");
int nkids = kids.isArray() ? kids.getArrayNItems() : 0;
auto items = node.getKey(details.itemsKey());
int nitems = items.isArray() ? items.getArrayNItems() : 0;
if (nitems > 0)
{
int idx = binarySearch(
key, items, nitems / 2, return_prev_if_not_found,
&NNTreeImpl::compareKeyItem);
if (idx >= 0)
{
result.setItemNumber(node, 2 * idx);
}
break;
}
else if (nkids > 0)
{
int idx = binarySearch(
key, kids, nkids, true,
&NNTreeImpl::compareKeyKid);
if (idx == -1)
{
QTC::TC("qpdf", "NNTree -1 in binary search");
error(qpdf, node,
"unexpected -1 from binary search of kids;"
" limits may by wrong");
}
result.addPathElement(node, idx);
node = kids.getArrayItem(idx);
}
else
{
QTC::TC("qpdf", "NNTree bad node during find");
error(qpdf, node, "bad node during find");
}
}
return result;
}
NNTreeImpl::iterator
NNTreeImpl::insertFirst(QPDFObjectHandle key, QPDFObjectHandle value)
{
auto iter = begin();
QPDFObjectHandle items;
if (iter.node.isInitialized() &&
iter.node.isDictionary())
{
items = iter.node.getKey(details.itemsKey());
}
if (! (items.isInitialized() && items.isArray()))
{
QTC::TC("qpdf", "NNTree no valid items node in insertFirst");
error(qpdf, this->oh, "unable to find a valid items node");
}
items.insertItem(0, key);
items.insertItem(1, value);
iter.setItemNumber(iter.node, 0);
iter.resetLimits(iter.node, iter.lastPathElement());
iter.split(iter.node, iter.lastPathElement());
return iter;
}
NNTreeImpl::iterator
NNTreeImpl::insert(QPDFObjectHandle key, QPDFObjectHandle value)
{
auto iter = find(key, true);
if (! iter.valid())
{
QTC::TC("qpdf", "NNTree insert inserts first");
return insertFirst(key, value);
}
else if (details.compareKeys(key, iter->first) == 0)
{
QTC::TC("qpdf", "NNTree insert replaces");
auto items = iter.node.getKey(details.itemsKey());
items.setArrayItem(iter.item_number + 1, value);
iter.updateIValue();
}
else
{
QTC::TC("qpdf", "NNTree insert inserts after");
iter.insertAfter(key, value);
}
return iter;
}
bool
NNTreeImpl::remove(QPDFObjectHandle key, QPDFObjectHandle* value)
{
auto iter = find(key, false);
if (! iter.valid())
{
QTC::TC("qpdf", "NNTree remove not found");
return false;
}
if (value)
{
*value = iter->second;
}
iter.remove();
return true;
}