@patrickdk77/pdf-defuse 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/document.js CHANGED
@@ -60,14 +60,17 @@ function repeatsANumber(ranges) {
60
60
  /** Entries are packed into one number each, so the index costs a few bytes per object. */
61
61
  const packOffset = (offset, gen) => offset * 65536 + (gen & 0xffff);
62
62
  exports.packOffset = packOffset;
63
- const packStream = (stream) => -stream - 1;
63
+ /** Index values from here up stand for an index too large to keep, which leads to no object. */
64
+ const INDEX_SPAN = 2 ** 21;
65
+ const packStream = (stream, index) => -(Math.min(stream, 2 ** 32 - 1) * INDEX_SPAN + Math.min(index, INDEX_SPAN - 1)) - 1;
64
66
  exports.packStream = packStream;
65
67
  function unpack(v) {
66
68
  if (v === undefined)
67
69
  return undefined;
68
70
  if (v >= 0)
69
71
  return { type: 1, offset: Math.floor(v / 65536), gen: v % 65536 };
70
- return { type: 2, stream: -v - 1 };
72
+ const x = -v - 1;
73
+ return { type: 2, stream: Math.floor(x / INDEX_SPAN), index: x % INDEX_SPAN };
71
74
  }
72
75
  class OpenError extends Error {
73
76
  kind;
@@ -88,6 +91,9 @@ class DecodeCapError extends Error {
88
91
  class ObjectLimitError extends Error {
89
92
  }
90
93
  exports.ObjectLimitError = ObjectLimitError;
94
+ /** Where pdf.js throws XRefEntryException: an entry that points at another object. pdf.js then rebuilds the map. */
95
+ class BrokenEntry extends Error {
96
+ }
91
97
  /** Holds at most `max` entries, whose weights add up to at most `maxWeight`. */
92
98
  class Lru {
93
99
  max;
@@ -106,6 +112,17 @@ class Lru {
106
112
  }
107
113
  return e?.value;
108
114
  }
115
+ delete(k) {
116
+ const e = this.map.get(k);
117
+ if (e === undefined)
118
+ return undefined;
119
+ this.map.delete(k);
120
+ this.weight -= e.weight;
121
+ return e.value;
122
+ }
123
+ values() {
124
+ return Array.from(this.map.values(), e => e.value);
125
+ }
109
126
  /** Returns the last entry pushed out, if any. An entry heavier than the whole cache is kept alone. */
110
127
  set(k, value, weight = 0) {
111
128
  const old = this.map.get(k);
@@ -132,6 +149,88 @@ const objectCache = () => new Lru(2048, 32 * 1024 * 1024);
132
149
  const enc = (s) => Buffer.from(s, 'latin1');
133
150
  const OBJ = enc('obj');
134
151
  const view = (u) => Buffer.from(u.buffer, u.byteOffset, u.byteLength);
152
+ /** pdf.js takes a /Root that is a dictionary written in the trailer as well as a reference to one. */
153
+ const hasRoot = (trailer) => {
154
+ const r = trailer.get('Root');
155
+ return r instanceof objects_1.PdfRef || r instanceof objects_1.PdfDict;
156
+ };
157
+ /** How many streams written inside one object may take their /Length from another object. */
158
+ const MAX_INDIRECT_LENGTHS = 8;
159
+ /**
160
+ * The heap that the dictionaries of streams written inside objects may hold for the whole run. An object whose streams
161
+ * would pass it reads as damaged, as one too large to parse does.
162
+ */
163
+ const MAX_SYNTHETIC_COST = 32 * 1024 * 1024;
164
+ /** Runs of whitespace one parse may leave out of its window before the window grows over them instead. */
165
+ const MAX_SKIPS = 64;
166
+ const SKIP_CHUNK = 262144;
167
+ let pads;
168
+ const padding = () => {
169
+ pads ??= { spaces: Buffer.alloc(SKIP_CHUNK, 0x20), zeros: Buffer.alloc(SKIP_CHUNK) };
170
+ return pads;
171
+ };
172
+ const ENDSTREAM = enc('endstream');
173
+ /** Nodes the page lookups visit or queue. A page tree can name one array of kids from inside each of its kids. */
174
+ const MAX_PAGE_STEPS = 200_000;
175
+ const refKey = (r) => `${r.num} ${r.gen}`;
176
+ /** JavaScript's \s, as pdf.js's recovery patterns match it against a file read as Latin-1. */
177
+ const jsSpace = (c) => c === 0x20 || (c >= 0x09 && c <= 0x0d) || c === 0xa0;
178
+ /** JavaScript's \w, whose edges are the \b of those patterns. Outside the data counts as no word. */
179
+ const jsWord = (c) => (c >= 0x30 && c <= 0x39) || (c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a) || c === 0x5f;
180
+ const isDigitByte = (c) => c >= 0x30 && c <= 0x39;
181
+ /** A run of digits as pdf.js turns it into an object number, with JavaScript's `| 0`. */
182
+ function int32(digits) {
183
+ const d = digits.replace(/^0+/, '');
184
+ return (d.length > 400 ? Number.POSITIVE_INFINITY : Number(d || '0')) | 0;
185
+ }
186
+ /** A file read forward in large chunks for the recovery scan, keeping a few bytes before the current position. */
187
+ class Bytes {
188
+ source;
189
+ size;
190
+ onRead;
191
+ buf = new Uint8Array(0);
192
+ at = 0;
193
+ constructor(source, size, onRead) {
194
+ this.source = source;
195
+ this.size = size;
196
+ this.onRead = onRead;
197
+ }
198
+ /** Whether [pos - 64, pos + ahead) is held, as far as the file goes. */
199
+ has(pos, ahead) {
200
+ return pos >= this.at && (pos - 64 >= this.at || this.at === 0) && Math.min(pos + ahead, this.size) <= this.at + this.buf.length;
201
+ }
202
+ async load(pos, ahead) {
203
+ this.onRead();
204
+ this.at = Math.max(0, pos - 64);
205
+ this.buf = await this.source.read(this.at, Math.min(Math.max(1 << 20, pos - this.at + ahead), this.size - this.at));
206
+ }
207
+ /** The byte at `pos`, which has() said is held, or -1 outside the file. */
208
+ get(pos) {
209
+ return pos < 0 || pos >= this.size ? -1 : this.buf[pos - this.at];
210
+ }
211
+ /** Whether `word` starts at `pos`, which has() said is held with room for it. */
212
+ is(pos, word) {
213
+ for (let i = 0; i < word.length; i++)
214
+ if (this.get(pos + i) !== word.charCodeAt(i))
215
+ return false;
216
+ return true;
217
+ }
218
+ /** The first `needle` in [pos, end), or `end`. */
219
+ async find(pos, needle, end = this.size) {
220
+ for (let p = pos; p + needle.length <= end;) {
221
+ if (!this.has(p, needle.length))
222
+ await this.load(p, needle.length);
223
+ const stop = Math.min(end, this.at + this.buf.length);
224
+ const k = view(this.buf.subarray(0, stop - this.at)).indexOf(needle, p - this.at);
225
+ if (k >= 0)
226
+ return this.at + k;
227
+ if (stop >= end)
228
+ break;
229
+ p = Math.max(p + 1, stop - needle.length + 1);
230
+ }
231
+ return end;
232
+ }
233
+ }
135
234
  /** A PDF opened for random access. Objects are read on demand and never all held at once. */
136
235
  class PdfDocument {
137
236
  reader;
@@ -139,6 +238,8 @@ class PdfDocument {
139
238
  opts;
140
239
  headerOffset = 0;
141
240
  headerVersion = '1.4';
241
+ /** No "%PDF-" in the first 1024 bytes, where pdf.js looks for one before it parses the file all the same. */
242
+ missingHeader = false;
142
243
  /** Added to every offset when the file has bytes before its header and the xref ignores them. */
143
244
  base = 0;
144
245
  /** Object number to packed entry. */
@@ -163,15 +264,53 @@ class PdfDocument {
163
264
  badXRefStm: 0,
164
265
  xrefStmOverFree: 0,
165
266
  badObjStm: new Set(),
267
+ objStmType: new Set(),
268
+ directStreams: new Set(),
269
+ directRoot: false,
166
270
  refGen: new Set(),
271
+ objStmStreams: new Set(),
167
272
  memoryFallback: [],
168
273
  };
169
- /** Body offsets of the streams already counted in issues.streamLengthWrong, so a second read adds nothing. */
274
+ /**
275
+ * Objects with no entry of their own: a catalog written in the trailer, and streams written inside other objects.
276
+ * Their numbers are negative, so no object of the file can take one, and the writer gives each a number of its own.
277
+ * `key` names the object whose key decrypts a stream, and is null for one that is never encrypted.
278
+ */
279
+ synthetic = new Map();
280
+ /** Synthetic numbers by the object that holds the stream and where its body starts, so a second read reuses one. */
281
+ syntheticAt = new Map();
282
+ /** The walker numbers the name trees it writes -1 and -2. */
283
+ nextSynthetic = -16;
284
+ /** Streams whose bodies sit in a decoded object stream, by object or synthetic number, and where they sit there. */
285
+ heldBodies = new Map();
286
+ /** What the streams inside each object hold, by that object, and the total, which MAX_SYNTHETIC_COST bounds. */
287
+ syntheticCost = new Map();
288
+ syntheticTotal = 0;
289
+ /** Values of the indirect /Length entries of streams written inside objects. */
290
+ directLengths = new Map();
291
+ /** The streams already counted in issues.streamLengthWrong, so a second read adds nothing. */
170
292
  lengthWrongAt = new Set();
171
293
  objCache = objectCache();
172
- objStmCache = new Lru(8);
173
- /** Object streams that spilled to a temporary file. Kept for the life of the document, so each is decoded once. */
174
- spilledObjStm = new Map();
294
+ /**
295
+ * The object streams decoded since the map or the key last changed, or null for one that does not read. Each is
296
+ * decoded once: memory holds them up to memoryThreshold, and the rest go to temporary files.
297
+ */
298
+ objStms = new Map();
299
+ /** The object streams held in memory, least recently used first, and the bytes they take. */
300
+ inMemory = new Set();
301
+ inMemoryBytes = 0;
302
+ /** The temporary file that takes the object streams memory cannot hold, its length, and one handle that reads it. */
303
+ pool;
304
+ /** Temporary files of object streams too large for memory. */
305
+ objStmFiles = [];
306
+ /**
307
+ * Readers over decoded object streams, so each keeps its blocks between lookups. Readers over a file of their own
308
+ * hold a file handle each, so fewer of them stay open.
309
+ */
310
+ objStmReaders = new Lru(256);
311
+ objStmFileReaders = new Lru(8);
312
+ /** By object stream, the objects whose entries point into it and the index each entry gives. */
313
+ wanted;
175
314
  /** Object streams found by scan(), indexed once the key exists. */
176
315
  pendingObjStm = [];
177
316
  /** Streams whose indirect /Length or filter entries are being resolved, so a cycle ends after one lap. */
@@ -199,6 +338,12 @@ class PdfDocument {
199
338
  /** Where the objects whose bad tokens are already in issues.malformed were read, so a second read adds nothing. */
200
339
  badTokensCounted = new Set();
201
340
  scanned = false;
341
+ /** The first trailer any cross-reference section gave, which pdf.js falls back on when its recovery finds none. */
342
+ topDict;
343
+ /** Set once recovery has looked past what pdf.js finds. */
344
+ extended = false;
345
+ /** What a read of every object of the rebuilt map found, kept until the map is rebuilt again. */
346
+ survey;
202
347
  /** Offsets of the xref sections and xref streams that read, which are not ordinary object definitions. */
203
348
  structuralOffsets = new Set();
204
349
  /** The definitions that older xref sections use and newer ones replaced or freed, as object number then offset. */
@@ -209,6 +354,8 @@ class PdfDocument {
209
354
  /** The extent of the live object insideLiveObject() read last, empty when it does not parse. */
210
355
  lastLive;
211
356
  unboundedScanBudget = 0;
357
+ /** The entries the page lookups found broken or sound, while they run. */
358
+ checkedEntries;
212
359
  /** Bytes parseWindowed() may still read past the next known object. */
213
360
  parseBudget = 0;
214
361
  constructor(reader, size, opts) {
@@ -240,12 +387,25 @@ class PdfDocument {
240
387
  this.parseBudget = this.size * 4;
241
388
  const head = await this.reader.read(0, Math.min(this.size, 1024));
242
389
  const h = view(head).indexOf(enc('%PDF-'));
243
- if (h < 0)
244
- throw new OpenError('not-pdf', 'No PDF header');
245
- this.headerOffset = h;
246
- const m = /^%PDF-(\d+\.\d+)/.exec(Buffer.from(head.subarray(h, h + 16)).toString('latin1'));
247
- if (m)
248
- this.headerVersion = m[1];
390
+ if (h >= 0) {
391
+ this.headerOffset = h;
392
+ const m = /^%PDF-(\d+\.\d+)/.exec(Buffer.from(head.subarray(h, h + 16)).toString('latin1'));
393
+ if (m)
394
+ this.headerVersion = m[1];
395
+ }
396
+ else
397
+ this.missingHeader = true;
398
+ try {
399
+ await this.load();
400
+ }
401
+ catch (e) {
402
+ // pdf.js parses a file without a header all the same. One whose structure does not read either is not a PDF.
403
+ if (this.missingHeader && e instanceof OpenError)
404
+ throw new OpenError('not-pdf', `No PDF header, and ${e.message.replace(/^No /, 'no ')}`);
405
+ throw e;
406
+ }
407
+ }
408
+ async load() {
249
409
  const tailLen = Math.min(this.size, 4096);
250
410
  const tail = await this.reader.read(this.size - tailLen, tailLen);
251
411
  const eof = view(tail).lastIndexOf(enc('%%EOF'));
@@ -264,7 +424,7 @@ class PdfDocument {
264
424
  if (mm) {
265
425
  try {
266
426
  await this.loadXrefChain(Number(mm[1]));
267
- ok = this.trailer.get('Root') instanceof objects_1.PdfRef;
427
+ ok = hasRoot(this.trailer);
268
428
  }
269
429
  catch (e) {
270
430
  if (e instanceof TimeLimitError || e instanceof ObjectLimitError)
@@ -283,27 +443,204 @@ class PdfDocument {
283
443
  // Without the key, encrypted object streams are unreadable, and the catalog or page tree may live in one.
284
444
  if (this.securityResult && this.securityResult.status !== 'ok')
285
445
  return;
286
- await this.indexObjStms();
287
446
  this.unboundedScanBudget = this.size * 4;
288
- let root = await this.resolve(this.trailer.get('Root'));
289
- if (!(root instanceof objects_1.PdfDict) && !this.scanned) {
290
- // The xref chain read, but its catalog did not: rebuild once by scanning.
447
+ let { root, pages } = await this.catalog();
448
+ // The xref chain read, but pdf.js would not trust it: rebuild once by scanning. pdf.js does when the catalog or its
449
+ // page tree does not resolve, and when an entry it reads looking for the first or the last page is broken.
450
+ if (!this.scanned && (!(root instanceof objects_1.PdfDict) || !(pages instanceof objects_1.PdfDict) || (await this.pageEntriesBroken(root, pages)))) {
291
451
  this.xref.clear();
292
- const trailer = this.trailer;
293
452
  await this.scan();
294
- if (!(this.trailer.get('Root') instanceof objects_1.PdfRef))
295
- this.trailer = trailer;
453
+ await this.initSecurity();
454
+ if (this.securityResult && this.securityResult.status !== 'ok')
455
+ return;
456
+ ({ root, pages } = await this.catalog());
457
+ }
458
+ // pdf.js's recovery found no catalog and page tree, and opens nothing. Other readers repair further.
459
+ if (this.scanned && !this.extended && (!(root instanceof objects_1.PdfDict) || !(pages instanceof objects_1.PdfDict))) {
460
+ await this.scanFurther();
296
461
  await this.initSecurity();
297
462
  if (this.securityResult && this.securityResult.status !== 'ok')
298
463
  return;
299
464
  await this.indexObjStms();
300
- root = await this.resolve(this.trailer.get('Root'));
465
+ ({ root, pages } = await this.catalog());
301
466
  }
302
467
  if (!(root instanceof objects_1.PdfDict))
303
468
  throw new OpenError(this.hasEof ? 'unparseable' : 'truncated', 'No document catalog');
304
- const pages = await this.resolve(root.get('Pages'));
305
469
  if (!(pages instanceof objects_1.PdfDict))
306
470
  throw new OpenError(this.hasEof ? 'unparseable' : 'truncated', 'No page tree');
471
+ // The walk and the writer reach the catalog by reference, so one written in the trailer gets a number. The trailer
472
+ // is never encrypted, and neither is a stream written inside it.
473
+ const direct = this.trailer.get('Root');
474
+ if (direct instanceof objects_1.PdfDict) {
475
+ this.issues.directRoot = true;
476
+ await this.lift(direct, null, 'trailer');
477
+ this.trailer.set('Root', new objects_1.PdfRef(this.addSynthetic('trailer', direct, null), 0));
478
+ }
479
+ }
480
+ async catalog() {
481
+ const root = await this.resolve(this.trailer.get('Root'));
482
+ return { root, pages: root instanceof objects_1.PdfDict ? await this.resolve(root.get('Pages')) : undefined };
483
+ }
484
+ /**
485
+ * Whether pdf.js rebuilds the map when it checks the first and the last page, as its checkFirstPage and checkLastPage
486
+ * do: an entry it reads on the way points at another object. When the last page is not where /Count puts it, pdf.js
487
+ * reads every page.
488
+ */
489
+ async pageEntriesBroken(root, pages) {
490
+ const top = root.get('Pages');
491
+ // pdf.js keeps how many pages each node holds from one lookup to the next.
492
+ const counts = new Map();
493
+ this.checkedEntries = new Map();
494
+ try {
495
+ await this.findPage(top, pages, 0, counts);
496
+ const count = await this.fetch(pages.get('Count'));
497
+ if (typeof count === 'number' && Number.isInteger(count) && (count <= 1 || (await this.findPage(top, pages, count - 1, counts))))
498
+ return false;
499
+ await this.readAllPages(top, pages);
500
+ return false;
501
+ }
502
+ catch (e) {
503
+ if (e instanceof BrokenEntry)
504
+ return true;
505
+ throw e;
506
+ }
507
+ finally {
508
+ this.checkedEntries = undefined;
509
+ }
510
+ }
511
+ /**
512
+ * Looks for page `index` as pdf.js's getPageDict does, which skips a node whose /Count says the page is not under it.
513
+ * False where pdf.js fails for another reason, such as a node it already visited.
514
+ */
515
+ async findPage(topRef, top, index, counts) {
516
+ // `id` names the object a dictionary was read from, under which pdf.js keeps its /Count.
517
+ const stack = [{ node: top, id: topRef instanceof objects_1.PdfRef ? refKey(topRef) : undefined }];
518
+ const seen = new Set();
519
+ if (topRef instanceof objects_1.PdfRef)
520
+ seen.add(refKey(topRef));
521
+ let current = 0;
522
+ for (let steps = 0; steps < MAX_PAGE_STEPS; steps++) {
523
+ this.checkTime();
524
+ const item = stack.pop();
525
+ if (item === undefined)
526
+ return false;
527
+ const { node, id } = item;
528
+ if (node instanceof objects_1.PdfRef) {
529
+ const k = refKey(node);
530
+ const known = counts.get(k);
531
+ if (known !== undefined && current + known <= index) {
532
+ current += known;
533
+ continue;
534
+ }
535
+ if (seen.has(k))
536
+ return false;
537
+ seen.add(k);
538
+ const obj = await this.fetch(node);
539
+ if (obj instanceof objects_1.PdfDict && (await this.isPageNode(obj))) {
540
+ if (!counts.has(k))
541
+ counts.set(k, 1);
542
+ if (current++ === index)
543
+ return true;
544
+ }
545
+ else
546
+ stack.push({ node: obj, id: k });
547
+ continue;
548
+ }
549
+ if (!(node instanceof objects_1.PdfDict))
550
+ return false;
551
+ const count = await this.fetch(node.get('Count'));
552
+ if (typeof count === 'number' && Number.isInteger(count) && count >= 0) {
553
+ if (id !== undefined && !counts.has(id))
554
+ counts.set(id, count);
555
+ if (current + count <= index) {
556
+ current += count;
557
+ continue;
558
+ }
559
+ }
560
+ const kids = await this.fetch(node.get('Kids'));
561
+ if (Array.isArray(kids)) {
562
+ steps += kids.length;
563
+ for (let i = kids.length - 1; i >= 0; i--)
564
+ stack.push({ node: kids[i] });
565
+ }
566
+ else if (!(await this.isPageNode(node)))
567
+ return false;
568
+ else if (current++ === index)
569
+ return true;
570
+ }
571
+ return false;
572
+ }
573
+ /** Reads every page as pdf.js's getAllPageDicts does, which stops at the first node it cannot read. */
574
+ async readAllPages(topRef, top) {
575
+ const queue = [{ node: top, at: 0 }];
576
+ const seen = new Set();
577
+ if (topRef instanceof objects_1.PdfRef)
578
+ seen.add(refKey(topRef));
579
+ for (let steps = 0; steps < MAX_PAGE_STEPS; steps++) {
580
+ this.checkTime();
581
+ const item = queue.at(-1);
582
+ if (item === undefined)
583
+ return;
584
+ const kids = await this.fetch(item.node.get('Kids'));
585
+ if (!Array.isArray(kids)) {
586
+ await this.isPageNode(item.node);
587
+ return;
588
+ }
589
+ if (item.at >= kids.length) {
590
+ queue.pop();
591
+ continue;
592
+ }
593
+ let kid = kids[item.at++];
594
+ if (kid instanceof objects_1.PdfRef) {
595
+ if (seen.has(refKey(kid)))
596
+ return;
597
+ seen.add(refKey(kid));
598
+ kid = await this.fetch(kid);
599
+ }
600
+ if (!(kid instanceof objects_1.PdfDict))
601
+ return;
602
+ if (!(await this.isPageNode(kid)))
603
+ queue.push({ node: kid, at: 0 });
604
+ }
605
+ }
606
+ /** A node typed /Page, or any node without /Kids, is a page to pdf.js. */
607
+ async isPageNode(d) {
608
+ const type = await this.fetch(d.get('Type'));
609
+ return (type instanceof objects_1.PdfName && type.name === 'Page') || !d.has('Kids');
610
+ }
611
+ /** Reads a value one level deep, as pdf.js reads one on its way to a page. Throws BrokenEntry where pdf.js would. */
612
+ async fetch(v) {
613
+ if (!(v instanceof objects_1.PdfRef))
614
+ return v;
615
+ let broken = this.checkedEntries?.get(refKey(v));
616
+ if (broken === undefined) {
617
+ broken = await this.entryBroken(v);
618
+ this.checkedEntries?.set(refKey(v), broken);
619
+ }
620
+ if (broken)
621
+ throw new BrokenEntry();
622
+ return this.getObject(v);
623
+ }
624
+ /**
625
+ * Whether an entry points at another object or at nothing that reads as an object header. pdf.js refuses such an
626
+ * entry, and an entry whose generation differs from the reference, before it reads the object. A missing entry
627
+ * reads as null instead. An object stream's entry is left out: pdf.js finds the object by its index there.
628
+ */
629
+ async entryBroken(ref) {
630
+ const entry = unpack(this.xref.get(ref.num));
631
+ if (entry?.type !== 1)
632
+ return false;
633
+ if (entry.gen !== ref.gen)
634
+ return true;
635
+ try {
636
+ const head = await this.parseWindowed(this.reader, this.size, entry.offset + this.base, p => p.parseObjectHeader());
637
+ return head.num !== ref.num || head.gen !== ref.gen;
638
+ }
639
+ catch (e) {
640
+ if (e instanceof parser_1.ParseError || e instanceof RangeError)
641
+ return true;
642
+ throw e;
643
+ }
307
644
  }
308
645
  async initSecurity() {
309
646
  const e = this.trailer.get('Encrypt');
@@ -353,7 +690,7 @@ class PdfDocument {
353
690
  if (r.status === 'ok')
354
691
  this.security = r.handler;
355
692
  // Objects and object streams read before the key existed hold ciphertext.
356
- this.objCache = objectCache();
693
+ this.forgetParsed();
357
694
  await this.dropObjStms();
358
695
  }
359
696
  // ---------- cross-reference ----------
@@ -384,12 +721,15 @@ class PdfDocument {
384
721
  if (firstTrailer) {
385
722
  // pdf.js reads /Root from the newest trailer only, and rebuilds the map by scanning when it is missing.
386
723
  this.trailer = section.trailer;
724
+ this.topDict ??= section.trailer;
387
725
  firstTrailer = false;
388
726
  }
389
727
  else if (!this.trailer.has('Info') && info !== undefined)
390
728
  this.trailer.set('Info', info);
729
+ // pdf.js follows a /Prev that is a whole number and stops at any other. One before the start of the file fails
730
+ // to read here, as in pdf.js and qpdf, and the map is rebuilt by scanning.
391
731
  const prev = section.trailer.get('Prev');
392
- offset = typeof prev === 'number' && !seen.has(prev) ? prev : undefined;
732
+ offset = typeof prev === 'number' && Number.isInteger(prev) && !seen.has(prev) ? prev : undefined;
393
733
  // Free rows matter only to older sections. Each is held one number at a time, so they may not outnumber the
394
734
  // live objects by much: a few bytes of xref stream can free millions of numbers.
395
735
  if (offset === undefined)
@@ -407,8 +747,11 @@ class PdfDocument {
407
747
  // Offsets cached while the map was still loading would bound every later endstream search by the end of the file.
408
748
  this.sortedOffsets = undefined;
409
749
  }
410
- /** Reads one section; for hybrid files its XRefStm entries add the objects the table does not give. */
411
- async readXrefSection(offset) {
750
+ /**
751
+ * Reads one section; for hybrid files its XRefStm entries add the objects the table does not give. Without `rebase`,
752
+ * an offset that misses is not tried again past bytes before the header.
753
+ */
754
+ async readXrefSection(offset, rebase = true) {
412
755
  const attempt = async (off) => {
413
756
  const peek = await this.reader.read(off, 16);
414
757
  const s = Buffer.from(peek).toString('latin1');
@@ -421,7 +764,7 @@ class PdfDocument {
421
764
  result = await attempt(offset + this.base);
422
765
  }
423
766
  catch (e) {
424
- if (this.headerOffset > 0 && this.base === 0) {
767
+ if (rebase && this.headerOffset > 0 && this.base === 0) {
425
768
  this.base = this.headerOffset;
426
769
  result = await attempt(offset + this.base);
427
770
  }
@@ -430,8 +773,9 @@ class PdfDocument {
430
773
  }
431
774
  // Recorded only once the section reads, so an offset that holds something else cannot pass for one.
432
775
  this.structuralOffsets.add(result.at);
776
+ // pdf.js and qpdf read only a whole-number /XRefStm.
433
777
  const stm = result.trailer.get('XRefStm');
434
- if (typeof stm === 'number') {
778
+ if (typeof stm === 'number' && Number.isInteger(stm)) {
435
779
  try {
436
780
  const extra = await this.readXrefStream(stm + this.base);
437
781
  this.structuralOffsets.add(extra.at);
@@ -451,9 +795,11 @@ class PdfDocument {
451
795
  else
452
796
  hi = mid;
453
797
  }
798
+ // pdf.js keeps the table's free row, and qpdf takes the stream's. Either way the rewrite leaves one reading.
454
799
  if (lo > 0 && num < ends[lo - 1])
455
800
  this.issues.xrefStmOverFree++;
456
- result.entries.set(num, e);
801
+ else
802
+ result.entries.set(num, e);
457
803
  }
458
804
  }
459
805
  catch (e) {
@@ -501,7 +847,8 @@ class PdfDocument {
501
847
  const t = await token();
502
848
  if (t === 'trailer' || t.startsWith('trailer')) {
503
849
  const at = pos + i - (t.length - 'trailer'.length);
504
- const obj = await this.parseWindowed(this.reader, this.size, at, p => p.parseObject());
850
+ // pdf.js reads a stream written inside the trailer, as in a catalog written there.
851
+ const obj = await this.parseWindowed(this.reader, this.size, at, p => p.parseObject(), undefined, undefined, { streams: true });
505
852
  if (!(obj instanceof objects_1.PdfDict))
506
853
  throw new parser_1.ParseError('Bad trailer');
507
854
  if (repeatsANumber(ranges))
@@ -613,6 +960,8 @@ class PdfDocument {
613
960
  let r = 0;
614
961
  let k = 0;
615
962
  let have = 0;
963
+ // pdf.js reads none of a Brotli stream that fails at its end, so one is decoded to its end.
964
+ const whole = (0, filters_1.filtersOf)(obj.dict).some(f => f.name === 'BrotliDecode');
616
965
  // Rows are read as the stream decodes, and decoding stops once /Index is filled, so trailing data costs nothing.
617
966
  rows: for await (const c of (0, filters_1.decodeChunks)(this.reader.chunks(obj.offset, obj.length), obj.dict, this.opts.decompressedBytes, () => this.checkTime())) {
618
967
  this.checkTime();
@@ -621,6 +970,8 @@ class PdfDocument {
621
970
  r += 2;
622
971
  k = 0;
623
972
  }
973
+ if (r >= ranges.length && whole)
974
+ continue rows;
624
975
  if (r >= ranges.length)
625
976
  break rows;
626
977
  let b = c;
@@ -646,7 +997,7 @@ class PdfDocument {
646
997
  if (t > 2)
647
998
  throw new parser_1.ParseError('Bad xref stream entry type');
648
999
  if (t === 2)
649
- entries.set(num, (0, exports.packStream)(off));
1000
+ entries.set(num, (0, exports.packStream)(off, field(b, p + w1 + w2, w3, 0)));
650
1001
  else if (t === 1)
651
1002
  entries.set(num, (0, exports.packOffset)(off, field(b, p + w1 + w2, w3, 0)));
652
1003
  else if (free.length && free[free.length - 1] === num)
@@ -660,14 +1011,428 @@ class PdfDocument {
660
1011
  throw new parser_1.ParseError('xref stream data ends before its rows');
661
1012
  return { entries, free, trailer: obj.dict.clone(), at };
662
1013
  }
663
- /** Rebuilds the object map by scanning every byte. Used when the xref is missing or wrong. */
1014
+ /**
1015
+ * Rebuilds the object map by scanning the file, as pdf.js's indexObjects does when the cross-reference data is
1016
+ * missing or wrong, and picks the trailer pdf.js picks.
1017
+ */
664
1018
  async scan() {
665
1019
  this.rebuilt = true;
666
1020
  this.scanned = true;
667
1021
  this.base = 0;
668
1022
  this.sortedOffsets = undefined;
669
1023
  this.pendingObjStm = [];
1024
+ this.directLengths = new Map();
1025
+ this.survey = undefined;
670
1026
  await this.dropObjStms();
1027
+ const { trailers, xrefStms } = await this.indexObjects();
1028
+ this.checkObjectLimit(this.xref.size);
1029
+ await this.readRecovered(xrefStms);
1030
+ this.checkObjectLimit(this.xref.size);
1031
+ // Each candidate is read only up to the next one, so a run of unterminated ones stays linear. Like pdf.js, a
1032
+ // trailer that the end of the file cuts off is read as far as it goes.
1033
+ const dicts = [];
1034
+ for (let i = 0; i < trailers.length; i++) {
1035
+ this.checkTime();
1036
+ const at = trailers[i];
1037
+ try {
1038
+ const d = await this.parseWindowed(this.reader, this.size, at, p => {
1039
+ // pdf.js reads the keyword and then one object, which must be a dictionary and not a stream.
1040
+ if (!p.matchKeyword('trailer'))
1041
+ return undefined;
1042
+ const v = p.parseObject();
1043
+ return v instanceof objects_1.PdfDict && p.streamStart() < 0 ? v : undefined;
1044
+ }, i + 1 < trailers.length ? trailers[i + 1] : this.size, undefined, { recover: true, streams: true });
1045
+ if (d)
1046
+ dicts.push(d);
1047
+ }
1048
+ catch (e) {
1049
+ if (e instanceof TimeLimitError)
1050
+ throw e;
1051
+ }
1052
+ }
1053
+ this.trailer = (await this.pickTrailer(dicts)) ?? this.topDict?.clone() ?? (dicts.length ? undefined : await this.rootHolder()) ?? new objects_1.PdfDict();
1054
+ this.forgetParsed();
1055
+ await this.dropObjStms();
1056
+ }
1057
+ /**
1058
+ * pdf.js's indexObjects. It reads the file token by token, a token running to the next LF, CR or "<", and skips
1059
+ * comments. A token that starts "N G obj" defines an object, and the scan jumps from it to the next "endobj", "N G
1060
+ * obj", "xref" or "trailer<<" and a letter. From "xref" it jumps to "trailer" and then to "startxref", and from
1061
+ * "trailer" to the next "startxref" or "N G obj". So a "trailer" inside an object, a comment or right after
1062
+ * another one is no candidate. Returns the candidates, and the objects that look like xref streams.
1063
+ */
1064
+ async indexObjects() {
1065
+ const size = this.size;
1066
+ const w = new Bytes(this.reader.source, size, () => this.checkTime());
1067
+ const trailers = [];
1068
+ const xrefStms = [];
1069
+ // pdf.js compares whole generation numbers, which the packed entries cut to 16 bits.
1070
+ const gens = new Map();
1071
+ let pos = 0;
1072
+ while (pos < size) {
1073
+ if (!w.has(pos, 32))
1074
+ await w.load(pos, 32);
1075
+ const c = w.get(pos);
1076
+ if (c === 0x09 || c === 0x0a || c === 0x0d || c === 0x20) {
1077
+ pos++;
1078
+ continue;
1079
+ }
1080
+ if (c === 0x25) {
1081
+ for (pos++; pos < size; pos++) {
1082
+ if (!w.has(pos, 1))
1083
+ await w.load(pos, 1);
1084
+ const d = w.get(pos);
1085
+ if (d === 0x0a || d === 0x0d)
1086
+ break;
1087
+ }
1088
+ continue;
1089
+ }
1090
+ // The token never takes the last byte of the file, as pdf.js reads it.
1091
+ let end = pos;
1092
+ for (; end < size - 1; end++) {
1093
+ if (!w.has(end, 1))
1094
+ await w.load(end, 1);
1095
+ const d = w.get(end);
1096
+ if (d === 0x0a || d === 0x0d || d === 0x3c)
1097
+ break;
1098
+ }
1099
+ if (!w.has(pos, 16))
1100
+ await w.load(pos, 16);
1101
+ const len = end - pos;
1102
+ if (len >= 4 && w.is(pos, 'xref') && (len === 4 || jsSpace(w.get(pos + 4)))) {
1103
+ pos = await w.find(pos, enc('trailer'));
1104
+ trailers.push(pos);
1105
+ pos = await w.find(pos, enc('startxref'));
1106
+ continue;
1107
+ }
1108
+ if (len >= 7 && w.is(pos, 'trailer') && (len === 7 || jsSpace(w.get(pos + 7)))) {
1109
+ trailers.push(pos);
1110
+ const m = await this.mark(w, end, false);
1111
+ pos = m === undefined ? size : m.obj ? m.start : m.end + 1;
1112
+ continue;
1113
+ }
1114
+ const head = await this.objHead(w, pos, end);
1115
+ if (!head) {
1116
+ pos = end + 1;
1117
+ continue;
1118
+ }
1119
+ if (head.num >= 0) {
1120
+ let update = !this.xref.has(head.num);
1121
+ if (!update && gens.get(head.num) === head.gen) {
1122
+ // pdf.js takes a later definition of the same object unless the file ends inside it.
1123
+ try {
1124
+ await this.quietly(() => this.parseWindowed(this.reader, size, end, p => p.parseObject()));
1125
+ update = true;
1126
+ }
1127
+ catch (e) {
1128
+ if (e instanceof TimeLimitError)
1129
+ throw e;
1130
+ update = !(e instanceof parser_1.EndOfData);
1131
+ }
1132
+ }
1133
+ if (update) {
1134
+ this.xref.set(head.num, (0, exports.packOffset)(pos, head.gen));
1135
+ gens.set(head.num, head.gen);
1136
+ this.checkObjectLimit(this.xref.size);
1137
+ }
1138
+ }
1139
+ const m = await this.mark(w, end, true);
1140
+ const next = m === undefined ? size : m.obj ? m.start : m.end + 1;
1141
+ // pdf.js reads an object as an xref stream when "/XRef" and a byte below "@" come before the jump target.
1142
+ const tag = await w.find(pos, enc('/XRef'), next);
1143
+ if (tag + 5 < next) {
1144
+ if (!w.has(tag + 5, 1))
1145
+ await w.load(tag + 5, 1);
1146
+ if (w.get(tag + 5) < 64)
1147
+ xrefStms.push(pos);
1148
+ }
1149
+ pos = next;
1150
+ }
1151
+ return { trailers, xrefStms };
1152
+ }
1153
+ /** pdf.js's /^(\d+)\s+(\d+)\s+obj\b/ against the token [pos, end): the numbers, or undefined. */
1154
+ async objHead(w, pos, end) {
1155
+ let p = pos;
1156
+ const run = async (test, keep) => {
1157
+ let text = '';
1158
+ const from = p;
1159
+ for (; p < end; p++) {
1160
+ if (!w.has(p, 1))
1161
+ await w.load(p, 1);
1162
+ const c = w.get(p);
1163
+ if (!test(c))
1164
+ break;
1165
+ if (keep && text.length < 512)
1166
+ text += String.fromCharCode(c);
1167
+ }
1168
+ return p > from ? text : undefined;
1169
+ };
1170
+ const num = await run(isDigitByte, true);
1171
+ if (num === undefined || (await run(jsSpace, false)) === undefined)
1172
+ return undefined;
1173
+ const gen = await run(isDigitByte, true);
1174
+ if (gen === undefined || (await run(jsSpace, false)) === undefined)
1175
+ return undefined;
1176
+ if (p + 3 > end)
1177
+ return undefined;
1178
+ if (!w.has(p, 4))
1179
+ await w.load(p, 4);
1180
+ if (!w.is(p, 'obj') || (p + 3 < end && jsWord(w.get(p + 3))))
1181
+ return undefined;
1182
+ return { num: int32(num), gen: int32(gen) };
1183
+ }
1184
+ /**
1185
+ * The first match from `from` on of pdf.js's /\b(endobj|\d+\s+\d+\s+obj|xref|trailer\s*<<)\b/, or with `endobj`
1186
+ * false of /\b(startxref|\d+\s+\d+\s+obj)\b/. The "N G obj" pattern is followed as states, so a long run of digits or
1187
+ * spaces costs one pass: 1 in N, 2 the spaces after it, 3 in G, 4 the spaces after G, 5 digits that cannot start N.
1188
+ */
1189
+ async mark(w, from, endobj) {
1190
+ const size = this.size;
1191
+ if (!w.has(from, 32))
1192
+ await w.load(from, 32);
1193
+ let prev = from > 0 ? w.get(from - 1) : -1;
1194
+ let state = 0;
1195
+ let n = -1;
1196
+ let g = -1;
1197
+ for (let p = from; p < size; p++) {
1198
+ if (!w.has(p, 32))
1199
+ await w.load(p, 32);
1200
+ const c = w.get(p);
1201
+ if (isDigitByte(c)) {
1202
+ if (state === 2) {
1203
+ state = 3;
1204
+ g = p;
1205
+ }
1206
+ else if (state === 4) {
1207
+ // A third number: the pattern starts again at the second.
1208
+ n = g;
1209
+ g = p;
1210
+ state = 3;
1211
+ }
1212
+ else if (state === 0) {
1213
+ state = jsWord(prev) ? 5 : 1;
1214
+ n = p;
1215
+ }
1216
+ }
1217
+ else if (jsSpace(c)) {
1218
+ if (state === 1)
1219
+ state = 2;
1220
+ else if (state === 3)
1221
+ state = 4;
1222
+ else if (state === 5)
1223
+ state = 0;
1224
+ }
1225
+ else {
1226
+ if (state === 4 && w.is(p, 'obj') && !jsWord(w.get(p + 3)))
1227
+ return { start: n, end: p + 3, obj: true };
1228
+ state = 0;
1229
+ if (!jsWord(prev)) {
1230
+ if (endobj) {
1231
+ if (w.is(p, 'endobj') && !jsWord(w.get(p + 6)))
1232
+ return { start: p, end: p + 6, obj: false };
1233
+ if (w.is(p, 'xref') && !jsWord(w.get(p + 4)))
1234
+ return { start: p, end: p + 4, obj: false };
1235
+ if (w.is(p, 'trailer')) {
1236
+ let q = p + 7;
1237
+ for (; q < size; q++) {
1238
+ if (!w.has(q, 3))
1239
+ await w.load(q, 3);
1240
+ if (!jsSpace(w.get(q)))
1241
+ break;
1242
+ }
1243
+ if (!w.has(q, 3))
1244
+ await w.load(q, 3);
1245
+ if (w.is(q, '<<') && jsWord(w.get(q + 2)))
1246
+ return { start: p, end: q + 2, obj: false };
1247
+ }
1248
+ }
1249
+ else if (w.is(p, 'startxref') && !jsWord(w.get(p + 9)))
1250
+ return { start: p, end: p + 9, obj: false };
1251
+ }
1252
+ }
1253
+ prev = c;
1254
+ }
1255
+ return undefined;
1256
+ }
1257
+ /**
1258
+ * Reads the xref streams the scan found, and the sections their /Prev chains lead to, as pdf.js does: each fills
1259
+ * only the numbers nothing before it gave, and a free row counts as given.
1260
+ */
1261
+ async readRecovered(starts) {
1262
+ // Free rows already merged, as [start, end) runs.
1263
+ const free = [];
1264
+ const isFree = (num) => {
1265
+ for (let i = 0; i < free.length; i += 2)
1266
+ if (num >= free[i] && num < free[i + 1])
1267
+ return true;
1268
+ return false;
1269
+ };
1270
+ for (const start of starts) {
1271
+ const queue = [start];
1272
+ const seen = new Set();
1273
+ // Sections the chain adds join the queue while it is read.
1274
+ for (const at of queue) {
1275
+ this.checkTime();
1276
+ if (seen.has(at))
1277
+ continue;
1278
+ seen.add(at);
1279
+ let section;
1280
+ try {
1281
+ section = await this.readXrefSection(at, false);
1282
+ }
1283
+ catch (e) {
1284
+ if (e instanceof TimeLimitError || e instanceof ObjectLimitError)
1285
+ throw e;
1286
+ continue;
1287
+ }
1288
+ for (const [num, entry] of section.entries)
1289
+ if (!this.xref.has(num) && !isFree(num))
1290
+ this.xref.set(num, entry);
1291
+ // Free runs are few next to the rows, since a few bytes of xref stream can free millions of numbers.
1292
+ if (free.length < 65536)
1293
+ free.push(...section.free.slice(0, 65536));
1294
+ this.topDict ??= section.trailer;
1295
+ const prev = section.trailer.get('Prev');
1296
+ if (typeof prev === 'number' && Number.isInteger(prev))
1297
+ queue.push(prev);
1298
+ else if (prev instanceof objects_1.PdfRef)
1299
+ queue.push(prev.num);
1300
+ }
1301
+ }
1302
+ }
1303
+ /**
1304
+ * pdf.js's choice among the trailers its recovery finds: the first, in file order, whose /Root and /Pages are
1305
+ * dictionaries, whose /Count is a whole number and that has an /ID, and an /Encrypt if any candidate has one.
1306
+ * Failing that, the last whose /Root and /Pages are dictionaries. A reference whose generation is higher than its
1307
+ * entry's counts in a second pass, once any lookup failed.
1308
+ */
1309
+ async pickTrailer(dicts) {
1310
+ const encrypted = dicts.some(d => d.has('Encrypt'));
1311
+ let chosen;
1312
+ let failed = false;
1313
+ return this.quietly(async () => {
1314
+ for (const fallback of [false, true]) {
1315
+ if (fallback && !failed)
1316
+ break;
1317
+ for (const d of dicts) {
1318
+ let valid;
1319
+ try {
1320
+ const root = await this.fetchStrict(d.get('Root'), fallback);
1321
+ if (!(root instanceof objects_1.PdfDict))
1322
+ continue;
1323
+ const pages = await this.fetchStrict(root.get('Pages'), fallback);
1324
+ if (!(pages instanceof objects_1.PdfDict))
1325
+ continue;
1326
+ valid = Number.isInteger(await this.fetchStrict(pages.get('Count'), fallback));
1327
+ }
1328
+ catch (e) {
1329
+ if (!(e instanceof BrokenEntry))
1330
+ throw e;
1331
+ failed = true;
1332
+ continue;
1333
+ }
1334
+ if (valid && (!encrypted || d.has('Encrypt')) && d.has('ID'))
1335
+ return d;
1336
+ chosen = d;
1337
+ }
1338
+ }
1339
+ return chosen;
1340
+ });
1341
+ }
1342
+ /** Reads a value one level deep, throwing BrokenEntry where pdf.js's lookup throws. */
1343
+ async fetchStrict(v, fallback) {
1344
+ if (!(v instanceof objects_1.PdfRef))
1345
+ return v;
1346
+ const entry = unpack(this.xref.get(v.num));
1347
+ if (!entry)
1348
+ return null;
1349
+ let ref = v;
1350
+ if (entry.type === 1 && entry.gen !== v.gen) {
1351
+ if (!fallback || entry.gen > v.gen)
1352
+ throw new BrokenEntry();
1353
+ ref = new objects_1.PdfRef(v.num, entry.gen);
1354
+ }
1355
+ if (await this.entryBroken(ref))
1356
+ throw new BrokenEntry();
1357
+ return this.getObject(ref);
1358
+ }
1359
+ /** With no trailer at all, pdf.js takes the lowest-numbered object, or stream dictionary, that holds /Root. */
1360
+ async rootHolder() {
1361
+ return (await this.surveyObjects()).root?.clone();
1362
+ }
1363
+ /**
1364
+ * Reads every object of the rebuilt map once, in number order, for the readings recovery falls back on: the first
1365
+ * that holds /Root, the object streams, the newest xref stream and the earliest catalog in the file.
1366
+ */
1367
+ async surveyObjects() {
1368
+ if (this.survey)
1369
+ return this.survey;
1370
+ const found = { objStms: [] };
1371
+ let xrefAt = -1;
1372
+ let catalogAt = Number.POSITIVE_INFINITY;
1373
+ await this.quietly(async () => {
1374
+ for (const num of Array.from(this.xref.keys()).sort((a, b) => a - b)) {
1375
+ this.checkTime();
1376
+ const entry = unpack(this.xref.get(num));
1377
+ if (entry?.type !== 1)
1378
+ continue;
1379
+ let o;
1380
+ try {
1381
+ o = (await this.parseIndirectAt(entry.offset)).obj;
1382
+ }
1383
+ catch (e) {
1384
+ if (e instanceof TimeLimitError)
1385
+ throw e;
1386
+ continue;
1387
+ }
1388
+ const d = o instanceof objects_1.PdfStream ? o.dict : o;
1389
+ if (d instanceof objects_1.PdfDict && d.has('Root'))
1390
+ found.root ??= d;
1391
+ if (o instanceof objects_1.PdfStream && o.dict.name('Type') === 'ObjStm')
1392
+ found.objStms.push(num);
1393
+ else if (o instanceof objects_1.PdfStream && o.dict.name('Type') === 'XRef' && entry.offset > xrefAt) {
1394
+ xrefAt = entry.offset;
1395
+ found.xrefDict = o.dict;
1396
+ }
1397
+ else if (o instanceof objects_1.PdfDict && o.name('Type') === 'Catalog' && entry.offset < catalogAt) {
1398
+ catalogAt = entry.offset;
1399
+ found.catalog = new objects_1.PdfRef(num, entry.gen);
1400
+ }
1401
+ }
1402
+ });
1403
+ this.survey = found;
1404
+ return found;
1405
+ }
1406
+ /** Runs `fn` with the issues as they were before it, for lookups that only decide how to read the file. */
1407
+ async quietly(fn) {
1408
+ const i = this.issues;
1409
+ const saved = Object.fromEntries(Object.entries(i).map(([k, v]) => [k, v instanceof Set ? new Set(v) : Array.isArray(v) ? [...v] : v]));
1410
+ const counted = new Set(this.badTokensCounted);
1411
+ const wrong = new Set(this.lengthWrongAt);
1412
+ const quiet = this.quiet;
1413
+ this.quiet = true;
1414
+ try {
1415
+ return await fn();
1416
+ }
1417
+ finally {
1418
+ this.quiet = quiet;
1419
+ Object.assign(i, saved);
1420
+ this.badTokensCounted.clear();
1421
+ for (const k of counted)
1422
+ this.badTokensCounted.add(k);
1423
+ this.lengthWrongAt.clear();
1424
+ for (const k of wrong)
1425
+ this.lengthWrongAt.add(k);
1426
+ }
1427
+ }
1428
+ /**
1429
+ * Where pdf.js's recovery gives no catalog and page tree, as other readers do: every object is read, the objects in
1430
+ * object streams it finds are added, and a catalog is taken from an xref stream dictionary or a /Type /Catalog.
1431
+ */
1432
+ async scanFurther() {
1433
+ this.extended = true;
1434
+ // Any definition after whitespace, ">" or "]", even in a comment, for a number pdf.js's scan did not find, and any
1435
+ // "trailer", newest first, as other readers search for them.
671
1436
  const trailers = new Set();
672
1437
  let tail = '';
673
1438
  const headers = this.objectHeaders((buf, pos) => {
@@ -677,63 +1442,53 @@ class PdfDocument {
677
1442
  trailers.add(pos - tail.length + t + 7);
678
1443
  tail = s.slice(-6);
679
1444
  });
680
- for await (const h of headers) {
681
- if (h.before >= 0 && !(0, parser_1.isWhite)(h.before) && h.before !== 0x3e && h.before !== 0x5d)
1445
+ const more = new Map();
1446
+ for await (const h of headers)
1447
+ if (h.before < 0 || (0, parser_1.isWhite)(h.before) || h.before === 0x3e || h.before === 0x5d)
1448
+ more.set(h.num, (0, exports.packOffset)(h.at, h.gen));
1449
+ let added = 0;
1450
+ for (const [num, entry] of more) {
1451
+ if (this.xref.has(num))
682
1452
  continue;
683
- this.xref.set(h.num, (0, exports.packOffset)(h.at, h.gen));
684
- this.checkObjectLimit(this.xref.size);
1453
+ this.xref.set(num, entry);
1454
+ added++;
685
1455
  }
686
- // Newest first. Each candidate is parsed only up to the next one, so a run of unterminated ones stays linear.
687
- const cands = Array.from(trailers).sort((a, b) => a - b);
688
- for (let i = cands.length - 1; i >= 0; i--) {
689
- this.checkTime();
690
- try {
691
- const d = await this.parseWindowed(this.reader, this.size, cands[i], p => p.parseObject(), i + 1 < cands.length ? cands[i + 1] - 7 : this.size);
692
- if (d instanceof objects_1.PdfDict && d.get('Root') instanceof objects_1.PdfRef) {
693
- this.trailer = d;
694
- break;
695
- }
696
- }
697
- catch (e) {
698
- if (e instanceof TimeLimitError)
699
- throw e;
700
- }
1456
+ this.checkObjectLimit(this.xref.size);
1457
+ if (added) {
1458
+ this.sortedOffsets = undefined;
1459
+ this.survey = undefined;
701
1460
  }
702
- // Object streams, and the newest xref stream and the first catalog in case no trailer was found.
703
- let xrefAt = -1;
704
- let xrefDict;
705
- let catalog;
706
- for (const num of Array.from(this.xref.keys())) {
1461
+ const { objStms, xrefDict, catalog } = await this.surveyObjects();
1462
+ this.pendingObjStm = [...objStms];
1463
+ const cands = Array.from(trailers).sort((a, b) => a - b);
1464
+ let found;
1465
+ for (let i = cands.length - 1; i >= 0 && !found; i--) {
707
1466
  this.checkTime();
708
- const entry = unpack(this.xref.get(num));
709
- if (entry?.type !== 1)
710
- continue;
711
- let o;
712
1467
  try {
713
- o = (await this.parseIndirectAt(entry.offset)).obj;
1468
+ const d = await this.parseWindowed(this.reader, this.size, cands[i], p => p.parseObject(), i + 1 < cands.length ? cands[i + 1] - 7 : this.size, undefined, {
1469
+ recover: true,
1470
+ streams: true,
1471
+ });
1472
+ if (d instanceof objects_1.PdfDict && hasRoot(d))
1473
+ found = d;
714
1474
  }
715
1475
  catch (e) {
716
1476
  if (e instanceof TimeLimitError)
717
1477
  throw e;
718
- continue;
719
1478
  }
720
- if (o instanceof objects_1.PdfStream && o.dict.name('Type') === 'ObjStm')
721
- this.pendingObjStm.push(num);
722
- else if (o instanceof objects_1.PdfStream && o.dict.name('Type') === 'XRef' && entry.offset > xrefAt) {
723
- xrefAt = entry.offset;
724
- xrefDict = o.dict;
725
- }
726
- else if (o instanceof objects_1.PdfDict && o.name('Type') === 'Catalog')
727
- catalog ??= new objects_1.PdfRef(num, entry.gen);
728
1479
  }
729
- if (!(this.trailer.get('Root') instanceof objects_1.PdfRef)) {
1480
+ if (found)
1481
+ this.trailer = found;
1482
+ else if (xrefDict?.has('Root')) {
730
1483
  // The xref stream dictionary also carries /Encrypt and /ID, which a catalog alone would lose.
731
- if (xrefDict)
732
- this.trailer = xrefDict.clone();
733
- if (!(this.trailer.get('Root') instanceof objects_1.PdfRef) && catalog)
734
- this.trailer.set('Root', catalog);
1484
+ this.trailer = xrefDict.clone();
735
1485
  }
736
- this.objCache = objectCache();
1486
+ else if (catalog) {
1487
+ this.trailer = this.trailer.clone();
1488
+ this.trailer.set('Root', catalog);
1489
+ }
1490
+ this.forgetParsed();
1491
+ await this.dropObjStms();
737
1492
  }
738
1493
  /** Adds the objects held in the object streams scan() found. Their bodies may be encrypted, so this runs after initSecurity(). */
739
1494
  async indexObjStms() {
@@ -741,23 +1496,38 @@ class PdfDocument {
741
1496
  this.pendingObjStm = [];
742
1497
  for (const num of nums) {
743
1498
  this.checkTime();
744
- let stm;
1499
+ let stm = null;
745
1500
  try {
746
- stm = await this.loadObjStm(num);
1501
+ stm = await this.loadObjStm(num, true);
747
1502
  }
748
1503
  catch (e) {
749
1504
  if (e instanceof filters_1.DecompressionLimitError || e instanceof TimeLimitError)
750
1505
  throw e;
751
- continue; // a broken object stream adds nothing
752
1506
  }
753
- if (!stm)
754
- continue;
755
- for (const on of stm.offsets.keys())
756
- if (!this.xref.has(on))
757
- this.xref.set(on, (0, exports.packStream)(num));
758
- await stm.reader.source.close?.();
1507
+ // A broken object stream adds nothing.
1508
+ if (stm)
1509
+ for (const [on, index] of stm.byNum)
1510
+ if (!this.xref.has(on) && !(stm.badFrom !== undefined && index >= stm.badFrom))
1511
+ this.xref.set(on, (0, exports.packStream)(num, index));
1512
+ await this.dropObjStms();
759
1513
  this.checkObjectLimit(this.xref.size);
760
1514
  }
1515
+ // A /Length read before these objects had entries read as nothing.
1516
+ if (nums.length)
1517
+ this.forgetParsed();
1518
+ }
1519
+ /**
1520
+ * Forgets parsed objects, the stream lengths read for them and the streams lifted out of them, once the map or the
1521
+ * key changes. Only the catalog written in the trailer, lifted once the map is final, outlives this.
1522
+ */
1523
+ forgetParsed() {
1524
+ this.objCache = objectCache();
1525
+ this.directLengths = new Map();
1526
+ this.synthetic.clear();
1527
+ this.syntheticAt.clear();
1528
+ this.heldBodies.clear();
1529
+ this.syntheticCost.clear();
1530
+ this.syntheticTotal = 0;
761
1531
  }
762
1532
  // ---------- objects ----------
763
1533
  /**
@@ -765,32 +1535,34 @@ class PdfDocument {
765
1535
  * whitespace, and `end` where its value or its stream body ends.
766
1536
  */
767
1537
  parseIndirectAt(offset) {
768
- return this.parseWindowed(this.reader, this.size, offset, async (p) => {
1538
+ return this.parseWindowed(this.reader, this.size, offset, async (p, at) => {
769
1539
  p.skipWhitespace();
770
- const at = offset + p.pos;
1540
+ const head = at(p.pos);
771
1541
  const ref = p.parseObjectHeader();
772
1542
  const obj = p.parseObject();
1543
+ const { directCost } = p;
773
1544
  if (obj instanceof objects_1.PdfDict) {
774
1545
  const start = p.streamStart();
775
1546
  if (start >= 0) {
776
- const s = await this.locateStream(obj, offset + start, ref);
777
- return { ref, obj: s, at, end: s.offset + s.length, cost: p.cost };
1547
+ const s = await this.locateStream(obj, at(start), ref);
1548
+ return { ref, obj: s, at: head, end: s.offset + s.length, cost: p.cost, directCost };
778
1549
  }
779
1550
  }
780
- return { ref, obj, at, end: offset + p.pos, cost: p.cost };
781
- }, this.nextOffsetAfter(offset));
1551
+ return { ref, obj, at: head, end: at(p.pos - 1) + 1, cost: p.cost, directCost };
1552
+ }, this.nextOffsetAfter(offset), undefined, { streams: true });
782
1553
  }
783
1554
  /** Finds a stream body's true length, trusting /Length only when "endstream" follows it. */
784
1555
  async locateStream(dict, bodyStart, ref) {
1556
+ // pdf.js trusts only a whole-number /Length. A fractional one would read part of a byte.
785
1557
  let declared;
786
1558
  const L = dict.get('Length');
787
- if (typeof L === 'number')
1559
+ if (typeof L === 'number' && Number.isInteger(L))
788
1560
  declared = L;
789
1561
  else if (L instanceof objects_1.PdfRef && L.num !== ref.num && !this.lengthResolving.has(ref.num)) {
790
1562
  this.lengthResolving.add(ref.num);
791
1563
  try {
792
1564
  const v = await this.getObject(L);
793
- if (typeof v === 'number')
1565
+ if (typeof v === 'number' && Number.isInteger(v))
794
1566
  declared = v;
795
1567
  }
796
1568
  catch {
@@ -800,61 +1572,7 @@ class PdfDocument {
800
1572
  this.lengthResolving.delete(ref.num);
801
1573
  }
802
1574
  }
803
- // Outside xref streams the filter entries may be indirect. They are resolved one level, as viewers do, so every
804
- // reader of this dictionary decodes what a viewer decodes. A reference to a stream or to nothing stays in place.
805
- if (!this.lengthResolving.has(ref.num)) {
806
- this.lengthResolving.add(ref.num);
807
- const named = [];
808
- try {
809
- const direct = async (v) => {
810
- if (!(v instanceof objects_1.PdfRef) || this.lengthResolving.has(v.num))
811
- return v;
812
- const o = await this.getObject(v);
813
- if (o === null || o instanceof objects_1.PdfStream)
814
- return v;
815
- named.push(v.num);
816
- return o;
817
- };
818
- for (const key of ['Filter', 'DecodeParms', 'DP']) {
819
- const v = dict.get(key);
820
- if (v === undefined)
821
- continue;
822
- const r = await direct(v);
823
- if (!Array.isArray(r))
824
- dict.set(key, r);
825
- else {
826
- const items = [];
827
- for (const x of r)
828
- items.push(await direct(x));
829
- dict.set(key, items);
830
- }
831
- }
832
- // pdf.js and qpdf also resolve the values inside a parameter dictionary, such as an indirect /Predictor.
833
- for (const key of ['DecodeParms', 'DP']) {
834
- const v = dict.get(key);
835
- const list = Array.isArray(v) ? v : [v];
836
- for (let i = 0; i < list.length; i++) {
837
- const d = list[i];
838
- if (!(d instanceof objects_1.PdfDict) || !d.entries().some(([, x]) => x instanceof objects_1.PdfRef))
839
- continue;
840
- const c = d.clone();
841
- for (const [k, x] of c.entries())
842
- c.set(k, await direct(x));
843
- if (Array.isArray(v))
844
- v[i] = c;
845
- else
846
- dict.set(key, c);
847
- }
848
- }
849
- }
850
- finally {
851
- this.lengthResolving.delete(ref.num);
852
- }
853
- if (named.length)
854
- this.filterRefs.set(ref.num, named);
855
- else
856
- this.filterRefs.delete(ref.num);
857
- }
1575
+ await this.resolveFilters(dict, ref.num);
858
1576
  if (declared !== undefined && declared >= 0 && bodyStart + declared <= this.size) {
859
1577
  const after = await this.reader.read(bodyStart + declared, 32);
860
1578
  const s = Buffer.from(after).toString('latin1');
@@ -862,8 +1580,8 @@ class PdfDocument {
862
1580
  return new objects_1.PdfStream(dict, bodyStart, declared);
863
1581
  }
864
1582
  // Scan for "endstream", first only up to the next object's offset, which keeps total work linear.
865
- if (!this.quiet && !this.lengthWrongAt.has(bodyStart)) {
866
- this.lengthWrongAt.add(bodyStart);
1583
+ if (!this.quiet && !this.lengthWrongAt.has(String(bodyStart))) {
1584
+ this.lengthWrongAt.add(String(bodyStart));
867
1585
  this.issues.streamLengthWrong++;
868
1586
  }
869
1587
  const bound = this.nextOffsetAfter(bodyStart);
@@ -877,14 +1595,72 @@ class PdfDocument {
877
1595
  }
878
1596
  return new objects_1.PdfStream(dict, bodyStart, Math.max(0, bound - bodyStart));
879
1597
  }
880
- /** Position just before the EOL that precedes "endstream" in [from, to), or -1. */
1598
+ /**
1599
+ * Outside xref streams the filter entries may be indirect. They are resolved one level, as viewers do, so every
1600
+ * reader of this dictionary decodes what a viewer decodes. A reference to a stream or to nothing stays in place.
1601
+ */
1602
+ async resolveFilters(dict, num) {
1603
+ if (this.lengthResolving.has(num))
1604
+ return;
1605
+ this.lengthResolving.add(num);
1606
+ const named = [];
1607
+ try {
1608
+ const direct = async (v) => {
1609
+ if (!(v instanceof objects_1.PdfRef) || this.lengthResolving.has(v.num))
1610
+ return v;
1611
+ const o = await this.getObject(v);
1612
+ if (o === null || o instanceof objects_1.PdfStream)
1613
+ return v;
1614
+ named.push(v.num);
1615
+ return o;
1616
+ };
1617
+ for (const key of ['Filter', 'DecodeParms', 'DP']) {
1618
+ const v = dict.get(key);
1619
+ if (v === undefined)
1620
+ continue;
1621
+ const r = await direct(v);
1622
+ if (!Array.isArray(r))
1623
+ dict.set(key, r);
1624
+ else {
1625
+ const items = [];
1626
+ for (const x of r)
1627
+ items.push(await direct(x));
1628
+ dict.set(key, items);
1629
+ }
1630
+ }
1631
+ // pdf.js and qpdf also resolve the values inside a parameter dictionary, such as an indirect /Predictor.
1632
+ for (const key of ['DecodeParms', 'DP']) {
1633
+ const v = dict.get(key);
1634
+ const list = Array.isArray(v) ? v : [v];
1635
+ for (let i = 0; i < list.length; i++) {
1636
+ const d = list[i];
1637
+ if (!(d instanceof objects_1.PdfDict) || !d.entries().some(([, x]) => x instanceof objects_1.PdfRef))
1638
+ continue;
1639
+ const c = d.clone();
1640
+ for (const [k, x] of c.entries())
1641
+ c.set(k, await direct(x));
1642
+ if (Array.isArray(v))
1643
+ v[i] = c;
1644
+ else
1645
+ dict.set(key, c);
1646
+ }
1647
+ }
1648
+ }
1649
+ finally {
1650
+ this.lengthResolving.delete(num);
1651
+ }
1652
+ if (named.length)
1653
+ this.filterRefs.set(num, named);
1654
+ else
1655
+ this.filterRefs.delete(num);
1656
+ }
1657
+ /** Position just before the EOL that precedes "endstream", or a misspelling pdf.js takes, in [from, to), or -1. */
881
1658
  async findEndstream(from, to, unbounded) {
882
- const needle = enc('endstream');
883
1659
  let pos = from;
884
1660
  const chunk = 1 << 20;
885
1661
  while (pos < to) {
886
1662
  this.checkTime();
887
- const n = Math.min(chunk + needle.length, to + needle.length - pos, this.size - pos);
1663
+ const n = Math.min(chunk + ENDSTREAM.length, to + ENDSTREAM.length - pos, this.size - pos);
888
1664
  if (n <= 0)
889
1665
  break;
890
1666
  const buf = await this.reader.source.read(pos, n);
@@ -893,7 +1669,7 @@ class PdfDocument {
893
1669
  if (this.unboundedScanBudget < 0)
894
1670
  return -1;
895
1671
  }
896
- const k = view(buf).indexOf(needle);
1672
+ const k = (0, parser_1.endstreamIn)(buf, 0)?.at ?? -1;
897
1673
  if (k >= 0) {
898
1674
  let end = pos + k;
899
1675
  const back = await this.reader.read(Math.max(from, end - 2), Math.min(2, end - from));
@@ -934,6 +1710,9 @@ class PdfDocument {
934
1710
  }
935
1711
  /** Loads an object by reference, decrypted. Missing and free objects are null. */
936
1712
  async getObject(ref, raw = false) {
1713
+ const made = this.synthetic.get(ref.num);
1714
+ if (made !== undefined)
1715
+ return made.obj;
937
1716
  const entry = unpack(this.xref.get(ref.num));
938
1717
  // pdf.js refuses a reference whose generation differs from the entry. This one is read all the same, and reported.
939
1718
  if (entry?.type === 1 && entry.gen !== ref.gen && !this.quiet)
@@ -951,6 +1730,8 @@ class PdfDocument {
951
1730
  parsed = await this.parseIndirectAt(entry.offset + this.base);
952
1731
  if (parsed.ref.num !== ref.num)
953
1732
  throw new parser_1.ParseError('Object number mismatch');
1733
+ if (parsed.directCost)
1734
+ this.chargeSynthetic(String(ref.num), parsed.directCost);
954
1735
  }
955
1736
  catch (e) {
956
1737
  if (e instanceof parser_1.ParseError || e instanceof RangeError) {
@@ -966,11 +1747,15 @@ class PdfDocument {
966
1747
  this.issues.genMismatch++;
967
1748
  obj = parsed.obj;
968
1749
  cost = parsed.cost;
969
- if (this.security && !raw && !(this.encryptRef && this.encryptRef.num === ref.num))
1750
+ const decrypt = this.security !== undefined && !raw && !(this.encryptRef && this.encryptRef.num === ref.num);
1751
+ if (decrypt)
970
1752
  obj = this.decryptObject(obj, ref.num, entry.gen);
1753
+ // pdf.js decrypts a stream written inside an object with that object's key.
1754
+ if (parsed.directCost)
1755
+ await this.lift(obj, decrypt ? { num: ref.num, gen: entry.gen } : null, String(ref.num));
971
1756
  }
972
1757
  else {
973
- ({ obj, cost } = await this.getFromObjStm(entry.stream, ref.num));
1758
+ ({ obj, cost } = await this.getFromObjStm(entry.stream, ref.num, entry.index));
974
1759
  }
975
1760
  if (!raw)
976
1761
  this.objCache.set(ref.num, obj, cost);
@@ -1000,53 +1785,247 @@ class PdfDocument {
1000
1785
  };
1001
1786
  return walk(obj);
1002
1787
  }
1003
- async getFromObjStm(stmNum, num) {
1004
- let stm = this.objStmCache.get(stmNum);
1005
- if (!stm) {
1006
- const kept = this.spilledObjStm.get(stmNum);
1007
- try {
1008
- stm = kept ? { ...kept, reader: new io_1.Reader(kept.source, kept.size, 65536, 4) } : await this.loadObjStm(stmNum);
1009
- }
1010
- catch (e) {
1011
- if (e instanceof filters_1.DecompressionLimitError || e instanceof TimeLimitError)
1012
- throw e;
1013
- stm = undefined;
1014
- }
1015
- if (!stm) {
1016
- this.issues.malformed++;
1017
- return { obj: null, cost: 0 };
1788
+ /**
1789
+ * Gives each stream written inside `obj` a synthetic number and puts a reference to it in its place, so the walk
1790
+ * and the writer meet it as they meet any stream. `key` names the object whose key decrypts it. `owner` names
1791
+ * where `obj` came from, so a second read of it reuses the numbers. `stm` names the object stream whose decoded
1792
+ * data holds the bodies, when `obj` sits in one.
1793
+ */
1794
+ async lift(obj, key, owner, stm) {
1795
+ const stack = [];
1796
+ const push = (v) => {
1797
+ if (v instanceof objects_1.PdfDict || Array.isArray(v))
1798
+ stack.push(v);
1799
+ else if (v instanceof objects_1.PdfStream)
1800
+ stack.push(v.dict);
1801
+ };
1802
+ push(obj);
1803
+ for (let list = stack.pop(); list !== undefined; list = stack.pop()) {
1804
+ const slots = Array.isArray(list) ? list.map((v, i) => [i, v]) : list.entries();
1805
+ for (const [k, v] of slots) {
1806
+ push(v);
1807
+ if (!(v instanceof objects_1.PdfStream))
1808
+ continue;
1809
+ const at = `${owner} ${v.offset}`;
1810
+ const num = this.addSynthetic(at, v, key);
1811
+ if (stm !== undefined)
1812
+ this.heldBodies.set(num, { stm, offset: v.offset, length: v.length });
1813
+ await this.resolveFilters(v.dict, num);
1814
+ if (!this.quiet) {
1815
+ this.issues.directStreams.add(at);
1816
+ const L = v.dict.get('Length');
1817
+ const declared = typeof L === 'number' ? L : L instanceof objects_1.PdfRef ? this.directLengths.get(L.num) : undefined;
1818
+ if (declared !== v.length && !this.lengthWrongAt.has(at)) {
1819
+ this.lengthWrongAt.add(at);
1820
+ this.issues.streamLengthWrong++;
1821
+ }
1822
+ }
1823
+ const ref = new objects_1.PdfRef(num, 0);
1824
+ if (Array.isArray(list))
1825
+ list[k] = ref;
1826
+ else
1827
+ list.set(k, ref);
1018
1828
  }
1019
- // An evicted spilled stream gives up its file handle; it reopens from spilledObjStm when needed again.
1020
- await this.objStmCache.set(stmNum, stm)?.reader.source.close?.();
1021
1829
  }
1022
- const off = stm.offsets.get(num);
1023
- if (off === undefined)
1830
+ }
1831
+ /** Numbers an object that has no entry of its own, the same number each time the same object is read. */
1832
+ addSynthetic(at, obj, key) {
1833
+ let num = this.syntheticAt.get(at);
1834
+ if (num === undefined) {
1835
+ num = this.nextSynthetic--;
1836
+ this.syntheticAt.set(at, num);
1837
+ }
1838
+ this.synthetic.set(num, { obj, key });
1839
+ return num;
1840
+ }
1841
+ /** Records what the streams inside the object `owner` hold. Throws ParseError when that would pass the budget. */
1842
+ chargeSynthetic(owner, cost) {
1843
+ const total = this.syntheticTotal - (this.syntheticCost.get(owner) ?? 0) + cost;
1844
+ if (total > MAX_SYNTHETIC_COST)
1845
+ throw new parser_1.ParseError('Streams written inside objects hold too much');
1846
+ this.syntheticTotal = total;
1847
+ this.syntheticCost.set(owner, cost);
1848
+ }
1849
+ /** The object whose key decrypts a stream: the stream's own, or for a synthetic one the object that held it. */
1850
+ keyOf(num) {
1851
+ const made = this.synthetic.get(num);
1852
+ return made !== undefined ? made.key : { num, gen: this.genOf(num) };
1853
+ }
1854
+ /**
1855
+ * Reads object `num` from object stream `stmNum`. pdf.js finds an object by the number the stream's header gives it,
1856
+ * and by `index`, the index its entry gives, when the header does not name it. Each object ends where the next one
1857
+ * starts, and the last at the end of the stream, as pdf.js reads them.
1858
+ */
1859
+ async getFromObjStm(stmNum, num, index) {
1860
+ const stm = await this.objStm(stmNum);
1861
+ if (!stm) {
1862
+ this.issues.malformed++;
1024
1863
  return { obj: null, cost: 0 };
1864
+ }
1865
+ const i = stm.byNum.get(num) ?? index;
1866
+ const off = stm.offsets.get(i);
1867
+ if (off === undefined || (stm.badFrom !== undefined && i >= stm.badFrom))
1868
+ return { obj: null, cost: 0 };
1869
+ let end = stm.size;
1870
+ const next = stm.offsets.get(i + 1);
1871
+ if (next !== undefined) {
1872
+ // pdf.js reads no object whose next one does not start after it. The header was reported already.
1873
+ if (next <= off)
1874
+ return { obj: null, cost: 0 };
1875
+ end = Math.min(stm.size, stm.first + next);
1876
+ }
1025
1877
  try {
1026
- return await this.parseWindowed(stm.reader, stm.size, stm.first + off, p => ({ obj: p.parseObject(), cost: p.cost }), stm.size, `${stmNum} ${off}`);
1878
+ const { obj, cost, directCost } = await this.parseWindowed(this.objStmReader(stmNum, stm), stm.size, stm.first + off, p => ({ obj: p.parseObject(), cost: p.cost, directCost: p.directCost }), end, `${stmNum} ${off}`, { streams: true, hard: true, topStream: true });
1879
+ if (directCost) {
1880
+ // pdf.js reads a stream stored in an object stream, or written inside an object there, from the decoded data,
1881
+ // with no key of its own. The body stays where it is, since the decoded data stays until the run ends.
1882
+ this.chargeSynthetic(String(num), directCost);
1883
+ if (obj instanceof objects_1.PdfStream) {
1884
+ this.heldBodies.set(num, { stm: stmNum, offset: obj.offset, length: obj.length });
1885
+ if (!this.quiet)
1886
+ this.issues.objStmStreams.add(num);
1887
+ }
1888
+ await this.lift(obj, null, String(num), stmNum);
1889
+ }
1890
+ return { obj, cost };
1027
1891
  }
1028
1892
  catch (e) {
1029
- if (e instanceof TimeLimitError)
1893
+ if (e instanceof TimeLimitError || e instanceof filters_1.DecompressionLimitError || e instanceof ObjectLimitError)
1030
1894
  throw e;
1031
1895
  this.issues.malformed++;
1032
1896
  return { obj: null, cost: 0 };
1033
1897
  }
1034
1898
  }
1035
- /** Decodes an object stream in chunks; large ones go to a temporary file instead of memory. */
1036
- async loadObjStm(stmNum) {
1899
+ /** The decoded object stream `stmNum`, decoded on first use, or null when it does not read. */
1900
+ async objStm(stmNum) {
1901
+ const known = this.objStms.get(stmNum);
1902
+ if (known !== undefined) {
1903
+ if (known?.mem && this.inMemory.delete(stmNum))
1904
+ this.inMemory.add(stmNum);
1905
+ return known;
1906
+ }
1907
+ let stm = null;
1908
+ try {
1909
+ stm = await this.loadObjStm(stmNum);
1910
+ }
1911
+ catch (e) {
1912
+ if (e instanceof filters_1.DecompressionLimitError || e instanceof TimeLimitError)
1913
+ throw e;
1914
+ }
1915
+ this.objStms.set(stmNum, stm);
1916
+ if (stm?.mem)
1917
+ await this.holdInMemory(stmNum, stm.mem.length);
1918
+ return stm;
1919
+ }
1920
+ /** Counts a decoded object stream against memoryThreshold, and moves the least recently used ones out past it. */
1921
+ async holdInMemory(stmNum, size) {
1922
+ this.inMemory.add(stmNum);
1923
+ this.inMemoryBytes += size;
1924
+ const threshold = this.opts.memoryThreshold ?? 8 * 1024 * 1024;
1925
+ for (const old of this.inMemory) {
1926
+ if (this.inMemoryBytes <= threshold)
1927
+ break;
1928
+ if (old === stmNum)
1929
+ continue;
1930
+ const o = this.objStms.get(old);
1931
+ const mem = o?.mem;
1932
+ this.inMemory.delete(old);
1933
+ if (!o || !mem)
1934
+ continue;
1935
+ const temp = this.tempDir();
1936
+ try {
1937
+ if (!this.pool) {
1938
+ const path = await temp.file('.objstm');
1939
+ this.pool = { path, size: 0, handle: await fsp.open(path, 'a+') };
1940
+ this.issues.memoryFallback.push('Decoded object streams');
1941
+ }
1942
+ for (let done = 0; done < mem.length;)
1943
+ done += (await this.pool.handle.write(mem, done, mem.length - done)).bytesWritten;
1944
+ }
1945
+ catch (e) {
1946
+ temp.ioError ??= e;
1947
+ throw e;
1948
+ }
1949
+ o.pooled = this.pool.size;
1950
+ this.pool.size += mem.length;
1951
+ o.mem = undefined;
1952
+ this.inMemoryBytes -= mem.length;
1953
+ this.objStmReaders.delete(old);
1954
+ }
1955
+ }
1956
+ tempDir() {
1957
+ if (this.opts.temp)
1958
+ return this.opts.temp;
1959
+ this.ownTemp ??= new io_1.TempDir();
1960
+ return this.ownTemp;
1961
+ }
1962
+ /** A reader over a decoded object stream. Only a few stay open, so a file handle is given up when its reader goes. */
1963
+ objStmReader(stmNum, stm) {
1964
+ const own = stm.path !== undefined;
1965
+ const readers = own ? this.objStmFileReaders : this.objStmReaders;
1966
+ const kept = readers.get(stmNum);
1967
+ if (kept)
1968
+ return kept;
1969
+ let source;
1970
+ if (stm.mem)
1971
+ source = (0, io_1.bufferSource)(stm.mem);
1972
+ else if (stm.pooled !== undefined && this.pool) {
1973
+ const { handle } = this.pool;
1974
+ const at = stm.pooled;
1975
+ const temp = this.tempDir();
1976
+ source = {
1977
+ size: async () => stm.size,
1978
+ read: async (offset, length) => {
1979
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset >= stm.size || !(length >= 1))
1980
+ return new Uint8Array(0);
1981
+ const n = Math.min(Math.floor(length), stm.size - offset);
1982
+ const buf = Buffer.allocUnsafe(n);
1983
+ try {
1984
+ for (let done = 0; done < n;) {
1985
+ const { bytesRead } = await handle.read(buf, done, n - done, at + offset + done);
1986
+ if (bytesRead === 0)
1987
+ throw Object.assign(new Error(`The temporary file of object streams ends at ${at + offset + done}`), { code: 'EIO' });
1988
+ done += bytesRead;
1989
+ }
1990
+ }
1991
+ catch (e) {
1992
+ temp.ioError ??= e;
1993
+ throw e;
1994
+ }
1995
+ return buf;
1996
+ },
1997
+ };
1998
+ }
1999
+ else if (stm.path !== undefined)
2000
+ source = this.tempDir().watch((0, io_1.fileSource)(stm.path));
2001
+ else
2002
+ throw new Error(`Object stream ${stmNum} has no data`);
2003
+ // Objects are small, so the pooled ones read in small blocks.
2004
+ const reader = stm.mem ? new io_1.Reader(source, stm.size, 65536, 64) : stm.pooled !== undefined ? new io_1.Reader(source, stm.size, 4096, 4) : new io_1.Reader(source, stm.size, 65536, 4);
2005
+ const out = readers.set(stmNum, reader);
2006
+ // A reader pushed out gives up its file handle. It closes after the current read, which this one may still be.
2007
+ if (out)
2008
+ void out.source.close?.()?.catch(() => undefined);
2009
+ return reader;
2010
+ }
2011
+ /**
2012
+ * Decodes an object stream in chunks; one larger than memoryThreshold goes to a temporary file. Of its header it
2013
+ * keeps the pairs that the entries pointing into it can reach, or with `all` every pair, for indexing it.
2014
+ */
2015
+ async loadObjStm(stmNum, all = false) {
1037
2016
  const entry = unpack(this.xref.get(stmNum));
1038
2017
  if (entry?.type !== 1)
1039
- return undefined;
2018
+ return null;
1040
2019
  const { obj } = await this.parseIndirectAt(entry.offset + this.base);
1041
- if (!(obj instanceof objects_1.PdfStream) || obj.dict.name('Type') !== 'ObjStm')
1042
- return undefined;
1043
- const threshold = this.opts.memoryThreshold ?? 8 * 1024 * 1024;
1044
- let temp = this.opts.temp;
1045
- if (temp === undefined) {
1046
- this.ownTemp ??= new io_1.TempDir();
1047
- temp = this.ownTemp;
2020
+ if (!(obj instanceof objects_1.PdfStream))
2021
+ return null;
2022
+ // pdf.js reads any stream an entry names as an object stream, once /N and /First are whole numbers.
2023
+ if (obj.dict.name('Type') !== 'ObjStm') {
2024
+ if (!Number.isInteger(obj.dict.get('N')) || !Number.isInteger(obj.dict.get('First')))
2025
+ return null;
2026
+ this.issues.objStmType.add(stmNum);
1048
2027
  }
1049
- const sink = new io_1.SpillSink(temp, threshold);
2028
+ const sink = new io_1.SpillSink(this.tempDir(), this.opts.memoryThreshold ?? 8 * 1024 * 1024);
1050
2029
  try {
1051
2030
  for await (const c of (0, filters_1.decodeChunks)(this.plainChunks(obj, stmNum), obj.dict, this.opts.decompressedBytes, () => this.checkTime()))
1052
2031
  await sink.write(c);
@@ -1056,92 +2035,202 @@ class PdfDocument {
1056
2035
  }
1057
2036
  const source = sink.source();
1058
2037
  const size = await source.size();
1059
- const reader = new io_1.Reader(source, size, 65536, sink.spilled ? 4 : 64);
1060
2038
  const n = Math.min(obj.dict.number('N') ?? 0, 10_000_000);
1061
2039
  const first = obj.dict.number('First') ?? 0;
1062
- // pdf.js reads no object of a stream whose header it cannot follow: these entries, then each offset in turn.
1063
- if (!Number.isInteger(obj.dict.get('N')) || !Number.isInteger(obj.dict.get('First')))
2040
+ const stm = { size, first, byNum: new Map(), offsets: new Map(), pairs: 0 };
2041
+ // pdf.js reads no object of a stream whose header it cannot follow: these entries, then each pair in turn.
2042
+ if (!Number.isInteger(obj.dict.get('N')) || !Number.isInteger(obj.dict.get('First')) || n < 0) {
1064
2043
  this.issues.badObjStm.add(stmNum);
1065
- const offsets = new Map();
2044
+ stm.badFrom = 0;
2045
+ }
1066
2046
  if (sink.spilled) {
1067
- // Recorded before the header is read, so release() closes and deletes the file even if that read fails.
2047
+ // Recorded before the header is read, so release() deletes the file even if that read fails.
1068
2048
  const spillPath = sink.path;
1069
2049
  if (spillPath === undefined)
1070
2050
  throw new Error(`Object stream ${stmNum} spilled without a file`);
1071
- this.spilledObjStm.set(stmNum, { path: spillPath, source, size, offsets, first });
2051
+ this.objStmFiles.push(spillPath);
2052
+ stm.path = spillPath;
1072
2053
  const part = `Object stream ${stmNum}`;
1073
2054
  if (!this.issues.memoryFallback.includes(part))
1074
2055
  this.issues.memoryFallback.push(part);
1075
2056
  }
1076
- // The header pairs are read in bounded windows: /First is untrusted and can claim the whole decoded stream.
1077
- const end = Math.min(size, Math.max(first, 0));
1078
- let win = 65536;
1079
- let last = -1;
1080
- for (let pos = 0, k = 0; k < n && pos < end;) {
1081
- this.checkTime();
1082
- const buf = await reader.read(pos, Math.min(win, end - pos));
1083
- const hp = new parser_1.Parser(buf, 0, pos + buf.length >= end);
1084
- let consumed = 0;
1085
- try {
1086
- for (; k < n; k++) {
1087
- const on = hp.parseObject();
1088
- const off = hp.parseObject();
1089
- if (typeof on === 'number' && typeof off === 'number')
1090
- offsets.set(on, off);
1091
- if (typeof off !== 'number' || !Number.isInteger(on) || !Number.isInteger(off) || off <= last)
1092
- this.issues.badObjStm.add(stmNum);
1093
- else
1094
- last = off;
1095
- consumed = hp.pos;
2057
+ else
2058
+ stm.mem = await source.read(0, size);
2059
+ // The pairs a lookup can reach: those naming an object whose entry points here, and the index each entry gives.
2060
+ const wanted = all ? undefined : this.wantedIn(stmNum);
2061
+ const reach = new Set();
2062
+ if (wanted)
2063
+ for (const index of wanted.values())
2064
+ reach.add(index).add(index + 1);
2065
+ const reader = new io_1.Reader(source, size, 65536, sink.spilled ? 4 : 64);
2066
+ try {
2067
+ // pdf.js reads /N pairs from the start of the data, wherever /First puts the objects. They are read here in
2068
+ // bounded windows.
2069
+ let win = 65536;
2070
+ let last = -1;
2071
+ let prev;
2072
+ for (let pos = 0, k = 0; k < n && pos < size && stm.badFrom !== 0;) {
2073
+ this.checkTime();
2074
+ const buf = await reader.read(pos, Math.min(win, size - pos));
2075
+ const hp = new parser_1.Parser(buf, 0, pos + buf.length >= size);
2076
+ let consumed = 0;
2077
+ try {
2078
+ for (; k < n; k++) {
2079
+ const on = hp.parseObject();
2080
+ const off = hp.parseObject();
2081
+ if (typeof on === 'number' && typeof off === 'number') {
2082
+ if (wanted === undefined || wanted.has(on)) {
2083
+ stm.byNum.set(on, k);
2084
+ reach.add(k + 1);
2085
+ }
2086
+ if (wanted === undefined || reach.has(k) || stm.byNum.get(on) === k)
2087
+ stm.offsets.set(k, off);
2088
+ }
2089
+ if (typeof off !== 'number' || !Number.isInteger(on) || !Number.isInteger(off)) {
2090
+ this.issues.badObjStm.add(stmNum);
2091
+ stm.badFrom = 0;
2092
+ break;
2093
+ }
2094
+ if (off <= last)
2095
+ this.issues.badObjStm.add(stmNum);
2096
+ else
2097
+ last = off;
2098
+ if (prev !== undefined && off < prev)
2099
+ stm.badFrom ??= k - 1;
2100
+ prev = off;
2101
+ consumed = hp.pos;
2102
+ stm.pairs = k + 1;
2103
+ }
2104
+ }
2105
+ catch (e) {
2106
+ if (!(e instanceof parser_1.NeedMoreData))
2107
+ break;
2108
+ }
2109
+ if (consumed > 0) {
2110
+ pos += consumed;
2111
+ win = 65536;
2112
+ }
2113
+ else {
2114
+ win *= 4;
2115
+ if (win > 1 << 20)
2116
+ break;
1096
2117
  }
1097
2118
  }
1098
- catch (e) {
1099
- // A short or damaged header still yields the pairs read so far.
1100
- if (!(e instanceof parser_1.NeedMoreData))
1101
- break;
1102
- }
1103
- if (consumed > 0) {
1104
- pos += consumed;
1105
- win = 65536;
2119
+ // A header with fewer than /N pairs that pdf.js can read leaves pdf.js no object at all.
2120
+ if (stm.pairs < n) {
2121
+ this.issues.badObjStm.add(stmNum);
2122
+ stm.badFrom = 0;
1106
2123
  }
1107
- else {
1108
- win *= 4;
1109
- if (win > 1 << 20)
1110
- break;
2124
+ }
2125
+ finally {
2126
+ await source.close?.();
2127
+ }
2128
+ return stm;
2129
+ }
2130
+ /** The objects whose entries point into object stream `stmNum`, with the index each entry gives. */
2131
+ wantedIn(stmNum) {
2132
+ if (!this.wanted) {
2133
+ const wanted = new Map();
2134
+ for (const [num, v] of this.xref) {
2135
+ const e = unpack(v);
2136
+ if (e?.type !== 2)
2137
+ continue;
2138
+ const m = wanted.get(e.stream) ?? new Map();
2139
+ wanted.set(e.stream, m);
2140
+ m.set(num, e.index);
1111
2141
  }
2142
+ this.wanted = wanted;
1112
2143
  }
1113
- return { reader, size, offsets, first };
2144
+ return this.wanted.get(stmNum) ?? new Map();
1114
2145
  }
1115
2146
  ownTemp;
1116
2147
  /**
1117
- * Parses at an offset in any reader, growing the window as needed. A window that reaches `bound`, the next known
1118
- * object, grows further only while a budget for the whole file lasts, so damaged files still parse and crafted
1119
- * ones stay linear. `key` names the place read, so its bad tokens count once however often it is parsed.
2148
+ * Parses at an offset in any reader. The window starts small and grows while the parser asks for more. A run of
2149
+ * whitespace and comments that reaches the end of the window is skipped in the source and stands in the window as one
2150
+ * space, and the body of a stream written inside the object is found in the source and left out, so the window holds
2151
+ * the object's tokens and not its padding. A window that reaches `bound`, the next known object, grows further only
2152
+ * while a budget for the whole file lasts, unless `hard` makes the bound the end of the data. `key` names the place
2153
+ * read, so its bad tokens count once however often it is parsed.
1120
2154
  */
1121
- async parseWindowed(reader, size, offset, parse, bound = size, key = String(offset)) {
1122
- let len = 4096;
1123
- let end = Math.min(bound, size);
2155
+ async parseWindowed(reader, size, offset, parse, bound = size, key = String(offset), options = {}) {
2156
+ // Offsets come from the file: /Prev, /XRefStm, an object stream's /First plus an offset. One before the start,
2157
+ // or between two bytes, holds no object.
2158
+ if (!Number.isSafeInteger(offset) || offset < 0)
2159
+ throw new parser_1.ParseError(`No object at offset ${offset}`);
2160
+ const limit = options.hard ? Math.min(bound, size) : size;
2161
+ // The window is `kept`, the pieces already parsed past, then the tail read from `tail` on. A piece marked as one
2162
+ // byte stands for what was left out there: a skipped run of whitespace, or a stream body, starting at `from`.
2163
+ const kept = [];
2164
+ let tail = offset;
2165
+ let grow = 4096;
2166
+ let end = Math.min(bound, limit);
2167
+ let skips = 0;
2168
+ // Stream bodies found in the source, by where they start: the length, or null with no endstream. And the lines
2169
+ // after "stream" keywords, by where they start.
2170
+ const bodies = new Map();
2171
+ const lines = new Map();
1124
2172
  for (;;) {
1125
2173
  // Each larger window parses the object again from the start.
1126
2174
  this.checkTime();
1127
- const n = Math.min(len, end - offset);
1128
- if (offset + n > bound) {
1129
- this.parseBudget -= n;
2175
+ let keptLength = 0;
2176
+ for (const k of kept)
2177
+ keptLength += k.bytes.length;
2178
+ const n = Math.max(0, Math.min(grow, end - tail));
2179
+ if (!options.hard && tail + n > bound) {
2180
+ this.parseBudget -= keptLength + n;
1130
2181
  if (this.parseBudget < 0)
1131
2182
  throw new parser_1.ParseError('Object runs past the next object');
1132
2183
  }
1133
- const window = await reader.read(offset, n);
1134
- const atEnd = offset + window.length >= size;
2184
+ const tailBytes = n > 0 ? await reader.read(tail, n) : new Uint8Array(0);
2185
+ const window = kept.length ? Buffer.concat([...kept.map(k => k.bytes), tailBytes]) : tailBytes;
2186
+ const atEnd = tail + tailBytes.length >= limit;
2187
+ /** Where window byte `pos` sits in the source. */
2188
+ const at = (pos) => {
2189
+ let base = 0;
2190
+ for (const k of kept) {
2191
+ if (pos < base + k.bytes.length)
2192
+ return k.one ? k.from : k.from + pos - base;
2193
+ base += k.bytes.length;
2194
+ }
2195
+ return tail + pos - base;
2196
+ };
1135
2197
  let bad = 0;
1136
- const p = new parser_1.Parser(window, 0, atEnd, { ...this.hooks, onBadToken: () => bad++ });
2198
+ // Each /Length not read yet costs one more parse of the window, so an object may name only a few.
2199
+ let lengths = 0;
2200
+ const streams = options.streams
2201
+ ? {
2202
+ at,
2203
+ length: (ref) => {
2204
+ if (++lengths > MAX_INDIRECT_LENGTHS)
2205
+ throw new parser_1.ParseError('Too many streams with an indirect /Length');
2206
+ if (!this.directLengths.has(ref.num))
2207
+ throw new parser_1.NeedObject(ref);
2208
+ return this.directLengths.get(ref.num);
2209
+ },
2210
+ known: (start) => bodies.get(start),
2211
+ line: (start) => lines.get(start),
2212
+ }
2213
+ : undefined;
2214
+ const p = new parser_1.Parser(window, 0, atEnd, { ...this.hooks, onBadToken: () => bad++ }, { recover: options.recover, streams, topStream: options.topStream });
1137
2215
  let final = true;
2216
+ let more;
1138
2217
  try {
1139
- return await parse(p);
2218
+ return await parse(p, at);
1140
2219
  }
1141
2220
  catch (e) {
2221
+ if (e instanceof parser_1.NeedObject) {
2222
+ // The same window parses again once the length is known. It is marked first, so a length that leads back
2223
+ // to the object being parsed reads as unknown.
2224
+ final = false;
2225
+ this.directLengths.set(e.ref.num, undefined);
2226
+ const v = await this.getObject(e.ref);
2227
+ this.directLengths.set(e.ref.num, typeof v === 'number' && Number.isInteger(v) ? v : undefined);
2228
+ continue;
2229
+ }
1142
2230
  final = !(e instanceof parser_1.NeedMoreData) || atEnd;
1143
2231
  if (final)
1144
2232
  throw e;
2233
+ more = e;
1145
2234
  }
1146
2235
  finally {
1147
2236
  // A larger window reads the same tokens again; only the read that ends counts them.
@@ -1150,13 +2239,202 @@ class PdfDocument {
1150
2239
  this.issues.malformed += bad;
1151
2240
  }
1152
2241
  }
1153
- if (offset + window.length >= end)
1154
- end = size;
1155
- len *= 4;
1156
- if (len > 256 * 1024 * 1024)
2242
+ const cut = (pos) => {
2243
+ // Keeps window bytes [0, pos), which the parser has read past, and drops the rest.
2244
+ let base = 0;
2245
+ for (let i = 0; i < kept.length; i++) {
2246
+ const k = kept[i];
2247
+ if (pos <= base + k.bytes.length) {
2248
+ kept[i] = { ...k, bytes: k.bytes.subarray(0, pos - base) };
2249
+ kept.length = i + 1;
2250
+ return;
2251
+ }
2252
+ base += k.bytes.length;
2253
+ }
2254
+ if (pos > base)
2255
+ kept.push({ from: tail, bytes: tailBytes.subarray(0, pos - base) });
2256
+ };
2257
+ const { body, idle, line } = more;
2258
+ const softBound = options.hard ? limit : bound;
2259
+ // Past a number of these, the window grows over what is left instead, so the passes stay few.
2260
+ if (skips < MAX_SKIPS && body && options.streams) {
2261
+ // The body of a stream written inside the object: found in the source, then left out of the window.
2262
+ skips++;
2263
+ const start = at(body.start);
2264
+ if (bodies.has(start))
2265
+ throw new parser_1.ParseError('A stream body was found twice');
2266
+ const found = await this.findBody(reader, start, body.declared, limit, softBound);
2267
+ bodies.set(start, found);
2268
+ if (found) {
2269
+ cut(body.start);
2270
+ kept.push({ from: start, bytes: Buffer.from(' '), one: true });
2271
+ tail = found.after;
2272
+ if (tail >= end)
2273
+ end = limit;
2274
+ }
2275
+ continue;
2276
+ }
2277
+ if (skips < MAX_SKIPS && line && options.streams) {
2278
+ // The line after a "stream" keyword: its end found in the source. The body starts after it.
2279
+ skips++;
2280
+ const start = at(line.from - 1) + 1;
2281
+ if (lines.has(start))
2282
+ throw new parser_1.ParseError('A stream line was found twice');
2283
+ const found = await this.lineEnd(reader, start, limit, softBound);
2284
+ lines.set(start, { text: found.text });
2285
+ cut(line.from);
2286
+ tail = found.after;
2287
+ if (tail >= end)
2288
+ end = limit;
2289
+ continue;
2290
+ }
2291
+ if (skips < MAX_SKIPS && idle) {
2292
+ // A run of whitespace or comments reaches the end of the window: the rest of it is skipped in the source.
2293
+ let comment = false;
2294
+ for (let i = idle.from; i < window.length; i++) {
2295
+ const c = window[i];
2296
+ if (comment)
2297
+ comment = c !== 0x0a && c !== 0x0d;
2298
+ else
2299
+ comment = idle.comments && c === 0x25;
2300
+ }
2301
+ const edge = tail + tailBytes.length;
2302
+ const next = await this.skipRun(reader, edge, comment, idle.comments, limit, softBound);
2303
+ if (next > edge || idle.from < window.length) {
2304
+ skips++;
2305
+ const from = idle.from < window.length ? at(idle.from) : edge;
2306
+ cut(idle.from);
2307
+ kept.push({ from, bytes: Buffer.from(' '), one: true });
2308
+ tail = next;
2309
+ // The window goes on past the next known object only while the budget lasts.
2310
+ if (tail >= end)
2311
+ end = limit;
2312
+ continue;
2313
+ }
2314
+ }
2315
+ if (tail + tailBytes.length >= end)
2316
+ end = limit;
2317
+ grow *= 4;
2318
+ if (grow > 256 * 1024 * 1024)
1157
2319
  throw new parser_1.ParseError('Object too large');
1158
2320
  }
1159
2321
  }
2322
+ /**
2323
+ * Where the line after a "stream" keyword ends, as pdf.js finds it: after the first CR, LF or CRLF from `start` on, or
2324
+ * at `limit`. `text` says whether anything but spaces came first. Bytes past `bound` come out of the parse budget.
2325
+ */
2326
+ async lineEnd(reader, start, limit, bound) {
2327
+ let text = false;
2328
+ let charged = Math.max(start, bound);
2329
+ for (let pos = start; pos < limit;) {
2330
+ this.checkTime();
2331
+ const buf = await reader.source.read(pos, Math.min(SKIP_CHUNK, limit - pos));
2332
+ if (buf.length === 0)
2333
+ break;
2334
+ let i = 0;
2335
+ for (; i < buf.length && buf[i] !== 0x0d && buf[i] !== 0x0a; i++)
2336
+ if (buf[i] !== 0x20 && buf[i] !== 0x09 && buf[i] !== 0x0c && buf[i] !== 0x00)
2337
+ text = true;
2338
+ const reached = pos + Math.min(i + 2, buf.length);
2339
+ if (reached > charged) {
2340
+ this.parseBudget -= reached - charged;
2341
+ charged = reached;
2342
+ if (this.parseBudget < 0)
2343
+ throw new parser_1.ParseError('Object runs past the next object');
2344
+ }
2345
+ if (i < buf.length) {
2346
+ const eol = pos + i;
2347
+ const next = i + 1 < buf.length ? buf[i + 1] : eol + 1 < limit ? (await reader.read(eol + 1, 1))[0] : -1;
2348
+ const crlf = buf[i] === 0x0d && next === 0x0a;
2349
+ return { after: Math.min(limit, eol + (crlf ? 2 : 1)), text };
2350
+ }
2351
+ pos += buf.length;
2352
+ }
2353
+ return { after: limit, text };
2354
+ }
2355
+ /**
2356
+ * Where a run of whitespace, and of comments when `comments` is set, ends: the first byte at or after `from` that
2357
+ * belongs to neither, or `limit`. `comment` says whether `from` is inside a comment. Bytes past `bound` come out of
2358
+ * the parse budget.
2359
+ */
2360
+ async skipRun(reader, from, comment, comments, limit, bound) {
2361
+ let inComment = comment;
2362
+ // Bytes past `bound` come out of the budget once each.
2363
+ let charged = Math.max(from, bound);
2364
+ const charge = (to) => {
2365
+ if (to > charged) {
2366
+ this.parseBudget -= to - charged;
2367
+ charged = to;
2368
+ }
2369
+ if (this.parseBudget < 0)
2370
+ throw new parser_1.ParseError('Object runs past the next object');
2371
+ return to;
2372
+ };
2373
+ for (let pos = from; pos < limit;) {
2374
+ this.checkTime();
2375
+ const buf = await reader.source.read(pos, Math.min(SKIP_CHUNK, limit - pos));
2376
+ if (buf.length === 0)
2377
+ break;
2378
+ // Padding is mostly one byte repeated, which a comparison skips at once.
2379
+ const pad = padding();
2380
+ if (view(buf).equals(pad.spaces.subarray(0, buf.length)) || view(buf).equals(pad.zeros.subarray(0, buf.length))) {
2381
+ pos += buf.length;
2382
+ if (pos > bound)
2383
+ charge(pos);
2384
+ continue;
2385
+ }
2386
+ for (let i = 0; i < buf.length; i++) {
2387
+ const c = buf[i];
2388
+ if (inComment)
2389
+ inComment = c !== 0x0a && c !== 0x0d;
2390
+ else if (comments && c === 0x25)
2391
+ inComment = true;
2392
+ else if (!(0, parser_1.isWhite)(c))
2393
+ return charge(pos + i);
2394
+ }
2395
+ pos += buf.length;
2396
+ }
2397
+ return charge(limit);
2398
+ }
2399
+ /**
2400
+ * Finds the body of a stream written inside an object, starting at `start`, as the parser would in a window that held
2401
+ * it: at its /Length when "endstream" follows after whitespace, and otherwise at the first "endstream". Returns its
2402
+ * length and where the object goes on after "endstream", or null when no "endstream" comes before `limit`.
2403
+ */
2404
+ async findBody(reader, start, declared, limit, bound) {
2405
+ if (declared !== undefined && declared >= 0 && start + declared <= limit) {
2406
+ const p = await this.skipRun(reader, start + declared, false, false, limit, bound);
2407
+ if (p + ENDSTREAM.length <= limit && view(await reader.read(p, ENDSTREAM.length)).equals(ENDSTREAM))
2408
+ return { length: declared, after: p + ENDSTREAM.length };
2409
+ }
2410
+ for (let pos = start; pos < limit;) {
2411
+ this.checkTime();
2412
+ const n = Math.min(65536, limit - pos);
2413
+ if (pos + n > bound) {
2414
+ this.parseBudget -= pos + n - Math.max(pos, bound);
2415
+ if (this.parseBudget < 0)
2416
+ throw new parser_1.ParseError('Object runs past the next object');
2417
+ }
2418
+ // Overlapping reads find a keyword that spans two of them.
2419
+ const buf = await reader.read(pos, Math.min(n + ENDSTREAM.length, limit - pos));
2420
+ const found = (0, parser_1.endstreamIn)(buf, 0);
2421
+ if (found && found.at < n) {
2422
+ const at = pos + found.at;
2423
+ // The EOL before "endstream" belongs to the keyword, as for any other stream.
2424
+ let end = at;
2425
+ const back = await reader.read(Math.max(start, at - 2), Math.min(2, at - start));
2426
+ if (back.length === 2 && back[0] === 0x0d && back[1] === 0x0a)
2427
+ end -= 2;
2428
+ else if (back.length >= 1 && (back[back.length - 1] === 0x0a || back[back.length - 1] === 0x0d))
2429
+ end -= 1;
2430
+ return { length: end - start, after: at + found.length };
2431
+ }
2432
+ if (buf.length < n)
2433
+ break;
2434
+ pos += n;
2435
+ }
2436
+ return null;
2437
+ }
1160
2438
  async resolve(v) {
1161
2439
  let x = v;
1162
2440
  for (let i = 0; i < 32 && x instanceof objects_1.PdfRef; i++)
@@ -1171,15 +2449,35 @@ class PdfDocument {
1171
2449
  }
1172
2450
  /** Length of the stream once decrypted, which is what gets written out. */
1173
2451
  async plainLength(stream, num) {
1174
- if (!this.security)
2452
+ const held = this.heldBodies.get(num);
2453
+ if (held)
2454
+ return held.length;
2455
+ const key = this.keyOf(num);
2456
+ if (!this.security || !key)
1175
2457
  return stream.length;
1176
- return this.security.plainLength(stream, num, this.genOf(num), (o, l) => this.reader.read(stream.offset + o, l));
2458
+ return this.security.plainLength(stream, key.num, key.gen, (o, l) => this.reader.read(stream.offset + o, l));
2459
+ }
2460
+ /** False for a stream encrypted with a key the password did not give, as attached files can be. */
2461
+ canDecrypt(stream, num) {
2462
+ return !this.security || this.heldBodies.has(num) || !this.keyOf(num) || this.security.canDecrypt(stream);
1177
2463
  }
1178
2464
  /** Yields the decrypted, still-encoded body in chunks. */
1179
2465
  async *plainChunks(stream, num) {
2466
+ const held = this.heldBodies.get(num);
2467
+ if (held) {
2468
+ const stm = this.objStms.get(held.stm);
2469
+ if (!stm)
2470
+ throw new Error(`Object stream ${held.stm} is no longer decoded`);
2471
+ for await (const c of this.objStmReader(held.stm, stm).chunks(held.offset, held.length)) {
2472
+ this.checkTime();
2473
+ yield c;
2474
+ }
2475
+ return;
2476
+ }
1180
2477
  let dec;
1181
- if (this.security)
1182
- dec = this.security.chunkDecryptor(stream, num, this.genOf(num), await this.plainLength(stream, num));
2478
+ const key = this.keyOf(num);
2479
+ if (this.security && key)
2480
+ dec = this.security.chunkDecryptor(stream, key.num, key.gen, await this.plainLength(stream, num));
1183
2481
  for await (const c of this.reader.chunks(stream.offset, stream.length)) {
1184
2482
  this.checkTime();
1185
2483
  const out = dec ? dec.update(c) : c;
@@ -1379,20 +2677,38 @@ class PdfDocument {
1379
2677
  await this.dropObjStms();
1380
2678
  this.objCache = new Lru(1);
1381
2679
  this.xref.clear();
2680
+ this.synthetic.clear();
2681
+ this.syntheticAt.clear();
2682
+ this.heldBodies.clear();
1382
2683
  this.sortedOffsets = undefined;
1383
2684
  this.liveByOffset = undefined;
1384
2685
  this.lastLive = undefined;
1385
2686
  await this.ownTemp?.cleanup();
1386
2687
  }
1387
- /** Forgets every decoded object stream, closing and deleting the ones that spilled to a temporary file. */
2688
+ /**
2689
+ * Forgets every decoded object stream, closing and deleting the temporary files, once the map or the key changes. The
2690
+ * streams whose bodies they held are forgotten with the parsed objects.
2691
+ */
1388
2692
  async dropObjStms() {
1389
- const spilled = Array.from(this.spilledObjStm.values());
1390
- this.objStmCache = new Lru(8);
1391
- this.spilledObjStm.clear();
1392
- for (const s of spilled) {
2693
+ const readers = [...this.objStmReaders.values(), ...this.objStmFileReaders.values()];
2694
+ const files = this.objStmFiles;
2695
+ const pool = this.pool;
2696
+ if (pool)
2697
+ files.push(pool.path);
2698
+ this.objStmReaders = new Lru(256);
2699
+ this.objStmFileReaders = new Lru(8);
2700
+ this.objStmFiles = [];
2701
+ this.pool = undefined;
2702
+ await pool?.handle.close().catch(() => undefined);
2703
+ this.objStms.clear();
2704
+ this.inMemory.clear();
2705
+ this.inMemoryBytes = 0;
2706
+ this.wanted = undefined;
2707
+ for (const r of readers)
2708
+ await r.source.close?.()?.catch(() => undefined);
2709
+ for (const f of files) {
1393
2710
  try {
1394
- await s.source.close?.();
1395
- await fsp.rm(s.path, { force: true });
2711
+ await fsp.rm(f, { force: true });
1396
2712
  }
1397
2713
  catch {
1398
2714
  /* the run's temporary directory is removed later anyway */