Coverage Report

Created: 2026-10-28 07:03

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/qpdf/libqpdf/QPDFTokenizer.cc
Line
Count
Source
1
#include <qpdf/QPDFTokenizer_private.hh>
2
3
// DO NOT USE ctype -- it is locale dependent for some things, and it's not worth the risk of
4
// including it in case it may accidentally be used.
5
6
#include <qpdf/InputSource_private.hh>
7
#include <qpdf/QIntC.hh>
8
#include <qpdf/QPDFExc.hh>
9
#include <qpdf/QPDFObjectHandle.hh>
10
#include <qpdf/QTC.hh>
11
#include <qpdf/QUtil.hh>
12
#include <qpdf/Util.hh>
13
14
#include <cstdlib>
15
#include <cstring>
16
#include <stdexcept>
17
18
using namespace qpdf;
19
20
using Token = QPDFTokenizer::Token;
21
using tt = QPDFTokenizer::token_type_e;
22
23
static inline bool
24
is_delimiter(char ch)
25
122M
{
26
122M
    return (
27
122M
        ch == ' ' || ch == '\n' || ch == '/' || ch == '(' || ch == ')' || ch == '{' || ch == '}' ||
28
109M
        ch == '<' || ch == '>' || ch == '[' || ch == ']' || ch == '%' || ch == '\t' || ch == '\r' ||
29
108M
        ch == '\v' || ch == '\f' || ch == 0);
30
122M
}
31
32
namespace
33
{
34
    class QPDFWordTokenFinder: public InputSource::Finder
35
    {
36
      public:
37
        QPDFWordTokenFinder(InputSource& is, std::string const& str) :
38
8.49k
            is(is),
39
8.49k
            str(str)
40
8.49k
        {
41
8.49k
        }
42
8.49k
        ~QPDFWordTokenFinder() override = default;
43
        bool check() override;
44
45
      private:
46
        InputSource& is;
47
        std::string str;
48
    };
49
} // namespace
50
51
bool
52
QPDFWordTokenFinder::check()
53
9.49k
{
54
    // Find a word token matching the given string, preceded by a delimiter, and followed by a
55
    // delimiter or EOF.
56
9.49k
    Tokenizer tokenizer;
57
9.49k
    tokenizer.nextToken(is, "finder", str.size() + 2);
58
9.49k
    qpdf_offset_t pos = is.tell();
59
9.49k
    if (tokenizer.getType() != tt::tt_word || tokenizer.getValue() != str) {
60
1.87k
        QTC::TC("qpdf", "QPDFTokenizer finder found wrong word");
61
1.87k
        return false;
62
1.87k
    }
63
7.62k
    qpdf_offset_t token_start = is.getLastOffset();
64
7.62k
    char next;
65
7.62k
    bool next_okay = false;
66
7.62k
    if (is.read(&next, 1) == 0) {
67
13
        QTC::TC("qpdf", "QPDFTokenizer inline image at EOF");
68
13
        next_okay = true;
69
7.60k
    } else {
70
7.60k
        next_okay = is_delimiter(next);
71
7.60k
    }
72
7.62k
    is.seek(pos, SEEK_SET);
73
7.62k
    if (!next_okay) {
74
0
        return false;
75
0
    }
76
7.62k
    if (token_start == 0) {
77
        // Can't actually happen...we never start the search at the beginning of the input.
78
0
        return false;
79
0
    }
80
7.62k
    return true;
81
7.62k
}
82
83
void
84
Tokenizer::reset()
85
30.4M
{
86
30.4M
    state = st_before_token;
87
30.4M
    type = tt::tt_bad;
88
30.4M
    val.clear();
89
30.4M
    raw_val.clear();
90
30.4M
    error_message = "";
91
30.4M
    before_token = true;
92
30.4M
    in_token = false;
93
30.4M
    char_to_unread = '\0';
94
30.4M
    inline_image_bytes = 0;
95
30.4M
    string_depth = 0;
96
30.4M
    bad = false;
97
30.4M
}
98
99
QPDFTokenizer::Token::Token(token_type_e type, std::string const& value) :
100
1.50k
    type(type),
101
1.50k
    value(value),
102
1.50k
    raw_value(value)
103
1.50k
{
104
1.50k
    if (type == tt_string) {
105
0
        raw_value = QPDFObjectHandle::newString(value).unparse();
106
1.50k
    } else if (type == tt_name) {
107
0
        raw_value = QPDFObjectHandle::newName(value).unparse();
108
0
    }
109
1.50k
}
110
111
QPDFTokenizer::QPDFTokenizer() :
112
3.35k
    m(std::make_unique<qpdf::Tokenizer>())
