Coverage Report

Created: 2025-08-26 07:13

/src/qpdf/libqpdf/NNTree.cc
Line
Count
Source (jump to first uncovered line)
1
#include <qpdf/assert_debug.h>
2
3
#include <qpdf/NNTree.hh>
4
5
#include <qpdf/QPDFObjectHandle_private.hh>
6
#include <qpdf/QPDF_private.hh>
7
#include <qpdf/QTC.hh>
8
#include <qpdf/QUtil.hh>
9
10
#include <bit>
11
#include <exception>
12
#include <utility>
13
14
using namespace qpdf;
15
16
static std::string
17
get_description(QPDFObjectHandle const& node)
18
33.5k
{
19
33.5k
    std::string result("Name/Number tree node");
20
33.5k
    if (node.indirect()) {
21
30.3k
        result += " (object " + std::to_string(node.getObjectID()) + ")";
22
30.3k
    }
23
33.5k
    return result;
24
33.5k
}
25
26
void
27
NNTreeImpl::warn(QPDFObjectHandle const& node, std::string const& msg)
28
23.5k
{
29
23.5k
    qpdf.warn(qpdf_e_damaged_pdf, get_description(node), 0, msg);
30
23.5k
    if (++error_count > 5 && qpdf.reconstructed_xref()) {
31
5.41k
        error(node, "too many errors - giving up");
32
5.41k
    }
33
23.5k
}
34
35
void
36
NNTreeImpl::error(QPDFObjectHandle const& node, std::string const& msg)
37
9.97k
{
38
9.97k
    throw QPDFExc(qpdf_e_damaged_pdf, qpdf.getFilename(), get_description(node), 0, msg);
39
9.97k
}
40
41
NNTreeIterator::NNTreeIterator(NNTreeImpl& impl) :
42
630k
    impl(impl)
43
630k
{
44
630k
}
45
46
void
47
NNTreeIterator::updateIValue(bool allow_invalid)
48
972k
{
49
    // ivalue should never be used inside the class since we return a pointer/reference to it. Every
50
    // bit of code that ever changes what object the iterator points to should take care to call
51
    // updateIValue. Failure to do this means that any old references to *iter will point to
52
    // incorrect objects, though the next dereference of the iterator will fix it. This isn't
53
    // necessarily catastrophic, but it would be confusing. The test suite attempts to exercise
54
    // various cases to ensure we don't introduce that bug in the future, but sadly it's tricky to
55
    // verify by reasoning about the code that this constraint is always satisfied. Whenever we
56
    // update what the iterator points to, we should call setItemNumber, which calls this. If we
57
    // change what the iterator points to in some other way, such as replacing a value or removing
58
    // an item and making the iterator point at a different item in potentially the same position,
59
    // we must call updateIValue as well. These cases are handled, and for good measure, we also
60
    // call updateIValue in operator* and operator->.
61
62
972k
    if (item_number < 0 || !node.isDictionary()) {
63
9.26k
        if (!allow_invalid) {
64
0
            throw std::logic_error(
65
0
                "attempt made to dereference an invalid name/number tree iterator");
66
0
        }
67
9.26k
        ivalue.first = QPDFObjectHandle();
68
9.26k
        ivalue.second = QPDFObjectHandle();
69
9.26k
        return;
70
9.26k
    }
71
963k
    Array items = node.getKey(impl.details.itemsKey());
72
963k
    if (!std::cmp_less(item_number + 1, items.size())) {
73
0
        impl.error(node, "update ivalue: items array is too short");
74
0
    }
75
963k
    ivalue.first = items[item_number];
76
963k
    ivalue.second = items[1 + item_number];
77
963k
}
78
79
NNTreeIterator::PathElement::PathElement(QPDFObjectHandle const& node, int kid_number) :
80
204k
    node(node),
81
204k
    kid_number(kid_number)
