@poe-platform/safe-fs 0.1.733 → 0.1.735
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.
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
import { FsError, toFsError } from "../contracts/errors.js";
|
|
2
2
|
import { finishCleanup } from "../contracts/cleanup.js";
|
|
3
3
|
import { inspectStagingBindings } from "./staging-ancestry.js";
|
|
4
|
+
const signalWaiters = new WeakMap();
|
|
5
|
+
function getSignalWaiters(signal) {
|
|
6
|
+
let entry = signalWaiters.get(signal);
|
|
7
|
+
if (!entry) {
|
|
8
|
+
const waiters = new Set();
|
|
9
|
+
const listener = () => {
|
|
10
|
+
signalWaiters.delete(signal);
|
|
11
|
+
signal.removeEventListener("abort", listener);
|
|
12
|
+
const pending = [...waiters];
|
|
13
|
+
waiters.clear();
|
|
14
|
+
for (const fn of pending)
|
|
15
|
+
fn();
|
|
16
|
+
};
|
|
17
|
+
entry = { waiters, listener };
|
|
18
|
+
signalWaiters.set(signal, entry);
|
|
19
|
+
signal.addEventListener("abort", listener, { once: true });
|
|
20
|
+
}
|
|
21
|
+
return entry;
|
|
22
|
+
}
|
|
23
|
+
function releaseSignalWaiter(signal, entry, abort) {
|
|
24
|
+
entry.waiters.delete(abort);
|
|
25
|
+
if (entry.waiters.size === 0 && signalWaiters.get(signal) === entry) {
|
|
26
|
+
signalWaiters.delete(signal);
|
|
27
|
+
signal.removeEventListener("abort", entry.listener);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function awaitWithSignal(promise, signal) {
|
|
31
|
+
if (!signal)
|
|
32
|
+
return promise;
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const entry = getSignalWaiters(signal);
|
|
35
|
+
const abort = () => { releaseSignalWaiter(signal, entry, abort); reject(signal.reason); };
|
|
36
|
+
entry.waiters.add(abort);
|
|
37
|
+
promise.then(value => { releaseSignalWaiter(signal, entry, abort); resolve(value); }, error => { releaseSignalWaiter(signal, entry, abort); reject(error); });
|
|
38
|
+
if (signal.aborted)
|
|
39
|
+
abort();
|
|
40
|
+
});
|
|
41
|
+
}
|
|
4
42
|
export function retainedReadCapabilities(filesystem, capabilities = filesystem.capabilities) {
|
|
5
43
|
return typeof filesystem.openReadFile === "function" ? capabilities : { ...capabilities, retainedRead: false };
|
|
6
44
|
}
|
|
@@ -16,13 +54,7 @@ export async function openRetainedReadFile(filesystem, path, options) {
|
|
|
16
54
|
const query = filesystem.capabilitiesFor;
|
|
17
55
|
signal?.throwIfAborted();
|
|
18
56
|
const metadata = Promise.resolve(query == null ? undefined : Reflect.apply(query, filesystem, [path, options]));
|
|
19
|
-
const queried = await (
|
|
20
|
-
const abort = () => { signal.removeEventListener("abort", abort); reject(signal.reason); };
|
|
21
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
22
|
-
metadata.then(value => { signal.removeEventListener("abort", abort); resolve(value); }, error => { signal.removeEventListener("abort", abort); reject(error); });
|
|
23
|
-
if (signal.aborted)
|
|
24
|
-
abort();
|
|
25
|
-
}) : metadata);
|
|
57
|
+
const queried = await awaitWithSignal(metadata, signal);
|
|
26
58
|
signal?.throwIfAborted();
|
|
27
59
|
const capabilities = queried ?? filesystem.capabilities;
|
|
28
60
|
signal?.throwIfAborted();
|
|
@@ -68,13 +100,7 @@ export async function openRetainedResizeFile(filesystem, path, options) {
|
|
|
68
100
|
const intent = { ...(signal === undefined ? {} : { signal }), create: options.create === true };
|
|
69
101
|
signal?.throwIfAborted();
|
|
70
102
|
const metadata = Promise.resolve(query === undefined ? filesystem.capabilities : Reflect.apply(query, filesystem, [path, intent]));
|
|
71
|
-
const capabilities = await (
|
|
72
|
-
const abort = () => { signal.removeEventListener("abort", abort); reject(signal.reason); };
|
|
73
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
74
|
-
metadata.then(value => { signal.removeEventListener("abort", abort); resolve(value); }, error => { signal.removeEventListener("abort", abort); reject(error); });
|
|
75
|
-
if (signal.aborted)
|
|
76
|
-
abort();
|
|
77
|
-
}) : metadata);
|
|
103
|
+
const capabilities = await awaitWithSignal(metadata, signal);
|
|
78
104
|
signal?.throwIfAborted();
|
|
79
105
|
const readOnly = capabilities.readOnly;
|
|
80
106
|
signal?.throwIfAborted();
|
|
@@ -257,8 +257,8 @@ class FastDirectoryEntriesMap {
|
|
|
257
257
|
table[slot] = -2;
|
|
258
258
|
keys[idx] = "";
|
|
259
259
|
this._vals[idx] = DUMMY_POOL_FILE_NODE;
|
|
260
|
-
|
|
261
|
-
|
|
260
|
+
// Keep deleted indices until compaction: reusing the tail would let
|
|
261
|
+
// tombstones fill the table without reaching set()'s rebuild threshold.
|
|
262
262
|
this.size--;
|
|
263
263
|
return true;
|
|
264
264
|
}
|
|
@@ -81,9 +81,20 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
|
|
|
81
81
|
close() { closed = true; return close(); },
|
|
82
82
|
};
|
|
83
83
|
};
|
|
84
|
+
let cachedCallerSignal;
|
|
85
|
+
let cachedCombinedSignal;
|
|
86
|
+
const combineSignal = (callerSignal) => {
|
|
87
|
+
if (!callerSignal || callerSignal === signal)
|
|
88
|
+
return signal;
|
|
89
|
+
if (callerSignal === cachedCallerSignal && cachedCombinedSignal)
|
|
90
|
+
return cachedCombinedSignal;
|
|
91
|
+
cachedCallerSignal = callerSignal;
|
|
92
|
+
cachedCombinedSignal = AbortSignal.any([signal, callerSignal]);
|
|
93
|
+
return cachedCombinedSignal;
|
|
94
|
+
};
|
|
84
95
|
const resizeOptions = (options) => {
|
|
85
96
|
const scopedOptions = {
|
|
86
|
-
...options, signal:
|
|
97
|
+
...options, signal: combineSignal(options.signal),
|
|
87
98
|
};
|
|
88
99
|
scopedOptions.signal.throwIfAborted();
|
|
89
100
|
return scopedOptions;
|
|
@@ -253,7 +264,7 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
|
|
|
253
264
|
if (property === "createStagedFile")
|
|
254
265
|
args[3] = snapshotStagingCreation(args[3], args[0]);
|
|
255
266
|
const callerSignal = args[property === "createStagedFile" ? 3 : 2]?.signal;
|
|
256
|
-
stagingSignal =
|
|
267
|
+
stagingSignal = combineSignal(callerSignal);
|
|
257
268
|
stagingSignal.throwIfAborted();
|
|
258
269
|
}
|
|
259
270
|
if (operations.has(property)) {
|
|
@@ -496,6 +507,10 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
|
|
|
496
507
|
originals.set(view, originalRecord);
|
|
497
508
|
retargeters.set(view, (nextCharge, nextSignal, nextCleanupCharge, nextMaxPathComponents) => {
|
|
498
509
|
charge = nextCharge;
|
|
510
|
+
if (signal !== nextSignal) {
|
|
511
|
+
cachedCallerSignal = undefined;
|
|
512
|
+
cachedCombinedSignal = undefined;
|
|
513
|
+
}
|
|
499
514
|
signal = nextSignal;
|
|
500
515
|
cleanupCharge = nextCleanupCharge;
|
|
501
516
|
maxPathComponents = nextMaxPathComponents ?? originals.get(filesystem)?.maxPathComponents;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
1
|
import type { FsOptions } from "../contracts/filesystem.js";
|
|
3
2
|
import { type ScopedTransportBudgetFrame } from "./transport-budget.js";
|
|
4
3
|
export type PlatformErrno = number;
|
|
@@ -15,7 +14,7 @@ export declare const platform: Readonly<{
|
|
|
15
14
|
maxCollectionBytes: number;
|
|
16
15
|
errno(code: string): PlatformErrno;
|
|
17
16
|
callbackAuthorities: true;
|
|
18
|
-
randomUUID:
|
|
17
|
+
randomUUID: () => string;
|
|
19
18
|
}>;
|
|
20
19
|
export declare const comparisonContext: Readonly<{
|
|
21
20
|
active(_options: FsOptions): boolean;
|
package/dist/safe-fs/xml.js
CHANGED
|
@@ -6,21 +6,16 @@ export class XmlLimitError extends SyntaxError {
|
|
|
6
6
|
}
|
|
7
7
|
}
|
|
8
8
|
function* find(source, needle, start) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
if (++work === 512) {
|
|
17
|
-
yield work;
|
|
18
|
-
work = 0;
|
|
19
|
-
}
|
|
9
|
+
const found = source.indexOf(needle, start);
|
|
10
|
+
const end = found < 0 ? source.length : found;
|
|
11
|
+
let remaining = end - start;
|
|
12
|
+
while (remaining >= 512) {
|
|
13
|
+
yield 512;
|
|
14
|
+
remaining -= 512;
|
|
20
15
|
}
|
|
21
|
-
if (
|
|
22
|
-
yield
|
|
23
|
-
return
|
|
16
|
+
if (remaining > 0)
|
|
17
|
+
yield remaining;
|
|
18
|
+
return found;
|
|
24
19
|
}
|
|
25
20
|
const xmlNamespace = "http://www.w3.org/XML/1998/namespace";
|
|
26
21
|
const xmlnsNamespace = "http://www.w3.org/2000/xmlns/";
|
|
@@ -47,6 +42,30 @@ function namePart(point) {
|
|
|
47
42
|
|| (point >= 48 && point <= 57) || (point >= 0x300 && point <= 0x36f)
|
|
48
43
|
|| (point >= 0x203f && point <= 0x2040);
|
|
49
44
|
}
|
|
45
|
+
function qualifiedNameSync(name) {
|
|
46
|
+
let prefix = "";
|
|
47
|
+
let start = 0;
|
|
48
|
+
let first = true;
|
|
49
|
+
for (let offset = 0; offset < name.length;) {
|
|
50
|
+
const point = name.codePointAt(offset);
|
|
51
|
+
if (point === 58) {
|
|
52
|
+
if (start !== 0 || first)
|
|
53
|
+
invalid("invalid qualified name");
|
|
54
|
+
prefix = name.slice(0, offset);
|
|
55
|
+
start = offset + 1;
|
|
56
|
+
first = true;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
if (!(first ? nameStart(point) : namePart(point)))
|
|
60
|
+
invalid("invalid qualified name");
|
|
61
|
+
first = false;
|
|
62
|
+
}
|
|
63
|
+
offset += point > 0xffff ? 2 : 1;
|
|
64
|
+
}
|
|
65
|
+
if (first)
|
|
66
|
+
invalid("invalid qualified name");
|
|
67
|
+
return [prefix, name.slice(start)];
|
|
68
|
+
}
|
|
50
69
|
function* qualifiedName(name) {
|
|
51
70
|
let prefix = "";
|
|
52
71
|
let start = 0;
|
|
@@ -258,67 +277,81 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
258
277
|
if (++contentNodes > maxContentNodes)
|
|
259
278
|
throw new XmlLimitError("maxContentNodes", "XML content node limit exceeded");
|
|
260
279
|
};
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
280
|
+
let pendingWork = 0;
|
|
281
|
+
const skipWhitespace = () => {
|
|
282
|
+
const start = offset;
|
|
283
|
+
while (offset < source.length) {
|
|
284
|
+
const c = source.charCodeAt(offset);
|
|
285
|
+
if (c !== 32 && c !== 9 && c !== 10 && c !== 13)
|
|
286
|
+
break;
|
|
264
287
|
offset++;
|
|
265
|
-
if (++work === 512) {
|
|
266
|
-
yield work;
|
|
267
|
-
work = 0;
|
|
268
|
-
}
|
|
269
288
|
}
|
|
270
|
-
|
|
271
|
-
yield work;
|
|
289
|
+
return offset - start;
|
|
272
290
|
};
|
|
273
|
-
const
|
|
291
|
+
const scanName = () => {
|
|
274
292
|
const start = offset;
|
|
275
|
-
while (offset < source.length
|
|
293
|
+
while (offset < source.length) {
|
|
294
|
+
const c = source.charCodeAt(offset);
|
|
295
|
+
if (c === 32 || c === 9 || c === 13 || c === 10 || c === 47 || c === 61 || c === 62 || c === 63)
|
|
296
|
+
break;
|
|
276
297
|
offset++;
|
|
277
|
-
if ((offset - start) % 512 === 0)
|
|
278
|
-
yield 512;
|
|
279
298
|
}
|
|
280
|
-
if ((offset - start) % 512)
|
|
281
|
-
yield (offset - start) % 512;
|
|
282
299
|
const name = source.slice(start, offset);
|
|
283
|
-
|
|
284
|
-
return name;
|
|
300
|
+
const [prefix, localName] = qualifiedNameSync(name);
|
|
301
|
+
return [name, prefix, localName, (offset - start) * 2];
|
|
285
302
|
};
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
admitContent();
|
|
293
|
-
if (retainContent)
|
|
294
|
-
parent.content.push({ kind, text });
|
|
303
|
+
while (offset < source.length) {
|
|
304
|
+
pendingWork += 1;
|
|
305
|
+
if (pendingWork >= 512) {
|
|
306
|
+
while (pendingWork >= 512) {
|
|
307
|
+
yield 512;
|
|
308
|
+
pendingWork -= 512;
|
|
295
309
|
}
|
|
296
310
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
yield text.length % 512;
|
|
306
|
-
if (text.length) {
|
|
307
|
-
admitContent();
|
|
308
|
-
if (retainContent)
|
|
309
|
-
(root ? epilog : prolog).push({ kind, text });
|
|
311
|
+
if (source.charCodeAt(offset) !== 60) {
|
|
312
|
+
const next = source.indexOf("<", offset);
|
|
313
|
+
const endPos = next < 0 ? source.length : next;
|
|
314
|
+
const text = source.slice(offset, endPos);
|
|
315
|
+
pendingWork += (endPos - offset) + text.length;
|
|
316
|
+
while (pendingWork >= 512) {
|
|
317
|
+
yield 512;
|
|
318
|
+
pendingWork -= 512;
|
|
310
319
|
}
|
|
311
|
-
|
|
312
|
-
};
|
|
313
|
-
while (offset < source.length) {
|
|
314
|
-
yield 1;
|
|
315
|
-
if (source[offset] !== "<") {
|
|
316
|
-
const next = yield* find(source, "<", offset);
|
|
317
|
-
const text = source.slice(offset, next < 0 ? source.length : next);
|
|
318
|
-
if ((yield* find(text, "]]>", 0)) >= 0)
|
|
320
|
+
if (text.indexOf("]]>") >= 0)
|
|
319
321
|
invalid("CDATA terminator in text");
|
|
320
|
-
|
|
321
|
-
|
|
322
|
+
const resolved = stack.length ? (text.indexOf("&") < 0 ? (pendingWork += text.length, text) : yield* entities(text)) : text;
|
|
323
|
+
while (pendingWork >= 512) {
|
|
324
|
+
yield 512;
|
|
325
|
+
pendingWork -= 512;
|
|
326
|
+
}
|
|
327
|
+
admitText(resolved);
|
|
328
|
+
const parent = stack.at(-1);
|
|
329
|
+
if (parent) {
|
|
330
|
+
parent.element.text += resolved;
|
|
331
|
+
if (resolved.length) {
|
|
332
|
+
admitContent();
|
|
333
|
+
if (retainContent)
|
|
334
|
+
parent.content.push({ kind: "text", text: resolved });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
for (let index = 0; index < resolved.length; index++) {
|
|
339
|
+
const c = resolved.charCodeAt(index);
|
|
340
|
+
if (c !== 32 && c !== 9 && c !== 13 && c !== 10)
|
|
341
|
+
invalid("text outside the root");
|
|
342
|
+
}
|
|
343
|
+
pendingWork += resolved.length;
|
|
344
|
+
while (pendingWork >= 512) {
|
|
345
|
+
yield 512;
|
|
346
|
+
pendingWork -= 512;
|
|
347
|
+
}
|
|
348
|
+
if (resolved.length) {
|
|
349
|
+
admitContent();
|
|
350
|
+
if (retainContent)
|
|
351
|
+
(root ? epilog : prolog).push({ kind: "text", text: resolved });
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
offset = endPos;
|
|
322
355
|
}
|
|
323
356
|
else if (source.startsWith("<!--", offset)) {
|
|
324
357
|
const end = yield* find(source, "-->", offset + 4);
|
|
@@ -340,13 +373,24 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
340
373
|
const end = yield* find(source, "]]>", offset + 9);
|
|
341
374
|
if (end < 0)
|
|
342
375
|
invalid("unterminated CDATA");
|
|
343
|
-
|
|
376
|
+
const cdataText = source.slice(offset + 9, end);
|
|
377
|
+
admitText(cdataText);
|
|
378
|
+
const parent = stack.at(-1);
|
|
379
|
+
parent.element.text += cdataText;
|
|
380
|
+
admitContent();
|
|
381
|
+
if (retainContent)
|
|
382
|
+
parent.content.push({ kind: "cdata", text: cdataText });
|
|
344
383
|
offset = end + 3;
|
|
345
384
|
}
|
|
346
385
|
else if (source.startsWith("<?", offset)) {
|
|
347
386
|
const start = offset;
|
|
348
387
|
offset += 2;
|
|
349
|
-
const target =
|
|
388
|
+
const [target, , , wName] = scanName();
|
|
389
|
+
pendingWork += wName;
|
|
390
|
+
while (pendingWork >= 512) {
|
|
391
|
+
yield 512;
|
|
392
|
+
pendingWork -= 512;
|
|
393
|
+
}
|
|
350
394
|
const end = yield* find(source, "?>", offset);
|
|
351
395
|
if (end < 0)
|
|
352
396
|
invalid("unterminated processing instruction");
|
|
@@ -363,15 +407,15 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
363
407
|
invalid("invalid processing instruction");
|
|
364
408
|
if (!(target.length === 3 && target.toLowerCase() === "xml")) {
|
|
365
409
|
const parent = stack.at(-1);
|
|
366
|
-
let
|
|
367
|
-
while (
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
410
|
+
let wsStart = 0;
|
|
411
|
+
while (wsStart < content.length && " \t\n\r".includes(content[wsStart]))
|
|
412
|
+
wsStart++;
|
|
413
|
+
pendingWork += wsStart;
|
|
414
|
+
while (pendingWork >= 512) {
|
|
415
|
+
yield 512;
|
|
416
|
+
pendingWork -= 512;
|
|
371
417
|
}
|
|
372
|
-
|
|
373
|
-
yield start % 512;
|
|
374
|
-
const text = content.slice(start);
|
|
418
|
+
const text = content.slice(wsStart);
|
|
375
419
|
admitText(text);
|
|
376
420
|
admitContent();
|
|
377
421
|
if (retainContent) {
|
|
@@ -383,92 +427,129 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
383
427
|
else if (source.startsWith("<!", offset)) {
|
|
384
428
|
invalid("DTD and entity declarations are forbidden");
|
|
385
429
|
}
|
|
386
|
-
else if (source.
|
|
430
|
+
else if (source.charCodeAt(offset + 1) === 47) {
|
|
387
431
|
offset += 2;
|
|
388
|
-
const name =
|
|
389
|
-
|
|
432
|
+
const [name, , , wName] = scanName();
|
|
433
|
+
pendingWork += wName + skipWhitespace();
|
|
434
|
+
while (pendingWork >= 512) {
|
|
435
|
+
yield 512;
|
|
436
|
+
pendingWork -= 512;
|
|
437
|
+
}
|
|
390
438
|
if (source[offset++] !== ">" || stack.pop()?.name !== name)
|
|
391
439
|
invalid("mismatched closing tag");
|
|
392
440
|
}
|
|
393
441
|
else {
|
|
394
442
|
offset++;
|
|
395
|
-
const name =
|
|
443
|
+
const [name, prefix, localName, wName] = scanName();
|
|
444
|
+
pendingWork += wName;
|
|
445
|
+
while (pendingWork >= 512) {
|
|
446
|
+
yield 512;
|
|
447
|
+
pendingWork -= 512;
|
|
448
|
+
}
|
|
396
449
|
const attributes = new Map();
|
|
450
|
+
const attrMeta = [];
|
|
397
451
|
let namespaces = stack.at(-1)?.namespaces ?? new Map([["xml", xmlNamespace]]);
|
|
398
452
|
let ownsNamespaces = stack.length === 0;
|
|
399
453
|
while (true) {
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
454
|
+
const ws = skipWhitespace();
|
|
455
|
+
pendingWork += ws;
|
|
456
|
+
while (pendingWork >= 512) {
|
|
457
|
+
yield 512;
|
|
458
|
+
pendingWork -= 512;
|
|
459
|
+
}
|
|
460
|
+
const ch = source.charCodeAt(offset);
|
|
461
|
+
if (ch === 47 || ch === 62)
|
|
403
462
|
break;
|
|
404
|
-
if (
|
|
463
|
+
if (ws === 0)
|
|
405
464
|
invalid("attributes require whitespace");
|
|
406
|
-
const attribute =
|
|
465
|
+
const [attribute, attrPrefix, attrLocal, wAttr] = scanName();
|
|
466
|
+
pendingWork += wAttr;
|
|
467
|
+
while (pendingWork >= 512) {
|
|
468
|
+
yield 512;
|
|
469
|
+
pendingWork -= 512;
|
|
470
|
+
}
|
|
407
471
|
if (attributes.has(attribute))
|
|
408
472
|
invalid("duplicate attribute");
|
|
409
473
|
if (++attributeCount > maxAttributes)
|
|
410
474
|
throw new XmlLimitError("maxAttributes", "XML attribute limit exceeded");
|
|
411
475
|
if (attributes.size >= maxAttributesPerElement)
|
|
412
476
|
throw new XmlLimitError("maxAttributesPerElement", "XML attribute limit exceeded");
|
|
413
|
-
|
|
477
|
+
pendingWork += skipWhitespace();
|
|
414
478
|
if (source[offset++] !== "=")
|
|
415
479
|
invalid("missing attribute equals");
|
|
416
|
-
|
|
480
|
+
pendingWork += skipWhitespace();
|
|
417
481
|
const quote = source[offset++];
|
|
418
482
|
if (quote !== '"' && quote !== "'")
|
|
419
483
|
invalid("unquoted attribute");
|
|
420
|
-
const end =
|
|
484
|
+
const end = source.indexOf(quote, offset);
|
|
485
|
+
const scanLen = (end < 0 ? source.length : end) - offset;
|
|
486
|
+
pendingWork += scanLen;
|
|
487
|
+
while (pendingWork >= 512) {
|
|
488
|
+
yield 512;
|
|
489
|
+
pendingWork -= 512;
|
|
490
|
+
}
|
|
421
491
|
if (end < 0)
|
|
422
492
|
invalid("unterminated attribute");
|
|
423
493
|
const raw = source.slice(offset, end);
|
|
424
|
-
|
|
494
|
+
pendingWork += raw.length * 2;
|
|
495
|
+
while (pendingWork >= 512) {
|
|
496
|
+
yield 512;
|
|
497
|
+
pendingWork -= 512;
|
|
498
|
+
}
|
|
499
|
+
if (raw.indexOf("<") >= 0)
|
|
425
500
|
invalid("less-than in attribute");
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
yield 512;
|
|
501
|
+
const normalized = /[\t\n\r]/.test(raw) ? raw.replace(/[\t\n\r]/g, " ") : raw;
|
|
502
|
+
const value = normalized.indexOf("&") < 0 ? (pendingWork += normalized.length, normalized) : yield* entities(normalized);
|
|
503
|
+
while (pendingWork >= 512) {
|
|
504
|
+
yield 512;
|
|
505
|
+
pendingWork -= 512;
|
|
432
506
|
}
|
|
433
|
-
if (raw.length % 512)
|
|
434
|
-
yield raw.length % 512;
|
|
435
|
-
const value = yield* entities(normalized);
|
|
436
507
|
admitText(value);
|
|
437
508
|
attributes.set(attribute, value);
|
|
509
|
+
attrMeta.push([attribute, attrPrefix, attrLocal]);
|
|
438
510
|
offset = end + 1;
|
|
439
511
|
if (attribute === "xmlns" || attribute.startsWith("xmlns:")) {
|
|
440
|
-
const
|
|
441
|
-
if (
|
|
442
|
-
|| (
|
|
443
|
-
|| (
|
|
512
|
+
const nsPrefix = attribute === "xmlns" ? "" : attribute.slice(6);
|
|
513
|
+
if (nsPrefix === "xmlns" || value === xmlnsNamespace
|
|
514
|
+
|| (nsPrefix === "xml") !== (value === xmlNamespace)
|
|
515
|
+
|| (nsPrefix !== "" && value === ""))
|
|
444
516
|
invalid("invalid namespace binding");
|
|
445
517
|
if (!ownsNamespaces) {
|
|
446
518
|
const copy = new Map();
|
|
447
519
|
for (const [key, uri] of namespaces) {
|
|
448
520
|
copy.set(key, uri);
|
|
449
|
-
|
|
521
|
+
pendingWork += 1;
|
|
450
522
|
}
|
|
451
523
|
namespaces = copy;
|
|
452
524
|
ownsNamespaces = true;
|
|
453
525
|
}
|
|
454
|
-
namespaces.set(
|
|
526
|
+
namespaces.set(nsPrefix, value);
|
|
455
527
|
if (namespaces.size > maxNamespaces)
|
|
456
528
|
throw new XmlLimitError("maxNamespaces", "XML namespace scope limit exceeded");
|
|
457
529
|
}
|
|
458
530
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
531
|
+
if (attrMeta.length > 0) {
|
|
532
|
+
const expanded = attrMeta.length > 1 ? new Set() : undefined;
|
|
533
|
+
for (let i = 0; i < attrMeta.length; i++) {
|
|
534
|
+
const [attribute, attrPrefix, attrLocal] = attrMeta[i];
|
|
535
|
+
if (attribute === "xmlns" || attribute.startsWith("xmlns:"))
|
|
536
|
+
continue;
|
|
537
|
+
pendingWork += attribute.length;
|
|
538
|
+
if (attrPrefix && !namespaces.has(attrPrefix))
|
|
539
|
+
invalid("unbound attribute prefix");
|
|
540
|
+
if (expanded) {
|
|
541
|
+
const key = `${attrPrefix ? namespaces.get(attrPrefix) : ""}\0${attrLocal}`;
|
|
542
|
+
if (expanded.has(key))
|
|
543
|
+
invalid("duplicate expanded attribute");
|
|
544
|
+
expanded.add(key);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
pendingWork += name.length;
|
|
549
|
+
while (pendingWork >= 512) {
|
|
550
|
+
yield 512;
|
|
551
|
+
pendingWork -= 512;
|
|
470
552
|
}
|
|
471
|
-
const [prefix, localName] = yield* qualifiedName(name);
|
|
472
553
|
if (prefix === "xmlns" || (prefix && !namespaces.has(prefix)))
|
|
473
554
|
invalid("unbound element prefix");
|
|
474
555
|
if (++nodes > maxNodes)
|
|
@@ -479,12 +560,19 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
479
560
|
limits.onElement?.({ name, namespace, localName }, stack.at(-1)?.element, stack.length + 1);
|
|
480
561
|
admitContent();
|
|
481
562
|
const retainedAttributes = [];
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
563
|
+
if (retainContent) {
|
|
564
|
+
for (let i = 0; i < attrMeta.length; i++) {
|
|
565
|
+
const [attribute, attrPrefix, attrLocal] = attrMeta[i];
|
|
566
|
+
const value = attributes.get(attribute);
|
|
567
|
+
admitContent();
|
|
568
|
+
retainedAttributes.push({ name: attribute, localName: attrLocal,
|
|
569
|
+
namespace: attribute === "xmlns" || attrPrefix === "xmlns" ? xmlnsNamespace : attrPrefix ? namespaces.get(attrPrefix) : "", value });
|
|
570
|
+
pendingWork += attribute.length + 1;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
while (pendingWork >= 512) {
|
|
574
|
+
yield 512;
|
|
575
|
+
pendingWork -= 512;
|
|
488
576
|
}
|
|
489
577
|
const content = retainContent ? [] : undefined;
|
|
490
578
|
const element = { kind: "element", name, namespace, localName, children: [], text: "", content: content ?? emptyContent, attributes: retainContent ? retainedAttributes : emptyAttributes, namespaces: retainContent ? namespaces : emptyNamespaces, ...(root === undefined && retainContent ? { prolog, epilog, ...(declaration === undefined ? {} : { declaration }) } : {}) };
|
|
@@ -506,6 +594,10 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
506
594
|
stack.push({ element, content, name, namespaces });
|
|
507
595
|
}
|
|
508
596
|
}
|
|
597
|
+
if (pendingWork > 0) {
|
|
598
|
+
yield pendingWork;
|
|
599
|
+
pendingWork = 0;
|
|
600
|
+
}
|
|
509
601
|
if (stack.length || !root)
|
|
510
602
|
invalid("incomplete document");
|
|
511
603
|
return root;
|