@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/README.md
CHANGED
|
@@ -8,7 +8,7 @@ It has no runtime dependencies, keeps memory bounded, and runs the same code for
|
|
|
8
8
|
npm install @patrickdk77/pdf-defuse
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Node
|
|
11
|
+
Node 18 or later.
|
|
12
12
|
|
|
13
13
|
## Quick start
|
|
14
14
|
|
|
@@ -31,7 +31,7 @@ ES modules work too: `import { disarmPdf } from '@patrickdk77/pdf-defuse'`.
|
|
|
31
31
|
|
|
32
32
|
The engine reads through a source with `size()` and `read(offset, length)`, and writes to a sink that only appends, with `write(chunk)` and `close()`. A local file, a buffer and an S3 object all fit, so large files never need to sit in memory. `disarmPdfSource` reads the source once, start to end, into its own copy and works from that. For S3 that is one pass of ranged reads.
|
|
33
33
|
|
|
34
|
-
`inspectPdfSource` and `disarmPdfSource` call the source's `close()`, if it has one, when the run ends. `fileSource` opens its file again if something reads it after that. A `read` must return exactly the bytes asked for, or fewer only at the end of the source. A read that returns any other length fails the run with an error whose `code` is `EIO`, because it would shift every byte after it.
|
|
34
|
+
`inspectPdfSource` and `disarmPdfSource` call the source's `close()`, if it has one, when the run ends. `fileSource` opens its file again if something reads it after that. A `read` must return exactly the bytes asked for, or fewer only at the end of the source. A read that returns any other length fails the run with an error whose `code` is `EIO`, because it would shift every byte after it. pdf-defuse never asks a source for a read that starts outside it, even when a damaged file points there. `bufferSource`, `fileSource` and the source a file plugin receives return no bytes for a read that starts outside the data or at an offset that is not a whole number.
|
|
35
35
|
|
|
36
36
|
```js
|
|
37
37
|
const { disarmPdfSource, fileSource, fileSink, writableSink } = require('@patrickdk77/pdf-defuse');
|
|
@@ -76,6 +76,12 @@ Kept: text, images, fonts, layout, bookmarks, form fields and their values, safe
|
|
|
76
76
|
|
|
77
77
|
Every file that is not rejected is rewritten, so the output holds only what pdf-defuse read. That includes a file with nothing to remove, whose status stays `clean`.
|
|
78
78
|
|
|
79
|
+
pdf-defuse reads damaged files the way pdf.js reads them, and a CORRUPTED finding reports each repair. That covers a file with no `%PDF-` header in its first 1024 bytes, which CORRUPTED/MISSING_HEADER reports, a catalog written in the trailer, a stream written inside another object or stored in an object stream, broken object streams, and a cross-reference table rebuilt by scanning. The rebuild ignores anything inside a comment and picks the trailer pdf.js picks. The output holds each repaired object as an object of its own.
|
|
80
|
+
|
|
81
|
+
Where pdf.js opens nothing, pdf-defuse searches the whole file for objects and trailers, as other readers do, and rejects the file only when that finds none. It reads a stream pdf.js gives up on, such as one with no `endstream`, the way qpdf reads it. pdf.js draws nothing for such a stream. When one object in an object stream fails to parse, pdf.js can lose the whole stream, depending on the order it looks objects up in. pdf-defuse reads each object that parses.
|
|
82
|
+
|
|
83
|
+
pdf-defuse decodes FlateDecode, LZWDecode, ASCIIHexDecode, ASCII85Decode, RunLengthDecode and BrotliDecode, so the objects, scripts and attached files inside them get the same checks as the rest of the file. Like pdf.js, it refuses Brotli data that is truncated, corrupt or followed by more bytes. It also refuses a BrotliDecode stream with a predictor, which pdf.js ignores and PDFium applies, and a filter chain with two BrotliDecode filters.
|
|
84
|
+
|
|
79
85
|
A rewrite invalidates a signature, so a signed file can keep its bytes instead. That happens when the file is clean and a signature field reachable from the catalog holds a signature whose `/ByteRange` covers every byte of the file except that signature's `/Contents` value. Bytes added after signing fall outside that range, so a file updated after signing is rewritten. The file must also have no CORRUPTED finding, whatever its action, and no second definition of any object, not even one inside another object, which only a reader that rebuilds the cross-reference table by scanning would take. pdf-defuse does not verify the signature. Anyone can sign a file, with a self-signed certificate too, so this exception trusts whoever signed it. `preserveSignatures: false` rewrites signed files as well.
|
|
80
86
|
|
|
81
87
|
Before anything reaches your sink, pdf-defuse inspects its own output. It rejects the file if anything removable is left, if the page count changed, or if any page's content changed length.
|
|
@@ -91,7 +97,7 @@ Every finding has two enums and a string. `category` is the kind, such as `ENCRY
|
|
|
91
97
|
|
|
92
98
|
`action` says what happens: `reject` refuses the file, `strip` removes the content and `info` only reports it. An override to `strip` removes the content for METADATA/XMP, METADATA/INFO_DICTIONARY, LINK/SAFE and EMBEDDED_FILE/TYPE_MISMATCH. Some `info` findings name content that every rewrite drops anyway, such as SIGNATURE/USAGE_RIGHTS, STRUCTURE/ESCAPED_NAMES and the ENCRYPTED findings that name the algorithm. A `strip` override on one of those changes only the status. The other `info` findings, such as FORM/FIELDS, SIGNATURE/SIGNED, STRUCTURE/VERSION_UPGRADED and the PLUGIN_PASSED and PLUGIN_SCRUBBED findings, name nothing that can be removed. A `strip` override on one of them leaves the finding in the output, so the output fails its own inspection and the upload is rejected.
|
|
93
99
|
|
|
94
|
-
Some content goes whatever an override says. Its finding then shows `strip`, or `reject` if you override it to that. This covers a script reference an action loses under the re-checking rules described under Plugins, a reference to an object stream or cross-reference stream, and an attached file a plugin removed or failed on. It also covers a reference in `/Annots` to an object that already has another role, and a reference to a file specification that is the catalog, a page, or the names or information dictionary and carries an embedded file. Such an object keeps its role and its own entries, `/EF` included, so that embedded file stays in the output. Only the reference that treated the object as a file specification goes.
|
|
100
|
+
Some content goes whatever an override says. Its finding then shows `strip`, or `reject` if you override it to that. This covers a script reference an action loses under the re-checking rules described under Plugins, a reference to an object stream or cross-reference stream, and an attached file a plugin removed or failed on, or that the password does not decrypt. It also covers any other stream encrypted with a key the password does not give, which ENCRYPTED/NO_KEY reports. It also covers a reference in `/Annots` to an object that already has another role, and a reference to a file specification that is the catalog, a page, or the names or information dictionary and carries an embedded file. Such an object keeps its role and its own entries, `/EF` included, so that embedded file stays in the output. Only the reference that treated the object as a file specification goes.
|
|
95
101
|
|
|
96
102
|
pdf-defuse compares what it would write with what it read. If it would remove something that no other finding names, the report gets PROCESSING/CONTENT_REMOVED, which follows the same rule. The status then shows the removal, and a signed file does not keep its bytes.
|
|
97
103
|
|
|
@@ -115,14 +121,14 @@ A run that stops before it has seen the whole file rejects the file with a null
|
|
|
115
121
|
|
|
116
122
|
| Option | Default | Purpose |
|
|
117
123
|
|---|---|---|
|
|
118
|
-
| `password` | `''` | Tried as the user password
|
|
124
|
+
| `password` | `''` | Tried as the user password, then as the owner password, then as the empty password. See Passwords below |
|
|
119
125
|
| `limits` | none | `fileSize`, `objects`, `decompressedBytes`, `nestingDepth` and `timeMs`. A limit left out is not enforced |
|
|
120
126
|
| `stripMetadata` | `false` | Remove the document information dictionary wherever it is referenced, and the XMP metadata of every object: the catalog, pages, images and the rest |
|
|
121
127
|
| `actionOverrides` | none | Change the action for a category, or for a category and detail |
|
|
122
128
|
| `scoreWeights`, `scoreBands` | built in | Change the scoring |
|
|
123
129
|
| `filePlugins`, `scriptPlugins` | none | Keep chosen attached files or scripts |
|
|
124
130
|
| `tempDir` | `os.tmpdir()` | Where temporary files go |
|
|
125
|
-
| `memoryThreshold` | 8 MiB | `disarmPdfSource` keeps its copy of the upload in memory up to this size and in a temporary file above it. Each decoded attachment
|
|
131
|
+
| `memoryThreshold` | 8 MiB | `disarmPdfSource` keeps its copy of the upload in memory up to this size and in a temporary file above it. Each decoded attachment above this size goes through a temporary file. Decoded object streams share this much memory, and the ones that do not fit go to temporary files, so each is decoded once. A decoded attachment's file is deleted once its plugins have decided it. Scrubbed attachment copies waiting for the write, and an attachment decoded again so a plugin can check its further names, share this much memory between them. One that does not fit goes to a temporary file. A script that decodes larger than this is removed without going to the script plugins |
|
|
126
132
|
| `preserveSignatures` | `true` | Keep the bytes of a clean file that is signed over all of it, as described under What it removes and what it keeps. `false` rewrites it like any other file, which invalidates the signature |
|
|
127
133
|
|
|
128
134
|
To reject every file that contains JavaScript instead of removing the scripts:
|
|
@@ -131,6 +137,14 @@ To reject every file that contains JavaScript instead of removing the scripts:
|
|
|
131
137
|
await disarmPdf(bytes, { actionOverrides: [{ category: 'JAVASCRIPT', action: 'reject' }] });
|
|
132
138
|
```
|
|
133
139
|
|
|
140
|
+
### Passwords
|
|
141
|
+
|
|
142
|
+
A revision 6 password, the AES-256 scheme of ISO 32000-2, is tried three ways: prepared with SASLprep, as that standard requires, prepared the way pdf.js prepares it, and as typed. The last opens files from tools that skip SASLprep. Other revisions use the password as typed.
|
|
143
|
+
|
|
144
|
+
An empty owner password opens a file, as in qpdf, and ENCRYPTED/OWNER_PASSWORD reports it. pdf.js asks for a password there.
|
|
145
|
+
|
|
146
|
+
A file that encrypts only its attached files, through a crypt filter with `/AuthEvent /EFOpen`, opens without the password, as in pdf.js, and ENCRYPTED/ATTACHMENTS_ONLY reports it. pdf-defuse removes the attached files the password does not decrypt and any other stream under their key, which ENCRYPTED/NO_KEY reports. pdf.js draws such a page blank.
|
|
147
|
+
|
|
134
148
|
## Plugins
|
|
135
149
|
|
|
136
150
|
Attached files and scripts are removed unless a plugin keeps them. A plugin says which files or scripts it accepts, then scrubs them, passes them through unchanged, or removes them after all. The first plugin that accepts handles it. If a plugin throws, or scrubs and writes nothing, the item is removed. An error with a `DEFUSE_LIMIT` or `DEFUSE_IO` code stops the whole run instead, as described under The defuse family. A file plugin that passes a file keeps its original bytes, or the bytes it wrote to the sink if it wrote any.
|
|
@@ -280,11 +294,15 @@ A `--config` module's default export is the options object. For CommonJS that is
|
|
|
280
294
|
|
|
281
295
|
Exit codes are 0 for clean, 1 for stripped or strippable, 2 for rejected and 3 for a usage or I/O error. An output or a report that could not be written is an I/O error, and so is a report whose reader has gone away. There is no password flag, because other users can read command-line arguments from the process list. An unknown option shows in the error without the text after its `=`, in case that text is a password typed in the wrong place.
|
|
282
296
|
|
|
283
|
-
## Memory
|
|
297
|
+
## Memory and time
|
|
284
298
|
|
|
285
299
|
`disarmPdfSource` first copies the upload, in memory up to `memoryThreshold` and into a temporary file above it. It analyzes and writes from that copy only, so a source that changes during the run cannot get anything past the inspection. It rejects an upload over `limits.fileSize` without reading it. `inspectPdfSource` reads the source directly.
|
|
286
300
|
|
|
287
|
-
The engine reads the cross-reference table from the end of the file, then reads only the objects reachable from the document root, one at a time. Stream bodies are copied and decrypted in chunks and are never loaded whole. Apart from the copy of the upload, memory grows with the number of objects, at roughly 250 bytes each, and not with the size of the file. A parsed value takes far more memory than its text, so pdf-defuse reads an object as damaged when its values would take more than about 16 MB, not counting the text of names and strings. It keeps at most about 32 MB of parsed objects for reuse.
|
|
301
|
+
The engine reads the cross-reference table from the end of the file, then reads only the objects reachable from the document root, one at a time. Stream bodies are copied and decrypted in chunks and are never loaded whole. A stream body inside an object stream stays in the decoded object stream. A Brotli decoder can take 16 MB for its window, which is why a chain with two BrotliDecode filters is refused. Apart from the copy of the upload, memory grows with the number of objects, at roughly 250 bytes each, and not with the size of the file. A parsed value takes far more memory than its text, so pdf-defuse reads an object as damaged when its values would take more than about 16 MB, not counting the text of names and strings. It keeps at most about 32 MB of parsed objects for reuse. The dictionaries of streams written inside other objects stay for the whole run, up to about 32 MB more, and an object whose streams would pass that reads as damaged.
|
|
302
|
+
|
|
303
|
+
pdf-defuse parses an object in a window that starts at 4 KB and grows as the parser needs more. Runs of whitespace and comments, stream bodies inside the object and the line after a `stream` keyword are skipped in the source instead of read into the window, so padding costs time but not memory. After 64 skips in one object, the window takes in the rest of it, up to 256 MB.
|
|
304
|
+
|
|
305
|
+
Each object stream is decoded once, the first time an object in it is read. Decoded object streams share `memoryThreshold` of memory. The least recently used move to a temporary file when they need more, and a stream larger than `memoryThreshold` gets a file of its own. Time and temporary space grow with the decoded size of the object streams read. `limits.decompressedBytes` caps each stream, and `limits.timeMs` the run.
|
|
288
306
|
|
|
289
307
|
Free rows in a cross-reference section cost a few numbers per run of them, since a few kilobytes of xref stream can free millions of object numbers. Only rows that an older section could use are held one by one. When those outnumber both 65,536 and the live objects, pdf-defuse rebuilds the table by scanning and reports CORRUPTED/XREF_REBUILT. `limits.objects` counts live rows only, in a table and in a stream alike.
|
|
290
308
|
|
|
@@ -293,9 +311,13 @@ Measured with Node 24. The heap cap is `--max-old-space-size`, and the semi-spac
|
|
|
293
311
|
| File | Size | Objects | Heap cap | Result |
|
|
294
312
|
|---|---|---|---|---|
|
|
295
313
|
| Generated, one large stream | 100 MB | 4 | 48 MB | Defused |
|
|
296
|
-
|
|
|
297
|
-
|
|
|
298
|
-
|
|
|
314
|
+
| A 756-page manual | 22 MB | 127,000 | 48 MB | Defused, about 200 MB resident |
|
|
315
|
+
| A 756-page manual | 22 MB | 127,000 | 48 MB, 2 MB semi-space | Defused, about 125 MB resident |
|
|
316
|
+
| A 756-page manual | 22 MB | 127,000 | Node default | Defused, about 350 MB resident |
|
|
317
|
+
| Generated, objects alternating across 9 object streams of 1 MB each decoded | 400 KB | 18,000 | Node default | Defused in 1 s |
|
|
318
|
+
| Generated, the same across 64 object streams | 760 KB | 32,000 | 48 MB | Defused in 3 s, about 190 MB resident |
|
|
319
|
+
| Generated, 16 object streams of 32 MB each decoded | 560 KB | 1,600 | 48 MB | Defused in 5 s, about 100 MB resident, with 512 MB of temporary files |
|
|
320
|
+
| Generated, an object stream whose last object never closes, then 128 MB of spaces decoded | 130 KB | 8 | 48 MB | Defused in 1 s, about 95 MB resident |
|
|
299
321
|
|
|
300
322
|
## What it does not protect against
|
|
301
323
|
|
|
@@ -305,7 +327,7 @@ Measured with Node 24. The heap cap is `--max-old-space-size`, and the semi-spac
|
|
|
305
327
|
|
|
306
328
|
## Development
|
|
307
329
|
|
|
308
|
-
Development needs Node 22.12 or later. mocha 12 requires it, and the CommonJS test build loads chai 6, an ES module, with `require()`, which Node 22 supports only from 22.12. The pre-commit hook needs Node 22.22.1 or later for lint-staged. The published package has no runtime dependencies and runs on
|
|
330
|
+
Development needs Node 22.12 or later. mocha 12 requires it, and the CommonJS test build loads chai 6, an ES module, with `require()`, which Node 22 supports only from 22.12. The pre-commit hook needs Node 22.22.1 or later for lint-staged. The published package has no runtime dependencies and runs on Node 18 or later.
|
|
309
331
|
|
|
310
332
|
```sh
|
|
311
333
|
npm install --ignore-scripts
|
|
@@ -317,6 +339,8 @@ npm run format
|
|
|
317
339
|
|
|
318
340
|
`npm run build` compiles `src` into `dist`. `npm test` runs the lint, builds the package and the tests, then runs mocha, so a lint error fails the tests. `npm run lint` runs `biome check` over the whole tree and changes nothing. `npm run format` rewrites files in Biome's format.
|
|
319
341
|
|
|
342
|
+
`test/fixtures/cases` holds one small PDF for each problem a review found. `node scripts/make-fixtures.js` writes every fixture from scratch, and a second run writes the same bytes. It needs qpdf and the test build from `npm run build:test`. `manifest.json` in that folder gives each fixture's password and options, the verdicts inspect and disarm must return, and what release 0.1.2 returned. `test/cases.test.ts` checks those verdicts for every entry. It also checks with pdf.js that no script, attached file or outside action is left in the output, and that the output's pages hold the text pdf.js finds in the input. Set `PDF_DEFUSE_BEFORE` to the `index.js` of a 0.1.2 build and it checks what 0.1.2 returned too. For a new problem, add a fixture to the script and an entry to the manifest.
|
|
343
|
+
|
|
320
344
|
`npx husky` activates the pre-commit hook. A plain `npm install` does the same through the `prepare` script. The hook runs Biome on the staged files, applying its safe fixes, then runs `npm audit`. The commit stops if either one fails.
|
|
321
345
|
|
|
322
346
|
## License
|
package/dist/crypto.d.ts
CHANGED
|
@@ -9,10 +9,12 @@ export declare class Rc4 {
|
|
|
9
9
|
}
|
|
10
10
|
export declare const rc4: (key: Uint8Array, data: Uint8Array) => Buffer<ArrayBufferLike>;
|
|
11
11
|
type CryptMethod = 'RC4' | 'AES128' | 'AES256' | 'Identity';
|
|
12
|
-
export type OpenResult =
|
|
12
|
+
export type OpenResult =
|
|
13
|
+
/** `none` opened a file that encrypts only its attached files, whose key the password did not give. */
|
|
14
|
+
{
|
|
13
15
|
status: 'ok';
|
|
14
16
|
handler: SecurityHandler;
|
|
15
|
-
password: 'empty' | 'user' | 'owner';
|
|
17
|
+
password: 'empty' | 'user' | 'owner' | 'none';
|
|
16
18
|
} | {
|
|
17
19
|
status: 'password-required' | 'certificate-handler' | 'unknown-handler' | 'unknown-filter';
|
|
18
20
|
detail?: string;
|
|
@@ -35,6 +37,8 @@ export declare class SecurityHandler {
|
|
|
35
37
|
private readonly unknownFilters;
|
|
36
38
|
private constructor();
|
|
37
39
|
static open(enc: PdfDict, id0: Uint8Array, password: string): OpenResult;
|
|
40
|
+
/** False for a stream whose crypt filter needs the key a file that encrypts only its attached files keeps back. */
|
|
41
|
+
canDecrypt(stream: PdfStream): boolean;
|
|
38
42
|
private readonly keyCache;
|
|
39
43
|
private objectKey;
|
|
40
44
|
decryptWith(method: CryptMethod, data: Uint8Array, num: number, gen: number): Uint8Array;
|
package/dist/crypto.js
CHANGED
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.SecurityHandler = exports.rc4 = exports.Rc4 = void 0;
|
|
37
37
|
const crypto = __importStar(require("node:crypto"));
|
|
38
38
|
const objects_1 = require("./objects");
|
|
39
|
+
const saslprep_1 = require("./saslprep");
|
|
39
40
|
const PAD = Buffer.from('28BF4E5E4E758A4164004E56FFFA01082E2E00B6D0683E802F0CA9FE6453697A', 'hex');
|
|
40
41
|
const md5 = (...parts) => {
|
|
41
42
|
const h = crypto.createHash('md5');
|
|
@@ -205,25 +206,35 @@ class SecurityHandler {
|
|
|
205
206
|
let which = password === '' ? 'empty' : 'user';
|
|
206
207
|
if (R === 5 || R === 6) {
|
|
207
208
|
keyBits = 256;
|
|
208
|
-
const
|
|
209
|
-
const uKey = () => {
|
|
209
|
+
const uKey = (pw) => {
|
|
210
210
|
if (!hash2B(pw, U.subarray(32, 40), Buffer.alloc(0), R).equals(Buffer.from(U.subarray(0, 32))))
|
|
211
211
|
return undefined;
|
|
212
212
|
return aes256NoIv(hash2B(pw, U.subarray(40, 48), Buffer.alloc(0), R), bytesOf(enc.get('UE')));
|
|
213
213
|
};
|
|
214
|
-
const oKey = () => {
|
|
214
|
+
const oKey = (pw) => {
|
|
215
215
|
const u48 = U.subarray(0, 48);
|
|
216
216
|
if (!hash2B(pw, O.subarray(32, 40), u48, R).equals(Buffer.from(O.subarray(0, 32))))
|
|
217
217
|
return undefined;
|
|
218
218
|
return aes256NoIv(hash2B(pw, O.subarray(40, 48), u48, R), bytesOf(enc.get('OE')));
|
|
219
219
|
};
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
220
|
+
// ISO 32000-2 prepares a revision 6 password with SASLprep. pdf.js prepares it its own way and then tries it as
|
|
221
|
+
// typed, which opens files from writers that skip SASLprep, so every form a reader tries is tried here too.
|
|
222
|
+
// Revision 5, Adobe's extension, takes it as typed, as pdf.js does.
|
|
223
|
+
const forms = R === 6 ? [(0, saslprep_1.saslprep)(password), (0, saslprep_1.pdfjsPrep)(password), password] : [password];
|
|
224
|
+
for (const form of new Set(forms)) {
|
|
225
|
+
if (form === undefined)
|
|
226
|
+
continue;
|
|
227
|
+
const pw = Buffer.from(form, 'utf8').subarray(0, 127);
|
|
228
|
+
fileKey = uKey(pw);
|
|
229
|
+
if (fileKey)
|
|
230
|
+
break;
|
|
231
|
+
// The empty password is tried as the owner password too, as qpdf tries it. pdf.js does not.
|
|
232
|
+
fileKey = oKey(pw);
|
|
233
|
+
if (fileKey) {
|
|
234
|
+
which = 'owner';
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
224
237
|
}
|
|
225
|
-
if (!fileKey)
|
|
226
|
-
return { status: 'password-required' };
|
|
227
238
|
}
|
|
228
239
|
else if (R >= 2 && R <= 4) {
|
|
229
240
|
const n = V === 1 ? 5 : V >= 4 ? 16 : (enc.number('Length') ?? 40) / 8;
|
|
@@ -252,7 +263,7 @@ class SecurityHandler {
|
|
|
252
263
|
const asUser = keyFor(padded);
|
|
253
264
|
if (checkUser(asUser))
|
|
254
265
|
fileKey = asUser;
|
|
255
|
-
else
|
|
266
|
+
else {
|
|
256
267
|
// Owner password: derive the RC4 key from it, decrypt O to recover the padded user password.
|
|
257
268
|
let h = md5(padded);
|
|
258
269
|
if (R >= 3)
|
|
@@ -273,16 +284,38 @@ class SecurityHandler {
|
|
|
273
284
|
which = 'owner';
|
|
274
285
|
}
|
|
275
286
|
}
|
|
276
|
-
if (!fileKey)
|
|
277
|
-
return { status: 'password-required' };
|
|
278
287
|
}
|
|
279
288
|
else {
|
|
280
289
|
return { status: 'unknown-filter', detail: `R=${R}` };
|
|
281
290
|
}
|
|
291
|
+
if (!fileKey) {
|
|
292
|
+
// Without a password, pdf.js opens a file whose /AuthEvent /EFOpen filter encrypts only the attached files, and
|
|
293
|
+
// asks for the password when one is opened. Nothing here can read those files.
|
|
294
|
+
const plain = (key) => enc.get(key) === undefined || enc.name(key) === 'Identity';
|
|
295
|
+
const efFilter = cf instanceof objects_1.PdfDict ? cf.get(enc.name('EFF') ?? '') : undefined;
|
|
296
|
+
const onOpen = efFilter instanceof objects_1.PdfDict && efFilter.name('AuthEvent') === 'EFOpen';
|
|
297
|
+
if (password !== '' || V < 4 || !plain('StmF') || !plain('StrF') || !onOpen)
|
|
298
|
+
return { status: 'password-required' };
|
|
299
|
+
which = 'none';
|
|
300
|
+
}
|
|
282
301
|
return { status: 'ok', password: which, handler: new SecurityHandler(V, keyBits, fileKey, stm, str, eff, encryptMetadata, cryptFilters, unknownFilters) };
|
|
283
302
|
}
|
|
303
|
+
/** False for a stream whose crypt filter needs the key a file that encrypts only its attached files keeps back. */
|
|
304
|
+
canDecrypt(stream) {
|
|
305
|
+
if (this.fileKey)
|
|
306
|
+
return true;
|
|
307
|
+
try {
|
|
308
|
+
return this.methodForStream(stream) === 'Identity';
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
// An unknown crypt filter fails where the stream is decoded, as it always has.
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
284
315
|
keyCache = new Map();
|
|
285
316
|
objectKey(num, gen, method) {
|
|
317
|
+
if (!this.fileKey)
|
|
318
|
+
throw new Error('No key for an encrypted stream');
|
|
286
319
|
if (method === 'AES256')
|
|
287
320
|
return Buffer.from(this.fileKey);
|
|
288
321
|
const ck = `${num} ${gen} ${method}`;
|
|
@@ -341,11 +374,10 @@ class SecurityHandler {
|
|
|
341
374
|
const full = 16 + Math.floor((len - 16) / 16) * 16;
|
|
342
375
|
const tail = await readRaw(full - 32, 32);
|
|
343
376
|
const key = this.objectKey(num, gen, method);
|
|
344
|
-
|
|
377
|
+
// In CBC the block before the last is the last block's IV, so the last block decrypts on its own.
|
|
378
|
+
const d = crypto.createDecipheriv(key.length === 16 ? 'aes-128-cbc' : 'aes-256-cbc', key, tail.subarray(0, 16));
|
|
345
379
|
d.setAutoPadding(false);
|
|
346
380
|
const last = Buffer.concat([d.update(tail.subarray(16, 32)), d.final()]);
|
|
347
|
-
for (let i = 0; i < 16; i++)
|
|
348
|
-
last[i] ^= tail[i];
|
|
349
381
|
const pad = last[15];
|
|
350
382
|
const valid = pad >= 1 && pad <= 16 && last.subarray(16 - pad).every(b => b === pad);
|
|
351
383
|
return full - 16 - (valid ? pad : 0);
|
package/dist/document.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type OpenResult, SecurityHandler } from './crypto';
|
|
|
2
2
|
import { Reader, TempDir } from './io';
|
|
3
3
|
import { PdfDict, type PdfObject, PdfRef, PdfStream } from './objects';
|
|
4
4
|
import type { ByteSource } from './types';
|
|
5
|
+
/** A compressed object's entry names its object stream and its index there, as pdf.js reads it. */
|
|
5
6
|
type XrefEntry = {
|
|
6
7
|
type: 1;
|
|
7
8
|
offset: number;
|
|
@@ -9,10 +10,11 @@ type XrefEntry = {
|
|
|
9
10
|
} | {
|
|
10
11
|
type: 2;
|
|
11
12
|
stream: number;
|
|
13
|
+
index: number;
|
|
12
14
|
};
|
|
13
15
|
/** Entries are packed into one number each, so the index costs a few bytes per object. */
|
|
14
16
|
export declare const packOffset: (offset: number, gen: number) => number;
|
|
15
|
-
export declare const packStream: (stream: number) => number;
|
|
17
|
+
export declare const packStream: (stream: number, index: number) => number;
|
|
16
18
|
export declare function unpack(v: number | undefined): XrefEntry | undefined;
|
|
17
19
|
export declare class OpenError extends Error {
|
|
18
20
|
readonly kind: 'unparseable' | 'truncated' | 'not-pdf';
|
|
@@ -45,8 +47,16 @@ interface DocumentIssues {
|
|
|
45
47
|
xrefStmOverFree: number;
|
|
46
48
|
/** Object streams whose header pdf.js refuses, offsets that do not increase or numbers that are not whole. */
|
|
47
49
|
badObjStm: Set<number>;
|
|
50
|
+
/** Object streams an entry names whose /Type is not /ObjStm, which pdf.js reads and qpdf warns about. */
|
|
51
|
+
objStmType: Set<number>;
|
|
52
|
+
/** Streams written inside other objects, by the object and where the body starts. Only indirect objects may be streams. */
|
|
53
|
+
directStreams: Set<string>;
|
|
54
|
+
/** A catalog written in the trailer, which pdf.js reads and qpdf does not. */
|
|
55
|
+
directRoot: boolean;
|
|
48
56
|
/** Objects referenced with a generation their entry does not have, which pdf.js refuses to resolve. */
|
|
49
57
|
refGen: Set<number>;
|
|
58
|
+
/** Streams stored in an object stream, which pdf.js reads from the decoded data. */
|
|
59
|
+
objStmStreams: Set<number>;
|
|
50
60
|
/** Parts held in a temporary file instead of memory. */
|
|
51
61
|
memoryFallback: string[];
|
|
52
62
|
}
|
|
@@ -65,6 +75,8 @@ export declare class PdfDocument {
|
|
|
65
75
|
private readonly opts;
|
|
66
76
|
headerOffset: number;
|
|
67
77
|
headerVersion: string;
|
|
78
|
+
/** No "%PDF-" in the first 1024 bytes, where pdf.js looks for one before it parses the file all the same. */
|
|
79
|
+
missingHeader: boolean;
|
|
68
80
|
/** Added to every offset when the file has bytes before its header and the xref ignores them. */
|
|
69
81
|
private base;
|
|
70
82
|
/** Object number to packed entry. */
|
|
@@ -78,12 +90,46 @@ export declare class PdfDocument {
|
|
|
78
90
|
securityResult?: OpenResult;
|
|
79
91
|
encryptRef?: PdfRef;
|
|
80
92
|
readonly issues: DocumentIssues;
|
|
81
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* Objects with no entry of their own: a catalog written in the trailer, and streams written inside other objects.
|
|
95
|
+
* Their numbers are negative, so no object of the file can take one, and the writer gives each a number of its own.
|
|
96
|
+
* `key` names the object whose key decrypts a stream, and is null for one that is never encrypted.
|
|
97
|
+
*/
|
|
98
|
+
private readonly synthetic;
|
|
99
|
+
/** Synthetic numbers by the object that holds the stream and where its body starts, so a second read reuses one. */
|
|
100
|
+
private readonly syntheticAt;
|
|
101
|
+
/** The walker numbers the name trees it writes -1 and -2. */
|
|
102
|
+
private nextSynthetic;
|
|
103
|
+
/** Streams whose bodies sit in a decoded object stream, by object or synthetic number, and where they sit there. */
|
|
104
|
+
private readonly heldBodies;
|
|
105
|
+
/** What the streams inside each object hold, by that object, and the total, which MAX_SYNTHETIC_COST bounds. */
|
|
106
|
+
private readonly syntheticCost;
|
|
107
|
+
private syntheticTotal;
|
|
108
|
+
/** Values of the indirect /Length entries of streams written inside objects. */
|
|
109
|
+
private directLengths;
|
|
110
|
+
/** The streams already counted in issues.streamLengthWrong, so a second read adds nothing. */
|
|
82
111
|
private readonly lengthWrongAt;
|
|
83
112
|
private objCache;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
113
|
+
/**
|
|
114
|
+
* The object streams decoded since the map or the key last changed, or null for one that does not read. Each is
|
|
115
|
+
* decoded once: memory holds them up to memoryThreshold, and the rest go to temporary files.
|
|
116
|
+
*/
|
|
117
|
+
private readonly objStms;
|
|
118
|
+
/** The object streams held in memory, least recently used first, and the bytes they take. */
|
|
119
|
+
private readonly inMemory;
|
|
120
|
+
private inMemoryBytes;
|
|
121
|
+
/** The temporary file that takes the object streams memory cannot hold, its length, and one handle that reads it. */
|
|
122
|
+
private pool?;
|
|
123
|
+
/** Temporary files of object streams too large for memory. */
|
|
124
|
+
private objStmFiles;
|
|
125
|
+
/**
|
|
126
|
+
* Readers over decoded object streams, so each keeps its blocks between lookups. Readers over a file of their own
|
|
127
|
+
* hold a file handle each, so fewer of them stay open.
|
|
128
|
+
*/
|
|
129
|
+
private objStmReaders;
|
|
130
|
+
private objStmFileReaders;
|
|
131
|
+
/** By object stream, the objects whose entries point into it and the index each entry gives. */
|
|
132
|
+
private wanted?;
|
|
87
133
|
/** Object streams found by scan(), indexed once the key exists. */
|
|
88
134
|
private pendingObjStm;
|
|
89
135
|
/** Streams whose indirect /Length or filter entries are being resolved, so a cycle ends after one lap. */
|
|
@@ -101,6 +147,12 @@ export declare class PdfDocument {
|
|
|
101
147
|
/** Where the objects whose bad tokens are already in issues.malformed were read, so a second read adds nothing. */
|
|
102
148
|
private readonly badTokensCounted;
|
|
103
149
|
private scanned;
|
|
150
|
+
/** The first trailer any cross-reference section gave, which pdf.js falls back on when its recovery finds none. */
|
|
151
|
+
private topDict?;
|
|
152
|
+
/** Set once recovery has looked past what pdf.js finds. */
|
|
153
|
+
private extended;
|
|
154
|
+
/** What a read of every object of the rebuilt map found, kept until the map is rebuilt again. */
|
|
155
|
+
private survey?;
|
|
104
156
|
/** Offsets of the xref sections and xref streams that read, which are not ordinary object definitions. */
|
|
105
157
|
readonly structuralOffsets: Set<number>;
|
|
106
158
|
/** The definitions that older xref sections use and newer ones replaced or freed, as object number then offset. */
|
|
@@ -111,6 +163,8 @@ export declare class PdfDocument {
|
|
|
111
163
|
/** The extent of the live object insideLiveObject() read last, empty when it does not parse. */
|
|
112
164
|
private lastLive?;
|
|
113
165
|
private unboundedScanBudget;
|
|
166
|
+
/** The entries the page lookups found broken or sound, while they run. */
|
|
167
|
+
private checkedEntries?;
|
|
114
168
|
/** Bytes parseWindowed() may still read past the next known object. */
|
|
115
169
|
private parseBudget;
|
|
116
170
|
private constructor();
|
|
@@ -118,17 +172,97 @@ export declare class PdfDocument {
|
|
|
118
172
|
checkTime(): void;
|
|
119
173
|
private checkObjectLimit;
|
|
120
174
|
private init;
|
|
175
|
+
private load;
|
|
176
|
+
private catalog;
|
|
177
|
+
/**
|
|
178
|
+
* Whether pdf.js rebuilds the map when it checks the first and the last page, as its checkFirstPage and checkLastPage
|
|
179
|
+
* do: an entry it reads on the way points at another object. When the last page is not where /Count puts it, pdf.js
|
|
180
|
+
* reads every page.
|
|
181
|
+
*/
|
|
182
|
+
private pageEntriesBroken;
|
|
183
|
+
/**
|
|
184
|
+
* Looks for page `index` as pdf.js's getPageDict does, which skips a node whose /Count says the page is not under it.
|
|
185
|
+
* False where pdf.js fails for another reason, such as a node it already visited.
|
|
186
|
+
*/
|
|
187
|
+
private findPage;
|
|
188
|
+
/** Reads every page as pdf.js's getAllPageDicts does, which stops at the first node it cannot read. */
|
|
189
|
+
private readAllPages;
|
|
190
|
+
/** A node typed /Page, or any node without /Kids, is a page to pdf.js. */
|
|
191
|
+
private isPageNode;
|
|
192
|
+
/** Reads a value one level deep, as pdf.js reads one on its way to a page. Throws BrokenEntry where pdf.js would. */
|
|
193
|
+
private fetch;
|
|
194
|
+
/**
|
|
195
|
+
* Whether an entry points at another object or at nothing that reads as an object header. pdf.js refuses such an
|
|
196
|
+
* entry, and an entry whose generation differs from the reference, before it reads the object. A missing entry
|
|
197
|
+
* reads as null instead. An object stream's entry is left out: pdf.js finds the object by its index there.
|
|
198
|
+
*/
|
|
199
|
+
private entryBroken;
|
|
121
200
|
private initSecurity;
|
|
122
201
|
private loadXrefChain;
|
|
123
|
-
/**
|
|
202
|
+
/**
|
|
203
|
+
* Reads one section; for hybrid files its XRefStm entries add the objects the table does not give. Without `rebase`,
|
|
204
|
+
* an offset that misses is not tried again past bytes before the header.
|
|
205
|
+
*/
|
|
124
206
|
private readXrefSection;
|
|
125
207
|
private readXrefTable;
|
|
126
208
|
/** `at` is where the stream's object header starts, which can be past whitespace at `offset`. */
|
|
127
209
|
private readXrefStream;
|
|
128
|
-
/**
|
|
210
|
+
/**
|
|
211
|
+
* Rebuilds the object map by scanning the file, as pdf.js's indexObjects does when the cross-reference data is
|
|
212
|
+
* missing or wrong, and picks the trailer pdf.js picks.
|
|
213
|
+
*/
|
|
129
214
|
private scan;
|
|
215
|
+
/**
|
|
216
|
+
* pdf.js's indexObjects. It reads the file token by token, a token running to the next LF, CR or "<", and skips
|
|
217
|
+
* comments. A token that starts "N G obj" defines an object, and the scan jumps from it to the next "endobj", "N G
|
|
218
|
+
* obj", "xref" or "trailer<<" and a letter. From "xref" it jumps to "trailer" and then to "startxref", and from
|
|
219
|
+
* "trailer" to the next "startxref" or "N G obj". So a "trailer" inside an object, a comment or right after
|
|
220
|
+
* another one is no candidate. Returns the candidates, and the objects that look like xref streams.
|
|
221
|
+
*/
|
|
222
|
+
private indexObjects;
|
|
223
|
+
/** pdf.js's /^(\d+)\s+(\d+)\s+obj\b/ against the token [pos, end): the numbers, or undefined. */
|
|
224
|
+
private objHead;
|
|
225
|
+
/**
|
|
226
|
+
* The first match from `from` on of pdf.js's /\b(endobj|\d+\s+\d+\s+obj|xref|trailer\s*<<)\b/, or with `endobj`
|
|
227
|
+
* 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
|
|
228
|
+
* 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.
|
|
229
|
+
*/
|
|
230
|
+
private mark;
|
|
231
|
+
/**
|
|
232
|
+
* Reads the xref streams the scan found, and the sections their /Prev chains lead to, as pdf.js does: each fills
|
|
233
|
+
* only the numbers nothing before it gave, and a free row counts as given.
|
|
234
|
+
*/
|
|
235
|
+
private readRecovered;
|
|
236
|
+
/**
|
|
237
|
+
* pdf.js's choice among the trailers its recovery finds: the first, in file order, whose /Root and /Pages are
|
|
238
|
+
* dictionaries, whose /Count is a whole number and that has an /ID, and an /Encrypt if any candidate has one.
|
|
239
|
+
* Failing that, the last whose /Root and /Pages are dictionaries. A reference whose generation is higher than its
|
|
240
|
+
* entry's counts in a second pass, once any lookup failed.
|
|
241
|
+
*/
|
|
242
|
+
private pickTrailer;
|
|
243
|
+
/** Reads a value one level deep, throwing BrokenEntry where pdf.js's lookup throws. */
|
|
244
|
+
private fetchStrict;
|
|
245
|
+
/** With no trailer at all, pdf.js takes the lowest-numbered object, or stream dictionary, that holds /Root. */
|
|
246
|
+
private rootHolder;
|
|
247
|
+
/**
|
|
248
|
+
* Reads every object of the rebuilt map once, in number order, for the readings recovery falls back on: the first
|
|
249
|
+
* that holds /Root, the object streams, the newest xref stream and the earliest catalog in the file.
|
|
250
|
+
*/
|
|
251
|
+
private surveyObjects;
|
|
252
|
+
/** Runs `fn` with the issues as they were before it, for lookups that only decide how to read the file. */
|
|
253
|
+
private quietly;
|
|
254
|
+
/**
|
|
255
|
+
* Where pdf.js's recovery gives no catalog and page tree, as other readers do: every object is read, the objects in
|
|
256
|
+
* object streams it finds are added, and a catalog is taken from an xref stream dictionary or a /Type /Catalog.
|
|
257
|
+
*/
|
|
258
|
+
private scanFurther;
|
|
130
259
|
/** Adds the objects held in the object streams scan() found. Their bodies may be encrypted, so this runs after initSecurity(). */
|
|
131
260
|
private indexObjStms;
|
|
261
|
+
/**
|
|
262
|
+
* Forgets parsed objects, the stream lengths read for them and the streams lifted out of them, once the map or the
|
|
263
|
+
* key changes. Only the catalog written in the trailer, lifted once the map is final, outlives this.
|
|
264
|
+
*/
|
|
265
|
+
private forgetParsed;
|
|
132
266
|
/**
|
|
133
267
|
* Parses the indirect object ("N G obj ...") at a source offset. `at` is where its header starts, past any
|
|
134
268
|
* whitespace, and `end` where its value or its stream body ends.
|
|
@@ -136,28 +270,85 @@ export declare class PdfDocument {
|
|
|
136
270
|
private parseIndirectAt;
|
|
137
271
|
/** Finds a stream body's true length, trusting /Length only when "endstream" follows it. */
|
|
138
272
|
private locateStream;
|
|
139
|
-
/**
|
|
273
|
+
/**
|
|
274
|
+
* Outside xref streams the filter entries may be indirect. They are resolved one level, as viewers do, so every
|
|
275
|
+
* reader of this dictionary decodes what a viewer decodes. A reference to a stream or to nothing stays in place.
|
|
276
|
+
*/
|
|
277
|
+
private resolveFilters;
|
|
278
|
+
/** Position just before the EOL that precedes "endstream", or a misspelling pdf.js takes, in [from, to), or -1. */
|
|
140
279
|
private findEndstream;
|
|
141
280
|
/** The smallest known object or xref offset after `pos`, or the end of the file. */
|
|
142
281
|
private nextOffsetAfter;
|
|
143
282
|
/** Loads an object by reference, decrypted. Missing and free objects are null. */
|
|
144
283
|
getObject(ref: PdfRef, raw?: boolean): Promise<PdfObject>;
|
|
145
284
|
private decryptObject;
|
|
285
|
+
/**
|
|
286
|
+
* Gives each stream written inside `obj` a synthetic number and puts a reference to it in its place, so the walk
|
|
287
|
+
* and the writer meet it as they meet any stream. `key` names the object whose key decrypts it. `owner` names
|
|
288
|
+
* where `obj` came from, so a second read of it reuses the numbers. `stm` names the object stream whose decoded
|
|
289
|
+
* data holds the bodies, when `obj` sits in one.
|
|
290
|
+
*/
|
|
291
|
+
private lift;
|
|
292
|
+
/** Numbers an object that has no entry of its own, the same number each time the same object is read. */
|
|
293
|
+
private addSynthetic;
|
|
294
|
+
/** Records what the streams inside the object `owner` hold. Throws ParseError when that would pass the budget. */
|
|
295
|
+
private chargeSynthetic;
|
|
296
|
+
/** The object whose key decrypts a stream: the stream's own, or for a synthetic one the object that held it. */
|
|
297
|
+
private keyOf;
|
|
298
|
+
/**
|
|
299
|
+
* Reads object `num` from object stream `stmNum`. pdf.js finds an object by the number the stream's header gives it,
|
|
300
|
+
* and by `index`, the index its entry gives, when the header does not name it. Each object ends where the next one
|
|
301
|
+
* starts, and the last at the end of the stream, as pdf.js reads them.
|
|
302
|
+
*/
|
|
146
303
|
private getFromObjStm;
|
|
147
|
-
/**
|
|
304
|
+
/** The decoded object stream `stmNum`, decoded on first use, or null when it does not read. */
|
|
305
|
+
private objStm;
|
|
306
|
+
/** Counts a decoded object stream against memoryThreshold, and moves the least recently used ones out past it. */
|
|
307
|
+
private holdInMemory;
|
|
308
|
+
private tempDir;
|
|
309
|
+
/** A reader over a decoded object stream. Only a few stay open, so a file handle is given up when its reader goes. */
|
|
310
|
+
private objStmReader;
|
|
311
|
+
/**
|
|
312
|
+
* Decodes an object stream in chunks; one larger than memoryThreshold goes to a temporary file. Of its header it
|
|
313
|
+
* keeps the pairs that the entries pointing into it can reach, or with `all` every pair, for indexing it.
|
|
314
|
+
*/
|
|
148
315
|
private loadObjStm;
|
|
316
|
+
/** The objects whose entries point into object stream `stmNum`, with the index each entry gives. */
|
|
317
|
+
private wantedIn;
|
|
149
318
|
private ownTemp?;
|
|
150
319
|
/**
|
|
151
|
-
* Parses at an offset in any reader
|
|
152
|
-
*
|
|
153
|
-
*
|
|
320
|
+
* Parses at an offset in any reader. The window starts small and grows while the parser asks for more. A run of
|
|
321
|
+
* whitespace and comments that reaches the end of the window is skipped in the source and stands in the window as one
|
|
322
|
+
* space, and the body of a stream written inside the object is found in the source and left out, so the window holds
|
|
323
|
+
* the object's tokens and not its padding. A window that reaches `bound`, the next known object, grows further only
|
|
324
|
+
* while a budget for the whole file lasts, unless `hard` makes the bound the end of the data. `key` names the place
|
|
325
|
+
* read, so its bad tokens count once however often it is parsed.
|
|
154
326
|
*/
|
|
155
327
|
private parseWindowed;
|
|
328
|
+
/**
|
|
329
|
+
* Where the line after a "stream" keyword ends, as pdf.js finds it: after the first CR, LF or CRLF from `start` on, or
|
|
330
|
+
* at `limit`. `text` says whether anything but spaces came first. Bytes past `bound` come out of the parse budget.
|
|
331
|
+
*/
|
|
332
|
+
private lineEnd;
|
|
333
|
+
/**
|
|
334
|
+
* Where a run of whitespace, and of comments when `comments` is set, ends: the first byte at or after `from` that
|
|
335
|
+
* belongs to neither, or `limit`. `comment` says whether `from` is inside a comment. Bytes past `bound` come out of
|
|
336
|
+
* the parse budget.
|
|
337
|
+
*/
|
|
338
|
+
private skipRun;
|
|
339
|
+
/**
|
|
340
|
+
* Finds the body of a stream written inside an object, starting at `start`, as the parser would in a window that held
|
|
341
|
+
* it: at its /Length when "endstream" follows after whitespace, and otherwise at the first "endstream". Returns its
|
|
342
|
+
* length and where the object goes on after "endstream", or null when no "endstream" comes before `limit`.
|
|
343
|
+
*/
|
|
344
|
+
private findBody;
|
|
156
345
|
resolve(v: PdfObject | undefined): Promise<PdfObject | undefined>;
|
|
157
346
|
/** The generation of a live object's entry, 0 for one in an object stream or none. */
|
|
158
347
|
genOf(num: number): number;
|
|
159
348
|
/** Length of the stream once decrypted, which is what gets written out. */
|
|
160
349
|
plainLength(stream: PdfStream, num: number): Promise<number>;
|
|
350
|
+
/** False for a stream encrypted with a key the password did not give, as attached files can be. */
|
|
351
|
+
canDecrypt(stream: PdfStream, num: number): boolean;
|
|
161
352
|
/** Yields the decrypted, still-encoded body in chunks. */
|
|
162
353
|
plainChunks(stream: PdfStream, num: number): AsyncGenerator<Uint8Array>;
|
|
163
354
|
/** Decrypts and decodes a whole stream body into memory, subject to the decompression limit and an optional cap. */
|
|
@@ -185,7 +376,10 @@ export declare class PdfDocument {
|
|
|
185
376
|
liveNumbers(): IterableIterator<number>;
|
|
186
377
|
/** Drops caches and the object index once the document is no longer read, and deletes spilled object streams. */
|
|
187
378
|
release(): Promise<void>;
|
|
188
|
-
/**
|
|
379
|
+
/**
|
|
380
|
+
* Forgets every decoded object stream, closing and deleting the temporary files, once the map or the key changes. The
|
|
381
|
+
* streams whose bodies they held are forgotten with the parsed objects.
|
|
382
|
+
*/
|
|
189
383
|
private dropObjStms;
|
|
190
384
|
}
|
|
191
385
|
export {};
|