@patrickdk77/pdf-defuse 0.2.3 → 0.3.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 +42 -7
- package/dist/engine.js +3 -3
- package/dist/findings.js +20 -7
- package/dist/types.d.ts +16 -1
- package/dist/types.js +8 -0
- package/dist/uri.d.ts +12 -0
- package/dist/uri.js +51 -1
- package/dist/walker.d.ts +32 -1
- package/dist/walker.js +387 -20
- package/dist/xfa.d.ts +113 -0
- package/dist/xfa.js +407 -0
- package/package.json +1 -1
package/dist/xfa.js
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Reads an XFA form's XML, the XDP, and plans what a defused copy keeps. The text is the packets' bytes read as
|
|
4
|
+
* latin1, so every offset is a byte offset into the concatenated packets. The reader is strict: anything it does not
|
|
5
|
+
* read exactly as a viewer would, such as a DOCTYPE or an unknown entity, fails the whole form instead of being
|
|
6
|
+
* guessed at.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.attrOf = exports.XfaReadError = void 0;
|
|
10
|
+
exports.decodeXmlText = decodeXmlText;
|
|
11
|
+
exports.escapeXmlText = escapeXmlText;
|
|
12
|
+
exports.readXml = readXml;
|
|
13
|
+
exports.textOf = textOf;
|
|
14
|
+
exports.isLocalHref = isLocalHref;
|
|
15
|
+
exports.planXfa = planXfa;
|
|
16
|
+
exports.applyEdits = applyEdits;
|
|
17
|
+
exports.splitEdits = splitEdits;
|
|
18
|
+
const uri_1 = require("./uri");
|
|
19
|
+
class XfaReadError extends Error {
|
|
20
|
+
}
|
|
21
|
+
exports.XfaReadError = XfaReadError;
|
|
22
|
+
const NAME_START = /[A-Za-z_:\x80-\xff]/;
|
|
23
|
+
const NAME_CHAR = /[A-Za-z0-9_.:\-\x80-\xff]/;
|
|
24
|
+
const PREDEFINED = { lt: '<', gt: '>', amp: '&', quot: '"', apos: "'" };
|
|
25
|
+
const localOf = (name) => name.slice(name.indexOf(':') + 1);
|
|
26
|
+
/** Decodes entity and character references in latin1 text read from UTF-8 bytes, then the UTF-8. */
|
|
27
|
+
function decodeXmlText(raw) {
|
|
28
|
+
const utf8 = (s) => Buffer.from(s, 'latin1').toString('utf8');
|
|
29
|
+
if (!raw.includes('&'))
|
|
30
|
+
return utf8(raw);
|
|
31
|
+
let out = '';
|
|
32
|
+
let i = 0;
|
|
33
|
+
for (;;) {
|
|
34
|
+
const amp = raw.indexOf('&', i);
|
|
35
|
+
if (amp < 0)
|
|
36
|
+
break;
|
|
37
|
+
out += utf8(raw.slice(i, amp));
|
|
38
|
+
const semi = raw.indexOf(';', amp);
|
|
39
|
+
if (semi < 0 || semi - amp > 12)
|
|
40
|
+
throw new XfaReadError('an entity reference that does not end');
|
|
41
|
+
const ref = raw.slice(amp + 1, semi);
|
|
42
|
+
if (ref.startsWith('#')) {
|
|
43
|
+
const code = ref[1] === 'x' ? Number.parseInt(ref.slice(2), 16) : Number.parseInt(ref.slice(1), 10);
|
|
44
|
+
if (!/^#(?:x[0-9a-fA-F]+|[0-9]+)$/.test(ref) || !Number.isFinite(code) || code > 0x10ffff || code === 0)
|
|
45
|
+
throw new XfaReadError(`a bad character reference &${ref};`);
|
|
46
|
+
out += String.fromCodePoint(code);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
const v = PREDEFINED[ref];
|
|
50
|
+
if (v === undefined)
|
|
51
|
+
throw new XfaReadError(`an undeclared entity &${ref};`);
|
|
52
|
+
out += v;
|
|
53
|
+
}
|
|
54
|
+
i = semi + 1;
|
|
55
|
+
}
|
|
56
|
+
return out + utf8(raw.slice(i));
|
|
57
|
+
}
|
|
58
|
+
/** Escapes text to go back between tags. A carriage return is written as a reference, so a reader keeps it. */
|
|
59
|
+
function escapeXmlText(s) {
|
|
60
|
+
return s.replace(/[&<>\r]/g, c => (c === '&' ? '&' : c === '<' ? '<' : c === '>' ? '>' : ' '));
|
|
61
|
+
}
|
|
62
|
+
/** Reads the XDP. It has one root element, and nothing but comments, processing instructions and space around it. */
|
|
63
|
+
function readXml(s) {
|
|
64
|
+
let i = 0;
|
|
65
|
+
const n = s.length;
|
|
66
|
+
if (s.startsWith('\xef\xbb\xbf'))
|
|
67
|
+
i = 3;
|
|
68
|
+
const stack = [];
|
|
69
|
+
let root;
|
|
70
|
+
const fail = (what) => {
|
|
71
|
+
throw new XfaReadError(`${what} at byte ${i}`);
|
|
72
|
+
};
|
|
73
|
+
const readName = () => {
|
|
74
|
+
const at = i;
|
|
75
|
+
if (!NAME_START.test(s[i] ?? ''))
|
|
76
|
+
fail('a bad name');
|
|
77
|
+
i++;
|
|
78
|
+
while (i < n && NAME_CHAR.test(s[i]))
|
|
79
|
+
i++;
|
|
80
|
+
return s.slice(at, i);
|
|
81
|
+
};
|
|
82
|
+
const isSpace = (c) => c === ' ' || c === '\t' || c === '\n' || c === '\r';
|
|
83
|
+
while (i < n) {
|
|
84
|
+
const c = s[i];
|
|
85
|
+
if (c === '\0')
|
|
86
|
+
fail('a NUL byte');
|
|
87
|
+
if (c !== '<') {
|
|
88
|
+
const lt = s.indexOf('<', i);
|
|
89
|
+
const end = lt < 0 ? n : lt;
|
|
90
|
+
const top = stack[stack.length - 1];
|
|
91
|
+
const chunk = s.slice(i, end);
|
|
92
|
+
if (chunk.includes('\0'))
|
|
93
|
+
fail('a NUL byte');
|
|
94
|
+
if (!top) {
|
|
95
|
+
if (chunk.trim())
|
|
96
|
+
fail('text outside the root element');
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
// References are checked now, so a bad one fails the read instead of a later decode.
|
|
100
|
+
if (chunk.includes('&'))
|
|
101
|
+
decodeXmlText(chunk);
|
|
102
|
+
top.texts.push({ start: i, end, cdata: false });
|
|
103
|
+
}
|
|
104
|
+
i = end;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (s.startsWith('<?', i)) {
|
|
108
|
+
const close = s.indexOf('?>', i + 2);
|
|
109
|
+
if (close < 0)
|
|
110
|
+
fail('a processing instruction that does not end');
|
|
111
|
+
const body = s.slice(i + 2, close);
|
|
112
|
+
if (/^xml[\s?]/i.test(`${body} `)) {
|
|
113
|
+
const enc = /\bencoding\s*=\s*["']([^"']*)["']/.exec(body)?.[1];
|
|
114
|
+
if (enc !== undefined && !/^utf-?8$/i.test(enc))
|
|
115
|
+
fail(`an encoding other than UTF-8 (${enc.slice(0, 20)})`);
|
|
116
|
+
}
|
|
117
|
+
i = close + 2;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (s.startsWith('<!--', i)) {
|
|
121
|
+
const close = s.indexOf('-->', i + 4);
|
|
122
|
+
if (close < 0)
|
|
123
|
+
fail('a comment that does not end');
|
|
124
|
+
i = close + 3;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (s.startsWith('<![CDATA[', i)) {
|
|
128
|
+
const top = stack[stack.length - 1];
|
|
129
|
+
if (!top)
|
|
130
|
+
fail('CDATA outside the root element');
|
|
131
|
+
const close = s.indexOf(']]>', i + 9);
|
|
132
|
+
if (close < 0)
|
|
133
|
+
fail('CDATA that does not end');
|
|
134
|
+
top?.texts.push({ start: i + 9, end: close, cdata: true });
|
|
135
|
+
i = close + 3;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (s.startsWith('<!', i))
|
|
139
|
+
fail('a DOCTYPE or declaration');
|
|
140
|
+
if (s.startsWith('</', i)) {
|
|
141
|
+
const at = i;
|
|
142
|
+
i += 2;
|
|
143
|
+
const name = readName();
|
|
144
|
+
while (isSpace(s[i]))
|
|
145
|
+
i++;
|
|
146
|
+
if (s[i] !== '>')
|
|
147
|
+
fail('a bad end tag');
|
|
148
|
+
i++;
|
|
149
|
+
const top = stack.pop();
|
|
150
|
+
if (!top || top.name !== name)
|
|
151
|
+
fail(`an end tag </${name}> that closes nothing open`);
|
|
152
|
+
if (top) {
|
|
153
|
+
top.closeStart = at;
|
|
154
|
+
top.end = i;
|
|
155
|
+
}
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
// A start tag.
|
|
159
|
+
const start = i;
|
|
160
|
+
i++;
|
|
161
|
+
const name = readName();
|
|
162
|
+
const el = { name, local: localOf(name), attrs: [], start, openEnd: 0, closeStart: 0, end: 0, children: [], texts: [], parent: stack[stack.length - 1] };
|
|
163
|
+
const seen = new Set();
|
|
164
|
+
for (;;) {
|
|
165
|
+
const wsAt = i;
|
|
166
|
+
while (isSpace(s[i]))
|
|
167
|
+
i++;
|
|
168
|
+
if (s[i] === '>' || s.startsWith('/>', i))
|
|
169
|
+
break;
|
|
170
|
+
if (i === wsAt)
|
|
171
|
+
fail('attributes without space between them');
|
|
172
|
+
const aname = readName();
|
|
173
|
+
while (isSpace(s[i]))
|
|
174
|
+
i++;
|
|
175
|
+
if (s[i] !== '=')
|
|
176
|
+
fail('an attribute without a value');
|
|
177
|
+
i++;
|
|
178
|
+
while (isSpace(s[i]))
|
|
179
|
+
i++;
|
|
180
|
+
const q = s[i];
|
|
181
|
+
if (q !== '"' && q !== "'")
|
|
182
|
+
fail('an unquoted attribute value');
|
|
183
|
+
const close = s.indexOf(q, i + 1);
|
|
184
|
+
if (close < 0)
|
|
185
|
+
fail('an attribute value that does not end');
|
|
186
|
+
const raw = s.slice(i + 1, close);
|
|
187
|
+
if (raw.includes('<') || raw.includes('\0'))
|
|
188
|
+
fail('a < in an attribute value');
|
|
189
|
+
if (seen.has(aname))
|
|
190
|
+
fail(`a repeated attribute ${aname}`);
|
|
191
|
+
seen.add(aname);
|
|
192
|
+
// Attribute values have their whitespace normalized to spaces, as a reader does.
|
|
193
|
+
el.attrs.push({ name: aname, local: localOf(aname), value: decodeXmlText(raw).replace(/[\t\n\r]/g, ' '), start: wsAt, end: close + 1 });
|
|
194
|
+
i = close + 1;
|
|
195
|
+
}
|
|
196
|
+
const empty = s[i] === '/';
|
|
197
|
+
i += empty ? 2 : 1;
|
|
198
|
+
el.openEnd = i;
|
|
199
|
+
const parent = stack[stack.length - 1];
|
|
200
|
+
if (parent)
|
|
201
|
+
parent.children.push(el);
|
|
202
|
+
else if (root)
|
|
203
|
+
fail('a second root element');
|
|
204
|
+
else
|
|
205
|
+
root = el;
|
|
206
|
+
if (empty) {
|
|
207
|
+
el.closeStart = i;
|
|
208
|
+
el.end = i;
|
|
209
|
+
}
|
|
210
|
+
else
|
|
211
|
+
stack.push(el);
|
|
212
|
+
}
|
|
213
|
+
if (stack.length)
|
|
214
|
+
throw new XfaReadError(`<${stack[stack.length - 1].name}> is never closed`);
|
|
215
|
+
if (!root)
|
|
216
|
+
throw new XfaReadError('no root element');
|
|
217
|
+
return root;
|
|
218
|
+
}
|
|
219
|
+
/** The text directly inside an element, decoded, with line ends as a reader gives them. */
|
|
220
|
+
function textOf(s, el) {
|
|
221
|
+
let out = '';
|
|
222
|
+
for (const t of el.texts) {
|
|
223
|
+
// Line ends are normalized before references are read, so a written stays a carriage return.
|
|
224
|
+
const raw = s.slice(t.start, t.end).replace(/\r\n?/g, '\n');
|
|
225
|
+
out += t.cdata ? Buffer.from(raw, 'latin1').toString('utf8') : decodeXmlText(raw);
|
|
226
|
+
}
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
const attrOf = (el, local) => el.attrs.find(a => a.local === local);
|
|
230
|
+
exports.attrOf = attrOf;
|
|
231
|
+
/** Packets a defused form keeps. The rest, such as stylesheets, embedded PDFs, XFDF and signatures, go. */
|
|
232
|
+
const KEPT_PACKETS = new Set(['template', 'datasets', 'config', 'localeSet', 'connectionSet', 'form', 'xmpmeta']);
|
|
233
|
+
/** Packets whose `<script>` elements are code. The config packet's `<script>` holds settings. */
|
|
234
|
+
const CODE_PACKETS = new Set(['template', 'form']);
|
|
235
|
+
/** Template and form elements that reach outside the form when an event fires, and go whole. */
|
|
236
|
+
const ACTIONS = new Map([
|
|
237
|
+
['submit', 'submit'],
|
|
238
|
+
['execute', 'execute'],
|
|
239
|
+
['exObject', 'exObject'],
|
|
240
|
+
['connect', 'connection'],
|
|
241
|
+
]);
|
|
242
|
+
/** Config elements that name a file, a URL or a transform. */
|
|
243
|
+
const CONFIG_REFERENCES = new Set(['uri', 'submitUrl', 'xsl', 'outputXSL', 'base']);
|
|
244
|
+
/** Template attributes that name a server, apart from href and usehref, which have rules of their own. */
|
|
245
|
+
const SERVER_ATTRIBUTES = new Set(['url', 'server']);
|
|
246
|
+
/** Elements whose name joins the SOM path. */
|
|
247
|
+
const NAMED = new Set(['subform', 'subformSet', 'field', 'exclGroup', 'draw', 'area', 'pageSet', 'pageArea', 'contentArea']);
|
|
248
|
+
/**
|
|
249
|
+
* Whether an image or rich text href is a relative name, which stays unless it is written to read as another path.
|
|
250
|
+
* An address with a scheme, a drive letter or a leading slash or backslash points outside the PDF.
|
|
251
|
+
*/
|
|
252
|
+
function isLocalHref(v) {
|
|
253
|
+
const t = v.trim();
|
|
254
|
+
if (!t)
|
|
255
|
+
return true;
|
|
256
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(t))
|
|
257
|
+
return false;
|
|
258
|
+
if (t.startsWith('/') || t.startsWith('\\'))
|
|
259
|
+
return false;
|
|
260
|
+
return (0, uri_1.pathTrick)(t, { backslash: 'separator' }) === undefined;
|
|
261
|
+
}
|
|
262
|
+
/** A prototype reference inside this form, `#id` or `.#som(...)`. A file or URL before the `#` is outside it. */
|
|
263
|
+
const isLocalUse = (v) => /^\.?#/.test(v.trim()) || !v.trim();
|
|
264
|
+
const somOf = (el) => {
|
|
265
|
+
const names = [];
|
|
266
|
+
for (let e = el; e; e = e.parent) {
|
|
267
|
+
const name = (0, exports.attrOf)(e, 'name')?.value;
|
|
268
|
+
if (name && (NAMED.has(e.local) || e === el))
|
|
269
|
+
names.unshift(name);
|
|
270
|
+
}
|
|
271
|
+
return names.join('.');
|
|
272
|
+
};
|
|
273
|
+
const activityOf = (el) => {
|
|
274
|
+
for (let e = el.parent; e; e = e.parent) {
|
|
275
|
+
if (e.local === 'event')
|
|
276
|
+
return (0, exports.attrOf)(e, 'activity')?.value || 'event';
|
|
277
|
+
if (e.local === 'calculate' || e.local === 'validate')
|
|
278
|
+
return e.local;
|
|
279
|
+
if (e.local === 'variables')
|
|
280
|
+
return 'script-object';
|
|
281
|
+
}
|
|
282
|
+
return 'unknown';
|
|
283
|
+
};
|
|
284
|
+
function planXfa(s, options = {}) {
|
|
285
|
+
const root = readXml(s);
|
|
286
|
+
if (root.local !== 'xdp')
|
|
287
|
+
throw new XfaReadError(`a root element <${root.name}> that is not an XDP`);
|
|
288
|
+
const plan = { root, scripts: [], removals: [], links: [], images: [] };
|
|
289
|
+
// An action is reported at the node it belongs to; a connection, which has a name of its own, at itself.
|
|
290
|
+
const remove = (el, kind, packet, what, own = false) => plan.removals.push({ start: el.start, end: el.end, kind, packet, som: somOf(own ? el : (el.parent ?? el)), what });
|
|
291
|
+
const cutAttr = (a, el, packet, what) => plan.removals.push({ start: a.start, end: a.end, kind: 'reference', packet, som: somOf(el), what });
|
|
292
|
+
const walk = (el, packet) => {
|
|
293
|
+
const code = CODE_PACKETS.has(packet);
|
|
294
|
+
if (code) {
|
|
295
|
+
const action = ACTIONS.get(el.local);
|
|
296
|
+
if (action) {
|
|
297
|
+
const target = (0, exports.attrOf)(el, 'target')?.value ?? (0, exports.attrOf)(el, 'connection')?.value ?? (0, exports.attrOf)(el, 'codeBase')?.value ?? (0, exports.attrOf)(el, 'classId')?.value ?? '';
|
|
298
|
+
remove(el, action, packet, target);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (packet === 'config' && CONFIG_REFERENCES.has(el.local) && (el.children.length || textOf(s, el).trim())) {
|
|
303
|
+
remove(el, 'reference', packet, `${el.local} ${textOf(s, el).trim().slice(0, 120)}`.trim());
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (packet === 'connectionSet') {
|
|
307
|
+
if (el.local.endsWith('Connection') && el.local !== 'xsdConnection') {
|
|
308
|
+
remove(el, 'connection', packet, `${el.local} ${(0, exports.attrOf)(el, 'name')?.value ?? ''}`.trim(), true);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (el.local === 'uri') {
|
|
312
|
+
remove(el, 'reference', packet, `uri ${textOf(s, el).trim().slice(0, 120)}`.trim());
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
for (const a of el.attrs) {
|
|
317
|
+
if (a.local === 'href' && a.value.trim()) {
|
|
318
|
+
if (el.local === 'a')
|
|
319
|
+
plan.links.push({ start: a.start, end: a.end, uri: a.value, packet, som: somOf(el) });
|
|
320
|
+
else if (!isLocalHref(a.value))
|
|
321
|
+
cutAttr(a, el, packet, `${el.local} href ${a.value.slice(0, 120)}`);
|
|
322
|
+
}
|
|
323
|
+
else if (a.local === 'usehref' && !isLocalUse(a.value))
|
|
324
|
+
cutAttr(a, el, packet, `${el.local} usehref ${a.value.slice(0, 120)}`);
|
|
325
|
+
else if (code && SERVER_ATTRIBUTES.has(a.local) && a.value.trim())
|
|
326
|
+
cutAttr(a, el, packet, `${el.local} ${a.local} ${a.value.slice(0, 120)}`);
|
|
327
|
+
}
|
|
328
|
+
// An image in the template or the form, or a data value the datasets give an image type.
|
|
329
|
+
const imageType = (0, exports.attrOf)(el, 'contentType')?.value.trim() ?? '';
|
|
330
|
+
if (((code && el.local === 'image') || (packet === 'datasets' && /^image\//i.test(imageType))) && !el.children.length) {
|
|
331
|
+
const data = textOf(s, el);
|
|
332
|
+
if (data.trim())
|
|
333
|
+
plan.images.push({
|
|
334
|
+
start: el.openEnd,
|
|
335
|
+
end: el.closeStart,
|
|
336
|
+
data,
|
|
337
|
+
contentType: imageType,
|
|
338
|
+
transferEncoding: ((0, exports.attrOf)(el, 'transferEncoding')?.value.trim() || 'base64').toLowerCase(),
|
|
339
|
+
packet,
|
|
340
|
+
som: somOf(el.parent ?? el),
|
|
341
|
+
});
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (code && el.local === 'script') {
|
|
345
|
+
const contentType = (0, exports.attrOf)(el, 'contentType')?.value.trim() ?? '';
|
|
346
|
+
const ct = contentType.toLowerCase();
|
|
347
|
+
const language = !ct || ct === 'application/x-formcalc' ? 'formcalc' : /javascript|ecmascript/.test(ct) ? 'javascript' : undefined;
|
|
348
|
+
const text = el.children.length ? '' : textOf(s, el);
|
|
349
|
+
if (el.children.length || text.trim())
|
|
350
|
+
plan.scripts.push({
|
|
351
|
+
start: el.openEnd,
|
|
352
|
+
end: el.closeStart,
|
|
353
|
+
text,
|
|
354
|
+
language: el.children.length ? undefined : language,
|
|
355
|
+
contentType,
|
|
356
|
+
som: somOf(el),
|
|
357
|
+
activity: activityOf(el),
|
|
358
|
+
packet,
|
|
359
|
+
});
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
for (const c of el.children)
|
|
363
|
+
walk(c, packet);
|
|
364
|
+
};
|
|
365
|
+
for (const p of root.children) {
|
|
366
|
+
if (!KEPT_PACKETS.has(p.local) || (p.local === 'xmpmeta' && options.stripMetadata)) {
|
|
367
|
+
remove(p, 'packet', p.local, p.local);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
walk(p, p.local);
|
|
371
|
+
}
|
|
372
|
+
return plan;
|
|
373
|
+
}
|
|
374
|
+
/** Applies edits that do not overlap, after dropping any that falls inside another. */
|
|
375
|
+
function applyEdits(s, edits) {
|
|
376
|
+
const sorted = [...edits].sort((a, b) => a.start - b.start || b.end - a.end);
|
|
377
|
+
let out = '';
|
|
378
|
+
let at = 0;
|
|
379
|
+
for (const e of sorted) {
|
|
380
|
+
if (e.start < at)
|
|
381
|
+
continue;
|
|
382
|
+
out += s.slice(at, e.start) + e.insert;
|
|
383
|
+
at = e.end;
|
|
384
|
+
}
|
|
385
|
+
return out + s.slice(at);
|
|
386
|
+
}
|
|
387
|
+
/** Splits edits of the concatenation by packet stream. `bounds` holds each stream's start and, last, the end. */
|
|
388
|
+
function splitEdits(bounds, edits) {
|
|
389
|
+
const per = bounds.slice(1).map(() => []);
|
|
390
|
+
const sorted = [...edits].sort((a, b) => a.start - b.start || b.end - a.end);
|
|
391
|
+
let at = 0;
|
|
392
|
+
for (const e of sorted) {
|
|
393
|
+
if (e.start < at)
|
|
394
|
+
continue;
|
|
395
|
+
at = e.end;
|
|
396
|
+
for (let k = 0; k < per.length; k++) {
|
|
397
|
+
const lo = bounds[k];
|
|
398
|
+
const hi = bounds[k + 1];
|
|
399
|
+
if (e.end <= lo || e.start >= hi)
|
|
400
|
+
continue;
|
|
401
|
+
// A cut that crosses packets cuts its part of each one. The text it puts in goes to the first.
|
|
402
|
+
const start = Math.max(e.start, lo);
|
|
403
|
+
per[k].push({ start: start - lo, end: Math.min(e.end, hi) - lo, insert: start === e.start ? e.insert : '' });
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return per;
|
|
407
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patrickdk77/pdf-defuse",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Sanitize and clean PDF files. Detects and removes JavaScript, attached files, launch actions, unsafe links and empty-password encryption, scores the risk, and writes a disarmed copy with bounded memory and no runtime dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "patrickdk77",
|