@patrickdk77/pdf-defuse 0.0.0-stage → 0.1.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.
@@ -0,0 +1,1403 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.PdfDocument = exports.ObjectLimitError = exports.TimeLimitError = exports.OpenError = exports.packStream = exports.packOffset = void 0;
37
+ exports.unpack = unpack;
38
+ const fsp = __importStar(require("node:fs/promises"));
39
+ const crypto_1 = require("./crypto");
40
+ const filters_1 = require("./filters");
41
+ const io_1 = require("./io");
42
+ const objects_1 = require("./objects");
43
+ const parser_1 = require("./parser");
44
+ /**
45
+ * Whether [first, count] ranges give some object number twice. pdf.js and qpdf read the first such row, which would
46
+ * take a record of every number read, so the map is rebuilt instead.
47
+ */
48
+ function repeatsANumber(ranges) {
49
+ const order = Array.from({ length: ranges.length / 2 }, (_, i) => i)
50
+ .filter(i => ranges[2 * i + 1] > 0)
51
+ .sort((a, b) => ranges[2 * a] - ranges[2 * b]);
52
+ let end = Number.NEGATIVE_INFINITY;
53
+ for (const i of order) {
54
+ if (ranges[2 * i] < end)
55
+ return true;
56
+ end = Math.max(end, ranges[2 * i] + ranges[2 * i + 1]);
57
+ }
58
+ return false;
59
+ }
60
+ /** Entries are packed into one number each, so the index costs a few bytes per object. */
61
+ const packOffset = (offset, gen) => offset * 65536 + (gen & 0xffff);
62
+ exports.packOffset = packOffset;
63
+ const packStream = (stream) => -stream - 1;
64
+ exports.packStream = packStream;
65
+ function unpack(v) {
66
+ if (v === undefined)
67
+ return undefined;
68
+ if (v >= 0)
69
+ return { type: 1, offset: Math.floor(v / 65536), gen: v % 65536 };
70
+ return { type: 2, stream: -v - 1 };
71
+ }
72
+ class OpenError extends Error {
73
+ kind;
74
+ constructor(kind, message) {
75
+ super(message);
76
+ this.kind = kind;
77
+ }
78
+ }
79
+ exports.OpenError = OpenError;
80
+ /** Past the deadline. The code makes every package of the family pass it up to the top. */
81
+ class TimeLimitError extends Error {
82
+ code = 'DEFUSE_LIMIT';
83
+ limit = 'time';
84
+ }
85
+ exports.TimeLimitError = TimeLimitError;
86
+ class DecodeCapError extends Error {
87
+ }
88
+ class ObjectLimitError extends Error {
89
+ }
90
+ exports.ObjectLimitError = ObjectLimitError;
91
+ /** Holds at most `max` entries, whose weights add up to at most `maxWeight`. */
92
+ class Lru {
93
+ max;
94
+ maxWeight;
95
+ map = new Map();
96
+ weight = 0;
97
+ constructor(max, maxWeight = Number.POSITIVE_INFINITY) {
98
+ this.max = max;
99
+ this.maxWeight = maxWeight;
100
+ }
101
+ get(k) {
102
+ const e = this.map.get(k);
103
+ if (e !== undefined) {
104
+ this.map.delete(k);
105
+ this.map.set(k, e);
106
+ }
107
+ return e?.value;
108
+ }
109
+ /** Returns the last entry pushed out, if any. An entry heavier than the whole cache is kept alone. */
110
+ set(k, value, weight = 0) {
111
+ const old = this.map.get(k);
112
+ if (old)
113
+ this.weight -= old.weight;
114
+ this.map.delete(k);
115
+ this.map.set(k, { value, weight });
116
+ this.weight += weight;
117
+ let out;
118
+ while (this.map.size > 1 && (this.map.size > this.max || this.weight > this.maxWeight)) {
119
+ const [oldest, e] = this.map.entries().next().value;
120
+ this.map.delete(oldest);
121
+ this.weight -= e.weight;
122
+ out = e.value;
123
+ }
124
+ return out;
125
+ }
126
+ }
127
+ /**
128
+ * The parsed objects kept for reuse, bounded by count and by the heap the parser estimates they take. Without the
129
+ * second bound, a few kilobytes of object stream could fill the cache with large objects.
130
+ */
131
+ const objectCache = () => new Lru(2048, 32 * 1024 * 1024);
132
+ const enc = (s) => Buffer.from(s, 'latin1');
133
+ const OBJ = enc('obj');
134
+ const view = (u) => Buffer.from(u.buffer, u.byteOffset, u.byteLength);
135
+ /** A PDF opened for random access. Objects are read on demand and never all held at once. */
136
+ class PdfDocument {
137
+ reader;
138
+ size;
139
+ opts;
140
+ headerOffset = 0;
141
+ headerVersion = '1.4';
142
+ /** Added to every offset when the file has bytes before its header and the xref ignores them. */
143
+ base = 0;
144
+ /** Object number to packed entry. */
145
+ xref = new Map();
146
+ trailer = new objects_1.PdfDict();
147
+ sections = 0;
148
+ rebuilt = false;
149
+ hasEof = false;
150
+ trailingBytes = 0;
151
+ security;
152
+ securityResult;
153
+ encryptRef;
154
+ issues = {
155
+ escapedNames: new Set(),
156
+ streamLengthWrong: 0,
157
+ malformed: 0,
158
+ genMismatch: 0,
159
+ badOffsets: 0,
160
+ duplicateKeys: new Set(),
161
+ superseded: 0,
162
+ structuralRefs: 0,
163
+ badXRefStm: 0,
164
+ xrefStmOverFree: 0,
165
+ badObjStm: new Set(),
166
+ refGen: new Set(),
167
+ memoryFallback: [],
168
+ };
169
+ /** Body offsets of the streams already counted in issues.streamLengthWrong, so a second read adds nothing. */
170
+ lengthWrongAt = new Set();
171
+ 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();
175
+ /** Object streams found by scan(), indexed once the key exists. */
176
+ pendingObjStm = [];
177
+ /** Streams whose indirect /Length or filter entries are being resolved, so a cycle ends after one lap. */
178
+ lengthResolving = new Set();
179
+ /**
180
+ * Stream number to the objects its filter entries name. Their values are copied into the stream, so the objects
181
+ * are referenced when the walk visits the stream, though it never visits them.
182
+ */
183
+ filterRefs = new Map();
184
+ /** Objects read for the Encrypt dictionary, which the walk never visits. */
185
+ encryptRefs = new Set();
186
+ /** Set while a lookup must leave the issues as they are. */
187
+ quiet = false;
188
+ hooks = {
189
+ onEscapedName: (n) => {
190
+ if (!this.quiet)
191
+ this.issues.escapedNames.add(n);
192
+ },
193
+ onDuplicateKey: (k) => {
194
+ if (!this.quiet)
195
+ this.issues.duplicateKeys.add(k);
196
+ },
197
+ onProgress: () => this.checkTime(),
198
+ };
199
+ /** Where the objects whose bad tokens are already in issues.malformed were read, so a second read adds nothing. */
200
+ badTokensCounted = new Set();
201
+ scanned = false;
202
+ /** Offsets of the xref sections and xref streams that read, which are not ordinary object definitions. */
203
+ structuralOffsets = new Set();
204
+ /** The definitions that older xref sections use and newer ones replaced or freed, as object number then offset. */
205
+ supersededAt = [];
206
+ sortedOffsets;
207
+ /** Live uncompressed objects sorted by offset, for finding the object around a position. */
208
+ liveByOffset;
209
+ /** The extent of the live object insideLiveObject() read last, empty when it does not parse. */
210
+ lastLive;
211
+ unboundedScanBudget = 0;
212
+ /** Bytes parseWindowed() may still read past the next known object. */
213
+ parseBudget = 0;
214
+ constructor(reader, size, opts) {
215
+ this.reader = reader;
216
+ this.size = size;
217
+ this.opts = opts;
218
+ }
219
+ static async open(source, opts = {}) {
220
+ const size = await source.size();
221
+ const doc = new PdfDocument(new io_1.Reader(source, size), size, opts);
222
+ try {
223
+ await doc.init();
224
+ }
225
+ catch (e) {
226
+ await doc.release();
227
+ throw e;
228
+ }
229
+ return doc;
230
+ }
231
+ checkTime() {
232
+ if (this.opts.deadline !== undefined && Date.now() > this.opts.deadline)
233
+ throw new TimeLimitError('Time limit exceeded');
234
+ }
235
+ checkObjectLimit(count) {
236
+ if (this.opts.objectLimit !== undefined && count > this.opts.objectLimit)
237
+ throw new ObjectLimitError(`${count} objects`);
238
+ }
239
+ async init() {
240
+ this.parseBudget = this.size * 4;
241
+ const head = await this.reader.read(0, Math.min(this.size, 1024));
242
+ 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];
249
+ const tailLen = Math.min(this.size, 4096);
250
+ const tail = await this.reader.read(this.size - tailLen, tailLen);
251
+ const eof = view(tail).lastIndexOf(enc('%%EOF'));
252
+ if (eof >= 0) {
253
+ this.hasEof = true;
254
+ let rest = 0;
255
+ for (let i = eof + 5; i < tail.length; i++)
256
+ if (!(0, parser_1.isWhite)(tail[i]))
257
+ rest++;
258
+ this.trailingBytes = rest;
259
+ }
260
+ const sx = view(tail).lastIndexOf(enc('startxref'));
261
+ let ok = false;
262
+ if (sx >= 0) {
263
+ const mm = /^startxref\s+(\d+)/.exec(Buffer.from(tail.subarray(sx, sx + 40)).toString('latin1'));
264
+ if (mm) {
265
+ try {
266
+ await this.loadXrefChain(Number(mm[1]));
267
+ ok = this.trailer.get('Root') instanceof objects_1.PdfRef;
268
+ }
269
+ catch (e) {
270
+ if (e instanceof TimeLimitError || e instanceof ObjectLimitError)
271
+ throw e;
272
+ ok = false;
273
+ }
274
+ }
275
+ }
276
+ if (!ok) {
277
+ this.xref.clear();
278
+ this.trailer = new objects_1.PdfDict();
279
+ await this.scan();
280
+ }
281
+ this.checkObjectLimit(this.xref.size);
282
+ await this.initSecurity();
283
+ // Without the key, encrypted object streams are unreadable, and the catalog or page tree may live in one.
284
+ if (this.securityResult && this.securityResult.status !== 'ok')
285
+ return;
286
+ await this.indexObjStms();
287
+ 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.
291
+ this.xref.clear();
292
+ const trailer = this.trailer;
293
+ await this.scan();
294
+ if (!(this.trailer.get('Root') instanceof objects_1.PdfRef))
295
+ this.trailer = trailer;
296
+ await this.initSecurity();
297
+ if (this.securityResult && this.securityResult.status !== 'ok')
298
+ return;
299
+ await this.indexObjStms();
300
+ root = await this.resolve(this.trailer.get('Root'));
301
+ }
302
+ if (!(root instanceof objects_1.PdfDict))
303
+ throw new OpenError(this.hasEof ? 'unparseable' : 'truncated', 'No document catalog');
304
+ const pages = await this.resolve(root.get('Pages'));
305
+ if (!(pages instanceof objects_1.PdfDict))
306
+ throw new OpenError(this.hasEof ? 'unparseable' : 'truncated', 'No page tree');
307
+ }
308
+ async initSecurity() {
309
+ const e = this.trailer.get('Encrypt');
310
+ if (e === undefined || e === null)
311
+ return;
312
+ if (e instanceof objects_1.PdfRef)
313
+ this.encryptRef = e;
314
+ // Viewers accept indirect values here and in /CF. They are read raw, since nothing in this dictionary is
315
+ // encrypted, and each object once, so naming one large object many times costs one read.
316
+ const read = new Map();
317
+ const direct = async (value) => {
318
+ let v = value;
319
+ for (let i = 0; i < 32 && v instanceof objects_1.PdfRef; i++) {
320
+ let got = read.get(v.num);
321
+ if (got === undefined) {
322
+ got = await this.getObject(v, true);
323
+ read.set(v.num, got);
324
+ }
325
+ v = got;
326
+ }
327
+ return v;
328
+ };
329
+ const found = await direct(e);
330
+ if (!(found instanceof objects_1.PdfDict))
331
+ return;
332
+ const dict = new objects_1.PdfDict();
333
+ for (const [k, v] of found.entries())
334
+ dict.set(k, await direct(v));
335
+ const cf = dict.get('CF');
336
+ if (cf instanceof objects_1.PdfDict) {
337
+ const filters = new objects_1.PdfDict();
338
+ for (const [k, v] of cf.entries())
339
+ filters.set(k, await direct(v));
340
+ dict.set('CF', filters);
341
+ }
342
+ // The walker never visits the Encrypt dictionary, so what it names would otherwise look unreferenced.
343
+ for (const num of read.keys())
344
+ this.encryptRefs.add(num);
345
+ const id = this.trailer.get('ID');
346
+ const id0 = Array.isArray(id) && id[0] instanceof objects_1.PdfString ? id[0].bytes : new Uint8Array(0);
347
+ const password = this.opts.password ?? '';
348
+ let r = crypto_1.SecurityHandler.open(dict, id0, password);
349
+ // A password meant for other files must not lock out one that opens without a password.
350
+ if (r.status === 'password-required' && password !== '')
351
+ r = crypto_1.SecurityHandler.open(dict, id0, '');
352
+ this.securityResult = r;
353
+ if (r.status === 'ok')
354
+ this.security = r.handler;
355
+ // Objects and object streams read before the key existed hold ciphertext.
356
+ this.objCache = objectCache();
357
+ await this.dropObjStms();
358
+ }
359
+ // ---------- cross-reference ----------
360
+ async loadXrefChain(start) {
361
+ const seen = new Set();
362
+ let offset = start;
363
+ let firstTrailer = true;
364
+ const deleted = new Set();
365
+ while (offset !== undefined && !seen.has(offset)) {
366
+ this.checkTime();
367
+ seen.add(offset);
368
+ const section = await this.readXrefSection(offset);
369
+ this.sections++;
370
+ for (const [num, entry] of section.entries) {
371
+ if (this.xref.has(num) || deleted.has(num)) {
372
+ // A newer revision already defined or freed this object: the older definition is hidden content.
373
+ if (this.xref.get(num) !== entry) {
374
+ this.issues.superseded++;
375
+ if (entry >= 0)
376
+ this.supersededAt.push(num, Math.floor(entry / 65536) + this.base);
377
+ }
378
+ continue;
379
+ }
380
+ this.xref.set(num, entry);
381
+ }
382
+ this.checkObjectLimit(this.xref.size);
383
+ const info = section.trailer.get('Info');
384
+ if (firstTrailer) {
385
+ // pdf.js reads /Root from the newest trailer only, and rebuilds the map by scanning when it is missing.
386
+ this.trailer = section.trailer;
387
+ firstTrailer = false;
388
+ }
389
+ else if (!this.trailer.has('Info') && info !== undefined)
390
+ this.trailer.set('Info', info);
391
+ const prev = section.trailer.get('Prev');
392
+ offset = typeof prev === 'number' && !seen.has(prev) ? prev : undefined;
393
+ // Free rows matter only to older sections. Each is held one number at a time, so they may not outnumber the
394
+ // live objects by much: a few bytes of xref stream can free millions of numbers.
395
+ if (offset === undefined)
396
+ continue;
397
+ const f = section.free;
398
+ for (let i = 0; i < f.length; i += 2) {
399
+ for (let num = f[i]; num < f[i + 1]; num++) {
400
+ if (!this.xref.has(num))
401
+ deleted.add(num);
402
+ if (deleted.size > Math.max(65536, this.xref.size))
403
+ throw new parser_1.ParseError('Too many free xref rows');
404
+ }
405
+ }
406
+ }
407
+ // Offsets cached while the map was still loading would bound every later endstream search by the end of the file.
408
+ this.sortedOffsets = undefined;
409
+ }
410
+ /** Reads one section; for hybrid files its XRefStm entries add the objects the table does not give. */
411
+ async readXrefSection(offset) {
412
+ const attempt = async (off) => {
413
+ const peek = await this.reader.read(off, 16);
414
+ const s = Buffer.from(peek).toString('latin1');
415
+ if (/^\s*xref/.test(s))
416
+ return this.readXrefTable(off);
417
+ return this.readXrefStream(off);
418
+ };
419
+ let result;
420
+ try {
421
+ result = await attempt(offset + this.base);
422
+ }
423
+ catch (e) {
424
+ if (this.headerOffset > 0 && this.base === 0) {
425
+ this.base = this.headerOffset;
426
+ result = await attempt(offset + this.base);
427
+ }
428
+ else
429
+ throw e;
430
+ }
431
+ // Recorded only once the section reads, so an offset that holds something else cannot pass for one.
432
+ this.structuralOffsets.add(result.at);
433
+ const stm = result.trailer.get('XRefStm');
434
+ if (typeof stm === 'number') {
435
+ try {
436
+ const extra = await this.readXrefStream(stm + this.base);
437
+ this.structuralOffsets.add(extra.at);
438
+ const f = result.free;
439
+ const order = Array.from({ length: f.length / 2 }, (_, i) => i).sort((a, b) => f[2 * a] - f[2 * b]);
440
+ const starts = Float64Array.from(order, i => f[2 * i]);
441
+ const ends = Float64Array.from(order, i => f[2 * i + 1]);
442
+ for (const [num, e] of extra.entries) {
443
+ if (result.entries.has(num))
444
+ continue;
445
+ let lo = 0;
446
+ let hi = starts.length;
447
+ while (lo < hi) {
448
+ const mid = (lo + hi) >> 1;
449
+ if (starts[mid] <= num)
450
+ lo = mid + 1;
451
+ else
452
+ hi = mid;
453
+ }
454
+ if (lo > 0 && num < ends[lo - 1])
455
+ this.issues.xrefStmOverFree++;
456
+ result.entries.set(num, e);
457
+ }
458
+ }
459
+ catch (e) {
460
+ if (e instanceof TimeLimitError || e instanceof ObjectLimitError)
461
+ throw e;
462
+ // The table alone still works here, but not for every reader.
463
+ this.issues.badXRefStm++;
464
+ }
465
+ }
466
+ return result;
467
+ }
468
+ async readXrefTable(offset) {
469
+ const entries = new Map();
470
+ const free = [];
471
+ const ranges = [];
472
+ let pos = offset;
473
+ let window = await this.reader.read(pos, 65536);
474
+ let i = 0;
475
+ const refill = async () => {
476
+ pos += i;
477
+ i = 0;
478
+ window = await this.reader.read(pos, 65536);
479
+ };
480
+ const token = async () => {
481
+ for (;;) {
482
+ while (i < window.length && (0, parser_1.isWhite)(window[i]))
483
+ i++;
484
+ if (i < window.length)
485
+ break;
486
+ if (pos + i >= this.size)
487
+ return '';
488
+ await refill();
489
+ }
490
+ if (window.length - i < 64 && pos + window.length < this.size)
491
+ await refill();
492
+ const start = i;
493
+ while (i < window.length && !(0, parser_1.isWhite)(window[i]))
494
+ i++;
495
+ return Buffer.from(window.subarray(start, i)).toString('latin1');
496
+ };
497
+ if ((await token()) !== 'xref')
498
+ throw new parser_1.ParseError('Not an xref table');
499
+ for (;;) {
500
+ this.checkTime();
501
+ const t = await token();
502
+ if (t === 'trailer' || t.startsWith('trailer')) {
503
+ const at = pos + i - (t.length - 'trailer'.length);
504
+ const obj = await this.parseWindowed(this.reader, this.size, at, p => p.parseObject());
505
+ if (!(obj instanceof objects_1.PdfDict))
506
+ throw new parser_1.ParseError('Bad trailer');
507
+ if (repeatsANumber(ranges))
508
+ throw new parser_1.ParseError('xref rows repeat an object number');
509
+ return { entries, free, trailer: obj, at: offset };
510
+ }
511
+ if (t === '')
512
+ throw new parser_1.ParseError('xref table without trailer');
513
+ let first = Number(t);
514
+ const count = Number(await token());
515
+ if (!Number.isInteger(first) || !Number.isInteger(count))
516
+ throw new parser_1.ParseError('Bad xref subsection');
517
+ for (let k = 0; k < count; k++) {
518
+ let off = 0;
519
+ let gen = 0;
520
+ let used = false;
521
+ // Fast path: the standard 20-byte entry "oooooooooo ggggg n\r\n".
522
+ while (i < window.length && (0, parser_1.isWhite)(window[i]))
523
+ i++;
524
+ if (window.length - i < 64 && pos + window.length < this.size)
525
+ await refill();
526
+ let fast = window.length - i >= 18 && window[i + 10] === 0x20 && window[i + 16] === 0x20 && (window[i + 17] === 0x6e || window[i + 17] === 0x66);
527
+ for (let q = 0; fast && q < 16; q++) {
528
+ const c = window[i + q] - 48;
529
+ if (q === 10)
530
+ continue;
531
+ if (c < 0 || c > 9)
532
+ fast = false;
533
+ else if (q < 10)
534
+ off = off * 10 + c;
535
+ else
536
+ gen = gen * 10 + c;
537
+ }
538
+ if (fast) {
539
+ used = window[i + 17] === 0x6e;
540
+ i += 18;
541
+ }
542
+ else {
543
+ const o = await token();
544
+ let g = await token();
545
+ let kind;
546
+ // Some writers glue the type letter to the generation, e.g. "00000n". Reading a third token would eat the next row.
547
+ if (/^\d+[nf]$/.test(g)) {
548
+ kind = g.slice(-1);
549
+ g = g.slice(0, -1);
550
+ }
551
+ else
552
+ kind = await token();
553
+ // pdf.js refuses a row whose numbers are not whole, and then rebuilds the map by scanning.
554
+ if (!/^\d+$/.test(o) || !/^\d+$/.test(g) || (kind !== 'n' && kind !== 'f'))
555
+ throw new parser_1.ParseError('Bad xref entry');
556
+ off = Number(o);
557
+ gen = Number(g);
558
+ used = kind === 'n';
559
+ }
560
+ // pdf.js numbers a subsection "1 n" from 0 when its first row is free, a repair for writers that start at 1.
561
+ if (k === 0 && first === 1 && !used)
562
+ first = 0;
563
+ const num = first + k;
564
+ // pdf.js rebuilds the map of a file whose object 0 is in use.
565
+ if (num === 0 && used)
566
+ throw new parser_1.ParseError('Object 0 in use');
567
+ if (used && off > 0)
568
+ entries.set(num, (0, exports.packOffset)(off, gen));
569
+ else if (free.length && free[free.length - 1] === num)
570
+ free[free.length - 1]++;
571
+ else
572
+ free.push(num, num + 1);
573
+ }
574
+ ranges.push(first, count);
575
+ }
576
+ }
577
+ /** `at` is where the stream's object header starts, which can be past whitespace at `offset`. */
578
+ async readXrefStream(offset) {
579
+ const { obj, at } = await this.parseIndirectAt(offset);
580
+ if (!(obj instanceof objects_1.PdfStream) || obj.dict.name('Type') !== 'XRef')
581
+ throw new parser_1.ParseError('Not an xref stream');
582
+ // pdf.js refuses widths, ranges, row types and data it cannot read, and then rebuilds the map by scanning.
583
+ const w = obj.dict.get('W');
584
+ const widths = Array.isArray(w) ? w.slice(0, 3) : [];
585
+ if (widths.length < 3 || !widths.every(x => typeof x === 'number' && Number.isInteger(x) && x >= 0 && x <= 8))
586
+ throw new parser_1.ParseError('Bad /W');
587
+ const [w1, w2, w3] = widths;
588
+ // A row of zero width never consumes data, so /Index alone would decide how many entries it makes.
589
+ if (w1 + w2 + w3 === 0)
590
+ throw new parser_1.ParseError('Bad /W');
591
+ const index = obj.dict.get('Index') ?? null;
592
+ const listed = index === null ? [0, obj.dict.get('Size')] : index;
593
+ if (!Array.isArray(listed) || listed.length % 2 || !listed.every(x => typeof x === 'number' && Number.isInteger(x) && x >= 0))
594
+ throw new parser_1.ParseError('Bad /Index');
595
+ const ranges = listed;
596
+ if (repeatsANumber(ranges))
597
+ throw new parser_1.ParseError('xref rows repeat an object number');
598
+ let left = 0;
599
+ for (let j = 1; j < ranges.length; j += 2)
600
+ left += ranges[j];
601
+ const rowLen = w1 + w2 + w3;
602
+ const row = new Uint8Array(rowLen);
603
+ const field = (b, p, n, dflt) => {
604
+ if (n === 0)
605
+ return dflt;
606
+ let v = 0;
607
+ for (let k = 0; k < n; k++)
608
+ v = v * 256 + b[p + k];
609
+ return v;
610
+ };
611
+ const entries = new Map();
612
+ const free = [];
613
+ let r = 0;
614
+ let k = 0;
615
+ let have = 0;
616
+ // Rows are read as the stream decodes, and decoding stops once /Index is filled, so trailing data costs nothing.
617
+ 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
+ this.checkTime();
619
+ for (let i = 0; i < c.length;) {
620
+ while (r < ranges.length && k >= ranges[r + 1]) {
621
+ r += 2;
622
+ k = 0;
623
+ }
624
+ if (r >= ranges.length)
625
+ break rows;
626
+ let b = c;
627
+ let p = i;
628
+ if (have === 0 && c.length - i >= rowLen)
629
+ i += rowLen;
630
+ else {
631
+ // A row split across chunks.
632
+ const n = Math.min(rowLen - have, c.length - i);
633
+ row.set(c.subarray(i, i + n), have);
634
+ have += n;
635
+ i += n;
636
+ if (have < rowLen)
637
+ break;
638
+ have = 0;
639
+ b = row;
640
+ p = 0;
641
+ }
642
+ const num = ranges[r] + k++;
643
+ left--;
644
+ const t = field(b, p, w1, 1);
645
+ const off = field(b, p + w1, w2, 0);
646
+ if (t > 2)
647
+ throw new parser_1.ParseError('Bad xref stream entry type');
648
+ if (t === 2)
649
+ entries.set(num, (0, exports.packStream)(off));
650
+ else if (t === 1)
651
+ entries.set(num, (0, exports.packOffset)(off, field(b, p + w1 + w2, w3, 0)));
652
+ else if (free.length && free[free.length - 1] === num)
653
+ free[free.length - 1]++;
654
+ else
655
+ free.push(num, num + 1);
656
+ this.checkObjectLimit(entries.size);
657
+ }
658
+ }
659
+ if (left > 0)
660
+ throw new parser_1.ParseError('xref stream data ends before its rows');
661
+ return { entries, free, trailer: obj.dict.clone(), at };
662
+ }
663
+ /** Rebuilds the object map by scanning every byte. Used when the xref is missing or wrong. */
664
+ async scan() {
665
+ this.rebuilt = true;
666
+ this.scanned = true;
667
+ this.base = 0;
668
+ this.sortedOffsets = undefined;
669
+ this.pendingObjStm = [];
670
+ await this.dropObjStms();
671
+ const trailers = new Set();
672
+ let tail = '';
673
+ const headers = this.objectHeaders((buf, pos) => {
674
+ // Each search starts with the end of the chunk before, where a "trailer" may have begun.
675
+ const s = tail + Buffer.from(buf).toString('latin1');
676
+ for (let t = s.indexOf('trailer'); t >= 0; t = s.indexOf('trailer', t + 1))
677
+ trailers.add(pos - tail.length + t + 7);
678
+ tail = s.slice(-6);
679
+ });
680
+ for await (const h of headers) {
681
+ if (h.before >= 0 && !(0, parser_1.isWhite)(h.before) && h.before !== 0x3e && h.before !== 0x5d)
682
+ continue;
683
+ this.xref.set(h.num, (0, exports.packOffset)(h.at, h.gen));
684
+ this.checkObjectLimit(this.xref.size);
685
+ }
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
+ }
701
+ }
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())) {
707
+ this.checkTime();
708
+ const entry = unpack(this.xref.get(num));
709
+ if (entry?.type !== 1)
710
+ continue;
711
+ let o;
712
+ try {
713
+ o = (await this.parseIndirectAt(entry.offset)).obj;
714
+ }
715
+ catch (e) {
716
+ if (e instanceof TimeLimitError)
717
+ throw e;
718
+ continue;
719
+ }
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
+ }
729
+ if (!(this.trailer.get('Root') instanceof objects_1.PdfRef)) {
730
+ // 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);
735
+ }
736
+ this.objCache = objectCache();
737
+ }
738
+ /** Adds the objects held in the object streams scan() found. Their bodies may be encrypted, so this runs after initSecurity(). */
739
+ async indexObjStms() {
740
+ const nums = this.pendingObjStm;
741
+ this.pendingObjStm = [];
742
+ for (const num of nums) {
743
+ this.checkTime();
744
+ let stm;
745
+ try {
746
+ stm = await this.loadObjStm(num);
747
+ }
748
+ catch (e) {
749
+ if (e instanceof filters_1.DecompressionLimitError || e instanceof TimeLimitError)
750
+ throw e;
751
+ continue; // a broken object stream adds nothing
752
+ }
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?.();
759
+ this.checkObjectLimit(this.xref.size);
760
+ }
761
+ }
762
+ // ---------- objects ----------
763
+ /**
764
+ * Parses the indirect object ("N G obj ...") at a source offset. `at` is where its header starts, past any
765
+ * whitespace, and `end` where its value or its stream body ends.
766
+ */
767
+ parseIndirectAt(offset) {
768
+ return this.parseWindowed(this.reader, this.size, offset, async (p) => {
769
+ p.skipWhitespace();
770
+ const at = offset + p.pos;
771
+ const ref = p.parseObjectHeader();
772
+ const obj = p.parseObject();
773
+ if (obj instanceof objects_1.PdfDict) {
774
+ const start = p.streamStart();
775
+ 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 };
778
+ }
779
+ }
780
+ return { ref, obj, at, end: offset + p.pos, cost: p.cost };
781
+ }, this.nextOffsetAfter(offset));
782
+ }
783
+ /** Finds a stream body's true length, trusting /Length only when "endstream" follows it. */
784
+ async locateStream(dict, bodyStart, ref) {
785
+ let declared;
786
+ const L = dict.get('Length');
787
+ if (typeof L === 'number')
788
+ declared = L;
789
+ else if (L instanceof objects_1.PdfRef && L.num !== ref.num && !this.lengthResolving.has(ref.num)) {
790
+ this.lengthResolving.add(ref.num);
791
+ try {
792
+ const v = await this.getObject(L);
793
+ if (typeof v === 'number')
794
+ declared = v;
795
+ }
796
+ catch {
797
+ /* fall back to scanning */
798
+ }
799
+ finally {
800
+ this.lengthResolving.delete(ref.num);
801
+ }
802
+ }
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
+ }
858
+ if (declared !== undefined && declared >= 0 && bodyStart + declared <= this.size) {
859
+ const after = await this.reader.read(bodyStart + declared, 32);
860
+ const s = Buffer.from(after).toString('latin1');
861
+ if (/^\s*endstream/.test(s))
862
+ return new objects_1.PdfStream(dict, bodyStart, declared);
863
+ }
864
+ // 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);
867
+ this.issues.streamLengthWrong++;
868
+ }
869
+ const bound = this.nextOffsetAfter(bodyStart);
870
+ const found = await this.findEndstream(bodyStart, bound, false);
871
+ if (found >= 0)
872
+ return new objects_1.PdfStream(dict, bodyStart, found - bodyStart);
873
+ if (bound < this.size && this.unboundedScanBudget > 0) {
874
+ const further = await this.findEndstream(bodyStart, this.size, true);
875
+ if (further >= 0)
876
+ return new objects_1.PdfStream(dict, bodyStart, further - bodyStart);
877
+ }
878
+ return new objects_1.PdfStream(dict, bodyStart, Math.max(0, bound - bodyStart));
879
+ }
880
+ /** Position just before the EOL that precedes "endstream" in [from, to), or -1. */
881
+ async findEndstream(from, to, unbounded) {
882
+ const needle = enc('endstream');
883
+ let pos = from;
884
+ const chunk = 1 << 20;
885
+ while (pos < to) {
886
+ this.checkTime();
887
+ const n = Math.min(chunk + needle.length, to + needle.length - pos, this.size - pos);
888
+ if (n <= 0)
889
+ break;
890
+ const buf = await this.reader.source.read(pos, n);
891
+ if (unbounded) {
892
+ this.unboundedScanBudget -= buf.length;
893
+ if (this.unboundedScanBudget < 0)
894
+ return -1;
895
+ }
896
+ const k = view(buf).indexOf(needle);
897
+ if (k >= 0) {
898
+ let end = pos + k;
899
+ const back = await this.reader.read(Math.max(from, end - 2), Math.min(2, end - from));
900
+ if (back.length === 2 && back[0] === 0x0d && back[1] === 0x0a)
901
+ end -= 2;
902
+ else if (back.length >= 1 && (back[back.length - 1] === 0x0a || back[back.length - 1] === 0x0d))
903
+ end -= 1;
904
+ return Math.max(from, end);
905
+ }
906
+ if (buf.length < chunk)
907
+ break;
908
+ pos += chunk;
909
+ }
910
+ return -1;
911
+ }
912
+ /** The smallest known object or xref offset after `pos`, or the end of the file. */
913
+ nextOffsetAfter(pos) {
914
+ if (!this.sortedOffsets) {
915
+ const list = [];
916
+ for (const v of this.xref.values())
917
+ if (v >= 0)
918
+ list.push(Math.floor(v / 65536) + this.base);
919
+ for (const o of this.structuralOffsets)
920
+ list.push(o);
921
+ this.sortedOffsets = Float64Array.from(list).sort();
922
+ }
923
+ const a = this.sortedOffsets;
924
+ let lo = 0;
925
+ let hi = a.length;
926
+ while (lo < hi) {
927
+ const mid = (lo + hi) >> 1;
928
+ if (a[mid] <= pos)
929
+ lo = mid + 1;
930
+ else
931
+ hi = mid;
932
+ }
933
+ return lo < a.length ? a[lo] : this.size;
934
+ }
935
+ /** Loads an object by reference, decrypted. Missing and free objects are null. */
936
+ async getObject(ref, raw = false) {
937
+ const entry = unpack(this.xref.get(ref.num));
938
+ // pdf.js refuses a reference whose generation differs from the entry. This one is read all the same, and reported.
939
+ if (entry?.type === 1 && entry.gen !== ref.gen && !this.quiet)
940
+ this.issues.refGen.add(ref.num);
941
+ const cached = raw ? undefined : this.objCache.get(ref.num);
942
+ if (cached !== undefined)
943
+ return cached;
944
+ if (!entry)
945
+ return null;
946
+ let obj;
947
+ let cost;
948
+ if (entry.type === 1) {
949
+ let parsed;
950
+ try {
951
+ parsed = await this.parseIndirectAt(entry.offset + this.base);
952
+ if (parsed.ref.num !== ref.num)
953
+ throw new parser_1.ParseError('Object number mismatch');
954
+ }
955
+ catch (e) {
956
+ if (e instanceof parser_1.ParseError || e instanceof RangeError) {
957
+ // A bad entry costs only this object; rebuilding the whole map could bring back older revisions.
958
+ this.issues.badOffsets++;
959
+ if (!raw)
960
+ this.objCache.set(ref.num, null);
961
+ return null;
962
+ }
963
+ throw e;
964
+ }
965
+ if (parsed.ref.gen !== entry.gen)
966
+ this.issues.genMismatch++;
967
+ obj = parsed.obj;
968
+ cost = parsed.cost;
969
+ if (this.security && !raw && !(this.encryptRef && this.encryptRef.num === ref.num))
970
+ obj = this.decryptObject(obj, ref.num, entry.gen);
971
+ }
972
+ else {
973
+ ({ obj, cost } = await this.getFromObjStm(entry.stream, ref.num));
974
+ }
975
+ if (!raw)
976
+ this.objCache.set(ref.num, obj, cost);
977
+ return obj;
978
+ }
979
+ decryptObject(obj, num, gen) {
980
+ const sec = this.security;
981
+ if (!sec)
982
+ throw new Error(`Object ${num} decrypted without a security handler`);
983
+ const walk = (v) => {
984
+ if (v instanceof objects_1.PdfString)
985
+ return new objects_1.PdfString(sec.decryptWith(sec.stringMethod, v.bytes, num, gen));
986
+ if (Array.isArray(v))
987
+ return v.map(walk);
988
+ if (v instanceof objects_1.PdfDict) {
989
+ const d = new objects_1.PdfDict();
990
+ const t = v.name('Type');
991
+ // ISO 32000-2 7.6.2: a signature's /Contents is never encrypted. qpdf also requires /ByteRange.
992
+ const sig = (t === 'Sig' || t === 'DocTimeStamp') && v.has('ByteRange');
993
+ for (const [k, x] of v.map)
994
+ d.set(k, sig && k === 'Contents' && x instanceof objects_1.PdfString ? x : walk(x));
995
+ return d;
996
+ }
997
+ if (v instanceof objects_1.PdfStream)
998
+ return new objects_1.PdfStream(walk(v.dict), v.offset, v.length);
999
+ return v;
1000
+ };
1001
+ return walk(obj);
1002
+ }
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 };
1018
+ }
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
+ }
1022
+ const off = stm.offsets.get(num);
1023
+ if (off === undefined)
1024
+ return { obj: null, cost: 0 };
1025
+ try {
1026
+ return await this.parseWindowed(stm.reader, stm.size, stm.first + off, p => ({ obj: p.parseObject(), cost: p.cost }), stm.size, `${stmNum} ${off}`);
1027
+ }
1028
+ catch (e) {
1029
+ if (e instanceof TimeLimitError)
1030
+ throw e;
1031
+ this.issues.malformed++;
1032
+ return { obj: null, cost: 0 };
1033
+ }
1034
+ }
1035
+ /** Decodes an object stream in chunks; large ones go to a temporary file instead of memory. */
1036
+ async loadObjStm(stmNum) {
1037
+ const entry = unpack(this.xref.get(stmNum));
1038
+ if (entry?.type !== 1)
1039
+ return undefined;
1040
+ 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;
1048
+ }
1049
+ const sink = new io_1.SpillSink(temp, threshold);
1050
+ try {
1051
+ for await (const c of (0, filters_1.decodeChunks)(this.plainChunks(obj, stmNum), obj.dict, this.opts.decompressedBytes, () => this.checkTime()))
1052
+ await sink.write(c);
1053
+ }
1054
+ finally {
1055
+ await sink.close();
1056
+ }
1057
+ const source = sink.source();
1058
+ const size = await source.size();
1059
+ const reader = new io_1.Reader(source, size, 65536, sink.spilled ? 4 : 64);
1060
+ const n = Math.min(obj.dict.number('N') ?? 0, 10_000_000);
1061
+ 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')))
1064
+ this.issues.badObjStm.add(stmNum);
1065
+ const offsets = new Map();
1066
+ if (sink.spilled) {
1067
+ // Recorded before the header is read, so release() closes and deletes the file even if that read fails.
1068
+ const spillPath = sink.path;
1069
+ if (spillPath === undefined)
1070
+ throw new Error(`Object stream ${stmNum} spilled without a file`);
1071
+ this.spilledObjStm.set(stmNum, { path: spillPath, source, size, offsets, first });
1072
+ const part = `Object stream ${stmNum}`;
1073
+ if (!this.issues.memoryFallback.includes(part))
1074
+ this.issues.memoryFallback.push(part);
1075
+ }
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;
1096
+ }
1097
+ }
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;
1106
+ }
1107
+ else {
1108
+ win *= 4;
1109
+ if (win > 1 << 20)
1110
+ break;
1111
+ }
1112
+ }
1113
+ return { reader, size, offsets, first };
1114
+ }
1115
+ ownTemp;
1116
+ /**
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.
1120
+ */
1121
+ async parseWindowed(reader, size, offset, parse, bound = size, key = String(offset)) {
1122
+ let len = 4096;
1123
+ let end = Math.min(bound, size);
1124
+ for (;;) {
1125
+ // Each larger window parses the object again from the start.
1126
+ this.checkTime();
1127
+ const n = Math.min(len, end - offset);
1128
+ if (offset + n > bound) {
1129
+ this.parseBudget -= n;
1130
+ if (this.parseBudget < 0)
1131
+ throw new parser_1.ParseError('Object runs past the next object');
1132
+ }
1133
+ const window = await reader.read(offset, n);
1134
+ const atEnd = offset + window.length >= size;
1135
+ let bad = 0;
1136
+ const p = new parser_1.Parser(window, 0, atEnd, { ...this.hooks, onBadToken: () => bad++ });
1137
+ let final = true;
1138
+ try {
1139
+ return await parse(p);
1140
+ }
1141
+ catch (e) {
1142
+ final = !(e instanceof parser_1.NeedMoreData) || atEnd;
1143
+ if (final)
1144
+ throw e;
1145
+ }
1146
+ finally {
1147
+ // A larger window reads the same tokens again; only the read that ends counts them.
1148
+ if (final && bad && !this.badTokensCounted.has(key)) {
1149
+ this.badTokensCounted.add(key);
1150
+ this.issues.malformed += bad;
1151
+ }
1152
+ }
1153
+ if (offset + window.length >= end)
1154
+ end = size;
1155
+ len *= 4;
1156
+ if (len > 256 * 1024 * 1024)
1157
+ throw new parser_1.ParseError('Object too large');
1158
+ }
1159
+ }
1160
+ async resolve(v) {
1161
+ let x = v;
1162
+ for (let i = 0; i < 32 && x instanceof objects_1.PdfRef; i++)
1163
+ x = await this.getObject(x);
1164
+ return x;
1165
+ }
1166
+ // ---------- streams ----------
1167
+ /** The generation of a live object's entry, 0 for one in an object stream or none. */
1168
+ genOf(num) {
1169
+ const e = unpack(this.xref.get(num));
1170
+ return e && e.type === 1 ? e.gen : 0;
1171
+ }
1172
+ /** Length of the stream once decrypted, which is what gets written out. */
1173
+ async plainLength(stream, num) {
1174
+ if (!this.security)
1175
+ return stream.length;
1176
+ return this.security.plainLength(stream, num, this.genOf(num), (o, l) => this.reader.read(stream.offset + o, l));
1177
+ }
1178
+ /** Yields the decrypted, still-encoded body in chunks. */
1179
+ async *plainChunks(stream, num) {
1180
+ let dec;
1181
+ if (this.security)
1182
+ dec = this.security.chunkDecryptor(stream, num, this.genOf(num), await this.plainLength(stream, num));
1183
+ for await (const c of this.reader.chunks(stream.offset, stream.length)) {
1184
+ this.checkTime();
1185
+ const out = dec ? dec.update(c) : c;
1186
+ if (out.length)
1187
+ yield out;
1188
+ }
1189
+ if (dec) {
1190
+ const f = dec.final();
1191
+ if (f.length)
1192
+ yield f;
1193
+ }
1194
+ }
1195
+ /** Decrypts and decodes a whole stream body into memory, subject to the decompression limit and an optional cap. */
1196
+ async decode(stream, num, limit = this.opts.decompressedBytes, cap) {
1197
+ const parts = [];
1198
+ let total = 0;
1199
+ for await (const c of (0, filters_1.decodeChunks)(this.plainChunks(stream, num), stream.dict, limit, () => this.checkTime())) {
1200
+ parts.push(c);
1201
+ total += c.length;
1202
+ if (cap !== undefined && total > cap)
1203
+ throw new DecodeCapError(`Decoded stream exceeds ${cap} bytes`);
1204
+ }
1205
+ return Buffer.concat(parts, total);
1206
+ }
1207
+ /**
1208
+ * Counts object definitions in the file body that no xref section uses. Reads the whole file once, in chunks.
1209
+ * Definitions an older section uses are left to the superseded count. A header inside a live object counts only
1210
+ * with `inLive`, since only a reader that rebuilds the map by scanning takes it.
1211
+ */
1212
+ async countDeadDefinitions(inLive = false) {
1213
+ let dead = 0;
1214
+ // The older sections' entries, ordered by object number and then offset.
1215
+ const old = this.supersededAt;
1216
+ const order = Uint32Array.from({ length: old.length / 2 }, (_, i) => i).sort((a, b) => old[2 * a] - old[2 * b] || old[2 * a + 1] - old[2 * b + 1]);
1217
+ // Unlike scan(), this takes a header after any byte: pdf.js's recovery scan also finds one right after "endobj".
1218
+ for await (const h of this.objectHeaders()) {
1219
+ this.checkTime();
1220
+ if (this.structuralOffsets.has(h.at))
1221
+ continue;
1222
+ // The nearest entry for this object at or before the header, in the newest section or an older one.
1223
+ const e = unpack(this.xref.get(h.num));
1224
+ let near = e && e.type === 1 && e.offset + this.base <= h.at ? e.offset + this.base : -1;
1225
+ let lo = 0;
1226
+ let hi = order.length;
1227
+ while (lo < hi) {
1228
+ const mid = (lo + hi) >> 1;
1229
+ const n = old[2 * order[mid]];
1230
+ if (n < h.num || (n === h.num && old[2 * order[mid] + 1] <= h.at))
1231
+ lo = mid + 1;
1232
+ else
1233
+ hi = mid;
1234
+ }
1235
+ if (lo > 0 && old[2 * order[lo - 1]] === h.num)
1236
+ near = Math.max(near, old[2 * order[lo - 1] + 1]);
1237
+ if (near === h.at)
1238
+ continue;
1239
+ // Some writers point at the EOL before "N G obj", and the loader skips whitespace to reach the header. Only an
1240
+ // entry that points at whitespace can reach past it. Headers never share the whitespace before them, so reading
1241
+ // it back stays linear, and each read is bounded.
1242
+ if (near >= 0 && (0, parser_1.isWhite)(h.before) && (0, parser_1.isWhite)((await this.reader.read(near, 1))[0])) {
1243
+ let from = h.at;
1244
+ for (let step = 16; from > near; step = Math.min(step * 2, 65536)) {
1245
+ const n = Math.min(from - near, step);
1246
+ const buf = await this.reader.read(from - n, n);
1247
+ let k = buf.length;
1248
+ while (k > 0 && (0, parser_1.isWhite)(buf[k - 1]))
1249
+ k--;
1250
+ from -= buf.length - k;
1251
+ if (k > 0 || buf.length === 0)
1252
+ break;
1253
+ }
1254
+ if (from === near)
1255
+ continue;
1256
+ }
1257
+ if (inLive || !(await this.insideLiveObject(h.at, h.end)))
1258
+ dead++;
1259
+ }
1260
+ return dead;
1261
+ }
1262
+ /**
1263
+ * Yields every "N G obj" header in the file, with the byte before it or -1 at the start of the file, and where its
1264
+ * "obj" ends. Each "obj" is read back to its two numbers. The whitespace inside a header can run to any length, as
1265
+ * other readers allow, so the read back can reach into earlier chunks: chunks that overlap by a fixed amount miss
1266
+ * such a header. `onChunk` sees each chunk once.
1267
+ */
1268
+ async *objectHeaders(onChunk) {
1269
+ const chunk = 1 << 20;
1270
+ for (let pos = 0; pos < this.size; pos += chunk) {
1271
+ this.checkTime();
1272
+ // Three bytes past the chunk hold the rest of an "obj" that starts in it, and the byte after that.
1273
+ const buf = await this.reader.source.read(pos, Math.min(chunk + 3, this.size - pos));
1274
+ const own = Math.min(chunk, buf.length);
1275
+ onChunk?.(buf.subarray(0, own), pos);
1276
+ let win = buf;
1277
+ let winAt = pos;
1278
+ const back = async (p) => {
1279
+ if (p < 0)
1280
+ return -1;
1281
+ this.checkTime();
1282
+ winAt = Math.max(0, p - 65535);
1283
+ win = await this.reader.read(winAt, p + 1 - winAt);
1284
+ return win[p - winAt];
1285
+ };
1286
+ const text = view(buf);
1287
+ for (let k = text.indexOf(OBJ); k >= 0 && k < own; k = text.indexOf(OBJ, k + 1)) {
1288
+ const after = k + 3 < buf.length ? buf[k + 3] : -1;
1289
+ // A letter, a digit or an underscore after "obj" makes it part of a longer word.
1290
+ if (after === 0x5f || (after >= 0x30 && after <= 0x39) || ((after | 0x20) >= 0x61 && (after | 0x20) <= 0x7a))
1291
+ continue;
1292
+ win = buf;
1293
+ winAt = pos;
1294
+ let p = pos + k - 1;
1295
+ let c = p >= winAt ? win[p - winAt] : await back(p);
1296
+ // Whitespace and the generation, then whitespace and the object number, each run of digits read whole and of
1297
+ // any length, as pdf.js reads "000001" as 1. A value past 2^53 stays at that bound.
1298
+ const found = [];
1299
+ while (found.length < 2) {
1300
+ if (!(0, parser_1.isWhite)(c))
1301
+ break;
1302
+ while ((0, parser_1.isWhite)(c))
1303
+ c = --p >= winAt ? win[p - winAt] : await back(p);
1304
+ let value = 0;
1305
+ let len = 0;
1306
+ for (let scale = 1; c >= 0x30 && c <= 0x39; len++) {
1307
+ value = Math.min(value + (c - 0x30) * scale, Number.MAX_SAFE_INTEGER);
1308
+ scale = Math.min(scale * 10, Number.MAX_SAFE_INTEGER);
1309
+ c = --p >= winAt ? win[p - winAt] : await back(p);
1310
+ }
1311
+ if (len === 0)
1312
+ break;
1313
+ found.push({ value, at: p + 1 });
1314
+ }
1315
+ if (found.length === 2)
1316
+ yield { at: found[1].at, end: pos + k + 3, num: found[1].value, gen: found[0].value, before: c };
1317
+ }
1318
+ }
1319
+ }
1320
+ /**
1321
+ * True when [pos, end) lies inside a live object, from its header to the end of its value or stream body. An object
1322
+ * header there is data, such as text quoted in a string or an uncompressed attached PDF, not a definition in this
1323
+ * file. Parsing the object must not change the issue counts, which describe the walked document and are final by now.
1324
+ */
1325
+ async insideLiveObject(pos, end) {
1326
+ if (!this.liveByOffset) {
1327
+ const pairs = [];
1328
+ for (const [num, v] of this.xref)
1329
+ if (v >= 0)
1330
+ pairs.push([Math.floor(v / 65536) + this.base, num]);
1331
+ pairs.sort((a, b) => a[0] - b[0]);
1332
+ this.liveByOffset = { offsets: Float64Array.from(pairs, p => p[0]), nums: Float64Array.from(pairs, p => p[1]) };
1333
+ }
1334
+ const { offsets, nums } = this.liveByOffset;
1335
+ let lo = 0;
1336
+ let hi = offsets.length;
1337
+ while (lo < hi) {
1338
+ const mid = (lo + hi) >> 1;
1339
+ if (offsets[mid] <= pos)
1340
+ lo = mid + 1;
1341
+ else
1342
+ hi = mid;
1343
+ }
1344
+ if (lo === 0)
1345
+ return false;
1346
+ const num = nums[lo - 1];
1347
+ // Positions arrive in file order, so the object before them changes only forward and each one is read once.
1348
+ if (this.lastLive?.num !== num) {
1349
+ const live = { num, start: 0, end: 0 };
1350
+ const i = this.issues;
1351
+ // The sets cannot change while quiet; the counts are restored.
1352
+ const saved = { ...i };
1353
+ const fallback = i.memoryFallback.length;
1354
+ this.quiet = true;
1355
+ try {
1356
+ const o = await this.parseIndirectAt(offsets[lo - 1]);
1357
+ live.start = o.at;
1358
+ live.end = o.end;
1359
+ }
1360
+ catch (e) {
1361
+ if (e instanceof TimeLimitError)
1362
+ throw e;
1363
+ }
1364
+ finally {
1365
+ this.quiet = false;
1366
+ Object.assign(i, saved);
1367
+ i.memoryFallback.length = fallback;
1368
+ }
1369
+ this.lastLive = live;
1370
+ }
1371
+ return pos >= this.lastLive.start && end <= this.lastLive.end;
1372
+ }
1373
+ /** Live object numbers, for the unreferenced-object count. */
1374
+ liveNumbers() {
1375
+ return this.xref.keys();
1376
+ }
1377
+ /** Drops caches and the object index once the document is no longer read, and deletes spilled object streams. */
1378
+ async release() {
1379
+ await this.dropObjStms();
1380
+ this.objCache = new Lru(1);
1381
+ this.xref.clear();
1382
+ this.sortedOffsets = undefined;
1383
+ this.liveByOffset = undefined;
1384
+ this.lastLive = undefined;
1385
+ await this.ownTemp?.cleanup();
1386
+ }
1387
+ /** Forgets every decoded object stream, closing and deleting the ones that spilled to a temporary file. */
1388
+ 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) {
1393
+ try {
1394
+ await s.source.close?.();
1395
+ await fsp.rm(s.path, { force: true });
1396
+ }
1397
+ catch {
1398
+ /* the run's temporary directory is removed later anyway */
1399
+ }
1400
+ }
1401
+ }
1402
+ }
1403
+ exports.PdfDocument = PdfDocument;