82
204k
{
83
204k
}
84
85
QPDFObjectHandle
86
NNTreeIterator::getNextKid(PathElement& pe, bool backward)
87
4.19k
{
88
7.83k
    while (true) {
89
7.02k
        pe.kid_number += backward ? -1 : 1;
90
7.02k
        Array kids = pe.node.getKey("/Kids");
91
7.02k
        if (pe.kid_number >= 0 && std::cmp_less(pe.kid_number, kids.size())) {
92
5.88k
            auto result = kids[pe.kid_number];
93
5.88k
            if (result.isDictionary() &&
94
5.88k
                (result.hasKey("/Kids") || result.hasKey(impl.details.itemsKey()))) {
95
2.24k
                return result;
96
3.64k
            } else {
97
3.64k
                impl.warn(
98
3.64k
                    pe.node, "skipping over invalid kid at index " + std::to_string(pe.kid_number));
99
3.64k
            }
100
5.88k
        } else {
101
1.13k
            return QPDFObjectHandle::newNull();
102
1.13k
        }
103
7.02k
    }
104
4.19k
}
105
106
bool
107
NNTreeIterator::valid() const
108
720k
{
109
720k
    return item_number >= 0 && ivalue.first && ivalue.second;
110
720k
}
111
112
void
113
NNTreeIterator::increment(bool backward)
114
182k
{
115
182k
    if (item_number < 0) {
116
0
        deepen(impl.oh, !backward, true);
117
0
        return;
118
0
    }
119
120
191k
    while (valid()) {
121
189k
        item_number += backward ? -2 : 2;
122
189k
        Array items = node.getKey(impl.details.itemsKey());
123
189k
        if (item_number < 0 || std::cmp_greater_equal(item_number, items.size())) {
124
3.48k
            bool found = false;
125
3.48k
            setItemNumber(QPDFObjectHandle(), -1);
126
7.67k
            while (!(found || path.empty())) {
127
4.19k
                auto& element = path.back();
128
4.19k
                auto pe_node = getNextKid(element, backward);
129
4.19k
                if (pe_node.null()) {
130
1.13k
                    path.pop_back();
131
3.05k
                } else {
132
3.05k
                    found = deepen(pe_node, !backward, false);
133
3.05k
                }
134
4.19k
            }
135
3.48k
        }
136
189k
        if (item_number >= 0) {
137
187k
            items = node.getKey(impl.details.itemsKey());
138
187k
            if (std::cmp_greater_equal(item_number + 1, items.size())) {
139
977
                impl.warn(node, "items array doesn't have enough elements");
140
186k
            } else if (!impl.details.keyValid(items[item_number])) {
141
6.19k
                impl.warn(node, ("item " + std::to_string(item_number) + " has the wrong type"));
142
180k
            } else if (!items[item_number + 1]) {
143
62
                impl.warn(node, "item " + std::to_string(item_number) + " is null");
144
180k
            } else {
145
180k
                return;
146
180k
            }
147
187k
        }
148
189k
    }
149
182k
}
150
151
void
152
NNTreeIterator::resetLimits(QPDFObjectHandle a_node, std::list<PathElement>::iterator parent)
153
53.2k
{
154
53.2k
    while (true) {
155
53.2k
        if (parent == path.end()) {
156
28.4k
            a_node.removeKey("/Limits");
157
28.4k
            break;
158
28.4k
        }
159
24.7k
        Array kids = a_node.getKey("/Kids");
160
24.7k
        size_t nkids = kids.size();
161
24.7k
        Array items = a_node.getKey(impl.details.itemsKey());
162
24.7k
        size_t nitems = items.size();
163
164
24.7k
        bool changed = true;
165
24.7k
        QPDFObjectHandle first;
166
24.7k
        QPDFObjectHandle last;
167
24.7k
        if (nitems >= 2) {
168
23.8k
            first = items[0];
169
23.8k
            last = items[(nitems - 1u) & ~1u];
170
23.8k
        } else if (nkids > 0) {
171
879
            auto first_kid = kids[0];
172
879
            auto last_kid = kids[nkids - 1u];
173
879
            if (first_kid.isDictionary() && last_kid.isDictionary()) {
174
879
                Array first_limits = first_kid.getKey("/Limits");
175
879
                Array last_limits = last_kid.getKey("/Limits");
176
879
                if (first_limits.size() >= 2 && last_limits.size() >= 2) {
177
879
                    first = first_limits[0];
178
879
                    last = last_limits[1];
179
879
                }
180
879
            }
181
879
        }
182
24.7k
        if (first && last) {
183
24.7k
            Array limits({first, last});
184
24.7k
            Array olimits = a_node.getKey("/Limits");
185
24.7k
            if (olimits.size() == 2) {
186
22.0k
                auto ofirst = olimits[0];
187
22.0k
                auto olast = olimits[1];
188
22.0k
                if (impl.details.keyValid(ofirst) && impl.details.keyValid(olast) &&
189
22.0k
                    impl.details.compareKeys(first, ofirst) == 0 &&
190
22.0k
                    impl.details.compareKeys(last, olast) == 0) {
191
19.3k
                    changed = false;
192
19.3k
                }
193
22.0k
            }
194
24.7k
            if (changed && !a_node.isSameObjectAs(path.begin()->node)) {
195
4.50k
                a_node.replaceKey("/Limits", limits);
196
4.50k
            }
197
24.7k
        } else {
198
0
            impl.warn(a_node, "unable to determine limits");
199
0
        }
200
201
24.7k
        if (!changed || parent == path.begin()) {
202
24.7k
            break;
203
24.7k
        } else {
204
0
            a_node = parent->node;
205
0
            --parent;
206
0
        }
207
24.7k
    }
208
53.2k
}
209
210
void
211
NNTreeIterator::split(QPDFObjectHandle to_split, std::list<PathElement>::iterator parent)
212
50.4k
{
213
    // Split some node along the path to the item pointed to by this iterator, and adjust the
214
    // iterator so it points to the same item.
215
216
    // In examples, for simplicity, /Nums is shown to just contain numbers instead of pairs. Imagine
217
    // this tree:
218
    //
219
    // root: << /Kids [ A B C D ] >>
220
    // A: << /Nums [ 1 2 3 4 ] >>
221
    // B: << /Nums [ 5 6 7 8 ] >>
222
    // C: << /Nums [ 9 10 11 12 ] >>
223
    // D: << /Kids [ E F ]
224
    // E: << /Nums [ 13 14 15 16 ] >>
225
    // F: << /Nums [ 17 18 19 20 ] >>
226
227
    // iter1 (points to 19)
228
    //   path:
229
    //   - { node: root: kid_number: 3 }
230
    //   - { node: D, kid_number: 1 }
231
    //   node: F
232
    //   item_number: 2
233
234
    // iter2 (points to 1)
235
    //   path:
236
    //   - { node: root, kid_number: 0}
237
    //   node: A
238
    //   item_number: 0
239
240
50.4k
    if (!valid()) {
241
0
        throw std::logic_error("NNTreeIterator::split called an invalid iterator");
242
0
    }
243
244
    // Find the array we actually need to split, which is either this node's kids or items.
245
50.4k
    Array kids = to_split.getKey("/Kids");
246
50.4k
    size_t nkids = kids.size();
247
50.4k
    Array items = to_split.getKey(impl.details.itemsKey());
248
50.4k
    size_t nitems = items.size();
249
250
50.4k
    Array first_half;
251
50.4k
    size_t n = 0;
252
50.4k
    std::string key;
253
50.4k
    size_t threshold = static_cast<size_t>(impl.split_threshold);
254
50.4k
    if (nkids > 0) {
255
879
        first_half = kids;
256
879
        n = nkids;
257
879
        key = "/Kids";
258
49.5k
    } else if (nitems > 0) {
259
49.5k
        first_half = items;
260
49.5k
        n = nitems;
261
49.5k
        threshold *= 2;
262
49.5k
        key = impl.details.itemsKey();
263
49.5k
    } else {
264
0
        throw std::logic_error("NNTreeIterator::split called on invalid node");
265
0
    }
266
267
50.4k
    if (n <= threshold) {
268
49.0k
        return;
269
49.0k
    }
270
271
1.38k
    bool is_root = parent == path.end();
272
1.38k
    bool is_leaf = nitems > 0;
273
274
    // CURRENT STATE: tree is in original state; iterator is valid and unchanged.
275
276
1.38k
    if (is_root) {
277
        // What we want to do is to create a new node for the second half of the items and put it in
278
        // the parent's /Kids array right after the element that points to the current to_split
279
        // node, but if we're splitting root, there is no parent, so handle that first.
280
281
        // In the non-root case, parent points to the path element whose /Kids contains the first
282
        // half node, and the first half node is to_split. If we are splitting the root, we need to
283
        // push everything down a level, but we want to keep the actual root object the same so that
284
        // indirect references to it remain intact (and also in case it might be a direct object,
285
        // which it shouldn't be but that case probably exists in the wild). To achieve this, we
286
        // create a new node for the first half and then replace /Kids in the root to contain it.
287
        // Then we adjust the path so that the first element is root and the second element, if any,
288
        // is the new first half. In this way, we make the root case identical to the non-root case
289
        // so remaining logic can handle them in the same way.
290
291
501
        auto first_node = impl.qpdf.makeIndirectObject(QPDFObjectHandle::newDictionary());
292
501
        first_node.replaceKey(key, first_half);
293
501
        Array new_kids;
294
501
        new_kids.push_back(first_node);
295
501
        to_split.removeKey("/Limits"); // already shouldn't be there for root
296
501
        to_split.removeKey(impl.details.itemsKey());
297
501
        to_split.replaceKey("/Kids", new_kids);
298
501
        if (is_leaf) {
299
492
            node = first_node;
300
492
        } else {
301
9
            auto next = path.begin();
302
9
            next->node = first_node;
303
9
        }
304
501
        this->path.emplace_front(to_split, 0);
305
501
        parent = path.begin();
306
501
        to_split = first_node;
307
501
    }
308
309
    // CURRENT STATE: parent is guaranteed to be defined, and we have the invariants that
310
    // parent[/Kids][kid_number] == to_split and (++parent).node == to_split.
311
312
    // Create a second half array, and transfer the second half of the items into the second half
313
    // array.
314
1.38k
    Array second_half;
315
1.38k
    auto start_idx = static_cast<int>((n / 2) & ~1u);
316
47.9k
    while (std::cmp_greater(first_half.size(), start_idx)) {
317
46.6k
        second_half.push_back(first_half[start_idx]);
318
46.6k
        first_half.erase(start_idx);
319
46.6k
    }
320
1.38k
    resetLimits(to_split, parent);
321
322
    // Create a new node to contain the second half
323
1.38k
    QPDFObjectHandle second_node = impl.qpdf.makeIndirectObject(QPDFObjectHandle::newDictionary());
324
1.38k
    second_node.replaceKey(key, second_half);
325
1.38k
    resetLimits(second_node, parent);
326
327
    // CURRENT STATE: half the items from the kids or items array in the node being split have been
328
    // moved into a new node. The new node is not yet attached to the tree. The iterator may have a
329
    // path element or leaf node that is out of bounds.
330
331
    // We need to adjust the parent to add the second node to /Kids and, if needed, update
332
    // kid_number to traverse through it. We need to update to_split's path element, or the node if
333
    // this is a leaf, so that the kid/item number points to the right place.
334
335
1.38k
    Array parent_kids = parent->node.getKey("/Kids");
336
1.38k
    parent_kids.insert(parent->kid_number + 1, second_node);
337
1.38k
    auto cur_elem = parent;
338
1.38k
    ++cur_elem; // points to end() for leaf nodes
339
1.38k
    int old_idx = (is_leaf ? item_number : cur_elem->kid_number);
340
1.38k
    if (old_idx >= start_idx) {
341
716
        ++parent->kid_number;
342
716
        if (is_leaf) {
343
716
            setItemNumber(second_node, item_number - start_idx);
344
716
        } else {
345
0
            cur_elem->node = second_node;
346
0
            cur_elem->kid_number -= start_idx;
347
0
        }
348
716
    }
349
1.38k
    if (!is_root) {
350
879
        auto next = parent->node;
351
879
        resetLimits(next, parent);
352
879
        --parent;
353
879
        split(next, parent);
354
879
    }
355
1.38k
}
356
357
std::list<NNTreeIterator::PathElement>::iterator
358
NNTreeIterator::lastPathElement()
359
99.1k
{
360
99.1k
    auto result = path.end();
361
99.1k
    if (!path.empty()) {
362
42.2k
        --result;
363
42.2k
    }
364
99.1k
    return result;
365
99.1k
}
366
367
void
368
NNTreeIterator::insertAfter(QPDFObjectHandle const& key, QPDFObjectHandle const& value)
369
44.3k
{
370
44.3k
    if (!valid()) {
371
0
        impl.insertFirst(key, value);
372
0
        deepen(impl.oh, true, false);
373
0
        return;
374
0
    }
375
376
44.3k
    Array items = node.getKey(impl.details.itemsKey());
377
44.3k
    if (!items) {
378
0
        impl.error(node, "node contains no items array");
379
0
    }
380
381
44.3k
    if (std::cmp_less(items.size(), item_number + 2)) {
382
0
        impl.error(node, "insert: items array is too short");
383
0
    }
384
44.3k
    if (!(key && value)) {
385
0
        impl.error(node, "insert: key or value is null");
386
0
    }
387
44.3k
    items.insert(item_number + 2, key);
388
44.3k
    items.insert(item_number + 3, value);
389
44.3k
    resetLimits(node, lastPathElement());
390
44.3k
    split(node, lastPathElement());
391
44.3k
    increment(false);
392
44.3k
}
393
394
void
395
NNTreeIterator::remove()
396
0
{
397
    // Remove this item, leaving the tree valid and this iterator pointing to the next item.
398
399
0
    if (!valid()) {
400
0
        throw std::logic_error("attempt made to remove an invalid iterator");
401
0
    }
402
0
    Array items = node.getKey(impl.details.itemsKey());
403
0
    int nitems = static_cast<int>(items.size());
404
0
    if (std::cmp_greater(item_number + 2, nitems)) {
405
0
        impl.error(node, "found short items array while removing an item");
406
0
    }
407
408
0
    items.erase(item_number);
409
0
    items.erase(item_number);
410
0
    nitems -= 2;
411
412
0
    if (nitems > 0) {
413
        // There are still items left
414
415
0
        if (item_number == 0 || item_number == nitems) {
416
            // We removed either the first or last item of an items array that remains non-empty, so
417
            // we have to adjust limits.
418
0
            resetLimits(node, lastPathElement());
419
0
        }
420
421
0
        if (item_number == nitems) {
422
            // We removed the last item of a non-empty items array, so advance to the successor of
423
            // the previous item.
424
0
            item_number -= 2;
425
0
            increment(false);
426
0
        } else if (item_number < nitems) {
427
            // We don't have to do anything since the removed item's successor now occupies its
428
            // former location.
429
0
            updateIValue();
430
0
        } else {
431
            // We already checked to ensure this condition would not happen.
432
0
            throw std::logic_error("NNTreeIterator::remove: item_number > nitems after erase");
433
0
        }
434
0
        return;
435
0
    }
436
437
0
    if (path.empty()) {
438
        // Special case: if this is the root node, we can leave it empty.
439
0
        setItemNumber(impl.oh, -1);
440
0
        return;
441
0
    }
442
443
    // We removed the last item from this items array, so we need to remove this node from the
444
    // parent on up the tree. Then we need to position ourselves at the removed item's successor.
445
0
    while (true) {
446
0
        auto element = lastPathElement();
447
0
        auto parent = element;
448
0
        --parent;
449
0
        Array kids = element->node.getKey("/Kids");
450
0
        kids.erase(element->kid_number);
451
0
        auto nkids = kids.size();
452
0
        if (nkids > 0) {
453
            // The logic here is similar to the items case.
454
0
            if (element->kid_number == 0 || std::cmp_equal(element->kid_number, nkids)) {
455
0
                resetLimits(element->node, parent);
456
0
            }
457
0
            if (std::cmp_equal(element->kid_number, nkids)) {
458
                // Move to the successor of the last child of the previous kid.
459
0
                setItemNumber({}, -1);
460
0
                --element->kid_number;
461
0
                deepen(kids[element->kid_number], false, true);
462
0
                if (valid()) {
463
0
                    increment(false);
464
0
                    QTC::TC("qpdf", "NNTree erased last kid/item in tree", valid() ? 0 : 1);
465
0
                }
466
0
            } else {
467
                // Next kid is in deleted kid's position
468
0
                deepen(kids.get(element->kid_number), true, true);
469
0
            }
470
0
            return;
471
0
        }
472
473
0
        if (parent == path.end()) {
474
            // We erased the very last item. Convert the root to an empty items array.
475
0
            element->node.removeKey("/Kids");
476
0
            element->node.replaceKey(impl.details.itemsKey(), Array());
477
0
            path.clear();
478
0
            setItemNumber(impl.oh, -1);
479
0
            return;
480
0
        }
481
482
        // Walk up the tree and continue
483
0
        path.pop_back();
484
0
    }
485
0
}
486
487
NNTreeIterator&
488
NNTreeIterator::operator++()
489
138k
{
490
138k
    increment(false);
491
138k
    return *this;
492
138k
}
493
494
NNTreeIterator&
495
NNTreeIterator::operator--()
496
0
{
497
0
    increment(true);
498
0
    return *this;
499
0
}
500
501
NNTreeIterator::reference
502
NNTreeIterator::operator*()
503
138k
{
504
138k
    updateIValue(false);
505
138k
    return ivalue;
506
138k
}
507
508
NNTreeIterator::pointer
509
NNTreeIterator::operator->()
510
440k
{
511
440k
    updateIValue(false);
512
440k
    return &ivalue;
513
440k
}
514
515
bool
516
NNTreeIterator::operator==(NNTreeIterator const& other) const
517
300k
{
518
300k
    if (item_number == -1 && other.item_number == -1) {
519
12.1k
        return true;
520
12.1k
    }
521
288k
    if (path.size() != other.path.size()) {
522
229k
        return false;
523
229k
    }
524
58.9k
    auto tpi = path.begin();
525
58.9k
    auto opi = other.path.begin();
526
58.9k
    while (tpi != path.end()) {
527
0
        if (tpi->kid_number != opi->kid_number) {
528
0
            return false;
529
0
        }
530
0
        ++tpi;
531
0
        ++opi;
532
0
    }
533
58.9k
    return item_number == other.item_number;
534
58.9k
}
535
536
void
537
NNTreeIterator::setItemNumber(QPDFObjectHandle const& a_node, int n)
538
305k
{
539
305k
    node = a_node;
540
305k
    item_number = n;
541
305k
    updateIValue();
542
305k
}
543
544
void
545
NNTreeIterator::addPathElement(QPDFObjectHandle const& a_node, int kid_number)
546
203k
{
547
203k
    path.emplace_back(a_node, kid_number);
548
203k
}
549
550
bool
551
NNTreeIterator::deepen(QPDFObjectHandle a_node, bool first, bool allow_empty)
552
164k
{
553
    // Starting at this node, descend through the first or last kid until we reach a node with
554
    // items. If we succeed, return true; otherwise return false and leave path alone.
555
556
164k
    auto opath = path;
557
558
164k
    auto fail = [this, &opath](QPDFObjectHandle const& failed_node, std::string const& msg) {
559
5.32k
        impl.warn(failed_node, msg);
560
5.32k
        path = opath;
561
5.32k
        return false;
562
5.32k
    };
563
564
164k
    QPDFObjGen::set seen;
565
164k
    for (auto const& i: path) {
566
3.16k
        seen.add(i.node);
567
3.16k
    }
568
274k
    while (true) {
569
273k
        if (!seen.add(a_node)) {
570
453
            return fail(a_node, "loop detected while traversing name/number tree");
571
453
        }
572
573
273k
        if (!a_node.isDictionary()) {
574
3.00k
            return fail(a_node, "non-dictionary node while traversing name/number tree");
575
3.00k
        }
576
577
270k
        Array items = a_node.getKey(impl.details.itemsKey());
578
270k
        int nitems = static_cast<int>(items.size());
579
270k
        if (nitems > 1) {
580
153k
            setItemNumber(a_node, first ? 0 : nitems - 2);
581
153k
            break;
582
153k
        }
583
584
117k
        Array kids = a_node.getKey("/Kids");
585
117k
        int nkids = static_cast<int>(kids.size());
586
117k
        if (nkids > 0) {
587
109k
            int kid_number = first ? 0 : nkids - 1;
588
109k
            addPathElement(a_node, kid_number);
589
109k
            auto next = kids[kid_number];
590
109k
            if (!next) {
591
133
                return fail(a_node, "kid number " + std::to_string(kid_number) + " is invalid");
592
133
            }
593
109k
            if (!next.indirect()) {
594
823
                if (impl.auto_repair) {
595
823
                    impl.warn(
596
823
                        a_node,
597
823
                        "converting kid number " + std::to_string(kid_number) +
598
823
                            " to an indirect object");
599
823
                    next = impl.qpdf.makeIndirectObject(next);
600
823
                    kids.set(kid_number, next);
601
823
                } else {
602
0
                    impl.warn(
603
0
                        a_node,
604
0
                        "kid number " + std::to_string(kid_number) + " is not an indirect object");
605
0
                }
606
823
            }
607
109k
            a_node = next;
608
109k
        } else if (allow_empty && items) {
609
5.77k
            setItemNumber(a_node, -1);
610
5.77k
            break;
611
5.77k
        } else {
612
2.01k
            return fail(
613
2.01k
                a_node,
614
2.01k
                "name/number tree node has neither non-empty " + impl.details.itemsKey() +
615
2.01k
                    " nor /Kids");
616
2.01k
        }
617
117k
    }
618
159k
    return true;
619
164k
}
620
621
NNTreeImpl::NNTreeImpl(
622
    NNTreeDetails const& details, QPDF& qpdf, QPDFObjectHandle& oh, bool auto_repair) :
