#include #include #include #include static std::string get_description(QPDFObjectHandle& node) { std::string result("Name/Number tree node"); if (node.isIndirect()) { result += " (object " + std::to_string(node.getObjectID()) + ")"; } return result; } static void warn(QPDF& qpdf, QPDFObjectHandle& node, std::string const& msg) { qpdf.warn(qpdf_e_damaged_pdf, get_description(node), 0, msg); } static void error(QPDF& qpdf, QPDFObjectHandle& node, std::string const& msg) { throw QPDFExc(qpdf_e_damaged_pdf, qpdf.getFilename(), get_description(node), 0, 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 points to 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.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 " + std::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 " + std::to_string(this->item_number) + " has the wrong type")); } else { found_valid_key = true; } } } } void NNTreeIterator::resetLimits(QPDFObjectHandle node, std::list::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::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 shown 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 (!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.emplace_front(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::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.emplace_back(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; QPDFObjGen::set seen; for (auto const& i: this->path) { seen.add(i.node); } while (!failed) { if (!seen.add(node)) { QTC::TC("qpdf", "NNTree deepen: loop"); warn(impl.qpdf, node, "loop detected while traversing name/number tree"); failed = true; break; } 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.auto_repair) { QTC::TC("qpdf", "NNTree fix indirect kid"); warn( impl.qpdf, node, ("converting kid number " + std::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 " + std::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 {*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 " + std::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 " + std::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; } } } 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(); } } QPDFObjGen::set seen; auto node = this->oh; iterator result(*this); while (true) { if (!seen.add(node)) { QTC::TC("qpdf", "NNTree loop in find"); error(qpdf, node, "loop detected in find"); } 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.isDictionary()) { items = iter.node.getKey(details.itemsKey()); } if (!(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; }