113
3.35k
{
114
3.35k
}
115
116
3.35k
QPDFTokenizer::~QPDFTokenizer() = default;
117
118
Tokenizer::Tokenizer()
119
46.9k
{
120
46.9k
    reset();
121
46.9k
}
122
123
void
124
QPDFTokenizer::allowEOF()
125
3.35k
{
126
3.35k
    m->allowEOF();
127
3.35k
}
128
129
void
130
Tokenizer::allowEOF()
131
29.7k
{
132
29.7k
    allow_eof = true;
133
29.7k
}
134
135
void
136
QPDFTokenizer::includeIgnorable()
137
3.35k
{
138
3.35k
    m->includeIgnorable();
139
3.35k
}
140
141
void
142
Tokenizer::includeIgnorable()
143
3.35k
{
144
3.35k
    include_ignorable = true;
145
3.35k
}
146
147
bool
148
Tokenizer::isSpace(char ch)
149
42.4M
{
150
42.4M
    return (ch == '\0' || util::is_space(ch));
151
42.4M
}
152
153
bool
154
Tokenizer::isDelimiter(char ch)
155
122M
{
156
122M
    return is_delimiter(ch);
157
122M
}
158
159
void
160
QPDFTokenizer::presentCharacter(char ch)
161
0
{
162
0
    m->presentCharacter(ch);
163
0
}
164
165
void
166
Tokenizer::presentCharacter(char ch)
167
21.1k
{
168
21.1k
    handleCharacter(ch);
169
170
21.1k
    if (in_token) {
171
0
        raw_val += ch;
172
0
    }
173
21.1k
}
174
175
void
176
Tokenizer::handleCharacter(char ch)
177
312M
{
178
    // In some cases, functions called below may call a second handler. This happens whenever you
179
    // have to use a character from the next token to detect the end of the current token.
180
181
312M
    switch (state) {
182
0
    case st_top:
183
0
        inTop(ch);
184
0
        return;
185
186
6.64M
    case st_in_space:
187
6.64M
        inSpace(ch);
188
6.64M
        return;
189
190
9.85M
    case st_in_comment:
191
9.85M
        inComment(ch);
192
9.85M
        return;
193
194
420k
    case st_lt:
195
420k
        inLt(ch);
196
420k
        return;
197
198
323k
    case st_gt:
199
323k
        inGt(ch);
200
323k
        return;
201
202
120M
    case st_in_string:
203
120M
        inString(ch);
204
120M
        return;
205
206
73.7M
    case st_name:
207
73.7M
        inName(ch);
208
73.7M
        return;
209
210
21.1M
    case st_number:
211
21.1M
        inNumber(ch);
212
21.1M
        return;
213
214
226k
    case st_real:
215
226k
        inReal(ch);
216
226k
        return;
217
218
97.1k
    case st_string_after_cr:
219
97.1k
        inStringAfterCR(ch);
220
97.1k
        return;
221
222
97.7k
    case st_string_escape:
223
97.7k
        inStringEscape(ch);
224
97.7k
        return;
225
226
5.11k
    case st_char_code:
227
5.11k
        inCharCode(ch);
228
5.11k
        return;
229
230
41.4M
    case st_literal:
231
41.4M
        inLiteral(ch);
232
41.4M
        return;
233
234
1.83M
    case st_inline_image:
235
1.83M
        inInlineImage(ch);
236
1.83M
        return;
237
238
115k
    case st_in_hexstring:
239
115k
        inHexstring(ch);
240
115k
        return;
241
242
102k
    case st_in_hexstring_2nd:
243
102k
        inHexstring2nd(ch);
244
102k
        return;
245
246
96.9k
    case st_name_hex1:
247
96.9k
        inNameHex1(ch);
248
96.9k
        return;
249
250
11.8k
    case st_name_hex2:
251
11.8k
        inNameHex2(ch);
252
11.8k
        return;
253
254
36.1k
    case st_sign:
255
36.1k
        inSign(ch);
256
36.1k
        return;
257
258
13.1k
    case st_decimal:
259
13.1k
        inDecimal(ch);
260
13.1k
        return;
261
262
35.6M
    case (st_before_token):
263
35.6M
        inBeforeToken(ch);
264
35.6M
        return;
265
266
0
    case (st_token_ready):
267
0
        inTokenReady(ch);
268
0
        return;
269
270
0
    default:
271
0
        throw std::logic_error("INTERNAL ERROR: invalid state while reading token");
272
312M
    }
273
312M
}
274
275
void
276
Tokenizer::inTokenReady(char ch)
277
0
{
278
0
    throw std::logic_error(
279
0
        "INTERNAL ERROR: QPDF tokenizer presented character while token is waiting");
280
0
}
281
282
void
283
Tokenizer::inBeforeToken(char ch)
284
35.6M
{
285
    // Note: we specifically do not use ctype here.  It is locale-dependent.
286
35.6M
    if (isSpace(ch)) {
287
17.4M
        before_token = !include_ignorable;
288
17.4M
        in_token = include_ignorable;
289
17.4M
        if (include_ignorable) {
290
2.87M
            state = st_in_space;
291
2.87M
        }
292
18.2M
    } else if (ch == '%') {
293
30.1k
        before_token = !include_ignorable;
294
30.1k
        in_token = include_ignorable;
295
30.1k
        state = st_in_comment;
296
18.1M
    } else {
297
18.1M
        before_token = false;
298
18.1M
        in_token = true;
299
18.1M
        inTop(ch);
300
18.1M
    }
301
35.6M
}
302
303
void
304
Tokenizer::inTop(char ch)
305
18.1M
{
306
18.1M
    switch (ch) {
307
82.4k
    case '(':
308
82.4k
        string_depth = 1;
309
82.4k
        state = st_in_string;
310
82.4k
        return;
311
312
420k
    case '<':
313
420k
        state = st_lt;
314
420k
        return;
315
316
324k
    case '>':
317
324k
        state = st_gt;
318
324k
        return;
319
320
107k
    case (')'):
321
107k
        type = tt::tt_bad;
322
107k
        QTC::TC("qpdf", "QPDFTokenizer bad )");
323
107k
        error_message = "unexpected )";
324
107k
        state = st_token_ready;
325
107k
        return;
326
327
329k
    case '[':
328
329k
        type = tt::tt_array_open;
329
329k
        state = st_token_ready;
330
329k
        return;
331
332
164k
    case ']':
333
164k
        type = tt::tt_array_close;
334
164k
        state = st_token_ready;
335
164k
        return;
336
337
72.7k
    case '{':
338
72.7k
        type = tt::tt_brace_open;
339
72.7k
        state = st_token_ready;
340
72.7k
        return;
341
342
90.0k
    case '}':
343
90.0k
        type = tt::tt_brace_close;
344
90.0k
        state = st_token_ready;
345
90.0k
        return;
346
347
4.85M
    case '/':
348
4.85M
        state = st_name;
349
4.85M
        val += ch;
350
4.85M
        return;
351
352
1.03M
    case '0':
353
3.52M
    case '1':
354
4.01M
    case '2':
355
4.48M
    case '3':
356
6.36M
    case '4':
357
6.54M
    case '5':
358
6.86M
    case '6':
359
6.94M
    case '7':
360
7.03M
    case '8':
361
7.09M
    case '9':
362
7.09M
        state = st_number;
363
7.09M
        return;
364
365
10.4k
    case '+':
366
36.1k
    case '-':
367
36.1k
        state = st_sign;
368
36.1k
        return;
369
370
12.7k
    case '.':
371
12.7k
        state = st_decimal;
372
12.7k
        return;
373
374
4.59M
    default:
375
4.59M
        state = st_literal;
376
4.59M
        return;
377
18.1M
    }
378
18.1M
}
379
380
void
381
Tokenizer::inSpace(char ch)
382
6.64M
{
383
    // We only enter this state if include_ignorable is true.
384
6.64M
    if (!isSpace(ch)) {
385
2.87M
        type = tt::tt_space;
386
2.87M
        in_token = false;
387
2.87M
        char_to_unread = ch;
388
2.87M
        state = st_token_ready;
389
2.87M
    }
390
6.64M
}
391
392
void
393
Tokenizer::inComment(char ch)
394
9.85M
{
395
9.85M
    if ((ch == '\r') || (ch == '\n')) {
396
28.8k
        if (include_ignorable) {
397
4.77k
            type = tt::tt_comment;
398
4.77k
            in_token = false;
399
4.77k
            char_to_unread = ch;
400
4.77k
            state = st_token_ready;
401
24.1k
        } else {
402
24.1k
            state = st_before_token;
403
24.1k
        }
404
28.8k
    }
405
9.85M
}
406
407
void
408
Tokenizer::inString(char ch)
409
120M
{
410
120M
    switch (ch) {
411
97.8k
    case '\\':
412
97.8k
        state = st_string_escape;
413
97.8k
        return;
414
415
169k
    case '(':
416
169k
        val += ch;
417
169k
        ++string_depth;
418
169k
        return;
419
420
184k
    case ')':
421
184k
        if (--string_depth == 0) {
422
75.9k
            type = tt::tt_string;
423
75.9k
            state = st_token_ready;
424
75.9k
            return;
425
75.9k
        }
426
427
108k
        val += ch;
428
108k
        return;
429
430
97.1k
    case '\r':
431
        // CR by itself is converted to LF
432
97.1k
        val += '\n';
433
97.1k
        state = st_string_after_cr;
434
97.1k
        return;
435
436
336k
    case '\n':
437
336k
        val += ch;
438
336k
        return;
439
440
119M
    default:
441
119M
        val += ch;
442
119M
        return;
443
120M
    }
444
120M
}
445
446
void
447
Tokenizer::inName(char ch)
448
73.7M
{
449
73.7M
    if (isDelimiter(ch)) {
450
        // A C-locale whitespace character or delimiter terminates token.  It is important to unread
451
        // the whitespace character even though it is ignored since it may be the newline after a
452
        // stream keyword.  Removing it here could make the stream-reading code break on some files,
453
        // though not on any files in the test suite as of this
454
        // writing.
455
456
4.82M
        type = bad ? tt::tt_bad : tt::tt_name;
457
4.82M
        in_token = false;
458
4.82M
        char_to_unread = ch;
459
4.82M
        state = st_token_ready;
460
68.9M
    } else if (ch == '#') {
461
96.9k
        char_code = 0;
462
96.9k
        state = st_name_hex1;
463
68.8M
    } else {
464
68.8M
        val += ch;
465
68.8M
    }
466
73.7M
}
467
468
void
469
Tokenizer::inNameHex1(char ch)
470
96.9k
{
471
96.9k
    hex_char = ch;
472
473
96.9k
    if (char hval = util::hex_decode_char(ch); hval < '\20') {
474
11.8k
        char_code = int(hval) << 4;
475
11.8k
        state = st_name_hex2;
476
85.0k
    } else {
477
85.0k
        QTC::TC("qpdf", "QPDFTokenizer bad name 1");
478
85.0k
        error_message = "name with stray # will not work with PDF >= 1.2";
479
        // Use null to encode a bad # -- this is reversed in QPDF_Name::normalizeName.
480
85.0k
        val += '\0';
481
85.0k
        state = st_name;
482
85.0k
        inName(ch);
483
85.0k
    }
484
96.9k
}
485
486
void
487
Tokenizer::inNameHex2(char ch)
488
11.8k
{
489
11.8k
    if (char hval = util::hex_decode_char(ch); hval < '\20') {
490
4.62k
        char_code |= int(hval);
491
7.20k
    } else {
492
7.20k
        QTC::TC("qpdf", "QPDFTokenizer bad name 2");
493
7.20k
        error_message = "name with stray # will not work with PDF >= 1.2";
494
        // Use null to encode a bad # -- this is reversed in QPDF_Name::normalizeName.
495
7.20k
        val += '\0';
496
7.20k
        val += hex_char;
497
7.20k
        state = st_name;
498
7.20k
        inName(ch);
499
7.20k
        return;
500
7.20k
    }
501
4.62k
    if (char_code == 0) {
502
999
        QTC::TC("qpdf", "QPDFTokenizer null in name");
503
999
        error_message = "null character not allowed in name token";
504
999
        val += "#00";
505
999
        state = st_name;
506
999
        bad = true;
507
3.62k
    } else {
508
3.62k
        val += char(char_code);
509
3.62k
        state = st_name;
510
3.62k
    }
511
4.62k
}
512
513
void
514
Tokenizer::inSign(char ch)
515
36.1k
{
516
36.1k
    if (util::is_digit(ch)) {
517
17.3k
        state = st_number;
518
18.7k
    } else if (ch == '.') {
519
379
        state = st_decimal;
520
18.4k
    } else {
521
18.4k
        state = st_literal;
522
18.4k
        inLiteral(ch);
523
18.4k
    }
524
36.1k
}
525
526
void
527
Tokenizer::inDecimal(char ch)
528
13.1k
{
529
13.1k
    if (util::is_digit(ch)) {
530
3.20k
        state = st_real;
531
9.90k
    } else {
532
9.90k
        state = st_literal;
533
9.90k
        inLiteral(ch);
534
9.90k
    }
535
13.1k
}
536
537
void
538
Tokenizer::inNumber(char ch)
539
21.1M
{
540
21.1M
    if (util::is_digit(ch)) {
541
14.0M
    } else if (ch == '.') {
542
60.0k
        state = st_real;
543
7.02M
    } else if (isDelimiter(ch)) {
544
6.82M
        type = tt::tt_integer;
545
6.82M
        state = st_token_ready;
546
6.82M
        in_token = false;
547
6.82M
        char_to_unread = ch;
548
6.82M
    } else {
549
193k
        state = st_literal;
550
193k
    }
551
21.1M
}
552
553
void
554
Tokenizer::inReal(char ch)
555
226k
{
556
226k
    if (util::is_digit(ch)) {
557
163k
    } else if (isDelimiter(ch)) {
558
61.1k
        type = tt::tt_real;
559
61.1k
        state = st_token_ready;
560
61.1k
        in_token = false;
561
61.1k
        char_to_unread = ch;
562
61.1k
    } else {
563
2.03k
        state = st_literal;
564
2.03k
    }
565
226k
}
566
void
567
Tokenizer::inStringEscape(char ch)
568
97.7k
{
569
97.7k
    state = st_in_string;
570
97.7k
    switch (ch) {
571
718
    case '0':
572
1.14k
    case '1':
573
1.67k
    case '2':
574
2.08k
    case '3':
575
2.86k
    case '4':
576
3.39k
    case '5':
577
3.70k
    case '6':
578
3.98k
    case '7':
579
3.98k
        state = st_char_code;
580
3.98k
        char_code = 0;
581
3.98k
        digit_count = 0;
582
3.98k
        inCharCode(ch);
583
3.98k
        return;
584
585
2.44k
    case 'n':
586
2.44k
        val += '\n';
587
2.44k
        return;
588
589
7.62k
    case 'r':
590
7.62k
        val += '\r';
591
7.62k
        return;
592
593
15.2k
    case 't':
594
15.2k
        val += '\t';
595
15.2k
        return;
596
597
852
    case 'b':
598
852
        val += '\b';
599
852
        return;
600
601
12.4k
    case 'f':
602
12.4k
        val += '\f';
603
12.4k
        return;
604
605
290
    case '\n':
606
290
        return;
607
608
226
    case '\r':
609
226
        state = st_string_after_cr;
610
226
        return;
611
612
54.5k
    default:
613
        // PDF spec says backslash is ignored before anything else
614
54.5k
        val += ch;
615
54.5k
        return;
616
97.7k
    }
617
97.7k
}
618
619
void
620
Tokenizer::inStringAfterCR(char ch)
621
97.1k
{
622
97.1k
    state = st_in_string;
623
97.1k
    if (ch != '\n') {
624
77.1k
        inString(ch);
625
77.1k
    }
626
97.1k
}
627
628
void
629
Tokenizer::inLt(char ch)
630
420k
{
631
420k
    if (ch == '<') {
632
343k
        type = tt::tt_dict_open;
633
343k
        state = st_token_ready;
634
343k
        return;
635
343k
    }
636
637
76.7k
    state = st_in_hexstring;
638
76.7k
    inHexstring(ch);
639
76.7k
}
640
641
void
642
Tokenizer::inGt(char ch)
643
323k
{
644
323k
    if (ch == '>') {
645
270k
        type = tt::tt_dict_close;
646
270k
        state = st_token_ready;
647
270k
    } else {
648
52.6k
        type = tt::tt_bad;
649
52.6k
        QTC::TC("qpdf", "QPDFTokenizer bad >");
650
52.6k
        error_message = "unexpected >";
651
52.6k
        in_token = false;
652
52.6k
        char_to_unread = ch;
653
52.6k
        state = st_token_ready;
654
52.6k
    }
655
323k
}
656
657
void
658
Tokenizer::inLiteral(char ch)
659
41.5M
{
660
41.5M
    if (isDelimiter(ch)) {
661
        // A C-locale whitespace character or delimiter terminates token.  It is important to unread
662
        // the whitespace character even though it is ignored since it may be the newline after a
663
        // stream keyword.  Removing it here could make the stream-reading code break on some files,
664
        // though not on any files in the test suite as of this writing.
665
666
4.65M
        in_token = false;
667
4.65M
        char_to_unread = ch;
668
4.65M
        state = st_token_ready;
669
4.65M
        type = (raw_val == "true") || (raw_val == "false")
670
4.65M
            ? tt::tt_bool
671
4.65M
            : (raw_val == "null" ? tt::tt_null : tt::tt_word);
672
4.65M
    }
673
41.5M
}
674
675
void
676
Tokenizer::inHexstring(char ch)
677
191k
{
678
191k
    if (char hval = util::hex_decode_char(ch); hval < '\20') {
679
94.2k
        char_code = int(hval) << 4;
680
94.2k
        state = st_in_hexstring_2nd;
681
682
97.7k
    } else if (ch == '>') {
683
9.77k
        type = tt::tt_string;
684
9.77k
        state = st_token_ready;
685
686
87.9k
    } else if (isSpace(ch)) {
687
        // ignore
688
689
56.6k
    } else {
690
56.6k
        type = tt::tt_bad;
691
56.6k
        QTC::TC("qpdf", "QPDFTokenizer bad hexstring character");
692
56.6k
        error_message = std::string("invalid character (") + ch + ") in hexstring";
693
56.6k
        state = st_token_ready;
694
56.6k
    }
695
191k
}
696
697
void
698
Tokenizer::inHexstring2nd(char ch)
699
102k
{
700
102k
    if (char hval = util::hex_decode_char(ch); hval < '\20') {
701
84.1k
        val += char(char_code) | hval;
702
84.1k
        state = st_in_hexstring;
703
704
84.1k
    } else if (ch == '>') {
705
        // PDF spec says odd hexstrings have implicit trailing 0.
706
3.49k
        val += char(char_code);
707
3.49k
        type = tt::tt_string;
708
3.49k
        state = st_token_ready;
709
710
14.8k
    } else if (isSpace(ch)) {
711
        // ignore
712
713
8.50k
    } else {
714
6.30k
        type = tt::tt_bad;
715
6.30k
        QTC::TC("qpdf", "QPDFTokenizer bad hexstring 2nd character");
716
6.30k
        error_message = std::string("invalid character (") + ch + ") in hexstring";
717
6.30k
        state = st_token_ready;
718
6.30k
    }
719
102k
}
720
721
void
722
Tokenizer::inCharCode(char ch)
723
9.09k
{
724
9.09k
    bool handled = false;
725
9.09k
    if (('0' <= ch) && (ch <= '7')) {
726
6.09k
        char_code = 8 * char_code + (int(ch) - int('0'));
727
6.09k
        if (++(digit_count) < 3) {
728
5.24k
            return;
729
5.24k
        }
730
849
        handled = true;
731
849
    }
732
    // We've accumulated \ddd or we have \d or \dd followed by other than an octal digit. The PDF
733
    // Spec says to ignore high-order overflow.
734
3.84k
    val += char(char_code % 256);
735
3.84k
    state = st_in_string;
736
3.84k
    if (!handled) {
737
2.99k
        inString(ch);
738
2.99k
    }
739
3.84k
}
740
741
void
742
Tokenizer::inInlineImage(char ch)
743
1.83M
{
744
1.83M
    if ((raw_val.length() + 1) == inline_image_bytes) {
745
1.42k
        QTC::TC("qpdf", "QPDFTokenizer found EI by byte count");
746
1.42k
        type = tt::tt_inline_image;
747
1.42k
        inline_image_bytes = 0;
748
1.42k
        state = st_token_ready;
749
1.42k
    }
750
1.83M
}
751
752
void
753
QPDFTokenizer::presentEOF()
754
0
{
755
0
    m->presentEOF();
756
0
}
757
758
void
759
Tokenizer::presentEOF()
760
49.3k
{
761
49.3k
    switch (state) {
762
3.21k
    case st_name:
763
3.41k
    case st_name_hex1:
764
3.56k
    case st_name_hex2:
765
10.6k
    case st_number:
766
10.9k
    case st_real:
767
11.1k
    case st_sign:
768
11.3k
    case st_decimal:
769
21.1k
    case st_literal:
770
21.1k
        QTC::TC("qpdf", "QPDFTokenizer EOF reading appendable token");
771
        // Push any delimiter to the state machine to finish off the final token.
772
21.1k
        presentCharacter('\f');
773
21.1k
        in_token = true;
774
21.1k
        break;
775
776
0
    case st_top:
777
21.4k
    case st_before_token:
778
21.4k
        type = tt::tt_eof;
779
21.4k
        break;
780
781
1.50k
    case st_in_space:
782
1.50k
        type = include_ignorable ? tt::tt_space : tt::tt_eof;
783
1.50k
        break;
784
785
1.23k
    case st_in_comment:
786
1.23k
        type = include_ignorable ? tt::tt_comment : tt::tt_bad;
787
1.23k
        break;
788
789
0
    case st_token_ready:
790
0
        break;
791
792
4.06k
    default:
793
4.06k
        QTC::TC("qpdf", "QPDFTokenizer EOF reading token");
794
4.06k
        type = tt::tt_bad;
795
4.06k
        error_message = "EOF while reading token";
796
49.3k
    }
797
49.3k
    state = st_token_ready;
798
49.3k
}
799
800
void
801
QPDFTokenizer::expectInlineImage(std::shared_ptr<InputSource> input)
802
0
{
803
0
    m->expectInlineImage(*input);
804
0
}
805
806
void
807
QPDFTokenizer::expectInlineImage(InputSource& input)
808
1.50k
{
809
1.50k
    m->expectInlineImage(input);
810
1.50k
}
811
812
void
813
Tokenizer::expectInlineImage(InputSource& input)
814
1.50k
{
815
1.50k
    if (state == st_token_ready) {
816
0
        reset();
817
1.50k
    } else if (state != st_before_token) {
818
0
        throw std::logic_error(
819
0
            "QPDFTokenizer::expectInlineImage called when tokenizer is in improper state");
820
0
    }
821
1.50k
    findEI(input);
822
1.50k
    before_token = false;
823
1.50k
    in_token = true;
824
1.50k
    state = st_inline_image;
825
1.50k
}
826
827
void
828
Tokenizer::findEI(InputSource& input)
829
1.50k
{
830
1.50k
    qpdf_offset_t last_offset = input.getLastOffset();
831
1.50k
    qpdf_offset_t pos = input.tell();
832
833
    // Use QPDFWordTokenFinder to find EI surrounded by delimiters. Then read the next several
834
    // tokens or up to EOF. If we find any suspicious-looking or tokens, this is probably still part
835
    // of the image data, so keep looking for EI. Stop at the first EI that passes. If we get to the
836
    // end without finding one, return the last EI we found. Store the number of bytes expected in
837
    // the inline image including the EI and use that to break out of inline image, falling back to
838
    // the old method if needed.
839
840
1.50k
    bool okay = false;
841
1.50k
    bool first_try = true;
842
9.12k
    while (!okay) {
843
8.49k
        QPDFWordTokenFinder f(input, "EI");
844
8.49k
        if (!input.findFirst("EI", input.tell(), 0, f)) {
845
878
            break;
846
878
        }
847
7.62k
        inline_image_bytes = QIntC::to_size(input.tell() - pos - 2);
848
849
7.62k
        Tokenizer check;
850
7.62k
        bool found_bad = false;
851
        // Look at the next 10 tokens or up to EOF. The next inline image's image data would look
852
        // like bad tokens, but there will always be at least 10 tokens between one inline image's
853
        // EI and the next valid one's ID since width, height, bits per pixel, and color space are
854
        // all required as well as a BI and ID. If we get 10 good tokens in a row or hit EOF, we can
855
        // be pretty sure we've found the actual EI.
856
20.3k
        for (int i = 0; i < 10; ++i) {
857
19.6k
            check.nextToken(input, "checker");
858
19.6k
            auto typ = check.getType();
859
19.6k
            if (typ == tt::tt_eof) {
860
0
                okay = true;
861
19.6k
            } else if (typ == tt::tt_bad) {
862
2.13k
                found_bad = true;
863
17.5k
            } else if (typ == tt::tt_word) {
864
                // The qpdf tokenizer lumps alphabetic and otherwise uncategorized characters into
865
                // "words". We recognize strings of alphabetic characters as potential valid
866
                // operators for purposes of telling whether we're in valid content or not. It's not
867
                // perfect, but it should work more reliably than what we used to do, which was
868
                // already good enough for the vast majority of files.
869
11.2k
                bool found_alpha = false;
870
11.2k
                bool found_non_printable = false;
871
11.2k
                bool found_other = false;
872
30.3k
                for (char ch: check.getValue()) {
873
30.3k
                    if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch == '*')) {
874
                        // Treat '*' as alpha since there are valid PDF operators that contain *
875
                        // along with alphabetic characters.
876
16.0k
                        found_alpha = true;
877
16.0k
                    } else if (static_cast<signed char>(ch) < 32 && !isSpace(ch)) {
878
                        // Compare ch as a signed char so characters outside of 7-bit will be < 0.
879
2.87k
                        found_non_printable = true;
880
2.87k
                        break;
881
11.4k
                    } else {
882
11.4k
                        found_other = true;
883
11.4k
                    }
884
30.3k
                }
885
11.2k
                if (found_non_printable || (found_alpha && found_other)) {
886
4.85k
                    found_bad = true;
887
4.85k
                }
888
11.2k
            }
889
19.6k
            if (okay || found_bad) {
890
6.99k
                break;
891
6.99k
            }
892
19.6k
        }