623
7.37k
    details(details),
624
7.37k
    qpdf(qpdf),
625
7.37k
    oh(oh),
626
7.37k
    auto_repair(auto_repair)
627
7.37k
{
628
7.37k
}
629
630
void
631
NNTreeImpl::setSplitThreshold(int threshold)
632
0
{
633
0
    split_threshold = threshold;
634
0
}
635
636
NNTreeImpl::iterator
637
NNTreeImpl::begin()
638
162k
{
639
162k
    iterator result(*this);
640
162k
    result.deepen(oh, true, true);
641
162k
    return result;
642
162k
}
643
644
NNTreeImpl::iterator
645
NNTreeImpl::end()
646
324k
{
647
324k
    return {*this};
648
324k
}
649
650
NNTreeImpl::iterator
651
NNTreeImpl::last()
652
0
{
653
0
    iterator result(*this);
654
0
    result.deepen(oh, false, true);
655
0
    return result;
656
0
}
657
658
int
659
NNTreeImpl::withinLimits(QPDFObjectHandle const& key, QPDFObjectHandle const& node)
660
203k
{
661
203k
    Array limits = node.getKey("/Limits");
662
203k
    if (!(details.keyValid(limits[0]) && details.keyValid(limits[1]))) {
663
880
        error(node, "node is missing /Limits");
664
880
    }
665
203k
    if (details.compareKeys(key, limits[0]) < 0) {
666
92.2k
        return -1;
667
92.2k
    }
668
111k
    if (details.compareKeys(key, limits[1]) > 0) {
669
18.0k
        return 1;
670
18.0k
    }
671
93.1k
    return 0;
672
111k
}
673
674
int
675
NNTreeImpl::binarySearch(
676
    QPDFObjectHandle key,
677
    QPDFObjectHandle items,
678
    size_t num_items,
679
    bool return_prev_if_not_found,
680
    int (NNTreeImpl::*compare)(QPDFObjectHandle& key, QPDFObjectHandle& arr, int item))
