@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/README.md +35 -11
- package/dist/crypto.d.ts +6 -2
- package/dist/crypto.js +47 -15
- package/dist/document.d.ts +207 -13
- package/dist/document.js +1571 -255
- package/dist/engine.js +12 -1
- package/dist/filters.js +59 -8
- package/dist/findings.js +3 -0
- package/dist/io.d.ts +1 -1
- package/dist/io.js +32 -9
- package/dist/parser.d.ts +92 -3
- package/dist/parser.js +188 -51
- package/dist/saslprep.d.ts +11 -0
- package/dist/saslprep.js +178 -0
- package/dist/types.d.ts +3 -0
- package/dist/types.js +3 -0
- package/dist/walker.d.ts +2 -0
- package/dist/walker.js +34 -8
- package/package.json +3 -3
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
|
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.
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
295
|
-
|
|
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.
|
|
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.
|
|
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
|
-
/**
|
|
411
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
681
|
-
|
|
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(
|
|
684
|
-
|
|
1453
|
+
this.xref.set(num, entry);
|
|
1454
|
+
added++;
|
|
685
1455
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
this.
|
|
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
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
let
|
|
706
|
-
for (
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
await
|
|
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
|
|
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,
|
|
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:
|
|
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
|
-
|
|
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
|
-
/**
|
|
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 +
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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
|
-
|
|
1023
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
1036
|
-
async
|
|
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
|
|
2018
|
+
return null;
|
|
1040
2019
|
const { obj } = await this.parseIndirectAt(entry.offset + this.base);
|
|
1041
|
-
if (!(obj instanceof objects_1.PdfStream)
|
|
1042
|
-
return
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
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(
|
|
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
|
-
|
|
1063
|
-
|
|
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
|
-
|
|
2044
|
+
stm.badFrom = 0;
|
|
2045
|
+
}
|
|
1066
2046
|
if (sink.spilled) {
|
|
1067
|
-
// Recorded before the header is read, so release()
|
|
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.
|
|
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
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
const
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
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
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
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
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
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
|
|
2144
|
+
return this.wanted.get(stmNum) ?? new Map();
|
|
1114
2145
|
}
|
|
1115
2146
|
ownTemp;
|
|
1116
2147
|
/**
|
|
1117
|
-
* Parses at an offset in any reader
|
|
1118
|
-
*
|
|
1119
|
-
*
|
|
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
|
-
|
|
1123
|
-
|
|
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
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
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
|
|
1134
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
1182
|
-
|
|
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
|
-
/**
|
|
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
|
|
1390
|
-
|
|
1391
|
-
this.
|
|
1392
|
-
|
|
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
|
|
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 */
|