893
7.62k
        if (!found_bad) {
894
627
            okay = true;
895
627
        }
896
7.62k
        if (!okay) {
897
6.99k
            first_try = false;
898
6.99k
        }
899
7.62k
    }
900
1.50k
    if (okay && (!first_try)) {
901
45
        QTC::TC("qpdf", "QPDFTokenizer found EI after more than one try");
902
45
    }
903
904
1.50k
    input.seek(pos, SEEK_SET);
905
1.50k
    input.setLastOffset(last_offset);
906
1.50k
}
907
908
bool
909
QPDFTokenizer::getToken(Token& token, bool& unread_char, char& ch)
910
0
{
911
0
    return m->getToken(token, unread_char, ch);
912
0
}
913
914
bool
915
Tokenizer::getToken(Token& token, bool& unread_char, char& ch)
916
9.26M
{
917
9.26M
    bool ready = (state == st_token_ready);
918
9.26M
    unread_char = !in_token && !before_token;
919
9.26M
    ch = char_to_unread;
920
9.26M
    if (ready) {
921
9.26M
        token = (!(type == tt::tt_name || type == tt::tt_string))
922
9.26M
            ? Token(type, raw_val, raw_val, error_message)
923
9.26M
            : Token(type, val, raw_val, error_message);
924
925
9.26M
        reset();
926
9.26M
    }
927
9.26M
    return ready;
928
9.26M
}
929
930
bool
931
QPDFTokenizer::betweenTokens()
932
0
{
933
0
    return m->betweenTokens();
934
0
}
935
936
bool
937
Tokenizer::betweenTokens()
938
0
{
939
0
    return before_token;
940
0
}
941
942
QPDFTokenizer::Token
943
QPDFTokenizer::readToken(
944
    InputSource& input, std::string const& context, bool allow_bad, size_t max_len)
