@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
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.scanDelimited = scanDelimited;
|
|
4
|
+
exports.csvPlugin = csvPlugin;
|
|
5
|
+
exports.tsvPlugin = tsvPlugin;
|
|
6
|
+
exports.isJson = isJson;
|
|
7
|
+
exports.jsonPlugin = jsonPlugin;
|
|
8
|
+
const document_1 = require("./document");
|
|
9
|
+
const sniff_1 = require("./sniff");
|
|
10
|
+
const CHUNK = 65536;
|
|
11
|
+
const TAB = 0x09;
|
|
12
|
+
const LF = 0x0a;
|
|
13
|
+
const CR = 0x0d;
|
|
14
|
+
const QUOTE = 0x22;
|
|
15
|
+
const APOSTROPHE = 0x27;
|
|
16
|
+
const BOM = [0xef, 0xbb, 0xbf];
|
|
17
|
+
/** A cell that starts with a sign is left alone when it is a plain number. */
|
|
18
|
+
const SIGNED_NUMBER = /^[+-](?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
19
|
+
const NUMBER_BYTE = /[0-9.eE+-]/;
|
|
20
|
+
/**
|
|
21
|
+
* Fails on bytes that are not UTF-8 text: invalid sequences, and C0 control characters other than tab, LF and CR.
|
|
22
|
+
* With `c1`, also DEL and the C1 controls U+0080 to U+009F, which a terminal showing the file can take as commands.
|
|
23
|
+
*/
|
|
24
|
+
class TextCheck {
|
|
25
|
+
c1;
|
|
26
|
+
decoder = new TextDecoder('utf-8', { fatal: true });
|
|
27
|
+
ok = true;
|
|
28
|
+
constructor(c1) {
|
|
29
|
+
this.c1 = c1;
|
|
30
|
+
}
|
|
31
|
+
update(b) {
|
|
32
|
+
for (let i = 0; i < b.length && this.ok; i++)
|
|
33
|
+
if (b[i] < 0x20 && b[i] !== TAB && b[i] !== LF && b[i] !== CR)
|
|
34
|
+
this.ok = false;
|
|
35
|
+
this.decode(b, true);
|
|
36
|
+
return this.ok;
|
|
37
|
+
}
|
|
38
|
+
end() {
|
|
39
|
+
this.decode(new Uint8Array(0), false);
|
|
40
|
+
return this.ok;
|
|
41
|
+
}
|
|
42
|
+
decode(b, stream) {
|
|
43
|
+
if (!this.ok)
|
|
44
|
+
return;
|
|
45
|
+
try {
|
|
46
|
+
const text = this.decoder.decode(b, { stream });
|
|
47
|
+
// Decoded text, so a character split across two chunks is checked whole.
|
|
48
|
+
for (let i = 0; this.c1 && this.ok && i < text.length; i++)
|
|
49
|
+
if (text.charCodeAt(i) >= 0x7f && text.charCodeAt(i) <= 0x9f)
|
|
50
|
+
this.ok = false;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
this.ok = false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Reads a file in chunks. Past the run's deadline it throws the time limit, which stops the whole tree. */
|
|
58
|
+
async function* chunksOf(source, size, deadline) {
|
|
59
|
+
for (let pos = 0; pos < size; pos += CHUNK) {
|
|
60
|
+
if (deadline !== undefined && Date.now() > deadline)
|
|
61
|
+
throw new document_1.TimeLimitError('Time limit exceeded');
|
|
62
|
+
yield await source.read(pos, Math.min(CHUNK, size - pos));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Whether a plugin should take a contained file: its name carries one of `exts` or its declared type is one the
|
|
67
|
+
* plugin reads. Whether those agree with the content is the type check's finding. The plugin reads the content and
|
|
68
|
+
* removes a file that is not what it handles.
|
|
69
|
+
*/
|
|
70
|
+
function claims(file, exts, isType) {
|
|
71
|
+
const ext = (0, sniff_1.extensionOf)(file.name);
|
|
72
|
+
return (ext !== undefined && exts.includes(ext)) || isType((file.declaredType ?? '').toLowerCase().split(';')[0].trim());
|
|
73
|
+
}
|
|
74
|
+
// States of the delimited-text reader.
|
|
75
|
+
const FIELD_START = 0;
|
|
76
|
+
const BOM_1 = 1;
|
|
77
|
+
const BOM_2 = 2;
|
|
78
|
+
const UNQUOTED = 3;
|
|
79
|
+
const QUOTED_START = 4;
|
|
80
|
+
const QUOTED_START_QUOTE = 5;
|
|
81
|
+
const QUOTED = 6;
|
|
82
|
+
const QUOTED_QUOTE = 7;
|
|
83
|
+
const AFTER_QUOTED = 8;
|
|
84
|
+
const SIGNED = 9;
|
|
85
|
+
const SIGNED_IN_QUOTES = 10;
|
|
86
|
+
const SIGNED_QUOTE = 11;
|
|
87
|
+
/**
|
|
88
|
+
* Reads CSV or TSV bytes and counts the cells a spreadsheet would run as a formula: those starting with =, +, -,
|
|
89
|
+
* @, tab or CR, except a plain number or a lone sign. Quoting follows RFC 4180, and a stray quote counts as text,
|
|
90
|
+
* as spreadsheets take it. With a sink, writes the file with an apostrophe before each such cell and every other
|
|
91
|
+
* byte unchanged. Returns the count, or -1 when the bytes are not UTF-8 text or a quoted cell never closes.
|
|
92
|
+
*/
|
|
93
|
+
async function scanDelimited(chunks, delimiter, sink) {
|
|
94
|
+
const text = new TextCheck(true);
|
|
95
|
+
let state = FIELD_START;
|
|
96
|
+
let atFileStart = true;
|
|
97
|
+
let signed = [];
|
|
98
|
+
let found = 0;
|
|
99
|
+
let out = new Uint8Array(0);
|
|
100
|
+
let o = 0;
|
|
101
|
+
const put = (c) => {
|
|
102
|
+
if (sink)
|
|
103
|
+
out[o++] = c;
|
|
104
|
+
};
|
|
105
|
+
// A cell starting with a sign waits in `signed` until its end shows whether it is a plain number.
|
|
106
|
+
const releaseSigned = (formula) => {
|
|
107
|
+
if (formula) {
|
|
108
|
+
found++;
|
|
109
|
+
put(APOSTROPHE);
|
|
110
|
+
}
|
|
111
|
+
for (const c of signed)
|
|
112
|
+
put(c);
|
|
113
|
+
signed = [];
|
|
114
|
+
};
|
|
115
|
+
const startCell = (c, quoted) => {
|
|
116
|
+
if (c === 0x2b || c === 0x2d) {
|
|
117
|
+
signed = [c];
|
|
118
|
+
return quoted ? SIGNED_IN_QUOTES : SIGNED;
|
|
119
|
+
}
|
|
120
|
+
if (c === 0x3d || c === 0x40 || c === TAB || c === CR) {
|
|
121
|
+
found++;
|
|
122
|
+
put(APOSTROPHE);
|
|
123
|
+
}
|
|
124
|
+
put(c);
|
|
125
|
+
return quoted ? QUOTED : UNQUOTED;
|
|
126
|
+
};
|
|
127
|
+
const isPlainNumber = () => signed.length === 1 || SIGNED_NUMBER.test(String.fromCharCode(...signed));
|
|
128
|
+
for await (const chunk of chunks) {
|
|
129
|
+
if (!text.update(chunk))
|
|
130
|
+
return -1;
|
|
131
|
+
if (sink)
|
|
132
|
+
out = new Uint8Array(chunk.length * 2 + signed.length + 8);
|
|
133
|
+
o = 0;
|
|
134
|
+
let i = 0;
|
|
135
|
+
while (i < chunk.length) {
|
|
136
|
+
const c = chunk[i];
|
|
137
|
+
const quotedSigned = state === SIGNED_IN_QUOTES;
|
|
138
|
+
switch (state) {
|
|
139
|
+
case FIELD_START:
|
|
140
|
+
if (atFileStart && c === BOM[0]) {
|
|
141
|
+
atFileStart = false;
|
|
142
|
+
put(c);
|
|
143
|
+
state = BOM_1;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
atFileStart = false;
|
|
147
|
+
if (c === delimiter || c === CR || c === LF)
|
|
148
|
+
put(c);
|
|
149
|
+
else if (c === QUOTE) {
|
|
150
|
+
put(c);
|
|
151
|
+
state = QUOTED_START;
|
|
152
|
+
}
|
|
153
|
+
else
|
|
154
|
+
state = startCell(c, false);
|
|
155
|
+
break;
|
|
156
|
+
case BOM_1:
|
|
157
|
+
case BOM_2:
|
|
158
|
+
if (c === BOM[state === BOM_1 ? 1 : 2]) {
|
|
159
|
+
put(c);
|
|
160
|
+
state = state === BOM_1 ? BOM_2 : FIELD_START;
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
state = UNQUOTED;
|
|
164
|
+
continue;
|
|
165
|
+
case UNQUOTED:
|
|
166
|
+
case AFTER_QUOTED:
|
|
167
|
+
put(c);
|
|
168
|
+
if (c === delimiter || c === CR || c === LF)
|
|
169
|
+
state = FIELD_START;
|
|
170
|
+
break;
|
|
171
|
+
case QUOTED_START:
|
|
172
|
+
if (c === QUOTE) {
|
|
173
|
+
put(c);
|
|
174
|
+
state = QUOTED_START_QUOTE;
|
|
175
|
+
}
|
|
176
|
+
else
|
|
177
|
+
state = startCell(c, true);
|
|
178
|
+
break;
|
|
179
|
+
case QUOTED_START_QUOTE:
|
|
180
|
+
if (c === QUOTE) {
|
|
181
|
+
// The cell starts with an escaped quote, which is text.
|
|
182
|
+
put(c);
|
|
183
|
+
state = QUOTED;
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
state = AFTER_QUOTED;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
break;
|
|
190
|
+
case QUOTED:
|
|
191
|
+
put(c);
|
|
192
|
+
if (c === QUOTE)
|
|
193
|
+
state = QUOTED_QUOTE;
|
|
194
|
+
break;
|
|
195
|
+
case QUOTED_QUOTE:
|
|
196
|
+
if (c === QUOTE) {
|
|
197
|
+
put(c);
|
|
198
|
+
state = QUOTED;
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
state = AFTER_QUOTED;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
break;
|
|
205
|
+
case SIGNED:
|
|
206
|
+
case SIGNED_IN_QUOTES:
|
|
207
|
+
if (quotedSigned ? c === QUOTE : c === delimiter || c === CR || c === LF) {
|
|
208
|
+
if (quotedSigned) {
|
|
209
|
+
state = SIGNED_QUOTE;
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
releaseSigned(!isPlainNumber());
|
|
213
|
+
state = UNQUOTED;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (NUMBER_BYTE.test(String.fromCharCode(c)) && signed.length < 64) {
|
|
217
|
+
signed.push(c);
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
releaseSigned(true);
|
|
221
|
+
state = quotedSigned ? QUOTED : UNQUOTED;
|
|
222
|
+
continue;
|
|
223
|
+
case SIGNED_QUOTE:
|
|
224
|
+
if (c === QUOTE) {
|
|
225
|
+
// An escaped quote inside the cell: not a number.
|
|
226
|
+
releaseSigned(true);
|
|
227
|
+
put(QUOTE);
|
|
228
|
+
put(QUOTE);
|
|
229
|
+
state = QUOTED;
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
releaseSigned(!isPlainNumber());
|
|
233
|
+
put(QUOTE);
|
|
234
|
+
state = AFTER_QUOTED;
|
|
235
|
+
continue;
|
|
236
|
+
default:
|
|
237
|
+
throw new Error(`Delimited-text reader in unknown state ${state}`);
|
|
238
|
+
}
|
|
239
|
+
i++;
|
|
240
|
+
}
|
|
241
|
+
if (sink && o)
|
|
242
|
+
await sink.write(out.subarray(0, o));
|
|
243
|
+
}
|
|
244
|
+
if (!text.end())
|
|
245
|
+
return -1;
|
|
246
|
+
if (state === QUOTED_START || state === QUOTED || state === SIGNED_IN_QUOTES)
|
|
247
|
+
return -1;
|
|
248
|
+
o = 0;
|
|
249
|
+
if (sink)
|
|
250
|
+
out = new Uint8Array(signed.length + 2);
|
|
251
|
+
if (state === SIGNED)
|
|
252
|
+
releaseSigned(!isPlainNumber());
|
|
253
|
+
else if (state === SIGNED_QUOTE) {
|
|
254
|
+
releaseSigned(!isPlainNumber());
|
|
255
|
+
put(QUOTE);
|
|
256
|
+
}
|
|
257
|
+
if (sink && o)
|
|
258
|
+
await sink.write(out.subarray(0, o));
|
|
259
|
+
return found;
|
|
260
|
+
}
|
|
261
|
+
function delimitedPlugin(name, delimiter, exts, types, options) {
|
|
262
|
+
return {
|
|
263
|
+
kind: 'file',
|
|
264
|
+
name,
|
|
265
|
+
accepts: f => claims(f, exts, t => types.includes(t)),
|
|
266
|
+
async process(file, sink, context) {
|
|
267
|
+
const mode = options.formulas ?? 'escape';
|
|
268
|
+
const n = await scanDelimited(chunksOf(file.source, file.size, context.deadline), delimiter, mode === 'escape' ? sink : undefined);
|
|
269
|
+
if (n < 0 || (n > 0 && mode === 'remove'))
|
|
270
|
+
return 'removed';
|
|
271
|
+
return n > 0 && mode === 'escape' ? 'scrubbed' : 'passed';
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
/** Keeps attached CSV files that are UTF-8 text, and escapes cells a spreadsheet would run as formulas. */
|
|
276
|
+
function csvPlugin(options = {}) {
|
|
277
|
+
const types = ['text/csv', 'application/csv', 'text/comma-separated-values', 'text/x-csv', 'application/x-csv', 'text/x-comma-separated-values'];
|
|
278
|
+
return delimitedPlugin('csv', 0x2c, ['csv'], types, options);
|
|
279
|
+
}
|
|
280
|
+
/** The same for tab-separated files. */
|
|
281
|
+
function tsvPlugin(options = {}) {
|
|
282
|
+
return delimitedPlugin('tsv', TAB, ['tsv', 'tab'], ['text/tab-separated-values', 'text/tsv', 'text/x-tsv'], options);
|
|
283
|
+
}
|
|
284
|
+
// JSON grammar states: what may come next.
|
|
285
|
+
const J_VALUE = 0;
|
|
286
|
+
const J_VALUE_OR_CLOSE = 1;
|
|
287
|
+
const J_KEY_OR_CLOSE = 2;
|
|
288
|
+
const J_KEY = 3;
|
|
289
|
+
const J_COLON = 4;
|
|
290
|
+
const J_COMMA_OR_CLOSE = 5;
|
|
291
|
+
const J_DONE = 6;
|
|
292
|
+
// JSON token states.
|
|
293
|
+
const T_NONE = 0;
|
|
294
|
+
const T_STRING = 1;
|
|
295
|
+
const T_ESCAPE = 2;
|
|
296
|
+
const T_HEX = 3;
|
|
297
|
+
const T_NUMBER = 4;
|
|
298
|
+
const T_LITERAL = 5;
|
|
299
|
+
// Number states, named for what was read last.
|
|
300
|
+
const N_MINUS = 0;
|
|
301
|
+
const N_ZERO = 1;
|
|
302
|
+
const N_INT = 2;
|
|
303
|
+
const N_DOT = 3;
|
|
304
|
+
const N_FRAC = 4;
|
|
305
|
+
const N_E = 5;
|
|
306
|
+
const N_E_SIGN = 6;
|
|
307
|
+
const N_EXP = 7;
|
|
308
|
+
const isDigit = (c) => c >= 0x30 && c <= 0x39;
|
|
309
|
+
/**
|
|
310
|
+
* True when the bytes are exactly one JSON value (RFC 8259) in UTF-8, after an optional byte order mark. Reads in
|
|
311
|
+
* chunks, keeping one byte per open container, so memory does not grow with the file.
|
|
312
|
+
*/
|
|
313
|
+
async function isJson(chunks) {
|
|
314
|
+
const text = new TextCheck(false);
|
|
315
|
+
let stack = new Uint8Array(64);
|
|
316
|
+
let depth = 0;
|
|
317
|
+
let grammar = J_VALUE;
|
|
318
|
+
let token = T_NONE;
|
|
319
|
+
let num = N_MINUS;
|
|
320
|
+
let hexLeft = 0;
|
|
321
|
+
let literal = '';
|
|
322
|
+
let litPos = 0;
|
|
323
|
+
let inKey = false;
|
|
324
|
+
let bomPos = 0;
|
|
325
|
+
const valueDone = () => {
|
|
326
|
+
grammar = inKey ? J_COLON : depth === 0 ? J_DONE : J_COMMA_OR_CLOSE;
|
|
327
|
+
inKey = false;
|
|
328
|
+
};
|
|
329
|
+
const open = (kind) => {
|
|
330
|
+
if (depth === stack.length) {
|
|
331
|
+
const bigger = new Uint8Array(stack.length * 2);
|
|
332
|
+
bigger.set(stack);
|
|
333
|
+
stack = bigger;
|
|
334
|
+
}
|
|
335
|
+
stack[depth++] = kind;
|
|
336
|
+
grammar = kind === 0x7b ? J_KEY_OR_CLOSE : J_VALUE_OR_CLOSE;
|
|
337
|
+
};
|
|
338
|
+
const close = (kind) => {
|
|
339
|
+
if (depth === 0 || stack[depth - 1] !== kind)
|
|
340
|
+
return false;
|
|
341
|
+
depth--;
|
|
342
|
+
valueDone();
|
|
343
|
+
return true;
|
|
344
|
+
};
|
|
345
|
+
const startValue = (c) => {
|
|
346
|
+
if (c === 0x7b || c === 0x5b)
|
|
347
|
+
open(c);
|
|
348
|
+
else if (c === QUOTE)
|
|
349
|
+
token = T_STRING;
|
|
350
|
+
else if (c === 0x2d || isDigit(c)) {
|
|
351
|
+
token = T_NUMBER;
|
|
352
|
+
num = c === 0x2d ? N_MINUS : c === 0x30 ? N_ZERO : N_INT;
|
|
353
|
+
}
|
|
354
|
+
else if (c === 0x74 || c === 0x66 || c === 0x6e) {
|
|
355
|
+
token = T_LITERAL;
|
|
356
|
+
literal = c === 0x74 ? 'true' : c === 0x66 ? 'false' : 'null';
|
|
357
|
+
litPos = 1;
|
|
358
|
+
}
|
|
359
|
+
else
|
|
360
|
+
return false;
|
|
361
|
+
return true;
|
|
362
|
+
};
|
|
363
|
+
for await (const chunk of chunks) {
|
|
364
|
+
if (!text.update(chunk))
|
|
365
|
+
return false;
|
|
366
|
+
let i = 0;
|
|
367
|
+
while (i < chunk.length) {
|
|
368
|
+
const c = chunk[i];
|
|
369
|
+
if (bomPos < 3) {
|
|
370
|
+
if (c === BOM[bomPos]) {
|
|
371
|
+
bomPos++;
|
|
372
|
+
i++;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (bomPos > 0)
|
|
376
|
+
return false;
|
|
377
|
+
bomPos = 3;
|
|
378
|
+
}
|
|
379
|
+
switch (token) {
|
|
380
|
+
case T_STRING:
|
|
381
|
+
if (c === QUOTE) {
|
|
382
|
+
token = T_NONE;
|
|
383
|
+
valueDone();
|
|
384
|
+
}
|
|
385
|
+
else if (c === 0x5c)
|
|
386
|
+
token = T_ESCAPE;
|
|
387
|
+
else if (c < 0x20)
|
|
388
|
+
return false;
|
|
389
|
+
break;
|
|
390
|
+
case T_ESCAPE:
|
|
391
|
+
if (c === 0x75) {
|
|
392
|
+
token = T_HEX;
|
|
393
|
+
hexLeft = 4;
|
|
394
|
+
}
|
|
395
|
+
else if ('"\\/bfnrt'.includes(String.fromCharCode(c)))
|
|
396
|
+
token = T_STRING;
|
|
397
|
+
else
|
|
398
|
+
return false;
|
|
399
|
+
break;
|
|
400
|
+
case T_HEX:
|
|
401
|
+
if (!/[0-9a-fA-F]/.test(String.fromCharCode(c)))
|
|
402
|
+
return false;
|
|
403
|
+
if (--hexLeft === 0)
|
|
404
|
+
token = T_STRING;
|
|
405
|
+
break;
|
|
406
|
+
case T_LITERAL:
|
|
407
|
+
if (c !== literal.charCodeAt(litPos++))
|
|
408
|
+
return false;
|
|
409
|
+
if (litPos === literal.length) {
|
|
410
|
+
token = T_NONE;
|
|
411
|
+
valueDone();
|
|
412
|
+
}
|
|
413
|
+
break;
|
|
414
|
+
case T_NUMBER: {
|
|
415
|
+
const next = num === N_MINUS
|
|
416
|
+
? c === 0x30
|
|
417
|
+
? N_ZERO
|
|
418
|
+
: isDigit(c)
|
|
419
|
+
? N_INT
|
|
420
|
+
: -1
|
|
421
|
+
: num === N_ZERO
|
|
422
|
+
? c === 0x2e
|
|
423
|
+
? N_DOT
|
|
424
|
+
: c === 0x65 || c === 0x45
|
|
425
|
+
? N_E
|
|
426
|
+
: -2
|
|
427
|
+
: num === N_INT
|
|
428
|
+
? isDigit(c)
|
|
429
|
+
? N_INT
|
|
430
|
+
: c === 0x2e
|
|
431
|
+
? N_DOT
|
|
432
|
+
: c === 0x65 || c === 0x45
|
|
433
|
+
? N_E
|
|
434
|
+
: -2
|
|
435
|
+
: num === N_DOT
|
|
436
|
+
? isDigit(c)
|
|
437
|
+
? N_FRAC
|
|
438
|
+
: -1
|
|
439
|
+
: num === N_FRAC
|
|
440
|
+
? isDigit(c)
|
|
441
|
+
? N_FRAC
|
|
442
|
+
: c === 0x65 || c === 0x45
|
|
443
|
+
? N_E
|
|
444
|
+
: -2
|
|
445
|
+
: num === N_E
|
|
446
|
+
? c === 0x2b || c === 0x2d
|
|
447
|
+
? N_E_SIGN
|
|
448
|
+
: isDigit(c)
|
|
449
|
+
? N_EXP
|
|
450
|
+
: -1
|
|
451
|
+
: num === N_E_SIGN
|
|
452
|
+
? isDigit(c)
|
|
453
|
+
? N_EXP
|
|
454
|
+
: -1
|
|
455
|
+
: isDigit(c)
|
|
456
|
+
? N_EXP
|
|
457
|
+
: -2;
|
|
458
|
+
if (next === -1)
|
|
459
|
+
return false;
|
|
460
|
+
if (next >= 0) {
|
|
461
|
+
num = next;
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
// The number ended at a byte that belongs to what follows.
|
|
465
|
+
token = T_NONE;
|
|
466
|
+
valueDone();
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
default:
|
|
470
|
+
if (c === 0x20 || c === TAB || c === LF || c === CR)
|
|
471
|
+
break;
|
|
472
|
+
if (grammar === J_VALUE || grammar === J_VALUE_OR_CLOSE) {
|
|
473
|
+
if (grammar === J_VALUE_OR_CLOSE && c === 0x5d) {
|
|
474
|
+
close(0x5b);
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
if (!startValue(c))
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
else if (grammar === J_KEY_OR_CLOSE || grammar === J_KEY) {
|
|
481
|
+
if (grammar === J_KEY_OR_CLOSE && c === 0x7d) {
|
|
482
|
+
close(0x7b);
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
if (c !== QUOTE)
|
|
486
|
+
return false;
|
|
487
|
+
token = T_STRING;
|
|
488
|
+
inKey = true;
|
|
489
|
+
}
|
|
490
|
+
else if (grammar === J_COLON) {
|
|
491
|
+
if (c !== 0x3a)
|
|
492
|
+
return false;
|
|
493
|
+
grammar = J_VALUE;
|
|
494
|
+
}
|
|
495
|
+
else if (grammar === J_COMMA_OR_CLOSE) {
|
|
496
|
+
if (c === 0x2c)
|
|
497
|
+
grammar = stack[depth - 1] === 0x7b ? J_KEY : J_VALUE;
|
|
498
|
+
else if (!close(c === 0x7d ? 0x7b : c === 0x5d ? 0x5b : -1))
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
else
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
i++;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (!text.end())
|
|
508
|
+
return false;
|
|
509
|
+
// The input may end a number only in these states.
|
|
510
|
+
if (token === T_NUMBER && (num === N_ZERO || num === N_INT || num === N_FRAC || num === N_EXP)) {
|
|
511
|
+
token = T_NONE;
|
|
512
|
+
valueDone();
|
|
513
|
+
}
|
|
514
|
+
return token === T_NONE && grammar === J_DONE;
|
|
515
|
+
}
|
|
516
|
+
/** Keeps attached JSON files that parse, and removes the rest. */
|
|
517
|
+
function jsonPlugin() {
|
|
518
|
+
const types = ['application/json', 'text/json', 'text/x-json', 'application/x-json'];
|
|
519
|
+
return {
|
|
520
|
+
kind: 'file',
|
|
521
|
+
name: 'json',
|
|
522
|
+
accepts: f => claims(f, ['json'], t => types.includes(t) || t.endsWith('+json')),
|
|
523
|
+
process: async (file, _sink, context) => ((await isJson(chunksOf(file.source, file.size, context.deadline))) ? 'passed' : 'removed'),
|
|
524
|
+
};
|
|
525
|
+
}
|