@patrickdk77/pdf-defuse 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +323 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +390 -0
- package/dist/crypto.d.ts +51 -0
- package/dist/crypto.js +404 -0
- package/dist/data-plugins.d.ts +28 -0
- package/dist/data-plugins.js +525 -0
- package/dist/document.d.ts +191 -0
- package/dist/document.js +1403 -0
- package/dist/engine.d.ts +16 -0
- package/dist/engine.js +392 -0
- package/dist/filters.d.ts +27 -0
- package/dist/filters.js +616 -0
- package/dist/findings.d.ts +24 -0
- package/dist/findings.js +183 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +42 -0
- package/dist/index.mjs +3 -0
- package/dist/io.d.ts +81 -0
- package/dist/io.js +414 -0
- package/dist/objects.d.ts +52 -0
- package/dist/objects.js +178 -0
- package/dist/parser.d.ts +70 -0
- package/dist/parser.js +534 -0
- package/dist/plugins.d.ts +13 -0
- package/dist/plugins.js +56 -0
- package/dist/score.d.ts +17 -0
- package/dist/score.js +63 -0
- package/dist/sniff.d.ts +15 -0
- package/dist/sniff.js +118 -0
- package/dist/types.d.ts +294 -0
- package/dist/types.js +143 -0
- package/dist/uri.d.ts +29 -0
- package/dist/uri.js +150 -0
- package/dist/walker.d.ts +302 -0
- package/dist/walker.js +2914 -0
- package/dist/writer.d.ts +6 -0
- package/dist/writer.js +112 -0
- package/package.json +86 -4
package/dist/parser.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { type PdfObject, PdfRef } from './objects';
|
|
2
|
+
export declare class ParseError extends Error {
|
|
3
|
+
}
|
|
4
|
+
/** Thrown when the window ended before the object did. The caller retries with more bytes. */
|
|
5
|
+
export declare class NeedMoreData extends Error {
|
|
6
|
+
}
|
|
7
|
+
export declare const isWhite: (c: number) => boolean;
|
|
8
|
+
interface ParseHooks {
|
|
9
|
+
/** Called when a name used #xx to encode a letter or digit. */
|
|
10
|
+
onEscapedName?(name: string): void;
|
|
11
|
+
/** Called when a dictionary repeats a key. Readers disagree on which copy wins. */
|
|
12
|
+
onDuplicateKey?(key: string): void;
|
|
13
|
+
/**
|
|
14
|
+
* Called for junk in a dictionary or array, a bare keyword or a stray delimiter, which is read as null or skipped, and
|
|
15
|
+
* for syntax other readers take in a different way: a key with no value, a malformed number, and text between
|
|
16
|
+
* "stream" and its EOL.
|
|
17
|
+
*/
|
|
18
|
+
onBadToken?(token: string): void;
|
|
19
|
+
/** Called every 65536 values, so a caller can stop a long parse at its deadline. */
|
|
20
|
+
onProgress?(): void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Parses PDF objects from a byte window. `eof` tells the parser whether the window ends at the end of the source,
|
|
24
|
+
* so running off the end means NeedMoreData rather than a syntax error.
|
|
25
|
+
*/
|
|
26
|
+
export declare class Parser {
|
|
27
|
+
readonly buf: Uint8Array;
|
|
28
|
+
readonly eof: boolean;
|
|
29
|
+
readonly hooks: ParseHooks;
|
|
30
|
+
pos: number;
|
|
31
|
+
depth: number;
|
|
32
|
+
/** Estimated heap bytes of what this parser has built, with the length of every name and string. */
|
|
33
|
+
cost: number;
|
|
34
|
+
/** The part of `cost` that does not grow with the length of a name or string. */
|
|
35
|
+
private overhead;
|
|
36
|
+
private values;
|
|
37
|
+
constructor(buf: Uint8Array, pos?: number, eof?: boolean, hooks?: ParseHooks);
|
|
38
|
+
/** Rough heap costs: a slot for a number or keyword, an object for a name or reference, a buffer for a string. */
|
|
39
|
+
private charge;
|
|
40
|
+
private more;
|
|
41
|
+
peek(): number;
|
|
42
|
+
skipWhitespace(): void;
|
|
43
|
+
/** Reads a regular-character token without consuming delimiters. */
|
|
44
|
+
readToken(): string;
|
|
45
|
+
matchKeyword(word: string): boolean;
|
|
46
|
+
/** Parses "N G obj" and returns the reference. */
|
|
47
|
+
parseObjectHeader(): PdfRef;
|
|
48
|
+
parseObject(): PdfObject;
|
|
49
|
+
private parseNumberOrRef;
|
|
50
|
+
private parseName;
|
|
51
|
+
private parseLiteralString;
|
|
52
|
+
private parseHexString;
|
|
53
|
+
private parseArray;
|
|
54
|
+
private parseDict;
|
|
55
|
+
/**
|
|
56
|
+
* Skips junk inside a container instead of losing the whole object: an unknown bare keyword, or a delimiter that
|
|
57
|
+
* cannot start an object, such as "]" in a dictionary or ">>" in an array. qpdf reads each of these as null. The
|
|
58
|
+
* container's own closing delimiter was checked before this is called.
|
|
59
|
+
*/
|
|
60
|
+
private skipBadToken;
|
|
61
|
+
/**
|
|
62
|
+
* After a dictionary, checks for the "stream" keyword. Returns the position of the first body byte,
|
|
63
|
+
* or -1 when the object is not a stream.
|
|
64
|
+
*/
|
|
65
|
+
streamStart(): number;
|
|
66
|
+
}
|
|
67
|
+
export declare function parseNumber(tok: string): number;
|
|
68
|
+
/** Parses a complete object from a buffer that holds all of it. */
|
|
69
|
+
export declare function parseObjectFrom(buf: Uint8Array, pos?: number, hooks?: ParseHooks): PdfObject;
|
|
70
|
+
export {};
|
package/dist/parser.js
ADDED
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Parser = exports.isWhite = exports.NeedMoreData = exports.ParseError = void 0;
|
|
4
|
+
exports.parseNumber = parseNumber;
|
|
5
|
+
exports.parseObjectFrom = parseObjectFrom;
|
|
6
|
+
const filters_1 = require("./filters");
|
|
7
|
+
const objects_1 = require("./objects");
|
|
8
|
+
class ParseError extends Error {
|
|
9
|
+
}
|
|
10
|
+
exports.ParseError = ParseError;
|
|
11
|
+
/** Thrown when the window ended before the object did. The caller retries with more bytes. */
|
|
12
|
+
class NeedMoreData extends Error {
|
|
13
|
+
}
|
|
14
|
+
exports.NeedMoreData = NeedMoreData;
|
|
15
|
+
const WS = new Uint8Array(256);
|
|
16
|
+
for (const c of [0x00, 0x09, 0x0a, 0x0c, 0x0d, 0x20])
|
|
17
|
+
WS[c] = 1;
|
|
18
|
+
const DELIM = new Uint8Array(256);
|
|
19
|
+
for (const c of '()<>[]{}/%')
|
|
20
|
+
DELIM[c.charCodeAt(0)] = 1;
|
|
21
|
+
/** Keywords that frame objects. Skipping one as junk could run an unterminated object into the next. */
|
|
22
|
+
const KEYWORDS = new Set(['true', 'false', 'null', 'obj', 'endobj', 'stream', 'endstream', 'R', 'xref', 'trailer', 'startxref']);
|
|
23
|
+
/**
|
|
24
|
+
* A number as ISO 32000 writes it. It must match only one way: a pattern that can split a digit run backtracks
|
|
25
|
+
* quadratically on a long bad token.
|
|
26
|
+
*/
|
|
27
|
+
const NUMBER = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/;
|
|
28
|
+
/**
|
|
29
|
+
* The most per-value heap one parsed object may take, as charge() estimates it. A value takes far more heap than its
|
|
30
|
+
* text, about 240 bytes for "()" or "<<>>", so a few kilobytes of compressed object stream could otherwise fill any
|
|
31
|
+
* heap. The text itself is bounded by the parse window.
|
|
32
|
+
*/
|
|
33
|
+
const MAX_OVERHEAD = 16 * 1024 * 1024;
|
|
34
|
+
const isWhite = (c) => WS[c] === 1;
|
|
35
|
+
exports.isWhite = isWhite;
|
|
36
|
+
const isRegular = (c) => !WS[c] && !DELIM[c];
|
|
37
|
+
const isDigit = (c) => c >= 0x30 && c <= 0x39;
|
|
38
|
+
/**
|
|
39
|
+
* Parses PDF objects from a byte window. `eof` tells the parser whether the window ends at the end of the source,
|
|
40
|
+
* so running off the end means NeedMoreData rather than a syntax error.
|
|
41
|
+
*/
|
|
42
|
+
class Parser {
|
|
43
|
+
buf;
|
|
44
|
+
eof;
|
|
45
|
+
hooks;
|
|
46
|
+
pos;
|
|
47
|
+
depth = 0;
|
|
48
|
+
/** Estimated heap bytes of what this parser has built, with the length of every name and string. */
|
|
49
|
+
cost = 0;
|
|
50
|
+
/** The part of `cost` that does not grow with the length of a name or string. */
|
|
51
|
+
overhead = 0;
|
|
52
|
+
values = 0;
|
|
53
|
+
constructor(buf, pos = 0, eof = true, hooks = {}) {
|
|
54
|
+
this.buf = buf;
|
|
55
|
+
this.eof = eof;
|
|
56
|
+
this.hooks = hooks;
|
|
57
|
+
this.pos = pos;
|
|
58
|
+
}
|
|
59
|
+
/** Rough heap costs: a slot for a number or keyword, an object for a name or reference, a buffer for a string. */
|
|
60
|
+
charge(overhead, length = 0) {
|
|
61
|
+
this.cost += overhead + length;
|
|
62
|
+
this.overhead += overhead;
|
|
63
|
+
if (this.overhead > MAX_OVERHEAD)
|
|
64
|
+
throw new ParseError('Object too large');
|
|
65
|
+
if (++this.values % 65536 === 0)
|
|
66
|
+
this.hooks.onProgress?.();
|
|
67
|
+
}
|
|
68
|
+
more() {
|
|
69
|
+
if (this.eof)
|
|
70
|
+
throw new ParseError(`Unexpected end of data at ${this.pos}`);
|
|
71
|
+
throw new NeedMoreData();
|
|
72
|
+
}
|
|
73
|
+
peek() {
|
|
74
|
+
if (this.pos >= this.buf.length)
|
|
75
|
+
this.more();
|
|
76
|
+
return this.buf[this.pos];
|
|
77
|
+
}
|
|
78
|
+
skipWhitespace() {
|
|
79
|
+
const b = this.buf;
|
|
80
|
+
for (;;) {
|
|
81
|
+
while (this.pos < b.length && WS[b[this.pos]])
|
|
82
|
+
this.pos++;
|
|
83
|
+
if (this.pos < b.length && b[this.pos] === 0x25) {
|
|
84
|
+
while (this.pos < b.length && b[this.pos] !== 0x0a && b[this.pos] !== 0x0d)
|
|
85
|
+
this.pos++;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
if (this.pos >= b.length)
|
|
91
|
+
this.more();
|
|
92
|
+
}
|
|
93
|
+
/** Reads a regular-character token without consuming delimiters. */
|
|
94
|
+
readToken() {
|
|
95
|
+
const start = this.pos;
|
|
96
|
+
const b = this.buf;
|
|
97
|
+
while (this.pos < b.length && isRegular(b[this.pos]))
|
|
98
|
+
this.pos++;
|
|
99
|
+
if (this.pos >= b.length && !this.eof)
|
|
100
|
+
throw new NeedMoreData();
|
|
101
|
+
if (this.pos - start <= 32) {
|
|
102
|
+
let t = '';
|
|
103
|
+
for (let i = start; i < this.pos; i++)
|
|
104
|
+
t += String.fromCharCode(b[i]);
|
|
105
|
+
return t;
|
|
106
|
+
}
|
|
107
|
+
return Buffer.from(b.buffer, b.byteOffset + start, this.pos - start).toString('latin1');
|
|
108
|
+
}
|
|
109
|
+
matchKeyword(word) {
|
|
110
|
+
const save = this.pos;
|
|
111
|
+
try {
|
|
112
|
+
this.skipWhitespace();
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
if (e instanceof ParseError) {
|
|
116
|
+
this.pos = save;
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
throw e;
|
|
120
|
+
}
|
|
121
|
+
for (let i = 0; i < word.length; i++) {
|
|
122
|
+
if (this.pos + i >= this.buf.length) {
|
|
123
|
+
if (!this.eof)
|
|
124
|
+
throw new NeedMoreData();
|
|
125
|
+
this.pos = save;
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
if (this.buf[this.pos + i] !== word.charCodeAt(i)) {
|
|
129
|
+
this.pos = save;
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const after = this.pos + word.length;
|
|
134
|
+
if (after < this.buf.length && isRegular(this.buf[after])) {
|
|
135
|
+
this.pos = save;
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
this.pos = after;
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
/** Parses "N G obj" and returns the reference. */
|
|
142
|
+
parseObjectHeader() {
|
|
143
|
+
this.skipWhitespace();
|
|
144
|
+
const n = this.readToken();
|
|
145
|
+
this.skipWhitespace();
|
|
146
|
+
const g = this.readToken();
|
|
147
|
+
if (!/^\d+$/.test(n) || !/^\d+$/.test(g))
|
|
148
|
+
throw new ParseError(`Bad object header at ${this.pos}`);
|
|
149
|
+
if (!this.matchKeyword('obj'))
|
|
150
|
+
throw new ParseError(`Missing "obj" at ${this.pos}`);
|
|
151
|
+
return new objects_1.PdfRef(Number(n), Number(g));
|
|
152
|
+
}
|
|
153
|
+
parseObject() {
|
|
154
|
+
this.skipWhitespace();
|
|
155
|
+
const c = this.peek();
|
|
156
|
+
if (c === 0x2f)
|
|
157
|
+
return this.parseName();
|
|
158
|
+
if (c === 0x28)
|
|
159
|
+
return this.parseLiteralString();
|
|
160
|
+
if (c === 0x3c) {
|
|
161
|
+
if (this.pos + 1 >= this.buf.length)
|
|
162
|
+
this.more();
|
|
163
|
+
if (this.buf[this.pos + 1] === 0x3c)
|
|
164
|
+
return this.parseDict();
|
|
165
|
+
return this.parseHexString();
|
|
166
|
+
}
|
|
167
|
+
if (c === 0x5b)
|
|
168
|
+
return this.parseArray();
|
|
169
|
+
if (c === 0x2b || c === 0x2d || c === 0x2e || isDigit(c)) {
|
|
170
|
+
const v = this.parseNumberOrRef();
|
|
171
|
+
this.charge(v instanceof objects_1.PdfRef ? 56 : 16);
|
|
172
|
+
return v;
|
|
173
|
+
}
|
|
174
|
+
const tok = this.readToken();
|
|
175
|
+
if (tok === 'true' || tok === 'false' || tok === 'null') {
|
|
176
|
+
this.charge(16);
|
|
177
|
+
return tok === 'true' ? true : tok === 'false' ? false : null;
|
|
178
|
+
}
|
|
179
|
+
throw new ParseError(`Unexpected token "${tok || String.fromCharCode(c)}" at ${this.pos}`);
|
|
180
|
+
}
|
|
181
|
+
parseNumberOrRef() {
|
|
182
|
+
const b = this.buf;
|
|
183
|
+
const start = this.pos;
|
|
184
|
+
// Fast path: an unsigned integer followed by a delimiter or whitespace.
|
|
185
|
+
let p = start;
|
|
186
|
+
let v = 0;
|
|
187
|
+
while (p < b.length && isDigit(b[p]) && p - start < 15)
|
|
188
|
+
v = v * 10 + (b[p++] - 0x30);
|
|
189
|
+
if (p > start && p < b.length && !isRegular(b[p])) {
|
|
190
|
+
// Possible reference: int int R
|
|
191
|
+
let q = p;
|
|
192
|
+
while (q < b.length && WS[b[q]])
|
|
193
|
+
q++;
|
|
194
|
+
const gs = q;
|
|
195
|
+
let g = 0;
|
|
196
|
+
while (q < b.length && isDigit(b[q]) && q - gs < 6)
|
|
197
|
+
g = g * 10 + (b[q++] - 0x30);
|
|
198
|
+
const ge = q;
|
|
199
|
+
// pdf.js, poppler and Ghostscript also take an "R" written against the generation, as in "5 0R>>".
|
|
200
|
+
if (q > gs && q < b.length && (WS[b[q]] || b[q] === 0x52)) {
|
|
201
|
+
while (q < b.length && WS[b[q]])
|
|
202
|
+
q++;
|
|
203
|
+
if (b[q] === 0x52) {
|
|
204
|
+
if (q + 1 >= b.length && !this.eof)
|
|
205
|
+
throw new NeedMoreData();
|
|
206
|
+
if (q + 1 >= b.length || !isRegular(b[q + 1])) {
|
|
207
|
+
this.pos = q + 1;
|
|
208
|
+
return new objects_1.PdfRef(v, g);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (q >= b.length && !this.eof)
|
|
213
|
+
throw new NeedMoreData();
|
|
214
|
+
// A comment between the parts, a signed generation or one longer than this scan needs the slow path.
|
|
215
|
+
if (!(b[q] === 0x25 || (q === ge && isDigit(b[q])) || (q === gs && b[q] === 0x2b))) {
|
|
216
|
+
this.pos = p;
|
|
217
|
+
return v;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const tok = this.readToken();
|
|
221
|
+
const num = parseNumber(tok);
|
|
222
|
+
// pdf.js reads "5-0" as 50 and qpdf reads it as a string, so a number in any other form is reported.
|
|
223
|
+
if (!NUMBER.test(tok))
|
|
224
|
+
this.hooks.onBadToken?.(tok);
|
|
225
|
+
if (!/^\+?\d+$/.test(tok))
|
|
226
|
+
return num;
|
|
227
|
+
// Possible reference: int int R, where comments count as whitespace and integers may carry a '+'.
|
|
228
|
+
const save = this.pos;
|
|
229
|
+
try {
|
|
230
|
+
this.skipWhitespace();
|
|
231
|
+
const c = this.buf[this.pos];
|
|
232
|
+
if (!isDigit(c) && c !== 0x2b) {
|
|
233
|
+
this.pos = save;
|
|
234
|
+
return num;
|
|
235
|
+
}
|
|
236
|
+
const g = this.readToken();
|
|
237
|
+
// A token ends at a non-regular byte, so an "R" glued to the generation is followed by one.
|
|
238
|
+
if (/^\+?\d+R$/.test(g))
|
|
239
|
+
return new objects_1.PdfRef(num, Number(g.slice(0, -1)));
|
|
240
|
+
if (!/^\+?\d+$/.test(g)) {
|
|
241
|
+
this.pos = save;
|
|
242
|
+
return num;
|
|
243
|
+
}
|
|
244
|
+
this.skipWhitespace();
|
|
245
|
+
if (this.buf[this.pos] === 0x52) {
|
|
246
|
+
if (this.pos + 1 >= this.buf.length && !this.eof)
|
|
247
|
+
throw new NeedMoreData();
|
|
248
|
+
if (this.pos + 1 >= this.buf.length || !isRegular(this.buf[this.pos + 1])) {
|
|
249
|
+
this.pos++;
|
|
250
|
+
return new objects_1.PdfRef(num, Number(g));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch (e) {
|
|
255
|
+
if (e instanceof NeedMoreData)
|
|
256
|
+
throw e;
|
|
257
|
+
}
|
|
258
|
+
this.pos = save;
|
|
259
|
+
return num;
|
|
260
|
+
}
|
|
261
|
+
parseName() {
|
|
262
|
+
this.pos++; // '/'
|
|
263
|
+
const raw = this.readToken();
|
|
264
|
+
this.charge(48, raw.length);
|
|
265
|
+
if (!raw.includes('#'))
|
|
266
|
+
return new objects_1.PdfName(raw);
|
|
267
|
+
const out = new filters_1.Out(raw.length);
|
|
268
|
+
let escaped = false;
|
|
269
|
+
for (let i = 0; i < raw.length; i++) {
|
|
270
|
+
const hi = raw.charCodeAt(i) === 0x23 && i + 2 < raw.length ? (0, filters_1.hexVal)(raw.charCodeAt(i + 1)) : -1;
|
|
271
|
+
const lo = hi >= 0 ? (0, filters_1.hexVal)(raw.charCodeAt(i + 2)) : -1;
|
|
272
|
+
if (lo < 0) {
|
|
273
|
+
out.push(raw.charCodeAt(i));
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
const v = (hi << 4) | lo;
|
|
277
|
+
if ((v >= 0x30 && v <= 0x39) || (v >= 0x41 && v <= 0x5a) || (v >= 0x61 && v <= 0x7a))
|
|
278
|
+
escaped = true;
|
|
279
|
+
out.push(v);
|
|
280
|
+
i += 2;
|
|
281
|
+
}
|
|
282
|
+
const bytes = out.take();
|
|
283
|
+
const name = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.length).toString('latin1');
|
|
284
|
+
if (escaped)
|
|
285
|
+
this.hooks.onEscapedName?.(name);
|
|
286
|
+
return new objects_1.PdfName(name);
|
|
287
|
+
}
|
|
288
|
+
parseLiteralString() {
|
|
289
|
+
this.pos++; // '('
|
|
290
|
+
const b = this.buf;
|
|
291
|
+
const out = new filters_1.Out(64);
|
|
292
|
+
let depth = 1;
|
|
293
|
+
for (;;) {
|
|
294
|
+
if (this.pos >= b.length)
|
|
295
|
+
this.more();
|
|
296
|
+
let c = b[this.pos++];
|
|
297
|
+
if (c === 0x5c) {
|
|
298
|
+
if (this.pos >= b.length)
|
|
299
|
+
this.more();
|
|
300
|
+
c = b[this.pos++];
|
|
301
|
+
switch (c) {
|
|
302
|
+
case 0x6e:
|
|
303
|
+
out.push(0x0a);
|
|
304
|
+
break;
|
|
305
|
+
case 0x72:
|
|
306
|
+
out.push(0x0d);
|
|
307
|
+
break;
|
|
308
|
+
case 0x74:
|
|
309
|
+
out.push(0x09);
|
|
310
|
+
break;
|
|
311
|
+
case 0x62:
|
|
312
|
+
out.push(0x08);
|
|
313
|
+
break;
|
|
314
|
+
case 0x66:
|
|
315
|
+
out.push(0x0c);
|
|
316
|
+
break;
|
|
317
|
+
case 0x0d:
|
|
318
|
+
if (this.pos < b.length && b[this.pos] === 0x0a)
|
|
319
|
+
this.pos++;
|
|
320
|
+
break;
|
|
321
|
+
case 0x0a:
|
|
322
|
+
break;
|
|
323
|
+
default:
|
|
324
|
+
if (c >= 0x30 && c <= 0x37) {
|
|
325
|
+
let v = c - 0x30;
|
|
326
|
+
for (let i = 0; i < 2 && this.pos < b.length && b[this.pos] >= 0x30 && b[this.pos] <= 0x37; i++)
|
|
327
|
+
v = v * 8 + (b[this.pos++] - 0x30);
|
|
328
|
+
out.push(v & 0xff);
|
|
329
|
+
}
|
|
330
|
+
else
|
|
331
|
+
out.push(c);
|
|
332
|
+
}
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (c === 0x28)
|
|
336
|
+
depth++;
|
|
337
|
+
else if (c === 0x29) {
|
|
338
|
+
depth--;
|
|
339
|
+
if (depth === 0)
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
else if (c === 0x0d) {
|
|
343
|
+
if (this.pos < b.length && b[this.pos] === 0x0a)
|
|
344
|
+
this.pos++;
|
|
345
|
+
c = 0x0a;
|
|
346
|
+
}
|
|
347
|
+
out.push(c);
|
|
348
|
+
}
|
|
349
|
+
this.charge(256, out.len);
|
|
350
|
+
return new objects_1.PdfString(out.take());
|
|
351
|
+
}
|
|
352
|
+
parseHexString() {
|
|
353
|
+
this.pos++; // '<'
|
|
354
|
+
const b = this.buf;
|
|
355
|
+
const out = new filters_1.Out(64);
|
|
356
|
+
let hi = -1;
|
|
357
|
+
for (;;) {
|
|
358
|
+
if (this.pos >= b.length)
|
|
359
|
+
this.more();
|
|
360
|
+
const c = b[this.pos++];
|
|
361
|
+
if (c === 0x3e)
|
|
362
|
+
break;
|
|
363
|
+
const v = (0, filters_1.hexVal)(c);
|
|
364
|
+
if (v < 0)
|
|
365
|
+
continue;
|
|
366
|
+
if (hi < 0)
|
|
367
|
+
hi = v;
|
|
368
|
+
else {
|
|
369
|
+
out.push((hi << 4) | v);
|
|
370
|
+
hi = -1;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (hi >= 0)
|
|
374
|
+
out.push(hi << 4);
|
|
375
|
+
this.charge(256, out.len);
|
|
376
|
+
return new objects_1.PdfString(out.take());
|
|
377
|
+
}
|
|
378
|
+
parseArray() {
|
|
379
|
+
this.pos++; // '['
|
|
380
|
+
if (++this.depth > 512)
|
|
381
|
+
throw new ParseError('Objects nested too deeply');
|
|
382
|
+
this.charge(48);
|
|
383
|
+
const arr = [];
|
|
384
|
+
for (;;) {
|
|
385
|
+
this.skipWhitespace();
|
|
386
|
+
if (this.peek() === 0x5d) {
|
|
387
|
+
this.pos++;
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
arr.push(this.skipBadToken() ? null : this.parseObject());
|
|
391
|
+
}
|
|
392
|
+
this.depth--;
|
|
393
|
+
return arr;
|
|
394
|
+
}
|
|
395
|
+
parseDict() {
|
|
396
|
+
this.pos += 2; // '<<'
|
|
397
|
+
if (++this.depth > 512)
|
|
398
|
+
throw new ParseError('Objects nested too deeply');
|
|
399
|
+
this.charge(256);
|
|
400
|
+
const d = new objects_1.PdfDict();
|
|
401
|
+
for (;;) {
|
|
402
|
+
this.skipWhitespace();
|
|
403
|
+
const c = this.peek();
|
|
404
|
+
if (c === 0x3e) {
|
|
405
|
+
if (this.pos + 1 >= this.buf.length)
|
|
406
|
+
this.more();
|
|
407
|
+
if (this.buf[this.pos + 1] === 0x3e) {
|
|
408
|
+
this.pos += 2;
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (c !== 0x2f) {
|
|
413
|
+
// pdf.js skips one token where a key belongs, so the keys inside a "[" or "<<" written there belong to this
|
|
414
|
+
// dictionary. Other junk is skipped as one token or object.
|
|
415
|
+
if (c === 0x3c && this.pos + 1 >= this.buf.length)
|
|
416
|
+
this.more();
|
|
417
|
+
const open = c === 0x5b ? 1 : c === 0x3c && this.buf[this.pos + 1] === 0x3c ? 2 : 0;
|
|
418
|
+
if (open) {
|
|
419
|
+
this.hooks.onBadToken?.(open === 1 ? '[' : '<<');
|
|
420
|
+
this.pos += open;
|
|
421
|
+
}
|
|
422
|
+
else if (!this.skipBadToken())
|
|
423
|
+
this.parseObject();
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
const k = this.parseName();
|
|
427
|
+
if (d.has(k.name))
|
|
428
|
+
this.hooks.onDuplicateKey?.(k.name);
|
|
429
|
+
this.skipWhitespace();
|
|
430
|
+
if (this.peek() === 0x3e) {
|
|
431
|
+
if (this.pos + 1 >= this.buf.length)
|
|
432
|
+
this.more();
|
|
433
|
+
if (this.buf[this.pos + 1] === 0x3e) {
|
|
434
|
+
// pdf.js takes the ">>" as the value and reads on past the end of the object.
|
|
435
|
+
this.hooks.onBadToken?.(`/${k.name}`);
|
|
436
|
+
d.set(k.name, null);
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
d.set(k.name, this.skipBadToken() ? null : this.parseObject());
|
|
441
|
+
}
|
|
442
|
+
this.depth--;
|
|
443
|
+
return d;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Skips junk inside a container instead of losing the whole object: an unknown bare keyword, or a delimiter that
|
|
447
|
+
* cannot start an object, such as "]" in a dictionary or ">>" in an array. qpdf reads each of these as null. The
|
|
448
|
+
* container's own closing delimiter was checked before this is called.
|
|
449
|
+
*/
|
|
450
|
+
skipBadToken() {
|
|
451
|
+
const c = this.peek();
|
|
452
|
+
if (c === 0x5d || c === 0x29 || c === 0x7b || c === 0x7d || c === 0x3e) {
|
|
453
|
+
if (c === 0x3e && this.pos + 1 >= this.buf.length)
|
|
454
|
+
this.more();
|
|
455
|
+
const n = c === 0x3e && this.buf[this.pos + 1] === 0x3e ? 2 : 1;
|
|
456
|
+
this.hooks.onBadToken?.(n === 2 ? '>>' : String.fromCharCode(c));
|
|
457
|
+
this.pos += n;
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
if (!isRegular(c) || c === 0x2b || c === 0x2d || c === 0x2e || isDigit(c))
|
|
461
|
+
return false;
|
|
462
|
+
const save = this.pos;
|
|
463
|
+
const tok = this.readToken();
|
|
464
|
+
if (KEYWORDS.has(tok)) {
|
|
465
|
+
this.pos = save;
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
this.hooks.onBadToken?.(tok);
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* After a dictionary, checks for the "stream" keyword. Returns the position of the first body byte,
|
|
473
|
+
* or -1 when the object is not a stream.
|
|
474
|
+
*/
|
|
475
|
+
streamStart() {
|
|
476
|
+
const save = this.pos;
|
|
477
|
+
try {
|
|
478
|
+
this.skipWhitespace();
|
|
479
|
+
}
|
|
480
|
+
catch (e) {
|
|
481
|
+
if (e instanceof ParseError)
|
|
482
|
+
return -1;
|
|
483
|
+
throw e;
|
|
484
|
+
}
|
|
485
|
+
if (!this.matchKeyword('stream')) {
|
|
486
|
+
this.pos = save;
|
|
487
|
+
return -1;
|
|
488
|
+
}
|
|
489
|
+
const b = this.buf;
|
|
490
|
+
// Spaces and tabs before the EOL are skipped. Anything else leaves the body starting right after the keyword,
|
|
491
|
+
// while pdf.js skips everything up to the EOL, so that text is reported.
|
|
492
|
+
let p = this.pos;
|
|
493
|
+
while (p < b.length && (b[p] === 0x20 || b[p] === 0x09 || b[p] === 0x0c || b[p] === 0x00))
|
|
494
|
+
p++;
|
|
495
|
+
if (p >= b.length && !this.eof)
|
|
496
|
+
throw new NeedMoreData();
|
|
497
|
+
if (b[p] === 0x0d || b[p] === 0x0a)
|
|
498
|
+
this.pos = p;
|
|
499
|
+
else
|
|
500
|
+
this.hooks.onBadToken?.('stream');
|
|
501
|
+
if (this.pos >= b.length)
|
|
502
|
+
this.more();
|
|
503
|
+
if (b[this.pos] === 0x0d) {
|
|
504
|
+
this.pos++;
|
|
505
|
+
if (this.pos >= b.length && !this.eof)
|
|
506
|
+
throw new NeedMoreData();
|
|
507
|
+
if (b[this.pos] === 0x0a)
|
|
508
|
+
this.pos++;
|
|
509
|
+
}
|
|
510
|
+
else if (b[this.pos] === 0x0a)
|
|
511
|
+
this.pos++;
|
|
512
|
+
return this.pos;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
exports.Parser = Parser;
|
|
516
|
+
function parseNumber(tok) {
|
|
517
|
+
if (!NUMBER.test(tok)) {
|
|
518
|
+
// A malformed number such as "--5" or "1.2.3" reads as its leading numeric part, and a lone "." as 0, as pdf.js
|
|
519
|
+
// reads it.
|
|
520
|
+
const match = /^[+-]*(\d*\.?\d*)/.exec(tok);
|
|
521
|
+
if (!match)
|
|
522
|
+
throw new Error(`No numeric prefix pattern match for "${tok}"`);
|
|
523
|
+
const lead = match[1];
|
|
524
|
+
const v = Number(lead === '.' ? '' : lead);
|
|
525
|
+
if (!Number.isFinite(v))
|
|
526
|
+
throw new ParseError(`Bad number "${tok}"`);
|
|
527
|
+
return tok.startsWith('-') ? -v : v;
|
|
528
|
+
}
|
|
529
|
+
return Number(tok);
|
|
530
|
+
}
|
|
531
|
+
/** Parses a complete object from a buffer that holds all of it. */
|
|
532
|
+
function parseObjectFrom(buf, pos = 0, hooks) {
|
|
533
|
+
return new Parser(buf, pos, true, hooks).parseObject();
|
|
534
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ContainedFilePlugin, PdfOptions } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Keeps contained files unchanged by type or by name. An entry with a "/" is a MIME type. Any other entry, such as
|
|
4
|
+
* ".csv" or "csv", is a file name extension.
|
|
5
|
+
*/
|
|
6
|
+
export declare function passThrough(types: string[]): ContainedFilePlugin;
|
|
7
|
+
/**
|
|
8
|
+
* Defuses PDFs inside other files, with `options` when given. Without them, it takes the options of the pdf-defuse
|
|
9
|
+
* run that found the file, or none when another package found it. `limits.nestingDepth` applies to the depth in the
|
|
10
|
+
* context. The PDF's findings go into the containing file's report, and its output's findings into the output's
|
|
11
|
+
* report, each with its `weight`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function pdfPlugin(options?: PdfOptions): ContainedFilePlugin;
|
package/dist/plugins.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.passThrough = passThrough;
|
|
4
|
+
exports.pdfPlugin = pdfPlugin;
|
|
5
|
+
const engine_1 = require("./engine");
|
|
6
|
+
const score_1 = require("./score");
|
|
7
|
+
const sniff_1 = require("./sniff");
|
|
8
|
+
const walker_1 = require("./walker");
|
|
9
|
+
/**
|
|
10
|
+
* Keeps contained files unchanged by type or by name. An entry with a "/" is a MIME type. Any other entry, such as
|
|
11
|
+
* ".csv" or "csv", is a file name extension.
|
|
12
|
+
*/
|
|
13
|
+
function passThrough(types) {
|
|
14
|
+
const set = new Set(types.filter(t => t.includes('/')).map(t => t.toLowerCase()));
|
|
15
|
+
const exts = new Set(types.filter(t => !t.includes('/')).map(t => t.trim().toLowerCase().replace(/^\./, '')));
|
|
16
|
+
return {
|
|
17
|
+
kind: 'file',
|
|
18
|
+
name: `passThrough(${types.join(',')})`,
|
|
19
|
+
// Matches on type alone. Whether the content, the declared type and the name agree is the type check's finding.
|
|
20
|
+
accepts: f => {
|
|
21
|
+
const ext = (0, sniff_1.extensionOf)(f.name);
|
|
22
|
+
if (ext !== undefined && exts.has(ext))
|
|
23
|
+
return true;
|
|
24
|
+
return [f.sniffedType, f.declaredType, (0, sniff_1.typeFromName)(f.name)].some(t => t !== undefined && set.has(t.toLowerCase().split(';')[0].trim()));
|
|
25
|
+
},
|
|
26
|
+
process: async () => 'passed',
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Defuses PDFs inside other files, with `options` when given. Without them, it takes the options of the pdf-defuse
|
|
31
|
+
* run that found the file, or none when another package found it. `limits.nestingDepth` applies to the depth in the
|
|
32
|
+
* context. The PDF's findings go into the containing file's report, and its output's findings into the output's
|
|
33
|
+
* report, each with its `weight`.
|
|
34
|
+
*/
|
|
35
|
+
function pdfPlugin(options) {
|
|
36
|
+
return {
|
|
37
|
+
kind: 'file',
|
|
38
|
+
name: 'pdf',
|
|
39
|
+
// It can only read PDF bytes, so the content decides, whatever the name says.
|
|
40
|
+
accepts: f => f.sniffedType === 'application/pdf',
|
|
41
|
+
async process(file, sink, context) {
|
|
42
|
+
const run = options ?? context[walker_1.RUN_OPTIONS] ?? {};
|
|
43
|
+
const r = await (0, engine_1.disarmAtDepth)(file.source, sink, run, context.depth, context.deadline);
|
|
44
|
+
// A container in another package scores these by the weights this run used.
|
|
45
|
+
for (const f of [...r.before.findings, ...(r.after?.findings ?? [])])
|
|
46
|
+
f.weight = (0, score_1.weightOf)(f, run);
|
|
47
|
+
const findings = r.before.findings;
|
|
48
|
+
// The sink holds the output, a rewrite unless a signature kept the bytes the file arrived with.
|
|
49
|
+
if (r.status === 'clean')
|
|
50
|
+
return { result: 'passed', findings, outputFindings: r.after?.findings };
|
|
51
|
+
if (r.status === 'defused')
|
|
52
|
+
return { result: 'scrubbed', findings, outputFindings: r.after?.findings };
|
|
53
|
+
return { result: 'removed', findings };
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
package/dist/score.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { PdfCategory as C, PdfDetail as D, type DefuseFinding, type PdfOptions, PdfRisk, type PdfScoreBands } from './types';
|
|
2
|
+
export declare const DEFAULT_BANDS: PdfScoreBands;
|
|
3
|
+
interface Combination {
|
|
4
|
+
name: string;
|
|
5
|
+
weight: number;
|
|
6
|
+
test(has: (c: C, d?: D) => boolean): boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare const COMBINATIONS: Combination[];
|
|
9
|
+
/** The caller's weights first, then the built-in table. A kind from another package scores at the weight it carries. */
|
|
10
|
+
export declare function weightOf(f: DefuseFinding, options: PdfOptions): number;
|
|
11
|
+
/** Highest weight in full, a quarter of each other kind's weight, plus combinations, capped at 100. */
|
|
12
|
+
export declare function scoreFindings(findings: DefuseFinding[], options: PdfOptions): {
|
|
13
|
+
score: number;
|
|
14
|
+
risk: PdfRisk;
|
|
15
|
+
};
|
|
16
|
+
export declare function riskFor(score: number, bands: PdfScoreBands): PdfRisk;
|
|
17
|
+
export {};
|