File Coverage

src/pdfmake_parser.c
Criterion Covered Total %
statement 516 807 63.9
branch 260 538 48.3
condition n/a
subroutine n/a
pod n/a
total 776 1345 57.7


line stmt bran cond sub pod time code
1             /*
2             * pdfmake_parser.c — PDF object parser + cross-reference reader
3             *
4             * Implementation of recursive-descent parser for PDF objects and
5             * xref table/stream parsing.
6             */
7              
8             #include "pdfmake_parser.h"
9             #include "pdfmake_filter.h"
10             #include
11             #include
12             #include
13             #include
14              
15             /*============================================================================
16             * Internal helpers
17             *==========================================================================*/
18              
19             /* Set error state */
20 60           static void set_error(pdfmake_parser_t *p, pdfmake_err_t err,
21             size_t offset, const char *msg) {
22 60           p->last_err = err;
23 60           p->err_offset = offset;
24 60 50         if (msg) {
25 60           strncpy(p->err_msg, msg, sizeof(p->err_msg) - 1);
26 60           p->err_msg[sizeof(p->err_msg) - 1] = '\0';
27             } else {
28 0           p->err_msg[0] = '\0';
29             }
30 60           }
31              
32             /* Ensure xref table has capacity for object num */
33 1617           static int ensure_xref_cap(pdfmake_parser_t *p, uint32_t num) {
34             size_t new_cap;
35             pdfmake_xref_entry_t *new_xref;
36              
37 1617 100         if (num < p->xref_cap) return 1;
38            
39 117 50         new_cap = p->xref_cap ? p->xref_cap * 2 : 64;
40 117 50         while (new_cap <= num) new_cap *= 2;
41            
42 117           new_xref = realloc(p->xref, new_cap * sizeof(pdfmake_xref_entry_t));
43 117 50         if (!new_xref) return 0;
44            
45             /* Zero new entries */
46 117           memset(new_xref + p->xref_cap, 0, (new_cap - p->xref_cap) * sizeof(pdfmake_xref_entry_t));
47 117           p->xref = new_xref;
48 117           p->xref_cap = new_cap;
49 117           return 1;
50             }
51              
52             /* Check if offset was already visited in /Prev chain (cycle detection) */
53 48           static int prev_visited(pdfmake_parser_t *p, uint64_t offset) {
54             size_t i;
55 96 100         for (i = 0; i < p->prev_count; i++) {
56 80 100         if (p->prev_offsets[i] == offset) return 1;
57             }
58 16           return 0;
59             }
60              
61             /* Add offset to /Prev visited list */
62 228           static int prev_add(pdfmake_parser_t *p, uint64_t offset) {
63 228 100         if (p->prev_count >= p->prev_cap) {
64 116 50         size_t new_cap = p->prev_cap ? p->prev_cap * 2 : 8;
65 116           uint64_t *new_arr = realloc(p->prev_offsets, new_cap * sizeof(uint64_t));
66 116 50         if (!new_arr) return 0;
67 116           p->prev_offsets = new_arr;
68 116           p->prev_cap = new_cap;
69             }
70 228           p->prev_offsets[p->prev_count++] = offset;
71 228           return 1;
72             }
73              
74             /* Get interned name ID from arena */
75 2043           static uint32_t intern_name(pdfmake_parser_t *p, const char *bytes, size_t len) {
76 2043           return pdfmake_arena_intern_name(p->doc->arena, bytes, len);
77             }
78              
79             /* Decode hex digit */
80 14583           static int hex_digit(int c) {
81 14583 50         if (c >= '0' && c <= '9') return c - '0';
    100          
82 5873 100         if (c >= 'a' && c <= 'f') return c - 'a' + 10;
    50          
83 3951 50         if (c >= 'A' && c <= 'F') return c - 'A' + 10;
    50          
84 0           return -1;
85             }
86              
87             /*============================================================================
88             * Parser lifecycle
89             *==========================================================================*/
90              
91 122           pdfmake_parser_t *pdfmake_parser_new(const uint8_t *buf, size_t len) {
92 122           pdfmake_parser_t *p = calloc(1, sizeof(pdfmake_parser_t));
93 122 50         if (!p) return NULL;
94            
95 122           p->buf = buf;
96 122           p->buf_len = len;
97 122           pdfmake_tokenizer_init(&p->tok, buf, len);
98            
99 122           p->doc = pdfmake_doc_new();
100 122 50         if (!p->doc) {
101 0           free(p);
102 0           return NULL;
103             }
104            
105 122           return p;
106             }
107              
108 122           void pdfmake_parser_free(pdfmake_parser_t *parser) {
109 122 50         if (!parser) return;
110 122           free(parser->xref);
111 122           free(parser->prev_offsets);
112             /* Note: does NOT free doc - caller owns it */
113 122           free(parser);
114             }
115              
116 118           void pdfmake_parser_set_repair(pdfmake_parser_t *parser, int enable) {
117 118 50         if (parser) parser->repair = enable ? 1 : 0;
118 118           }
119              
120 0           const char *pdfmake_parser_errmsg(pdfmake_parser_t *parser) {
121 0 0         return parser ? parser->err_msg : "NULL parser";
122             }
123              
124 0           size_t pdfmake_parser_erroffset(pdfmake_parser_t *parser) {
125 0 0         return parser ? parser->err_offset : 0;
126             }
127              
128             /*============================================================================
129             * PDF header validation
130             *==========================================================================*/
131              
132 218           pdfmake_err_t pdfmake_check_header(pdfmake_parser_t *p, int *major, int *minor) {
133             /* Look for %PDF-N.M in first 1024 bytes */
134 218           const char *needle = "%PDF-";
135 218           size_t search_len = p->buf_len < 1024 ? p->buf_len : 1024;
136 218           const uint8_t *found = NULL;
137 218           int maj = 0, min = 0;
138             const uint8_t *ptr;
139             size_t i;
140            
141 218 50         for (i = 0; i + 7 < search_len; i++) {
142 218 50         if (memcmp(p->buf + i, needle, 5) == 0) {
143 218           found = p->buf + i;
144 218           break;
145             }
146             }
147            
148 218 50         if (!found) {
149 0           set_error(p, PDFMAKE_EHEADER, 0, "PDF header not found");
150 0           return PDFMAKE_EHEADER;
151             }
152            
153             /* Parse version */
154 218           ptr = found + 5;
155 436 100         while (isdigit(*ptr)) maj = maj * 10 + (*ptr++ - '0');
156 218 50         if (*ptr == '.') ptr++;
157 436 100         while (isdigit(*ptr)) min = min * 10 + (*ptr++ - '0');
158            
159 218 50         if (major) *major = maj;
160 218 50         if (minor) *minor = min;
161            
162 218           return PDFMAKE_OK;
163             }
164              
165             /*============================================================================
166             * Object parsing — recursive descent
167             *==========================================================================*/
168              
169             /* Forward declaration for mutual recursion */
170             static pdfmake_obj_t parse_object_internal(pdfmake_parser_t *p);
171              
172             /* Parse name token - decode #XX escapes */
173 7003           static pdfmake_obj_t parse_name(pdfmake_parser_t *p, pdfmake_tok_t *tok) {
174             /* Skip leading / - use tok.buf since it may be repositioned */
175 7003           const uint8_t *src = p->tok.buf + tok->offset + 1;
176 7003           size_t src_len = tok->length - 1;
177 7003           int has_escape = 0;
178             uint8_t *decoded;
179             size_t out_len;
180             size_t i;
181            
182             /* Check if we need to decode #XX */
183 49242 100         for (i = 0; i < src_len; i++) {
184 42240 100         if (src[i] == '#') { has_escape = 1; break; }
185             }
186            
187 7003 100         if (!has_escape) {
188 7002           return pdfmake_name(p->doc->arena, (const char *)src, src_len);
189             }
190            
191             /* Decode escapes */
192 1           decoded = pdfmake_arena_alloc(p->doc->arena, src_len);
193 1 50         if (!decoded) return pdfmake_null();
194            
195 1           out_len = 0;
196 6 100         for (i = 0; i < src_len; i++) {
197 5 100         if (src[i] == '#' && i + 2 < src_len) {
    50          
198 1           int hi = hex_digit(src[i + 1]);
199 1           int lo = hex_digit(src[i + 2]);
200 1 50         if (hi >= 0 && lo >= 0) {
    50          
201 1           decoded[out_len++] = (uint8_t)((hi << 4) | lo);
202 1           i += 2;
203 1           continue;
204             }
205             }
206 4           decoded[out_len++] = src[i];
207             }
208            
209 1           return pdfmake_name(p->doc->arena, (const char *)decoded, out_len);
210             }
211              
212             /* Parse literal string - decode escape sequences */
213 47           static pdfmake_obj_t parse_literal_string(pdfmake_parser_t *p, pdfmake_tok_t *tok) {
214             /* Content is between ( and ) - skip outer parens - use tok.buf */
215 47           const uint8_t *src = p->tok.buf + tok->offset + 1;
216 47           size_t src_len = tok->length - 2;
217             uint8_t *decoded;
218             size_t out_len;
219             int paren_depth;
220             size_t i;
221            
222             /* Decode string escapes */
223 47           decoded = pdfmake_arena_alloc(p->doc->arena, src_len + 1);
224 47 50         if (!decoded) return pdfmake_null();
225            
226 47           out_len = 0;
227 47           paren_depth = 0;
228            
229 1244 100         for (i = 0; i < src_len; i++) {
230 1197           uint8_t c = src[i];
231            
232 1197 100         if (c == '\\' && i + 1 < src_len) {
    50          
233 3           uint8_t next = src[i + 1];
234 3           switch (next) {
235 1           case 'n': decoded[out_len++] = '\n'; i++; continue;
236 0           case 'r': decoded[out_len++] = '\r'; i++; continue;
237 0           case 't': decoded[out_len++] = '\t'; i++; continue;
238 0           case 'b': decoded[out_len++] = '\b'; i++; continue;
239 0           case 'f': decoded[out_len++] = '\f'; i++; continue;
240 1           case '(': decoded[out_len++] = '('; i++; continue;
241 1           case ')': decoded[out_len++] = ')'; i++; continue;
242 0           case '\\': decoded[out_len++] = '\\'; i++; continue;
243 0           case '\n': i++; continue; /* Line continuation LF */
244 0           case '\r': /* Line continuation CR or CRLF */
245 0           i++;
246 0 0         if (i + 1 < src_len && src[i + 1] == '\n') i++;
    0          
247 0           continue;
248 0           default:
249             /* Octal escape? */
250 0 0         if (next >= '0' && next <= '7') {
    0          
251 0           int val = next - '0';
252 0           i++;
253 0 0         if (i + 1 < src_len && src[i + 1] >= '0' && src[i + 1] <= '7') {
    0          
    0          
254 0           val = val * 8 + (src[++i] - '0');
255 0 0         if (i + 1 < src_len && src[i + 1] >= '0' && src[i + 1] <= '7') {
    0          
    0          
256 0           val = val * 8 + (src[++i] - '0');
257             }
258             }
259 0           decoded[out_len++] = (uint8_t)(val & 0xFF);
260 0           continue;
261             }
262             /* Unknown escape - just keep the backslash per PDF spec */
263 0           decoded[out_len++] = c;
264 0           continue;
265             }
266             }
267            
268             /* Track balanced parens (tokenizer already validated) */
269 1194 50         if (c == '(') paren_depth++;
270 1194 50         else if (c == ')') paren_depth--;
271            
272 1194           decoded[out_len++] = c;
273             }
274            
275             (void)paren_depth; /* Validated by tokenizer */
276 47           return pdfmake_str(p->doc->arena, (const char *)decoded, out_len);
277             }
278              
279             /* Parse hex string - decode hex pairs */
280 420           static pdfmake_obj_t parse_hex_string(pdfmake_parser_t *p, pdfmake_tok_t *tok) {
281             /* Content is between < and > - use tok.buf */
282 420           const uint8_t *src = p->tok.buf + tok->offset + 1;
283 420           size_t src_len = tok->length - 2;
284             uint8_t *decoded;
285             size_t out_len;
286             int high;
287             size_t i;
288            
289             /* Decode hex pairs (skip whitespace) */
290 420           decoded = pdfmake_arena_alloc(p->doc->arena, (src_len + 1) / 2);
291 420 50         if (!decoded) return pdfmake_null();
292            
293 420           out_len = 0;
294 420           high = -1;
295            
296 15001 100         for (i = 0; i < src_len; i++) {
297 14581           uint8_t c = src[i];
298             int digit;
299 14581 50         if (pdfmake_is_whitespace(c)) continue;
300            
301 14581           digit = hex_digit(c);
302 14581 50         if (digit < 0) continue; /* Invalid hex digit - skip */
303            
304 14581 100         if (high < 0) {
305 7291           high = digit;
306             } else {
307 7290           decoded[out_len++] = (uint8_t)((high << 4) | digit);
308 7290           high = -1;
309             }
310             }
311            
312             /* Odd number of digits - pad with 0 */
313 420 100         if (high >= 0) {
314 1           decoded[out_len++] = (uint8_t)(high << 4);
315             }
316            
317 420           return pdfmake_hexstr(p->doc->arena, decoded, out_len);
318             }
319              
320             /* Parse array */
321 706           static pdfmake_obj_t parse_array(pdfmake_parser_t *p) {
322 706           pdfmake_obj_t arr = pdfmake_array_new(p->doc->arena);
323             pdfmake_tok_t tok;
324             pdfmake_obj_t item;
325            
326 706 50         if (arr.kind != PDFMAKE_ARRAY) return pdfmake_null();
327            
328             while (1) {
329 6542           pdfmake_tok_peek_significant(&p->tok, &tok);
330            
331 6542 100         if (tok.kind == PDFMAKE_TOK_ARR_CLOSE) {
332 706           pdfmake_tok_next_significant(&p->tok); /* Consume ] */
333 706           break;
334             }
335 5836 50         if (tok.kind == PDFMAKE_TOK_EOF) {
336 0           set_error(p, PDFMAKE_EPARSE, tok.offset, "Unterminated array");
337 0           return pdfmake_null();
338             }
339            
340 5836           item = parse_object_internal(p);
341 5836 50         if (p->last_err != PDFMAKE_OK) return pdfmake_null();
342            
343 5836 50         if (!pdfmake_array_push(p->doc->arena, &arr, item)) {
344 0           set_error(p, PDFMAKE_ENOMEM, tok.offset, "Array push failed");
345 0           return pdfmake_null();
346             }
347             }
348            
349 706           return arr;
350             }
351              
352             /* Parse dictionary (and possibly stream) */
353 1526           static pdfmake_obj_t parse_dict(pdfmake_parser_t *p) {
354 1526           pdfmake_obj_t dict = pdfmake_dict_new(p->doc->arena);
355             pdfmake_tok_t tok;
356             pdfmake_obj_t key_obj;
357             pdfmake_obj_t value;
358             pdfmake_tok_t next;
359             pdfmake_obj_t stream;
360             pdfmake_obj_t stream_dict_wrapper;
361             pdfmake_dict_iter_t iter;
362             const uint8_t *body_data;
363             size_t body_len;
364             pdfmake_err_t err;
365             size_t endstream_pos;
366             const char *endstream_kw;
367            
368 1526 50         if (dict.kind != PDFMAKE_DICT) return pdfmake_null();
369            
370             while (1) {
371 6946           pdfmake_tok_peek_significant(&p->tok, &tok);
372            
373 6946 100         if (tok.kind == PDFMAKE_TOK_DICT_CLOSE) {
374 1526           pdfmake_tok_next_significant(&p->tok); /* Consume >> */
375 1526           break;
376             }
377 5420 50         if (tok.kind == PDFMAKE_TOK_EOF) {
378 0           set_error(p, PDFMAKE_EPARSE, tok.offset, "Unterminated dictionary");
379 0           return pdfmake_null();
380             }
381            
382             /* Key must be a name */
383 5420 50         if (tok.kind != PDFMAKE_TOK_NAME) {
384 0           set_error(p, PDFMAKE_EPARSE, tok.offset, "Dictionary key must be a name");
385 0           return pdfmake_null();
386             }
387            
388 5420           pdfmake_tok_next_significant(&p->tok); /* Consume key */
389 5420           key_obj = parse_name(p, &tok);
390 5420 50         if (key_obj.kind != PDFMAKE_NAME) return pdfmake_null();
391            
392             /* Value */
393 5420           value = parse_object_internal(p);
394 5420 50         if (p->last_err != PDFMAKE_OK) return pdfmake_null();
395            
396 5420 50         if (!pdfmake_dict_set(p->doc->arena, &dict, key_obj.as.name.id, value)) {
397 0           set_error(p, PDFMAKE_ENOMEM, tok.offset, "Dict set failed");
398 0           return pdfmake_null();
399             }
400             }
401            
402             /* Check if followed by stream keyword */
403 1526           pdfmake_tok_peek_significant(&p->tok, &next);
404            
405 1526 100         if (next.kind == PDFMAKE_TOK_KW_STREAM) {
406 217           pdfmake_tok_next_significant(&p->tok); /* Consume stream */
407            
408             /* Create stream object */
409 217           stream = pdfmake_stream_new(p->doc->arena);
410 217 50         if (stream.kind != PDFMAKE_STREAM) return pdfmake_null();
411            
412             /* Copy dict entries to stream dict - need a wrapper pdfmake_obj_t */
413 217           stream_dict_wrapper.kind = PDFMAKE_DICT;
414 217           stream_dict_wrapper.as.dict = stream.as.stream->dict;
415            
416 217           pdfmake_dict_iter_init(&iter, &dict);
417 813 100         while (pdfmake_dict_iter_next(&iter)) {
418 596           pdfmake_dict_set(p->doc->arena, &stream_dict_wrapper,
419 596           iter.current_key, *iter.current_value);
420             }
421            
422             /* Extract stream body */
423 217           err = pdfmake_extract_stream_body(p, &dict, next.payload.body_offset,
424             &body_data, &body_len);
425 217 50         if (err != PDFMAKE_OK) return pdfmake_null();
426            
427             /* Copy stream data to arena */
428 217           pdfmake_stream_set_data(p->doc->arena, &stream, body_data, body_len);
429            
430             /* Skip tokenizer past stream body to find endstream.
431             * The tokenizer can't parse arbitrary binary stream content,
432             * so we manually position it after the stream body. */
433 217           endstream_pos = next.payload.body_offset + body_len;
434            
435             /* Skip optional EOL before endstream */
436 431 50         while (endstream_pos < p->buf_len &&
437 431 100         (p->buf[endstream_pos] == '\r' || p->buf[endstream_pos] == '\n')) {
    100          
438 214           endstream_pos++;
439             }
440            
441             /* Verify endstream keyword */
442 217           endstream_kw = "endstream";
443 217 50         if (endstream_pos + 9 > p->buf_len ||
444 217 50         memcmp(p->buf + endstream_pos, endstream_kw, 9) != 0) {
445 0           set_error(p, PDFMAKE_ESTREAM, endstream_pos, "Expected endstream");
446 0           return pdfmake_null();
447             }
448            
449             /* Position tokenizer after endstream */
450 217           p->tok.pos = endstream_pos + 9;
451 217           p->tok.has_peeked = 0;
452            
453 217           return stream;
454             }
455            
456 1309           return dict;
457             }
458              
459             /* Parse any object at current position */
460 12411           static pdfmake_obj_t parse_object_internal(pdfmake_parser_t *p) {
461 12411           pdfmake_tok_t tok = pdfmake_tok_next_significant(&p->tok);
462            
463 12411           switch (tok.kind) {
464 2           case PDFMAKE_TOK_KW_NULL:
465 2           return pdfmake_null();
466            
467 3           case PDFMAKE_TOK_KW_TRUE:
468 3           return pdfmake_bool(1);
469            
470 7           case PDFMAKE_TOK_KW_FALSE:
471 7           return pdfmake_bool(0);
472            
473 8108           case PDFMAKE_TOK_INT: {
474             /* Check for indirect reference: INT INT R */
475             pdfmake_tok_t peek1, peek2;
476 8108           pdfmake_tok_peek_significant(&p->tok, &peek1);
477            
478 8108 100         if (peek1.kind == PDFMAKE_TOK_INT) {
479             /* Save tokenizer state */
480 6561           size_t saved_pos = p->tok.pos;
481 6561           int saved_peeked = p->tok.has_peeked;
482 6561           pdfmake_tok_t saved_peek = p->tok.peeked;
483            
484 6561           pdfmake_tok_next_significant(&p->tok); /* Consume second int */
485 6561           pdfmake_tok_peek_significant(&p->tok, &peek2);
486            
487 6561 100         if (peek2.kind == PDFMAKE_TOK_KW_R) {
488 1893           pdfmake_tok_next_significant(&p->tok); /* Consume R */
489 1893           return pdfmake_ref((uint32_t)tok.payload.int_val,
490 1893           (uint16_t)peek1.payload.int_val);
491             }
492            
493             /* Not a reference - restore state */
494 4668           p->tok.pos = saved_pos;
495 4668           p->tok.has_peeked = saved_peeked;
496 4668           p->tok.peeked = saved_peek;
497             }
498 6215           return pdfmake_int(tok.payload.int_val);
499             }
500            
501 9           case PDFMAKE_TOK_REAL:
502 9           return pdfmake_real(tok.payload.real_val);
503            
504 1583           case PDFMAKE_TOK_NAME:
505 1583           return parse_name(p, &tok);
506            
507 47           case PDFMAKE_TOK_LSTR:
508 47           return parse_literal_string(p, &tok);
509            
510 420           case PDFMAKE_TOK_HSTR:
511 420           return parse_hex_string(p, &tok);
512            
513 706           case PDFMAKE_TOK_ARR_OPEN:
514 706           return parse_array(p);
515            
516 1526           case PDFMAKE_TOK_DICT_OPEN:
517 1526           return parse_dict(p);
518            
519 0           case PDFMAKE_TOK_EOF:
520 0           set_error(p, PDFMAKE_EPARSE, tok.offset, "Unexpected end of input");
521 0           return pdfmake_null();
522            
523 0           default:
524 0           set_error(p, PDFMAKE_EPARSE, tok.offset, "Unexpected token");
525 0           return pdfmake_null();
526             }
527             }
528              
529 0           pdfmake_obj_t pdfmake_parse_object(pdfmake_parser_t *parser) {
530 0           parser->last_err = PDFMAKE_OK;
531 0           return parse_object_internal(parser);
532             }
533              
534             /*============================================================================
535             * Stream body extraction
536             *==========================================================================*/
537              
538 217           pdfmake_err_t pdfmake_extract_stream_body(pdfmake_parser_t *p,
539             pdfmake_obj_t *stream_dict,
540             size_t body_offset,
541             const uint8_t **out_data,
542             size_t *out_len) {
543             /* Get /Length from dict */
544 217           uint32_t length_id = intern_name(p, "Length", 6);
545 217           pdfmake_obj_t *length_obj = pdfmake_dict_get(stream_dict, length_id);
546 217           size_t declared_len = 0;
547 217           int have_length = 0;
548             const char *endstream_marker;
549             size_t endstream_len;
550             size_t scan_start;
551             size_t scan_end;
552             size_t i;
553            
554 217 50         if (length_obj) {
555 217 50         if (length_obj->kind == PDFMAKE_INT) {
556 217           declared_len = (size_t)length_obj->as.i;
557 217           have_length = 1;
558 0 0         } else if (length_obj->kind == PDFMAKE_REF) {
559             /* /Length is indirect - need to resolve */
560 0           pdfmake_obj_t *resolved = pdfmake_parser_resolve(p, length_obj->as.ref);
561 0 0         if (resolved && resolved->kind == PDFMAKE_INT) {
    0          
562 0           declared_len = (size_t)resolved->as.i;
563 0           have_length = 1;
564             }
565             }
566             }
567            
568             /* Find endstream */
569 217           endstream_marker = "endstream";
570 217           endstream_len = 9;
571 217           scan_start = body_offset;
572 217           scan_end = p->buf_len;
573            
574             /* If we have declared length, try it first */
575 217 50         if (have_length && body_offset + declared_len + endstream_len <= p->buf_len) {
    50          
576 217           size_t expected_end = body_offset + declared_len;
577            
578             /* Skip optional EOL before endstream */
579 429 50         while (expected_end < p->buf_len &&
580 429 100         (p->buf[expected_end] == '\r' || p->buf[expected_end] == '\n')) {
    100          
581 212           expected_end++;
582             }
583            
584 217 50         if (expected_end + endstream_len <= p->buf_len &&
585 217 100         memcmp(p->buf + expected_end, endstream_marker, endstream_len) == 0) {
586 215           *out_data = p->buf + body_offset;
587 215           *out_len = declared_len;
588 215           return PDFMAKE_OK;
589             }
590             }
591            
592             /* Fallback: scan for endstream */
593 76 50         for (i = scan_start; i + endstream_len <= scan_end; i++) {
594 76 100         if (memcmp(p->buf + i, endstream_marker, endstream_len) == 0) {
595             /* Check preceding bytes for proper delimiter */
596 2           size_t body_end = i;
597            
598             /* Back up over optional EOL */
599 2 50         if (body_end > body_offset && p->buf[body_end - 1] == '\n') {
    50          
600 2           body_end--;
601             }
602 2 50         if (body_end > body_offset && p->buf[body_end - 1] == '\r') {
    50          
603 0           body_end--;
604             }
605            
606 2           *out_data = p->buf + body_offset;
607 2           *out_len = body_end - body_offset;
608 2           return PDFMAKE_OK;
609             }
610             }
611            
612 0           set_error(p, PDFMAKE_ESTREAM, body_offset, "endstream not found");
613 0           return PDFMAKE_ESTREAM;
614             }
615              
616             /*============================================================================
617             * Indirect object parsing
618             *==========================================================================*/
619              
620 911           pdfmake_err_t pdfmake_parse_indirect_object(pdfmake_parser_t *p) {
621             pdfmake_tok_t num_tok;
622             pdfmake_tok_t gen_tok;
623             pdfmake_tok_t obj_tok;
624             uint32_t num;
625             uint16_t gen;
626             pdfmake_obj_t obj;
627             pdfmake_tok_t endobj_tok;
628             size_t idx;
629            
630             /* Clear any residual error from previous operations */
631 911           p->last_err = PDFMAKE_OK;
632            
633 911           num_tok = pdfmake_tok_next_significant(&p->tok);
634 911 50         if (num_tok.kind != PDFMAKE_TOK_INT) {
635 0           set_error(p, PDFMAKE_EPARSE, num_tok.offset, "Expected object number");
636 0           return PDFMAKE_EPARSE;
637             }
638            
639 911           gen_tok = pdfmake_tok_next_significant(&p->tok);
640 911 50         if (gen_tok.kind != PDFMAKE_TOK_INT) {
641 0           set_error(p, PDFMAKE_EPARSE, gen_tok.offset, "Expected generation number");
642 0           return PDFMAKE_EPARSE;
643             }
644            
645 911           obj_tok = pdfmake_tok_next_significant(&p->tok);
646 911 50         if (obj_tok.kind != PDFMAKE_TOK_KW_OBJ) {
647 0           set_error(p, PDFMAKE_EPARSE, obj_tok.offset, "Expected 'obj' keyword");
648 0           return PDFMAKE_EPARSE;
649             }
650            
651 911           num = (uint32_t)num_tok.payload.int_val;
652 911           gen = (uint16_t)gen_tok.payload.int_val;
653            
654             /* Parse the object value */
655 911           obj = parse_object_internal(p);
656 911 50         if (p->last_err != PDFMAKE_OK) return p->last_err;
657            
658             /* Expect endobj */
659 911           endobj_tok = pdfmake_tok_next_significant(&p->tok);
660 911 50         if (endobj_tok.kind != PDFMAKE_TOK_KW_ENDOBJ) {
661             /* Some PDFs omit endobj before stream - tolerate */
662 0 0         if (endobj_tok.kind != PDFMAKE_TOK_KW_STREAM) {
663 0           set_error(p, PDFMAKE_EPARSE, endobj_tok.offset, "Expected 'endobj' keyword");
664 0           return PDFMAKE_EPARSE;
665             }
666             }
667            
668             /* Store in document */
669             /* Grow objects array if needed - use num directly as we need capacity for num items */
670             /* pdfmake_doc uses 1-based numbering with object N at index N-1 */
671 911 50         while (num > p->doc->obj_cap) {
672 0 0         size_t new_cap = p->doc->obj_cap ? p->doc->obj_cap * 2 : PDFMAKE_DOC_INIT_CAP;
673             pdfmake_indirect_t *new_objs;
674 0 0         while (new_cap < num) new_cap *= 2;
675 0           new_objs = realloc(p->doc->objects,
676             new_cap * sizeof(pdfmake_indirect_t));
677 0 0         if (!new_objs) {
678 0           set_error(p, PDFMAKE_ENOMEM, 0, "Failed to grow object table");
679 0           return PDFMAKE_ENOMEM;
680             }
681 0           memset(new_objs + p->doc->obj_cap, 0,
682 0           (new_cap - p->doc->obj_cap) * sizeof(pdfmake_indirect_t));
683 0           p->doc->objects = new_objs;
684 0           p->doc->obj_cap = new_cap;
685             }
686            
687             /* Store at index num-1 (1-based numbering) */
688 911           idx = num - 1;
689 911           p->doc->objects[idx].num = num;
690 911           p->doc->objects[idx].gen = gen;
691 911           p->doc->objects[idx].obj = obj;
692 911           p->doc->objects[idx].in_use = 1;
693 911           p->doc->objects[idx].byte_offset = num_tok.offset;
694            
695 911 100         if (num > p->doc->obj_count) {
696 244           p->doc->obj_count = num;
697             }
698            
699             /* Update xref if tracking */
700 911 50         if (ensure_xref_cap(p, num)) {
701 911           p->xref[num].num = num;
702 911           p->xref[num].gen = gen;
703 911           p->xref[num].type = PDFMAKE_XREF_UNCOMPRESSED;
704 911           p->xref[num].loc.offset = num_tok.offset;
705 911           p->xref[num].loaded = 1;
706 911 50         if (num >= p->xref_size) p->xref_size = num + 1;
707             }
708            
709 911           return PDFMAKE_OK;
710             }
711              
712             /*============================================================================
713             * startxref locator
714             *==========================================================================*/
715              
716 218           pdfmake_err_t pdfmake_locate_startxref(pdfmake_parser_t *p, uint64_t *offset) {
717             /* Scan backward from EOF - check last 1024 bytes per spec */
718 218           size_t search_start = p->buf_len > 1024 ? p->buf_len - 1024 : 0;
719 218           const char *keyword = "startxref";
720 218           size_t keyword_len = 9;
721 218           const uint8_t *found = NULL;
722             const uint8_t *ptr;
723             const uint8_t *end;
724             uint64_t off;
725             size_t i;
726            
727             /* Find last occurrence of startxref */
728 4432 100         for (i = p->buf_len; i > search_start + keyword_len; ) {
729 4426           i--;
730 4426 100         if (memcmp(p->buf + i, keyword, keyword_len) == 0) {
731 212           found = p->buf + i;
732 212           break;
733             }
734             }
735            
736 218 100         if (!found) {
737 6           set_error(p, PDFMAKE_EXREF, 0, "startxref not found");
738 6           return PDFMAKE_EXREF;
739             }
740            
741             /* Parse offset after startxref */
742 212           ptr = found + keyword_len;
743 212           end = p->buf + p->buf_len;
744            
745             /* Skip whitespace */
746 424 50         while (ptr < end && (*ptr == ' ' || *ptr == '\t' || *ptr == '\r' || *ptr == '\n')) {
    50          
    50          
    100          
    100          
747 212           ptr++;
748             }
749            
750             /* Parse number */
751 212           off = 0;
752 914 50         while (ptr < end && isdigit(*ptr)) {
    100          
753 702           off = off * 10 + (*ptr - '0');
754 702           ptr++;
755             }
756            
757 212           *offset = off;
758 212           return PDFMAKE_OK;
759             }
760              
761             /*============================================================================
762             * Classic xref table parser (§7.5.4)
763             *==========================================================================*/
764              
765             /* Consume a "first_num count" pair that introduces a classic-xref
766             * subsection. Caller must have already confirmed via peek that the next
767             * token is not KW_TRAILER. */
768             static pdfmake_err_t
769 206           parse_xref_subsection_header(pdfmake_parser_t *p, uint64_t table_offset,
770             uint32_t *out_first, uint32_t *out_count)
771             {
772             pdfmake_tok_t first_tok;
773             pdfmake_tok_t count_tok;
774            
775 206           first_tok = pdfmake_tok_next_significant(&p->tok);
776 206 50         if (first_tok.kind != PDFMAKE_TOK_INT) {
777 0           set_error(p, PDFMAKE_EXREF, table_offset + first_tok.offset,
778             "Expected subsection start object number");
779 0           return PDFMAKE_EXREF;
780             }
781              
782 206           count_tok = pdfmake_tok_next_significant(&p->tok);
783 206 50         if (count_tok.kind != PDFMAKE_TOK_INT) {
784 0           set_error(p, PDFMAKE_EXREF, table_offset + count_tok.offset,
785             "Expected subsection entry count");
786 0           return PDFMAKE_EXREF;
787             }
788              
789 206           *out_first = (uint32_t)first_tok.payload.int_val;
790 206           *out_count = (uint32_t)count_tok.payload.int_val;
791 206           return PDFMAKE_OK;
792             }
793              
794             /* Parse one classic-xref entry (offset, gen, n|f) from the token stream
795             * and store it into p->xref[obj_num] unless the slot is already loaded
796             * from a later xref section. The type byte is a single-character ERROR
797             * token rather than a keyword — the tokenizer does not recognise 'n'/'f'
798             * specially. */
799             static pdfmake_err_t
800 3046           parse_xref_entry(pdfmake_parser_t *p, uint64_t table_offset, uint32_t obj_num)
801             {
802             pdfmake_tok_t off_tok;
803             pdfmake_tok_t gen_tok;
804             pdfmake_tok_t type_tok;
805             char type_char;
806            
807 3046           off_tok = pdfmake_tok_next_significant(&p->tok);
808 3046 50         if (off_tok.kind != PDFMAKE_TOK_INT) {
809 0           set_error(p, PDFMAKE_EXREF, table_offset + off_tok.offset,
810             "Expected xref entry offset");
811 0           return PDFMAKE_EXREF;
812             }
813              
814 3046           gen_tok = pdfmake_tok_next_significant(&p->tok);
815 3046 50         if (gen_tok.kind != PDFMAKE_TOK_INT) {
816 0           set_error(p, PDFMAKE_EXREF, table_offset + gen_tok.offset,
817             "Expected xref entry generation");
818 0           return PDFMAKE_EXREF;
819             }
820              
821 3046           type_tok = pdfmake_tok_next(&p->tok);
822 6092 100         while (type_tok.kind == PDFMAKE_TOK_WS || type_tok.kind == PDFMAKE_TOK_COMMENT) {
    50          
823 3046           type_tok = pdfmake_tok_next(&p->tok);
824             }
825              
826 3046           type_char = 'n';
827 3046 50         if (type_tok.kind == PDFMAKE_TOK_ERROR && type_tok.length == 1) {
    50          
828 3046           type_char = (char)p->buf[table_offset + type_tok.offset];
829             }
830              
831 3046 50         if (!p->xref[obj_num].loaded) {
832 3046           p->xref[obj_num].num = obj_num;
833 3046           p->xref[obj_num].gen = (uint16_t)gen_tok.payload.int_val;
834              
835 3046 100         if (type_char == 'n') {
836 2868           p->xref[obj_num].type = PDFMAKE_XREF_UNCOMPRESSED;
837 2868           p->xref[obj_num].loc.offset = (uint64_t)off_tok.payload.int_val;
838             } else {
839 178           p->xref[obj_num].type = PDFMAKE_XREF_FREE;
840 178           p->xref[obj_num].loc.next_free = (uint32_t)off_tok.payload.int_val;
841             }
842             }
843              
844 3046 100         if (obj_num >= p->xref_size) p->xref_size = obj_num + 1;
845 3046           return PDFMAKE_OK;
846             }
847              
848 206           pdfmake_err_t pdfmake_parse_xref_table(pdfmake_parser_t *p, uint64_t offset) {
849             pdfmake_tok_t tok;
850             pdfmake_tok_t next;
851             uint32_t first_num, count;
852             pdfmake_err_t err;
853             uint32_t i;
854            
855 206 50         if (offset >= p->buf_len) {
856 0           set_error(p, PDFMAKE_EXREF, (size_t)offset, "Xref offset beyond EOF");
857 0           return PDFMAKE_EXREF;
858             }
859              
860 206           pdfmake_tokenizer_init(&p->tok, p->buf + offset, p->buf_len - offset);
861              
862 206           tok = pdfmake_tok_next_significant(&p->tok);
863 206 50         if (tok.kind != PDFMAKE_TOK_KW_XREF) {
864 0           set_error(p, PDFMAKE_EXREF, offset + tok.offset, "Expected 'xref' keyword");
865 0           return PDFMAKE_EXREF;
866             }
867              
868             for (;;) {
869 412           pdfmake_tok_peek_significant(&p->tok, &next);
870 412 100         if (next.kind == PDFMAKE_TOK_KW_TRAILER) break; /* trailer follows */
871              
872 206           err = parse_xref_subsection_header(p, offset, &first_num, &count);
873 206 50         if (err != PDFMAKE_OK) return err;
874              
875 206 50         if (!ensure_xref_cap(p, first_num + count)) {
876 0           set_error(p, PDFMAKE_ENOMEM, 0, "Failed to allocate xref table");
877 0           return PDFMAKE_ENOMEM;
878             }
879              
880 3252 100         for (i = 0; i < count; i++) {
881 3046           err = parse_xref_entry(p, offset, first_num + i);
882 3046 50         if (err != PDFMAKE_OK) return err;
883             }
884             }
885              
886 206           return PDFMAKE_OK;
887             }
888              
889             /*============================================================================
890             * Trailer parsing
891             *==========================================================================*/
892              
893 244           pdfmake_err_t pdfmake_parse_trailer(pdfmake_parser_t *p, pdfmake_obj_t *trailer) {
894             pdfmake_tok_t tok;
895             uint32_t root_id, info_id, size_id, prev_id, encrypt_id;
896             pdfmake_obj_t *root;
897             pdfmake_obj_t *info;
898             pdfmake_obj_t *encrypt;
899             uint32_t id_id;
900             pdfmake_obj_t *id_arr;
901             pdfmake_obj_t *size;
902             pdfmake_obj_t *prev;
903            
904 244           tok = pdfmake_tok_next_significant(&p->tok);
905 244 50         if (tok.kind != PDFMAKE_TOK_KW_TRAILER) {
906 0           set_error(p, PDFMAKE_ETRAILER, tok.offset, "Expected 'trailer' keyword");
907 0           return PDFMAKE_ETRAILER;
908             }
909            
910 244           *trailer = parse_object_internal(p);
911 244 50         if (p->last_err != PDFMAKE_OK) return p->last_err;
912            
913 244 50         if (trailer->kind != PDFMAKE_DICT) {
914 0           set_error(p, PDFMAKE_ETRAILER, tok.offset, "Trailer must be a dictionary");
915 0           return PDFMAKE_ETRAILER;
916             }
917            
918             /* Extract trailer values */
919 244           root_id = intern_name(p, "Root", 4);
920 244           info_id = intern_name(p, "Info", 4);
921 244           size_id = intern_name(p, "Size", 4);
922 244           prev_id = intern_name(p, "Prev", 4);
923 244           encrypt_id = intern_name(p, "Encrypt", 7);
924            
925 244           root = pdfmake_dict_get(trailer, root_id);
926 244 100         if (root && root->kind == PDFMAKE_REF) {
    50          
927 228           p->root_num = root->as.ref.num;
928 228           p->root_gen = root->as.ref.gen;
929             }
930            
931 244           info = pdfmake_dict_get(trailer, info_id);
932 244 100         if (info && info->kind == PDFMAKE_REF) {
    50          
933 172           p->info_num = info->as.ref.num;
934 172           p->info_gen = info->as.ref.gen;
935             }
936            
937 244           encrypt = pdfmake_dict_get(trailer, encrypt_id);
938 244 100         if (encrypt && encrypt->kind == PDFMAKE_REF) {
    50          
939 66           p->encrypt_num = encrypt->as.ref.num;
940 66           p->encrypt_gen = encrypt->as.ref.gen;
941             }
942              
943             /* Extract /ID array (needed for encryption key derivation) */
944 244           id_id = intern_name(p, "ID", 2);
945 244           id_arr = pdfmake_dict_get(trailer, id_id);
946 244 100         if (id_arr && id_arr->kind == PDFMAKE_ARRAY && pdfmake_array_len(id_arr) >= 1) {
    50          
    50          
947 192           pdfmake_obj_t *id0 = pdfmake_array_get(id_arr, 0);
948 192 50         if (id0 && id0->kind == PDFMAKE_STR && p->doc_id_len == 0) {
    50          
    100          
949 82           size_t len = id0->as.str.len;
950 82 50         if (len > sizeof(p->doc_id)) len = sizeof(p->doc_id);
951 82           memcpy(p->doc_id, id0->as.str.bytes, len);
952 82           p->doc_id_len = len;
953             }
954             }
955            
956 244           size = pdfmake_dict_get(trailer, size_id);
957 244 50         if (size && size->kind == PDFMAKE_INT) {
    50          
958 244 50         if (!ensure_xref_cap(p, (uint32_t)size->as.i)) {
959 0           return PDFMAKE_ENOMEM;
960             }
961 244 50         if ((size_t)size->as.i > p->xref_size) {
962 0           p->xref_size = (size_t)size->as.i;
963             }
964             }
965            
966             /* Check for /Prev - incremental update chain */
967 244           prev = pdfmake_dict_get(trailer, prev_id);
968 244 100         if (prev && prev->kind == PDFMAKE_INT) {
    50          
969 48           uint64_t prev_offset = (uint64_t)prev->as.i;
970             pdfmake_err_t err;
971             pdfmake_obj_t prev_trailer;
972            
973             /* Cycle detection */
974 48 100         if (prev_visited(p, prev_offset)) {
975 32           set_error(p, PDFMAKE_ECYCLE, 0, "Cycle detected in /Prev chain");
976 32           return PDFMAKE_ECYCLE;
977             }
978            
979 16 50         if (!prev_add(p, prev_offset)) {
980 0           return PDFMAKE_ENOMEM;
981             }
982            
983             /* Parse previous xref */
984 16           err = pdfmake_parse_xref_table(p, prev_offset);
985 16 50         if (err != PDFMAKE_OK) return err;
986            
987             /* Parse previous trailer */
988 16           err = pdfmake_parse_trailer(p, &prev_trailer);
989 16 50         if (err != PDFMAKE_OK) return err;
990             }
991            
992 212           return PDFMAKE_OK;
993             }
994              
995             /*============================================================================
996             * Xref stream parser (§7.5.8)
997             *==========================================================================*/
998              
999             /* Parsed shape of the xref-stream dictionary parameters. */
1000             typedef struct {
1001             int w1, w2, w3; /* /W field widths */
1002             int entry_size; /* sum of w1+w2+w3 */
1003             pdfmake_obj_t *index; /* /Index array (or NULL → default range) */
1004             int64_t size; /* /Size value (total object count) */
1005             } xref_stream_params_t;
1006              
1007             /* Extract /W, /Index and /Size from the xref-stream dict. Returns
1008             * PDFMAKE_EXREF if /W is missing or malformed; /Index and /Size are
1009             * optional (callers fall back to a default range). */
1010             static pdfmake_err_t
1011 0           xref_stream_extract_params(pdfmake_parser_t *p, uint64_t offset,
1012             pdfmake_obj_t *dict,
1013             xref_stream_params_t *out)
1014             {
1015 0           uint32_t w_id = intern_name(p, "W", 1);
1016 0           pdfmake_obj_t *w_arr = pdfmake_dict_get(dict, w_id);
1017             uint32_t index_id;
1018             uint32_t size_id;
1019             pdfmake_obj_t *size_obj;
1020            
1021 0 0         if (!w_arr || w_arr->kind != PDFMAKE_ARRAY || pdfmake_array_len(w_arr) < 3) {
    0          
    0          
1022 0           set_error(p, PDFMAKE_EXREF, offset, "Invalid /W array in xref stream");
1023 0           return PDFMAKE_EXREF;
1024             }
1025              
1026 0           out->w1 = (int)pdfmake_get_int(pdfmake_array_get(w_arr, 0));
1027 0           out->w2 = (int)pdfmake_get_int(pdfmake_array_get(w_arr, 1));
1028 0           out->w3 = (int)pdfmake_get_int(pdfmake_array_get(w_arr, 2));
1029 0           out->entry_size = out->w1 + out->w2 + out->w3;
1030              
1031 0           index_id = intern_name(p, "Index", 5);
1032 0           size_id = intern_name(p, "Size", 4);
1033 0           out->index = pdfmake_dict_get(dict, index_id);
1034              
1035 0           size_obj = pdfmake_dict_get(dict, size_id);
1036 0 0         out->size = (size_obj && size_obj->kind == PDFMAKE_INT) ? size_obj->as.i : 0;
    0          
1037              
1038 0           return PDFMAKE_OK;
1039             }
1040              
1041             /* Propagate /Root, /Info and /Encrypt references from the xref-stream dict
1042             * into the parser state. Mirrors the trailer dict handling used by classic
1043             * xref tables. The /Prev chain is handled by the orchestrator because it
1044             * recurses. */
1045             static void
1046 0           xref_stream_apply_trailer(pdfmake_parser_t *p, pdfmake_obj_t *dict)
1047             {
1048 0           uint32_t root_id = intern_name(p, "Root", 4);
1049 0           uint32_t info_id = intern_name(p, "Info", 4);
1050 0           uint32_t encrypt_id = intern_name(p, "Encrypt", 7);
1051             pdfmake_obj_t *root;
1052             pdfmake_obj_t *info;
1053             pdfmake_obj_t *encrypt;
1054              
1055 0           root = pdfmake_dict_get(dict, root_id);
1056 0 0         if (root && root->kind == PDFMAKE_REF) {
    0          
1057 0           p->root_num = root->as.ref.num;
1058 0           p->root_gen = root->as.ref.gen;
1059             }
1060              
1061 0           info = pdfmake_dict_get(dict, info_id);
1062 0 0         if (info && info->kind == PDFMAKE_REF) {
    0          
1063 0           p->info_num = info->as.ref.num;
1064 0           p->info_gen = info->as.ref.gen;
1065             }
1066              
1067 0           encrypt = pdfmake_dict_get(dict, encrypt_id);
1068 0 0         if (encrypt && encrypt->kind == PDFMAKE_REF) {
    0          
1069 0           p->encrypt_num = encrypt->as.ref.num;
1070 0           p->encrypt_gen = encrypt->as.ref.gen;
1071             }
1072 0           }
1073              
1074             /* Read one variable-width xref-stream entry at *pp (advancing it) and store
1075             * it into p->xref[obj_num] unless that slot is already loaded from a later
1076             * xref section. Called from both the /Index subsection loop and the
1077             * default-range loop, which are otherwise identical. */
1078             static PDFMAKE_INLINE void
1079 0           xref_stream_read_entry(pdfmake_parser_t *p,
1080             const uint8_t **pp,
1081             int w1, int w2, int w3,
1082             uint32_t obj_num)
1083             {
1084 0           const uint8_t *ptr = *pp;
1085 0           uint64_t field1 = 0, field2 = 0, field3 = 0;
1086             int j;
1087             int type;
1088              
1089 0 0         for (j = 0; j < w1; j++) field1 = (field1 << 8) | *ptr++;
1090 0 0         for (j = 0; j < w2; j++) field2 = (field2 << 8) | *ptr++;
1091 0 0         for (j = 0; j < w3; j++) field3 = (field3 << 8) | *ptr++;
1092 0           *pp = ptr;
1093              
1094             /* Default type is 1 when /W specifies a zero-width type field. */
1095 0 0         type = (w1 == 0) ? 1 : (int)field1;
1096              
1097 0 0         if (!p->xref[obj_num].loaded) {
1098 0           p->xref[obj_num].num = obj_num;
1099 0           switch (type) {
1100 0           case 0: /* Free object */
1101 0           p->xref[obj_num].type = PDFMAKE_XREF_FREE;
1102 0           p->xref[obj_num].loc.next_free = (uint32_t)field2;
1103 0           p->xref[obj_num].gen = (uint16_t)field3;
1104 0           break;
1105 0           case 1: /* Uncompressed object */
1106 0           p->xref[obj_num].type = PDFMAKE_XREF_UNCOMPRESSED;
1107 0           p->xref[obj_num].loc.offset = field2;
1108 0           p->xref[obj_num].gen = (uint16_t)field3;
1109 0           break;
1110 0           case 2: /* Compressed object */
1111 0           p->xref[obj_num].type = PDFMAKE_XREF_COMPRESSED;
1112 0           p->xref[obj_num].loc.compressed.obj_stm_num = (uint32_t)field2;
1113 0           p->xref[obj_num].loc.compressed.index = (uint32_t)field3;
1114 0           p->xref[obj_num].gen = 0;
1115 0           break;
1116             }
1117             }
1118              
1119 0 0         if (obj_num >= p->xref_size) {
1120 0           p->xref_size = obj_num + 1;
1121             }
1122 0           }
1123              
1124 20           pdfmake_err_t pdfmake_parse_xref_stream(pdfmake_parser_t *p, uint64_t offset) {
1125             pdfmake_tok_t num_tok;
1126             pdfmake_tok_t gen_tok;
1127             pdfmake_tok_t obj_tok;
1128             pdfmake_obj_t stream;
1129             pdfmake_obj_t stream_dict_obj;
1130             xref_stream_params_t params;
1131             pdfmake_err_t err;
1132             uint8_t *decoded_data;
1133             size_t decoded_len;
1134             const uint8_t *ptr;
1135             const uint8_t *end;
1136             uint32_t prev_id;
1137             pdfmake_obj_t *prev;
1138            
1139 20 50         if (offset >= p->buf_len) {
1140 0           set_error(p, PDFMAKE_EXREF, (size_t)offset, "Xref stream offset beyond EOF");
1141 0           return PDFMAKE_EXREF;
1142             }
1143            
1144             /* Position tokenizer at xref stream object */
1145 20           pdfmake_tokenizer_init(&p->tok, p->buf + offset, p->buf_len - offset);
1146            
1147             /* Parse as indirect object */
1148 20           num_tok = pdfmake_tok_next_significant(&p->tok);
1149 20 50         if (num_tok.kind != PDFMAKE_TOK_INT) {
1150 0           set_error(p, PDFMAKE_EXREF, offset, "Expected xref stream object number");
1151 0           return PDFMAKE_EXREF;
1152             }
1153            
1154 20           gen_tok = pdfmake_tok_next_significant(&p->tok);
1155 20 50         if (gen_tok.kind != PDFMAKE_TOK_INT) {
1156 0           set_error(p, PDFMAKE_EXREF, offset, "Expected xref stream generation");
1157 0           return PDFMAKE_EXREF;
1158             }
1159            
1160 20           obj_tok = pdfmake_tok_next_significant(&p->tok);
1161 20 50         if (obj_tok.kind != PDFMAKE_TOK_KW_OBJ) {
1162 20           set_error(p, PDFMAKE_EXREF, offset, "Expected 'obj' keyword");
1163 20           return PDFMAKE_EXREF;
1164             }
1165            
1166             /* Parse stream object */
1167 0           stream = parse_object_internal(p);
1168 0 0         if (p->last_err != PDFMAKE_OK) return p->last_err;
1169            
1170 0 0         if (stream.kind != PDFMAKE_STREAM) {
1171 0           set_error(p, PDFMAKE_EXREF, offset, "Xref stream is not a stream");
1172 0           return PDFMAKE_EXREF;
1173             }
1174            
1175             /* Wrap the stream dict so we can call pdfmake_dict_get on it. */
1176 0           stream_dict_obj.kind = PDFMAKE_DICT;
1177 0           stream_dict_obj.as.dict = stream.as.stream->dict;
1178              
1179 0           err = xref_stream_extract_params(p, offset, &stream_dict_obj, ¶ms);
1180 0 0         if (err != PDFMAKE_OK) return err;
1181              
1182             /* Decode stream data */
1183 0           err = pdfmake_decode_stream(p, stream.as.stream, &decoded_data, &decoded_len);
1184 0 0         if (err != PDFMAKE_OK) return err;
1185              
1186             /* Parse entries */
1187 0           ptr = decoded_data;
1188 0           end = decoded_data + decoded_len;
1189              
1190 0 0         if (params.index && params.index->kind == PDFMAKE_ARRAY) {
    0          
1191             /* Multiple subsections */
1192 0           size_t num_pairs = pdfmake_array_len(params.index) / 2;
1193             size_t pair;
1194 0 0         for (pair = 0; pair < num_pairs; pair++) {
1195 0           pdfmake_obj_t *first = pdfmake_array_get(params.index, pair * 2);
1196 0           pdfmake_obj_t *count = pdfmake_array_get(params.index, pair * 2 + 1);
1197             uint32_t first_num;
1198             uint32_t entry_count;
1199             uint32_t i;
1200 0 0         if (!first || !count) continue;
    0          
1201              
1202 0           first_num = (uint32_t)pdfmake_get_int(first);
1203 0           entry_count = (uint32_t)pdfmake_get_int(count);
1204              
1205 0 0         if (!ensure_xref_cap(p, first_num + entry_count)) return PDFMAKE_ENOMEM;
1206              
1207 0 0         for (i = 0; i < entry_count && ptr + params.entry_size <= end; i++) {
    0          
1208 0           xref_stream_read_entry(p, &ptr, params.w1, params.w2, params.w3,
1209             first_num + i);
1210             }
1211             }
1212             } else {
1213             /* Default: single subsection from 0 to Size-1 */
1214 0           uint32_t entry_count = (uint32_t)params.size;
1215             uint32_t obj_num;
1216 0 0         if (!ensure_xref_cap(p, entry_count)) return PDFMAKE_ENOMEM;
1217              
1218 0           for (obj_num = 0;
1219 0 0         obj_num < entry_count && ptr + params.entry_size <= end;
    0          
1220 0           obj_num++) {
1221 0           xref_stream_read_entry(p, &ptr, params.w1, params.w2, params.w3, obj_num);
1222             }
1223             }
1224              
1225             /* Propagate trailer-equivalent refs from the stream dict. */
1226 0           xref_stream_apply_trailer(p, &stream_dict_obj);
1227              
1228             /* Check for /Prev - incremental update chain */
1229 0           prev_id = intern_name(p, "Prev", 4);
1230 0           prev = pdfmake_dict_get(&stream_dict_obj, prev_id);
1231 0 0         if (prev && prev->kind == PDFMAKE_INT) {
    0          
1232 0           uint64_t prev_offset = (uint64_t)prev->as.i;
1233             pdfmake_tokenizer_t save_tok;
1234             pdfmake_err_t prev_err;
1235            
1236 0 0         if (prev_visited(p, prev_offset)) {
1237 0           set_error(p, PDFMAKE_ECYCLE, 0, "Cycle detected in /Prev chain");
1238 0           return PDFMAKE_ECYCLE;
1239             }
1240            
1241 0 0         if (!prev_add(p, prev_offset)) {
1242 0           return PDFMAKE_ENOMEM;
1243             }
1244            
1245             /* Could be classic xref or xref stream */
1246             /* Try xref stream first, fall back to classic */
1247 0           save_tok = p->tok;
1248 0           prev_err = pdfmake_parse_xref_stream(p, prev_offset);
1249 0 0         if (prev_err != PDFMAKE_OK) {
1250 0           p->tok = save_tok;
1251 0           prev_err = pdfmake_parse_xref_table(p, prev_offset);
1252 0 0         if (prev_err == PDFMAKE_OK) {
1253             pdfmake_obj_t prev_trailer;
1254 0           prev_err = pdfmake_parse_trailer(p, &prev_trailer);
1255             }
1256             }
1257 0 0         if (prev_err != PDFMAKE_OK) return prev_err;
1258             }
1259            
1260 0           return PDFMAKE_OK;
1261             }
1262              
1263             /*============================================================================
1264             * Stream decoding helper
1265             *==========================================================================*/
1266              
1267 181           pdfmake_err_t pdfmake_decode_stream(pdfmake_parser_t *p,
1268             pdfmake_stream_t *stream,
1269             uint8_t **out_data,
1270             size_t *out_len) {
1271             pdfmake_obj_t dict_obj;
1272             uint32_t filter_id;
1273             uint32_t parms_id;
1274             pdfmake_obj_t *filter;
1275             pdfmake_obj_t *parms;
1276             pdfmake_buf_t out;
1277             pdfmake_err_t err;
1278             size_t len;
1279             uint8_t *data;
1280            
1281 181 50         if (stream->filtered) {
1282             /* Already decoded - just return raw data */
1283 0           *out_data = (uint8_t *)stream->raw;
1284 0           *out_len = stream->raw_len;
1285 0           return PDFMAKE_OK;
1286             }
1287            
1288             /* Get /Filter and /DecodeParms from stream dict */
1289 181           dict_obj.kind = PDFMAKE_DICT;
1290 181           dict_obj.as.dict = stream->dict;
1291            
1292 181           filter_id = intern_name(p, "Filter", 6);
1293 181           parms_id = intern_name(p, "DecodeParms", 11);
1294            
1295 181           filter = pdfmake_dict_get(&dict_obj, filter_id);
1296 181           parms = pdfmake_dict_get(&dict_obj, parms_id);
1297            
1298 181 100         if (!filter) {
1299             /* No filter - raw data */
1300 88           *out_data = (uint8_t *)stream->raw;
1301 88           *out_len = stream->raw_len;
1302 88           return PDFMAKE_OK;
1303             }
1304            
1305             /* Decode through filter chain */
1306 93           pdfmake_buf_init(&out);
1307            
1308 93           err = pdfmake_filter_chain_decode(p->doc->arena, filter, parms,
1309 93           stream->raw, stream->raw_len, &out);
1310 93 50         if (err != PDFMAKE_OK) {
1311 0           pdfmake_buf_free(&out);
1312 0           set_error(p, err, 0, "Stream decode failed");
1313 0           return err;
1314             }
1315            
1316             /* Copy to arena */
1317 93           len = pdfmake_buf_len(&out);
1318 93           data = pdfmake_arena_alloc(p->doc->arena, len);
1319 93 50         if (!data) {
1320 0           pdfmake_buf_free(&out);
1321 0           return PDFMAKE_ENOMEM;
1322             }
1323 93           memcpy(data, pdfmake_buf_data(&out), len);
1324 93           pdfmake_buf_free(&out);
1325            
1326 93           *out_data = data;
1327 93           *out_len = len;
1328 93           return PDFMAKE_OK;
1329             }
1330              
1331             /*============================================================================
1332             * Repair mode (§7.5 fallback)
1333             *==========================================================================*/
1334              
1335 44           pdfmake_err_t pdfmake_repair_xref(pdfmake_parser_t *p) {
1336 44           size_t trailer_offset = 0;
1337            
1338             /* Scan entire file for "N G obj" patterns */
1339 44           pdfmake_tokenizer_init(&p->tok, p->buf, p->buf_len);
1340            
1341 2912           while (1) {
1342 2956           pdfmake_tok_t tok = pdfmake_tok_next_significant(&p->tok);
1343 2956 100         if (tok.kind == PDFMAKE_TOK_EOF) break;
1344            
1345 2912 100         if (tok.kind == PDFMAKE_TOK_INT) {
1346 1820           size_t save_pos = p->tok.pos;
1347             pdfmake_tok_t gen_tok;
1348            
1349 1820           gen_tok = pdfmake_tok_next_significant(&p->tok);
1350 1820 100         if (gen_tok.kind == PDFMAKE_TOK_INT) {
1351 1046           pdfmake_tok_t obj_tok = pdfmake_tok_next_significant(&p->tok);
1352 1046 100         if (obj_tok.kind == PDFMAKE_TOK_KW_OBJ) {
1353             /* Found object definition */
1354 256           uint32_t num = (uint32_t)tok.payload.int_val;
1355 256           uint16_t gen = (uint16_t)gen_tok.payload.int_val;
1356            
1357 256 50         if (ensure_xref_cap(p, num + 1)) {
1358 256           p->xref[num].num = num;
1359 256           p->xref[num].gen = gen;
1360 256           p->xref[num].type = PDFMAKE_XREF_UNCOMPRESSED;
1361 256           p->xref[num].loc.offset = tok.offset;
1362 256 100         if (num >= p->xref_size) {
1363 52           p->xref_size = num + 1;
1364             }
1365             }
1366            
1367             /* Skip to endobj */
1368 5801           while (1) {
1369 6057           pdfmake_tok_t skip = pdfmake_tok_next_significant(&p->tok);
1370 6057 100         if (skip.kind == PDFMAKE_TOK_KW_ENDOBJ ||
1371 5817 100         skip.kind == PDFMAKE_TOK_EOF) {
1372             break;
1373             }
1374             }
1375 256           continue;
1376             }
1377             }
1378            
1379             /* Not an object - restore position */
1380 1564           p->tok.pos = save_pos;
1381 1564           p->tok.has_peeked = 0;
1382             }
1383             }
1384            
1385             /* Look for trailer dict */
1386 44           pdfmake_tokenizer_init(&p->tok, p->buf, p->buf_len);
1387            
1388             /* Find last trailer keyword */
1389 17848           while (1) {
1390 17892           pdfmake_tok_t tok = pdfmake_tok_next(&p->tok);
1391 17892 100         if (tok.kind == PDFMAKE_TOK_EOF) break;
1392 17848 100         if (tok.kind == PDFMAKE_TOK_KW_TRAILER) {
1393 38           trailer_offset = tok.offset;
1394             }
1395             }
1396            
1397 44 100         if (trailer_offset > 0) {
1398             pdfmake_obj_t trailer;
1399             pdfmake_err_t trailer_err;
1400 38           pdfmake_tokenizer_init(&p->tok, p->buf + trailer_offset, p->buf_len - trailer_offset);
1401 38           p->last_err = PDFMAKE_OK; /* Clear any residual error from xref scan */
1402 38           trailer_err = pdfmake_parse_trailer(p, &trailer);
1403             (void)trailer_err; /* Ignore errors - best effort */
1404             }
1405            
1406 44           return PDFMAKE_OK;
1407             }
1408              
1409             /*============================================================================
1410             * Object resolution
1411             *==========================================================================*/
1412              
1413 11365           pdfmake_obj_t *pdfmake_parser_resolve(pdfmake_parser_t *p, pdfmake_ref_t ref) {
1414             pdfmake_xref_entry_t *entry;
1415            
1416 11365 100         if (ref.num >= p->xref_size) return NULL;
1417            
1418 11364           entry = &p->xref[ref.num];
1419 11364 50         if (entry->type == PDFMAKE_XREF_FREE) return NULL;
1420            
1421             /* Check generation */
1422 11364 50         if (entry->gen != ref.gen) return NULL;
1423            
1424             /* Already loaded? */
1425 11364 100         if (entry->loaded && ref.num < p->doc->obj_count) {
    100          
1426 10453           pdfmake_obj_t *obj = pdfmake_doc_get(p->doc, ref.num);
1427 10453 50         if (obj) return obj;
1428             }
1429            
1430             /* Load object */
1431 911 50         if (entry->type == PDFMAKE_XREF_UNCOMPRESSED) {
1432             /* Save tokenizer state */
1433 911           pdfmake_tokenizer_t save_tok = p->tok;
1434             pdfmake_err_t err;
1435            
1436             /* Position at object - use full buffer but set position */
1437 911           pdfmake_tokenizer_init(&p->tok, p->buf, p->buf_len);
1438 911           p->tok.pos = entry->loc.offset;
1439            
1440             /* Parse indirect object */
1441 911           err = pdfmake_parse_indirect_object(p);
1442            
1443             /* Restore tokenizer */
1444 911           p->tok = save_tok;
1445            
1446 911 50         if (err == PDFMAKE_OK) {
1447 911           entry->loaded = 1;
1448 911           return pdfmake_doc_get(p->doc, ref.num);
1449             }
1450             }
1451 0 0         else if (entry->type == PDFMAKE_XREF_COMPRESSED) {
1452             /* Object is in an object stream - need to decompress and parse */
1453             /* First resolve the object stream */
1454             pdfmake_ref_t stm_ref;
1455             pdfmake_obj_t *stm_obj;
1456             uint8_t *decoded;
1457             size_t decoded_len;
1458             pdfmake_obj_t stm_dict_obj;
1459             uint32_t n_id;
1460             uint32_t first_id;
1461             pdfmake_obj_t *n_obj;
1462             pdfmake_obj_t *first_obj;
1463             int64_t n, first;
1464             pdfmake_tokenizer_t stm_tok;
1465             uint32_t target_index;
1466             int64_t target_offset;
1467             int64_t i;
1468             pdfmake_tokenizer_t save_tok;
1469             pdfmake_obj_t obj;
1470             size_t idx;
1471            
1472 0           stm_ref.num = entry->loc.compressed.obj_stm_num;
1473 0           stm_ref.gen = 0;
1474 0           stm_obj = pdfmake_parser_resolve(p, stm_ref);
1475            
1476 0 0         if (!stm_obj || stm_obj->kind != PDFMAKE_STREAM) return NULL;
    0          
1477            
1478             /* Decode stream */
1479 0 0         if (pdfmake_decode_stream(p, stm_obj->as.stream, &decoded, &decoded_len) != PDFMAKE_OK) {
1480 0           return NULL;
1481             }
1482            
1483             /* Parse object stream header */
1484             /* Format: N1 offset1 N2 offset2 ... followed by objects */
1485 0           stm_dict_obj.kind = PDFMAKE_DICT;
1486 0           stm_dict_obj.as.dict = stm_obj->as.stream->dict;
1487            
1488 0           n_id = intern_name(p, "N", 1);
1489 0           first_id = intern_name(p, "First", 5);
1490            
1491 0           n_obj = pdfmake_dict_get(&stm_dict_obj, n_id);
1492 0           first_obj = pdfmake_dict_get(&stm_dict_obj, first_id);
1493            
1494 0 0         if (!n_obj || !first_obj) return NULL;
    0          
1495            
1496 0           n = pdfmake_get_int(n_obj);
1497 0           first = pdfmake_get_int(first_obj);
1498            
1499             /* Parse header (pairs of obj_num offset) */
1500 0           pdfmake_tokenizer_init(&stm_tok, decoded, decoded_len);
1501            
1502 0           target_index = entry->loc.compressed.index;
1503 0           target_offset = -1;
1504            
1505 0 0         for (i = 0; i < n; i++) {
1506 0           pdfmake_tok_t num_tok = pdfmake_tok_next_significant(&stm_tok);
1507 0           pdfmake_tok_t off_tok = pdfmake_tok_next_significant(&stm_tok);
1508            
1509 0 0         if (num_tok.kind != PDFMAKE_TOK_INT || off_tok.kind != PDFMAKE_TOK_INT) {
    0          
1510             break;
1511             }
1512            
1513 0 0         if ((uint32_t)i == target_index) {
1514 0           target_offset = first + off_tok.payload.int_val;
1515 0           break;
1516             }
1517             }
1518            
1519 0 0         if (target_offset < 0 || (size_t)target_offset >= decoded_len) {
    0          
1520 0           return NULL;
1521             }
1522            
1523             /* Parse object at offset */
1524 0           save_tok = p->tok;
1525 0           pdfmake_tokenizer_init(&p->tok, decoded + target_offset, decoded_len - target_offset);
1526            
1527 0           obj = parse_object_internal(p);
1528 0           p->tok = save_tok;
1529            
1530 0 0         if (p->last_err != PDFMAKE_OK) return NULL;
1531            
1532             /* Store in document (1-based numbering: object N at index N-1) */
1533 0 0         while (ref.num > p->doc->obj_cap) {
1534 0 0         size_t new_cap = p->doc->obj_cap ? p->doc->obj_cap * 2 : PDFMAKE_DOC_INIT_CAP;
1535             pdfmake_indirect_t *new_objs;
1536 0 0         while (new_cap < ref.num) new_cap *= 2;
1537 0           new_objs = realloc(p->doc->objects,
1538             new_cap * sizeof(pdfmake_indirect_t));
1539 0 0         if (!new_objs) return NULL;
1540 0           memset(new_objs + p->doc->obj_cap, 0,
1541 0           (new_cap - p->doc->obj_cap) * sizeof(pdfmake_indirect_t));
1542 0           p->doc->objects = new_objs;
1543 0           p->doc->obj_cap = new_cap;
1544             }
1545            
1546 0           idx = ref.num - 1;
1547 0           p->doc->objects[idx].num = ref.num;
1548 0           p->doc->objects[idx].gen = 0;
1549 0           p->doc->objects[idx].obj = obj;
1550 0           p->doc->objects[idx].in_use = 1;
1551            
1552 0 0         if (ref.num > p->doc->obj_count) {
1553 0           p->doc->obj_count = ref.num;
1554             }
1555            
1556 0           entry->loaded = 1;
1557 0           return &p->doc->objects[idx].obj;
1558             }
1559            
1560 0           return NULL;
1561             }
1562              
1563 0           pdfmake_obj_t *pdfmake_doc_resolve(pdfmake_doc_t *doc, pdfmake_parser_t *parser,
1564             uint32_t num, uint16_t gen) {
1565             pdfmake_ref_t ref;
1566             (void)doc; /* doc info is in parser->doc, used for consistency with other APIs */
1567 0           ref.num = num;
1568 0           ref.gen = gen;
1569 0           return pdfmake_parser_resolve(parser, ref);
1570             }
1571              
1572             /*============================================================================
1573             * Main parser entry point
1574             *==========================================================================*/
1575              
1576 218           pdfmake_err_t pdfmake_parser_run(pdfmake_parser_t *p, pdfmake_doc_t **out_doc) {
1577             pdfmake_err_t err;
1578             int major, minor;
1579             uint64_t xref_offset;
1580             pdfmake_tok_t first;
1581             pdfmake_obj_t trailer;
1582            
1583             /* Check header */
1584 218           err = pdfmake_check_header(p, &major, &minor);
1585 218 50         if (err != PDFMAKE_OK) return err;
1586            
1587             /* Locate startxref */
1588 218           err = pdfmake_locate_startxref(p, &xref_offset);
1589 218 100         if (err != PDFMAKE_OK) {
1590 6 50         if (p->repair) {
1591 6           err = pdfmake_repair_xref(p);
1592 6 50         if (err != PDFMAKE_OK) return err;
1593             } else {
1594 0           return err;
1595             }
1596             } else {
1597             /* Add to visited list for cycle detection */
1598 212 50         if (!prev_add(p, xref_offset)) {
1599 0           return PDFMAKE_ENOMEM;
1600             }
1601            
1602             /* Try xref stream first (common in modern PDFs) */
1603             /* Check what's at the offset */
1604 212 50         if (xref_offset < p->buf_len) {
1605 212           pdfmake_tokenizer_init(&p->tok, p->buf + xref_offset, p->buf_len - xref_offset);
1606 212           pdfmake_tok_peek_significant(&p->tok, &first);
1607            
1608 212 100         if (first.kind == PDFMAKE_TOK_INT) {
1609             /* Xref stream */
1610 20           err = pdfmake_parse_xref_stream(p, xref_offset);
1611 192 100         } else if (first.kind == PDFMAKE_TOK_KW_XREF) {
1612             /* Classic xref */
1613 190           err = pdfmake_parse_xref_table(p, xref_offset);
1614 190 50         if (err == PDFMAKE_OK) {
1615 190           err = pdfmake_parse_trailer(p, &trailer);
1616             }
1617             } else {
1618 2           err = PDFMAKE_EXREF;
1619 2           set_error(p, err, (size_t)xref_offset, "Invalid xref location");
1620             }
1621            
1622 212 100         if (err != PDFMAKE_OK) {
1623 38 50         if (p->repair) {
1624 38           err = pdfmake_repair_xref(p);
1625 38 50         if (err != PDFMAKE_OK) return err;
1626             } else {
1627 0           return err;
1628             }
1629             }
1630             } else {
1631             /* Offset beyond file - try repair if enabled */
1632 0 0         if (p->repair) {
1633 0           err = pdfmake_repair_xref(p);
1634 0 0         if (err != PDFMAKE_OK) return err;
1635             } else {
1636 0           set_error(p, PDFMAKE_EXREF, (size_t)xref_offset, "Xref offset beyond EOF");
1637 0           return PDFMAKE_EXREF;
1638             }
1639             }
1640             }
1641            
1642             /* Set document trailer refs */
1643 218 100         if (p->root_num > 0) {
1644 212           pdfmake_doc_set_root(p->doc, p->root_num, p->root_gen);
1645             }
1646 218 100         if (p->info_num > 0) {
1647 156           pdfmake_doc_set_info(p->doc, p->info_num, p->info_gen);
1648             }
1649            
1650 218           *out_doc = p->doc;
1651 218           return PDFMAKE_OK;
1652             }