681
236k
{
682
236k
    size_t max_idx = std::bit_ceil(num_items);
683
684
236k
    int step = static_cast<int>(max_idx / 2);
685
236k
    size_t checks = max_idx;
686
236k
    int idx = step;
687
236k
    int found_idx = -1;
688
689
872k
    while (checks > 0) {
690
818k
        int status = -1;
691
818k
        if (std::cmp_less(idx, num_items)) {
692
785k
            status = (this->*compare)(key, items, idx);
693
785k
            if (status == 0) {
694
182k
                return idx;
695
182k
            }
696
603k
            if (status > 0) {
697
224k
                found_idx = idx;
698
224k
            }
699
603k
        }
700
635k
        checks >>= 1;
701
635k
        if (checks > 0) {
702
581k
            step >>= 1;
703
581k
            if (step == 0) {
704
85.4k
                step = 1;
705
85.4k
            }
706
707
581k
            if (status < 0) {
708
382k
                idx -= step;
709
382k
            } else {
710
198k
                idx += step;
711
198k
            }
712
581k
        }
713
635k
    }
714
715
54.3k
    return return_prev_if_not_found ? found_idx : -1;
716
236k
}
717
718
int
719
NNTreeImpl::compareKeyItem(QPDFObjectHandle& key, QPDFObjectHandle& items, int idx)
720
580k
{
721
580k
    if (!(std::cmp_greater(items.size(), 2 * idx) && details.keyValid(items[2 * idx]))) {
722
1.49k
        error(oh, ("item at index " + std::to_string(2 * idx) + " is not the right type"));
723
1.49k
    }
724
580k
    return details.compareKeys(key, items[2 * idx]);
725
580k
}
726
727
int
728
NNTreeImpl::compareKeyKid(QPDFObjectHandle& key, QPDFObjectHandle& kids, int idx)
729
205k
{
730
205k
    if (!(std::cmp_less(idx, kids.size()) && kids[idx].isDictionary())) {
731
1.85k
        error(oh, "invalid kid at index " + std::to_string(idx));
732
1.85k
    }
733
205k
    return withinLimits(key, kids[idx]);
734
205k
}
735
736
void
737
NNTreeImpl::repair()
738
2.75k
{
739
2.75k
    auto new_node = QPDFObjectHandle::newDictionary();
740
2.75k
    new_node.replaceKey(details.itemsKey(), Array());
741
2.75k
    NNTreeImpl repl(details, qpdf, new_node, false);
742
138k
    for (auto const& [key, value]: *this) {
743
138k
        if (key && value) {
744
138k
            repl.insert(key, value);
745
138k
        }
746
138k
    }
747
2.75k
    oh.replaceKey("/Kids", new_node.getKey("/Kids"));
748
2.75k
    oh.replaceKey(details.itemsKey(), new_node.getKey(details.itemsKey()));
749
2.75k
}
750
751
NNTreeImpl::iterator
752
NNTreeImpl::find(QPDFObjectHandle key, bool return_prev_if_not_found)
753
153k
{
754
153k
    try {
755
153k
        return findInternal(key, return_prev_if_not_found);
756
153k
    } catch (QPDFExc& e) {
757
7.10k
        if (auto_repair) {
758
6.54k
            warn(oh, std::string("attempting to repair after error: ") + e.what());
759
6.54k
            repair();
760
6.54k
            return findInternal(key, return_prev_if_not_found);
761
6.54k
        } else {
762
565
            throw;
763
565
        }
764
7.10k
    }
765
153k
}
766
767
NNTreeImpl::iterator
768
NNTreeImpl::findInternal(QPDFObjectHandle const& key, bool return_prev_if_not_found)
769
154k
{
770
154k
    auto first_item = begin();
771
154k
    auto last_item = end();
772
154k
    if (first_item == end()) {
773
6.19k
        return end();
774
6.19k
    }
775
148k
    if (first_item.valid() && details.keyValid(first_item->first) &&
776
148k
        details.compareKeys(key, first_item->first) < 0) {
777
        // Before the first key
778
2.90k
        return end();
779
2.90k
    }
780
145k
    qpdf_assert_debug(!last_item.valid());
781
782
142k
    QPDFObjGen::set seen;
783
142k
    auto node = oh;
784
142k
    iterator result(*this);
785
786
240k
    while (true) {
787
237k
        if (!seen.add(node)) {
788
89
            error(node, "loop detected in find");
789
89
        }
790
791
237k
        Array items = node.getKey(details.itemsKey());
792
237k
        size_t nitems = items.size();
793
237k
        if (nitems > 1) {
794
139k
            int idx = binarySearch(
795
139k
                key, items, nitems / 2, return_prev_if_not_found, &NNTreeImpl::compareKeyItem);
796
139k
            if (idx >= 0) {
797
136k
                result.setItemNumber(node, 2 * idx);
798
136k
            }
799
139k
            return result;
800
139k
        }
801
802
97.4k
        Array kids = node.getKey("/Kids");
803
97.4k
        size_t nkids = kids.size();
804
97.4k
        if (nkids > 0) {
805
97.1k
            int idx = binarySearch(key, kids, nkids, true, &NNTreeImpl::compareKeyKid);
806
97.1k
            if (idx == -1) {
807
106
                error(node, "unexpected -1 from binary search of kids; limits may by wrong");
808
106
            }
809
97.1k
            result.addPathElement(node, idx);
810
97.1k
            node = kids[idx];
811
97.1k
        } else {
812
229
            error(node, "bad node during find");
813
229
        }
814
97.4k
    }
815
142k
}
816
817
NNTreeImpl::iterator
818
NNTreeImpl::insertFirst(QPDFObjectHandle const& key, QPDFObjectHandle const& value)
819
5.24k
{
820
5.24k
    auto iter = begin();
821
5.24k
    Array items(nullptr);
822
5.24k
    if (iter.node.isDictionary()) {
823
5.24k
        items = iter.node.getKey(details.itemsKey());
824
5.24k
    }
825
5.24k
    if (!items) {
826
0
        error(oh, "unable to find a valid items node");
827
0
    }
828
5.24k
    if (!(key && value)) {
829
0
        error(oh, "unable to insert null key or value");
830
0
    }
831
5.24k
    items.insert(0, key);
832
5.24k
    items.insert(1, value);
833
5.24k
    iter.setItemNumber(iter.node, 0);
834
5.24k
    iter.resetLimits(iter.node, iter.lastPathElement());
835
5.24k
    iter.split(iter.node, iter.lastPathElement());
836
5.24k
    return iter;
837
5.24k
}
838
839
NNTreeImpl::iterator
840
NNTreeImpl::insert(QPDFObjectHandle const& key, QPDFObjectHandle const& value)
841
138k
{
842
138k
    auto iter = find(key, true);
843
138k
    if (!iter.valid()) {
844
5.24k
        return insertFirst(key, value);
845
133k
    } else if (details.compareKeys(key, iter->first) == 0) {
846
88.7k
        Array items = iter.node.getKey(details.itemsKey());
847
88.7k
        items.set(iter.item_number + 1, value);
848
88.7k
        iter.updateIValue();
849
88.7k
    } else {
850
44.9k
        iter.insertAfter(key, value);
851
44.9k
    }
852
133k
    return iter;
853
138k
}
854
855
bool
856
NNTreeImpl::remove(QPDFObjectHandle const& key, QPDFObjectHandle* value)
857
0
{
858
0
    auto iter = find(key, false);
859
0
    if (!iter.valid()) {
860
0
        return false;
861
0
    }
862
0
    if (value) {
863
0
        *value = iter->second;
864
0
    }
865
0
    iter.remove();
866
0
    return true;
867
0
}