945
6.43M
{
946
6.43M
    return m->readToken(input, context, allow_bad, max_len);
947
6.43M
}
948
949
QPDFTokenizer::Token
950
QPDFTokenizer::readToken(
951
    std::shared_ptr<InputSource> input, std::string const& context, bool allow_bad, size_t max_len)
952
0
{
953
0
    return m->readToken(*input, context, allow_bad, max_len);
954
0
}
955
956
QPDFTokenizer::Token
957
Tokenizer::readToken(InputSource& input, std::string const& context, bool allow_bad, size_t max_len)
958
9.26M
{
959
9.26M
    nextToken(input, context, max_len);
960
961
9.26M
    Token token;
962
9.26M
    bool unread_char;
963
9.26M
    char char_to_unread;
964
9.26M
    getToken(token, unread_char, char_to_unread);
965
966
9.26M
    if (token.getType() == tt::tt_bad) {
967
427k
        if (allow_bad) {
968
427k
            QTC::TC("qpdf", "QPDFTokenizer allowing bad token");
969
427k
        } else {
970
0
            throw QPDFExc(
971
0
                qpdf_e_damaged_pdf,
972
0
                input.getName(),
973
0
                context.empty() ? "offset " + std::to_string(input.getLastOffset()) : context,
974
0
                input.getLastOffset(),
975
0
                token.getErrorMessage());
976
0
        }
977
427k
    }
978
9.26M
    return token;
979
9.26M
}
980
981
bool
982
Tokenizer::nextToken(InputSource& input, std::string const& context, size_t max_len)
983
21.0M
{
984
21.0M
    if (state != st_inline_image) {
985
21.0M
        reset();
986
21.0M
    }
987
21.0M
    qpdf_offset_t offset = input.fastTell();
988
989
333M
    while (state != st_token_ready) {
990
312M
        char ch;
991
312M
        if (!input.fastRead(ch)) {
992
49.3k
            presentEOF();
993
994
49.3k
            if ((type == tt::tt_eof) && (!allow_eof)) {
995
                // Nothing in the qpdf library calls readToken without allowEOF anymore, so this
996
                // case is not exercised.
997
362
                type = tt::tt_bad;
998
362
                error_message = "unexpected EOF";
999
362
                offset = input.getLastOffset();
1000
362
            }
1001
312M
        } else {
1002
312M
            handleCharacter(ch);
1003
312M
            if (before_token) {
1004
17.7M
                ++offset;
1005
17.7M
            }
1006
312M
            if (in_token) {
1007
275M
                raw_val += ch;
1008
275M
            }
1009
312M
            if (max_len && (raw_val.length() >= max_len) && (state != st_token_ready)) {
1010
                // terminate this token now
1011
235k
                QTC::TC("qpdf", "QPDFTokenizer block long token");
1012
235k
                type = tt::tt_bad;
1013
235k
                state = st_token_ready;
1014
235k
                error_message = "exceeded allowable length while reading token";
1015
235k
            }
1016
312M
        }
1017
312M
    }
1018
1019
21.0M
    input.fastUnread(!in_token && !before_token);
1020
1021
21.0M
    if (type != tt::tt_eof) {
1022
21.0M
        input.setLastOffset(offset);
1023
21.0M
    }
1024
1025
21.0M
    return error_message.empty();
1026
21.0M
}