File Coverage

src/pdfmake_tokenizer.c
Criterion Covered Total %
statement 245 262 93.5
branch 159 200 79.5
condition n/a
subroutine n/a
pod n/a
total 404 462 87.4


line stmt bran cond sub pod time code
1             /*
2             * pdfmake_tokenizer.c — PDF lexical tokenizer per §7.2
3             */
4              
5             #include "pdfmake_tokenizer.h"
6             #include
7             #include
8             #include
9              
10             /*============================================================================
11             * Character classification table (§7.2.2)
12             *
13             * PDF whitespace: NUL(0), TAB(9), LF(10), FF(12), CR(13), SPACE(32)
14             * PDF delimiters: ( ) < > [ ] { } / %
15             *==========================================================================*/
16              
17             const uint8_t pdfmake_char_class[256] = {
18             /* 0x00-0x0F */
19             PDFMAKE_CC_WHITESPACE, /* NUL */
20             PDFMAKE_CC_REGULAR, /* SOH */
21             PDFMAKE_CC_REGULAR, /* STX */
22             PDFMAKE_CC_REGULAR, /* ETX */
23             PDFMAKE_CC_REGULAR, /* EOT */
24             PDFMAKE_CC_REGULAR, /* ENQ */
25             PDFMAKE_CC_REGULAR, /* ACK */
26             PDFMAKE_CC_REGULAR, /* BEL */
27             PDFMAKE_CC_REGULAR, /* BS */
28             PDFMAKE_CC_WHITESPACE, /* TAB */
29             PDFMAKE_CC_WHITESPACE, /* LF */
30             PDFMAKE_CC_REGULAR, /* VT */
31             PDFMAKE_CC_WHITESPACE, /* FF */
32             PDFMAKE_CC_WHITESPACE, /* CR */
33             PDFMAKE_CC_REGULAR, /* SO */
34             PDFMAKE_CC_REGULAR, /* SI */
35              
36             /* 0x10-0x1F */
37             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
38             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
39             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
40             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
41              
42             /* 0x20-0x2F: SPACE ! " # $ % & ' ( ) * + , - . / */
43             PDFMAKE_CC_WHITESPACE, /* SPACE */
44             PDFMAKE_CC_REGULAR, /* ! */
45             PDFMAKE_CC_REGULAR, /* " */
46             PDFMAKE_CC_REGULAR, /* # */
47             PDFMAKE_CC_REGULAR, /* $ */
48             PDFMAKE_CC_DELIMITER, /* % comment start */
49             PDFMAKE_CC_REGULAR, /* & */
50             PDFMAKE_CC_REGULAR, /* ' */
51             PDFMAKE_CC_DELIMITER, /* ( */
52             PDFMAKE_CC_DELIMITER, /* ) */
53             PDFMAKE_CC_REGULAR, /* * */
54             PDFMAKE_CC_SIGN, /* + */
55             PDFMAKE_CC_REGULAR, /* , */
56             PDFMAKE_CC_SIGN, /* - */
57             PDFMAKE_CC_REGULAR, /* . */
58             PDFMAKE_CC_DELIMITER, /* / name start */
59              
60             /* 0x30-0x3F: 0-9 : ; < = > ? */
61             PDFMAKE_CC_DIGIT | PDFMAKE_CC_HEX, /* 0 */
62             PDFMAKE_CC_DIGIT | PDFMAKE_CC_HEX, /* 1 */
63             PDFMAKE_CC_DIGIT | PDFMAKE_CC_HEX, /* 2 */
64             PDFMAKE_CC_DIGIT | PDFMAKE_CC_HEX, /* 3 */
65             PDFMAKE_CC_DIGIT | PDFMAKE_CC_HEX, /* 4 */
66             PDFMAKE_CC_DIGIT | PDFMAKE_CC_HEX, /* 5 */
67             PDFMAKE_CC_DIGIT | PDFMAKE_CC_HEX, /* 6 */
68             PDFMAKE_CC_DIGIT | PDFMAKE_CC_HEX, /* 7 */
69             PDFMAKE_CC_DIGIT | PDFMAKE_CC_HEX, /* 8 */
70             PDFMAKE_CC_DIGIT | PDFMAKE_CC_HEX, /* 9 */
71             PDFMAKE_CC_REGULAR, /* : */
72             PDFMAKE_CC_REGULAR, /* ; */
73             PDFMAKE_CC_DELIMITER, /* < */
74             PDFMAKE_CC_REGULAR, /* = */
75             PDFMAKE_CC_DELIMITER, /* > */
76             PDFMAKE_CC_REGULAR, /* ? */
77              
78             /* 0x40-0x4F: @ A-O */
79             PDFMAKE_CC_REGULAR, /* @ */
80             PDFMAKE_CC_ALPHA | PDFMAKE_CC_HEX, /* A */
81             PDFMAKE_CC_ALPHA | PDFMAKE_CC_HEX, /* B */
82             PDFMAKE_CC_ALPHA | PDFMAKE_CC_HEX, /* C */
83             PDFMAKE_CC_ALPHA | PDFMAKE_CC_HEX, /* D */
84             PDFMAKE_CC_ALPHA | PDFMAKE_CC_HEX, /* E */
85             PDFMAKE_CC_ALPHA | PDFMAKE_CC_HEX, /* F */
86             PDFMAKE_CC_ALPHA, /* G */
87             PDFMAKE_CC_ALPHA, /* H */
88             PDFMAKE_CC_ALPHA, /* I */
89             PDFMAKE_CC_ALPHA, /* J */
90             PDFMAKE_CC_ALPHA, /* K */
91             PDFMAKE_CC_ALPHA, /* L */
92             PDFMAKE_CC_ALPHA, /* M */
93             PDFMAKE_CC_ALPHA, /* N */
94             PDFMAKE_CC_ALPHA, /* O */
95              
96             /* 0x50-0x5F: P-Z [ \ ] ^ _ */
97             PDFMAKE_CC_ALPHA, /* P */
98             PDFMAKE_CC_ALPHA, /* Q */
99             PDFMAKE_CC_ALPHA, /* R */
100             PDFMAKE_CC_ALPHA, /* S */
101             PDFMAKE_CC_ALPHA, /* T */
102             PDFMAKE_CC_ALPHA, /* U */
103             PDFMAKE_CC_ALPHA, /* V */
104             PDFMAKE_CC_ALPHA, /* W */
105             PDFMAKE_CC_ALPHA, /* X */
106             PDFMAKE_CC_ALPHA, /* Y */
107             PDFMAKE_CC_ALPHA, /* Z */
108             PDFMAKE_CC_DELIMITER, /* [ */
109             PDFMAKE_CC_REGULAR, /* \ */
110             PDFMAKE_CC_DELIMITER, /* ] */
111             PDFMAKE_CC_REGULAR, /* ^ */
112             PDFMAKE_CC_REGULAR, /* _ */
113              
114             /* 0x60-0x6F: ` a-o */
115             PDFMAKE_CC_REGULAR, /* ` */
116             PDFMAKE_CC_ALPHA | PDFMAKE_CC_HEX, /* a */
117             PDFMAKE_CC_ALPHA | PDFMAKE_CC_HEX, /* b */
118             PDFMAKE_CC_ALPHA | PDFMAKE_CC_HEX, /* c */
119             PDFMAKE_CC_ALPHA | PDFMAKE_CC_HEX, /* d */
120             PDFMAKE_CC_ALPHA | PDFMAKE_CC_HEX, /* e */
121             PDFMAKE_CC_ALPHA | PDFMAKE_CC_HEX, /* f */
122             PDFMAKE_CC_ALPHA, /* g */
123             PDFMAKE_CC_ALPHA, /* h */
124             PDFMAKE_CC_ALPHA, /* i */
125             PDFMAKE_CC_ALPHA, /* j */
126             PDFMAKE_CC_ALPHA, /* k */
127             PDFMAKE_CC_ALPHA, /* l */
128             PDFMAKE_CC_ALPHA, /* m */
129             PDFMAKE_CC_ALPHA, /* n */
130             PDFMAKE_CC_ALPHA, /* o */
131              
132             /* 0x70-0x7F: p-z { | } ~ DEL */
133             PDFMAKE_CC_ALPHA, /* p */
134             PDFMAKE_CC_ALPHA, /* q */
135             PDFMAKE_CC_ALPHA, /* r */
136             PDFMAKE_CC_ALPHA, /* s */
137             PDFMAKE_CC_ALPHA, /* t */
138             PDFMAKE_CC_ALPHA, /* u */
139             PDFMAKE_CC_ALPHA, /* v */
140             PDFMAKE_CC_ALPHA, /* w */
141             PDFMAKE_CC_ALPHA, /* x */
142             PDFMAKE_CC_ALPHA, /* y */
143             PDFMAKE_CC_ALPHA, /* z */
144             PDFMAKE_CC_DELIMITER, /* { */
145             PDFMAKE_CC_REGULAR, /* | */
146             PDFMAKE_CC_DELIMITER, /* } */
147             PDFMAKE_CC_REGULAR, /* ~ */
148             PDFMAKE_CC_REGULAR, /* DEL */
149              
150             /* 0x80-0xFF: High bytes - all regular in PDF */
151             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
152             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
153             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
154             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
155             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
156             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
157             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
158             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
159             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
160             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
161             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
162             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
163             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
164             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
165             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
166             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
167             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
168             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
169             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
170             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
171             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
172             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
173             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
174             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
175             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
176             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
177             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
178             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
179             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
180             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
181             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
182             PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR, PDFMAKE_CC_REGULAR,
183             };
184              
185             /*============================================================================
186             * Token kind names (for debugging)
187             *==========================================================================*/
188              
189             static const char *tok_kind_names[] = {
190             "EOF", "WS", "COMMENT",
191             "INT", "REAL", "NAME", "LSTR", "HSTR",
192             "ARR_OPEN", "ARR_CLOSE", "DICT_OPEN", "DICT_CLOSE",
193             "KW_NULL", "KW_TRUE", "KW_FALSE",
194             "KW_OBJ", "KW_ENDOBJ", "KW_STREAM", "KW_ENDSTREAM",
195             "KW_R", "KW_XREF", "KW_TRAILER", "KW_STARTXREF",
196             "ERROR"
197             };
198              
199 0           const char *pdfmake_tok_kind_name(pdfmake_tok_kind_t kind) {
200 0 0         if (kind >= 0 && kind <= PDFMAKE_TOK_ERROR) {
201 0           return tok_kind_names[kind];
202             }
203 0           return "UNKNOWN";
204             }
205              
206             /*============================================================================
207             * Tokenizer initialization
208             *==========================================================================*/
209              
210 1661           void pdfmake_tokenizer_init(pdfmake_tokenizer_t *t,
211             const uint8_t *buf, size_t len) {
212 1661           t->buf = buf;
213 1661           t->pos = 0;
214 1661           t->len = len;
215 1661           t->has_peeked = 0;
216 1661           }
217              
218 0           void pdfmake_tokenizer_seek(pdfmake_tokenizer_t *t, size_t pos) {
219 0           t->pos = pos < t->len ? pos : t->len;
220 0           t->has_peeked = 0; /* Invalidate peek cache */
221 0           }
222              
223             /*============================================================================
224             * Whitespace lexer (§7.2.2)
225             *==========================================================================*/
226              
227 64838           static pdfmake_tok_t lex_whitespace(pdfmake_tokenizer_t *t) {
228             pdfmake_tok_t tok;
229 64838           tok.kind = PDFMAKE_TOK_WS;
230 64838           tok.offset = t->pos;
231              
232 141044 100         while (t->pos < t->len && pdfmake_is_whitespace(t->buf[t->pos])) {
    100          
233 76206           t->pos++;
234             }
235              
236 64838           tok.length = t->pos - tok.offset;
237 64838           return tok;
238             }
239              
240             /*============================================================================
241             * Comment lexer (§7.2.3)
242             * % through end of line (LF or CR or CRLF)
243             *==========================================================================*/
244              
245 299           static pdfmake_tok_t lex_comment(pdfmake_tokenizer_t *t) {
246             pdfmake_tok_t tok;
247 299           tok.kind = PDFMAKE_TOK_COMMENT;
248 299           tok.offset = t->pos;
249              
250 299           t->pos++; /* Skip % */
251              
252 10476 100         while (t->pos < t->len) {
253 10466           uint8_t c = t->buf[t->pos];
254 10466 100         if (c == '\n') {
255 113           t->pos++;
256 113           break;
257             }
258 10353 100         if (c == '\r') {
259 176           t->pos++;
260 176 50         if (t->pos < t->len && t->buf[t->pos] == '\n') {
    100          
261 32           t->pos++;
262             }
263 176           break;
264             }
265 10177           t->pos++;
266             }
267              
268 299           tok.length = t->pos - tok.offset;
269 299           return tok;
270             }
271              
272             /*============================================================================
273             * Number lexer (§7.3.3)
274             * Integers: [+-]?\d+
275             * Reals: [+-]?\d*\.\d* (at least one digit somewhere)
276             * No scientific notation in PDF.
277             *==========================================================================*/
278              
279 38163           static pdfmake_tok_t lex_number(pdfmake_tokenizer_t *t) {
280             pdfmake_tok_t tok;
281 38163           int is_real = 0;
282 38163           int has_digits = 0;
283 38163           int is_negative = 0;
284 38163           double val = 0.0;
285 38163           double frac = 0.0;
286 38163           double frac_div = 1.0;
287 38163           int in_frac = 0;
288 38163           int64_t ival = 0;
289             size_t i;
290              
291 38163           tok.offset = t->pos;
292              
293             /* Optional sign */
294 38163 50         if (t->pos < t->len && (t->buf[t->pos] == '+' || t->buf[t->pos] == '-')) {
    50          
    100          
295 2422 50         if (t->buf[t->pos] == '-') is_negative = 1;
296 2422           t->pos++;
297             }
298              
299             /* Digits before decimal point */
300 154242 50         while (t->pos < t->len && pdfmake_is_digit(t->buf[t->pos])) {
    100          
301 116079           has_digits = 1;
302 116079           t->pos++;
303             }
304              
305             /* Decimal point */
306 38163 50         if (t->pos < t->len && t->buf[t->pos] == '.') {
    100          
307 4799           is_real = 1;
308 4799           t->pos++;
309              
310             /* Digits after decimal point */
311 15633 50         while (t->pos < t->len && pdfmake_is_digit(t->buf[t->pos])) {
    100          
312 10834           has_digits = 1;
313 10834           t->pos++;
314             }
315             }
316              
317 38163           tok.length = t->pos - tok.offset;
318              
319 38163 50         if (!has_digits) {
320             /* Just a sign or just a dot — error */
321 0           tok.kind = PDFMAKE_TOK_ERROR;
322 0           return tok;
323             }
324              
325             /* Parse the number */
326 38163 100         if (is_real) {
327 4799           tok.kind = PDFMAKE_TOK_REAL;
328             /* Parse directly — no need for strtod locale issues */
329 30561 100         for (i = tok.offset; i < tok.offset + tok.length; i++) {
330 25762           uint8_t c = t->buf[i];
331 25762 50         if (c == '+' || c == '-') continue;
    100          
332 23644 100         if (c == '.') {
333 4799           in_frac = 1;
334 4799           continue;
335             }
336 18845 100         if (in_frac) {
337 10834           frac_div *= 10.0;
338 10834           frac += (c - '0') / frac_div;
339             } else {
340 8011           val = val * 10.0 + (c - '0');
341             }
342             }
343 4799 100         tok.payload.real_val = is_negative ? -(val + frac) : (val + frac);
344             } else {
345 33364           tok.kind = PDFMAKE_TOK_INT;
346 141736 100         for (i = tok.offset; i < tok.offset + tok.length; i++) {
347 108372           uint8_t c = t->buf[i];
348 108372 50         if (c == '+' || c == '-') continue;
    100          
349 108068           ival = ival * 10 + (c - '0');
350             }
351 33364 100         tok.payload.int_val = is_negative ? -ival : ival;
352             }
353              
354 38163           return tok;
355             }
356              
357             /*============================================================================
358             * Name lexer (§7.3.5)
359             * /Name with #XX hex escape
360             *==========================================================================*/
361              
362 10866           static pdfmake_tok_t lex_name(pdfmake_tokenizer_t *t) {
363             pdfmake_tok_t tok;
364 10866           tok.kind = PDFMAKE_TOK_NAME;
365 10866           tok.offset = t->pos;
366              
367 10866           t->pos++; /* Skip / */
368              
369             /* Name continues until whitespace or delimiter */
370 73121 50         while (t->pos < t->len) {
371 73121           uint8_t c = t->buf[t->pos];
372 73121 100         if (pdfmake_is_whitespace(c) || pdfmake_is_delimiter(c)) {
    100          
373             break;
374             }
375 62255 100         if (c == '#') {
376             /* Hex escape: skip #XX */
377 66           if (t->pos + 2 < t->len &&
378 34 50         pdfmake_is_hex(t->buf[t->pos + 1]) &&
379 1           pdfmake_is_hex(t->buf[t->pos + 2])) {
380 1           t->pos += 3;
381             } else {
382             /* Invalid escape — treat # as regular char */
383 32           t->pos++;
384             }
385             } else {
386 62222           t->pos++;
387             }
388             }
389              
390 10866           tok.length = t->pos - tok.offset;
391 10866           return tok;
392             }
393              
394             /*============================================================================
395             * Literal string lexer (§7.3.4.2)
396             * (string) with balanced parens and escapes
397             *==========================================================================*/
398              
399 3805           static pdfmake_tok_t lex_literal_string(pdfmake_tokenizer_t *t) {
400             pdfmake_tok_t tok;
401             int depth;
402 3805           tok.kind = PDFMAKE_TOK_LSTR;
403 3805           tok.offset = t->pos;
404              
405 3805           t->pos++; /* Skip opening ( */
406 3805           depth = 1;
407              
408 2733951 100         while (t->pos < t->len && depth > 0) {
    100          
409 2730146           uint8_t c = t->buf[t->pos];
410              
411 2730146 100         if (c == '(') {
412 10176           depth++;
413 10176           t->pos++;
414 2719970 100         } else if (c == ')') {
415 13405           depth--;
416 13405           t->pos++;
417 2706565 100         } else if (c == '\\') {
418             /* Escape sequence */
419 10005           t->pos++;
420 10005 50         if (t->pos < t->len) {
421 10005           uint8_t esc = t->buf[t->pos];
422 10005 100         if (esc >= '0' && esc <= '7') {
    100          
423             /* Octal: 1-3 digits */
424 128           t->pos++;
425 128 50         if (t->pos < t->len && t->buf[t->pos] >= '0' && t->buf[t->pos] <= '7') {
    50          
    50          
426 0           t->pos++;
427 0 0         if (t->pos < t->len && t->buf[t->pos] >= '0' && t->buf[t->pos] <= '7') {
    0          
    0          
428 0           t->pos++;
429             }
430             }
431 9877 100         } else if (esc == '\r') {
432             /* Line continuation: \CR or \CRLF */
433 64           t->pos++;
434 64 50         if (t->pos < t->len && t->buf[t->pos] == '\n') {
    50          
435 0           t->pos++;
436             }
437 9813 100         } else if (esc == '\n') {
438             /* Line continuation: \LF */
439 96           t->pos++;
440             } else {
441             /* Single-char escape: n r t b f ( ) \ or unknown */
442 9717           t->pos++;
443             }
444             }
445             } else {
446 2696560           t->pos++;
447             }
448             }
449              
450 3805           tok.length = t->pos - tok.offset;
451              
452 3805 100         if (depth != 0) {
453             /* Unbalanced parens */
454 32           tok.kind = PDFMAKE_TOK_ERROR;
455             }
456              
457 3805           return tok;
458             }
459              
460             /*============================================================================
461             * Hex string lexer (§7.3.4.3)
462             * with whitespace ignored
463             *==========================================================================*/
464              
465 650           static pdfmake_tok_t lex_hex_string(pdfmake_tokenizer_t *t) {
466             pdfmake_tok_t tok;
467 650           tok.kind = PDFMAKE_TOK_HSTR;
468 650           tok.offset = t->pos;
469              
470 650           t->pos++; /* Skip < */
471              
472 17403 50         while (t->pos < t->len) {
473 17403           uint8_t c = t->buf[t->pos];
474              
475 17403 100         if (c == '>') {
476 490           t->pos++;
477 490           break;
478             }
479              
480             /* Skip whitespace inside hex string */
481 16913 50         if (pdfmake_is_whitespace(c)) {
482 0           t->pos++;
483 0           continue;
484             }
485              
486             /* Must be hex digit */
487 16913 100         if (!pdfmake_is_hex(c)) {
488             /* Invalid character in hex string */
489 160           tok.kind = PDFMAKE_TOK_ERROR;
490 160           t->pos++;
491 160           break;
492             }
493              
494 16753           t->pos++;
495             }
496              
497 650           tok.length = t->pos - tok.offset;
498 650           return tok;
499             }
500              
501             /*============================================================================
502             * Keyword/identifier lexer
503             * Identifies: null, true, false, obj, endobj, stream, endstream, R,
504             * xref, trailer, startxref
505             *==========================================================================*/
506              
507             /* Keyword table for fast lookup */
508             typedef struct {
509             const char *text;
510             size_t len;
511             pdfmake_tok_kind_t kind;
512             } keyword_entry_t;
513              
514             static const keyword_entry_t keywords[] = {
515             {"null", 4, PDFMAKE_TOK_KW_NULL},
516             {"true", 4, PDFMAKE_TOK_KW_TRUE},
517             {"false", 5, PDFMAKE_TOK_KW_FALSE},
518             {"obj", 3, PDFMAKE_TOK_KW_OBJ},
519             {"endobj", 6, PDFMAKE_TOK_KW_ENDOBJ},
520             {"stream", 6, PDFMAKE_TOK_KW_STREAM},
521             {"endstream", 9, PDFMAKE_TOK_KW_ENDSTREAM},
522             {"R", 1, PDFMAKE_TOK_KW_R},
523             {"xref", 4, PDFMAKE_TOK_KW_XREF},
524             {"trailer", 7, PDFMAKE_TOK_KW_TRAILER},
525             {"startxref", 9, PDFMAKE_TOK_KW_STARTXREF},
526             {NULL, 0, PDFMAKE_TOK_ERROR}
527             };
528              
529 17912           static pdfmake_tok_t lex_keyword_or_error(pdfmake_tokenizer_t *t) {
530             pdfmake_tok_t tok;
531             const keyword_entry_t *kw;
532 17912           tok.offset = t->pos;
533              
534             /* Read until whitespace or delimiter */
535 83399 100         while (t->pos < t->len && pdfmake_is_regular(t->buf[t->pos])) {
    100          
536 65487           t->pos++;
537             }
538              
539 17912           tok.length = t->pos - tok.offset;
540              
541             /* Check against keyword table */
542 177306 100         for (kw = keywords; kw->text != NULL; kw++) {
543 166672 100         if (tok.length == kw->len &&
544 15274 100         memcmp(t->buf + tok.offset, kw->text, kw->len) == 0) {
545 7278           tok.kind = kw->kind;
546              
547             /* For 'stream', compute body offset */
548 7278 100         if (tok.kind == PDFMAKE_TOK_KW_STREAM) {
549             /* §7.3.8.1: stream keyword followed by EOL (LF or CRLF)
550             * Body starts after the EOL */
551 305           size_t body_start = t->pos;
552              
553             /* Skip optional whitespace before EOL (lenient) */
554 305 50         while (body_start < t->len &&
555 305 50         (t->buf[body_start] == ' ' || t->buf[body_start] == '\t')) {
    50          
556 0           body_start++;
557             }
558              
559             /* Must have EOL */
560 305 50         if (body_start < t->len && t->buf[body_start] == '\r') {
    100          
561 189           body_start++;
562 189 50         if (body_start < t->len && t->buf[body_start] == '\n') {
    50          
563 189           body_start++;
564             }
565             /* Note: CR alone is technically not spec-compliant but
566             * we accept it for Adobe compatibility */
567 116 50         } else if (body_start < t->len && t->buf[body_start] == '\n') {
    50          
568 116           body_start++;
569             }
570              
571 305           tok.payload.body_offset = body_start;
572             }
573              
574 7278           return tok;
575             }
576             }
577              
578             /* Not a keyword — error (bare identifier not valid in PDF) */
579 10634           tok.kind = PDFMAKE_TOK_ERROR;
580 10634           return tok;
581             }
582              
583             /*============================================================================
584             * Main tokenizer
585             *==========================================================================*/
586              
587 187723           pdfmake_tok_t pdfmake_tok_next(pdfmake_tokenizer_t *t) {
588             pdfmake_tok_t tok;
589             uint8_t c;
590             /* Return peeked token if available */
591 187723 100         if (t->has_peeked) {
592 42627           t->has_peeked = 0;
593 42627           return t->peeked;
594             }
595              
596             /* EOF check */
597 145096 100         if (t->pos >= t->len) {
598 190           tok.kind = PDFMAKE_TOK_EOF;
599 190           tok.offset = t->len;
600 190           tok.length = 0;
601 190           return tok;
602             }
603              
604 144906           c = t->buf[t->pos];
605              
606             /* Whitespace */
607 144906 100         if (pdfmake_is_whitespace(c)) {
608 64838           return lex_whitespace(t);
609             }
610              
611             /* Comment */
612 80068 100         if (c == '%') {
613 299           return lex_comment(t);
614             }
615              
616             /* Number (starts with digit, +, -, or .) */
617 79769 100         if (pdfmake_is_digit(c) ||
    50          
618 44029 100         ((c == '+' || c == '-' || c == '.') &&
    100          
619 4846           t->pos + 1 < t->len &&
620 2423 0         (pdfmake_is_digit(t->buf[t->pos + 1]) || t->buf[t->pos + 1] == '.'))) {
621 38163           return lex_number(t);
622             }
623              
624             /* Name */
625 41606 100         if (c == '/') {
626 10866           return lex_name(t);
627             }
628              
629             /* Literal string */
630 30740 100         if (c == '(') {
631 3805           return lex_literal_string(t);
632             }
633              
634             /* Hex string or dict delimiter */
635 26935 100         if (c == '<') {
636 2911 50         if (t->pos + 1 < t->len && t->buf[t->pos + 1] == '<') {
    100          
637             /* << dict open */
638 2261           tok.kind = PDFMAKE_TOK_DICT_OPEN;
639 2261           tok.offset = t->pos;
640 2261           tok.length = 2;
641 2261           t->pos += 2;
642 2261           return tok;
643             }
644 650           return lex_hex_string(t);
645             }
646              
647             /* Dict close */
648 24024 100         if (c == '>') {
649 2421 50         if (t->pos + 1 < t->len && t->buf[t->pos + 1] == '>') {
    100          
650 2261           tok.kind = PDFMAKE_TOK_DICT_CLOSE;
651 2261           tok.offset = t->pos;
652 2261           tok.length = 2;
653 2261           t->pos += 2;
654 2261           return tok;
655             }
656             /* Lone > is an error */
657 160           tok.kind = PDFMAKE_TOK_ERROR;
658 160           tok.offset = t->pos;
659 160           tok.length = 1;
660 160           t->pos++;
661 160           return tok;
662             }
663              
664             /* Array delimiters */
665 21603 100         if (c == '[') {
666 1529           tok.kind = PDFMAKE_TOK_ARR_OPEN;
667 1529           tok.offset = t->pos;
668 1529           tok.length = 1;
669 1529           t->pos++;
670 1529           return tok;
671             }
672              
673 20074 100         if (c == ']') {
674 1874           tok.kind = PDFMAKE_TOK_ARR_CLOSE;
675 1874           tok.offset = t->pos;
676 1874           tok.length = 1;
677 1874           t->pos++;
678 1874           return tok;
679             }
680              
681             /* Braces (not used in PDF content, but are delimiters) */
682 18200 100         if (c == '{' || c == '}') {
    100          
683 256           tok.kind = PDFMAKE_TOK_ERROR;
684 256           tok.offset = t->pos;
685 256           tok.length = 1;
686 256           t->pos++;
687 256           return tok;
688             }
689              
690             /* Close paren without open (error) */
691 17944 100         if (c == ')') {
692 32           tok.kind = PDFMAKE_TOK_ERROR;
693 32           tok.offset = t->pos;
694 32           tok.length = 1;
695 32           t->pos++;
696 32           return tok;
697             }
698              
699             /* Keyword or unknown identifier */
700 17912           return lex_keyword_or_error(t);
701             }
702              
703             /*============================================================================
704             * Peek
705             *==========================================================================*/
706              
707 49678           void pdfmake_tok_peek(pdfmake_tokenizer_t *t, pdfmake_tok_t *out) {
708 49678 100         if (!t->has_peeked) {
709 43083           t->peeked = pdfmake_tok_next(t);
710 43083           t->has_peeked = 1;
711             }
712 49678           *out = t->peeked;
713 49678           }
714              
715             /*============================================================================
716             * Skip whitespace/comments
717             *==========================================================================*/
718              
719 66948           pdfmake_tok_t pdfmake_tok_next_significant(pdfmake_tokenizer_t *t) {
720             pdfmake_tok_t tok;
721             do {
722 101285           tok = pdfmake_tok_next(t);
723 101285 100         } while (tok.kind == PDFMAKE_TOK_WS || tok.kind == PDFMAKE_TOK_COMMENT);
    100          
724 66948           return tok;
725             }
726              
727 30307           void pdfmake_tok_peek_significant(pdfmake_tokenizer_t *t, pdfmake_tok_t *out) {
728             /* Skip whitespace/comments, then peek */
729             while (1) {
730 49678           pdfmake_tok_peek(t, out);
731 49678 100         if (out->kind != PDFMAKE_TOK_WS && out->kind != PDFMAKE_TOK_COMMENT) {
    100          
732 30307           break;
733             }
734 19371           pdfmake_tok_next(t); /* Consume and continue */
735             }
736 30307           }