@poe-platform/safe-fs 0.1.812 → 0.1.814
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/dist/safe-fs/core.d.ts +2 -2
- package/dist/safe-fs/core.js +1 -1
- package/dist/xml-ast/characters.d.ts +3 -0
- package/dist/xml-ast/characters.js +53 -0
- package/dist/xml-ast/frames.d.ts +28 -0
- package/dist/xml-ast/frames.js +45 -0
- package/dist/xml-ast/index.d.ts +27 -1
- package/dist/xml-ast/index.js +315 -154
- package/dist/xml-ast/source.d.ts +24 -0
- package/dist/xml-ast/source.js +71 -0
- package/dist/xml-ast/stream.d.ts +1 -0
- package/package.json +1 -1
package/dist/safe-fs/core.d.ts
CHANGED
|
@@ -8,8 +8,8 @@ export * from "./fs/object-publication/index.js";
|
|
|
8
8
|
export * from "./fs/webdav/index.js";
|
|
9
9
|
export * from "./bridge/index.js";
|
|
10
10
|
export * from "./python/index.js";
|
|
11
|
-
export { parseXml, parseXmlSteps, parseXmlStream, XmlLimitError } from "./xml.js";
|
|
12
|
-
export type { XmlName, XmlAttribute, XmlContent, XmlElement, XmlLimits } from "./xml.js";
|
|
11
|
+
export { parseXml, parseXmlSteps, parseXmlSourceSteps, normalizeXmlChunks, parseXmlStream, XmlLimitError } from "./xml.js";
|
|
12
|
+
export type { XmlName, XmlAttribute, XmlContent, XmlElement, XmlLimits, XmlParserFrame, XmlFrameRequest } from "./xml.js";
|
|
13
13
|
export * from "./contracts/object.js";
|
|
14
14
|
export { ObjectAuthority } from "./fs/object-authority.js";
|
|
15
15
|
export * from "./fs/s3/index.js";
|
package/dist/safe-fs/core.js
CHANGED
|
@@ -8,7 +8,7 @@ export * from "./fs/object-publication/index.js";
|
|
|
8
8
|
export * from "./fs/webdav/index.js";
|
|
9
9
|
export * from "./bridge/index.js";
|
|
10
10
|
export * from "./python/index.js";
|
|
11
|
-
export { parseXml, parseXmlSteps, parseXmlStream, XmlLimitError } from "./xml.js";
|
|
11
|
+
export { parseXml, parseXmlSteps, parseXmlSourceSteps, normalizeXmlChunks, parseXmlStream, XmlLimitError } from "./xml.js";
|
|
12
12
|
export * from "./contracts/object.js";
|
|
13
13
|
export { ObjectAuthority } from "./fs/object-authority.js";
|
|
14
14
|
export * from "./fs/s3/index.js";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function validCharacter(point: number): boolean;
|
|
2
|
+
/** Validate scalar values and normalize XML line endings in fixed windows. */
|
|
3
|
+
export declare function normalizeXmlChunks(source: AsyncIterable<string> | Iterable<string>, flushChunks?: boolean): AsyncGenerator<string>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export function validCharacter(point) {
|
|
2
|
+
return point === 9 || point === 10 || point === 13
|
|
3
|
+
|| (point >= 0x20 && point <= 0xd7ff)
|
|
4
|
+
|| (point >= 0xe000 && point <= 0xfffd)
|
|
5
|
+
|| (point >= 0x10000 && point <= 0x10ffff);
|
|
6
|
+
}
|
|
7
|
+
/** Validate scalar values and normalize XML line endings in fixed windows. */
|
|
8
|
+
export async function* normalizeXmlChunks(source, flushChunks = false) {
|
|
9
|
+
let first = true, afterCR = false, high = 0, output = '';
|
|
10
|
+
for await (const chunk of source) {
|
|
11
|
+
for (let index = 0; index < chunk.length; index++) {
|
|
12
|
+
const code = chunk.charCodeAt(index);
|
|
13
|
+
if (first) {
|
|
14
|
+
first = false;
|
|
15
|
+
if (code === 0xfeff)
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (high) {
|
|
19
|
+
if (code < 0xdc00 || code > 0xdfff)
|
|
20
|
+
throw new SyntaxError('Invalid XML: invalid character');
|
|
21
|
+
output += String.fromCharCode(high, code);
|
|
22
|
+
high = 0;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
if (afterCR) {
|
|
26
|
+
afterCR = false;
|
|
27
|
+
if (code === 10)
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
31
|
+
high = code;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (!validCharacter(code))
|
|
35
|
+
throw new SyntaxError('Invalid XML: invalid character');
|
|
36
|
+
afterCR = code === 13;
|
|
37
|
+
output += code === 13 ? '\n' : chunk[index];
|
|
38
|
+
}
|
|
39
|
+
if (output.length >= 512) {
|
|
40
|
+
yield output;
|
|
41
|
+
output = '';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (flushChunks && output) {
|
|
45
|
+
yield output;
|
|
46
|
+
output = '';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (high)
|
|
50
|
+
throw new SyntaxError('Invalid XML: invalid character');
|
|
51
|
+
if (output)
|
|
52
|
+
yield output;
|
|
53
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { XmlContent, XmlElement } from './index.js';
|
|
2
|
+
export interface XmlParserFrame {
|
|
3
|
+
element: XmlElement;
|
|
4
|
+
content: XmlContent[] | undefined;
|
|
5
|
+
name: string;
|
|
6
|
+
namespaces: Map<string, string>;
|
|
7
|
+
}
|
|
8
|
+
export type XmlFrameRequest = {
|
|
9
|
+
readonly frameOperation: 'push';
|
|
10
|
+
readonly frame: XmlParserFrame;
|
|
11
|
+
} | {
|
|
12
|
+
readonly frameOperation: 'pop';
|
|
13
|
+
} | {
|
|
14
|
+
readonly frameOperation: 'peek';
|
|
15
|
+
frame?: XmlParserFrame;
|
|
16
|
+
};
|
|
17
|
+
/** External mode caches only the current frame. The host owns the linked stack. */
|
|
18
|
+
export declare class XmlFrames {
|
|
19
|
+
private readonly external;
|
|
20
|
+
private readonly frames;
|
|
21
|
+
private depth;
|
|
22
|
+
private current;
|
|
23
|
+
constructor(external: boolean);
|
|
24
|
+
get length(): number;
|
|
25
|
+
peek(): Generator<XmlFrameRequest, XmlParserFrame | undefined, void>;
|
|
26
|
+
push(frame: XmlParserFrame): Generator<XmlFrameRequest, void, void>;
|
|
27
|
+
pop(): Generator<XmlFrameRequest, XmlParserFrame | undefined, void>;
|
|
28
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** External mode caches only the current frame. The host owns the linked stack. */
|
|
2
|
+
export class XmlFrames {
|
|
3
|
+
external;
|
|
4
|
+
frames = [];
|
|
5
|
+
depth = 0;
|
|
6
|
+
current;
|
|
7
|
+
constructor(external) {
|
|
8
|
+
this.external = external;
|
|
9
|
+
}
|
|
10
|
+
get length() { return this.external ? this.depth : this.frames.length; }
|
|
11
|
+
*peek() {
|
|
12
|
+
if (!this.external)
|
|
13
|
+
return this.frames.at(-1);
|
|
14
|
+
if (!this.depth)
|
|
15
|
+
return undefined;
|
|
16
|
+
if (!this.current) {
|
|
17
|
+
const request = { frameOperation: 'peek' };
|
|
18
|
+
yield request;
|
|
19
|
+
if (!request.frame)
|
|
20
|
+
throw new TypeError('Incomplete XML frame read');
|
|
21
|
+
this.current = request.frame;
|
|
22
|
+
}
|
|
23
|
+
return this.current;
|
|
24
|
+
}
|
|
25
|
+
*push(frame) {
|
|
26
|
+
if (!this.external) {
|
|
27
|
+
this.frames.push(frame);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
yield { frameOperation: 'push', frame };
|
|
31
|
+
this.current = frame;
|
|
32
|
+
this.depth++;
|
|
33
|
+
}
|
|
34
|
+
*pop() {
|
|
35
|
+
if (!this.external)
|
|
36
|
+
return this.frames.pop();
|
|
37
|
+
if (!this.depth)
|
|
38
|
+
return undefined;
|
|
39
|
+
const frame = yield* this.peek();
|
|
40
|
+
yield { frameOperation: 'pop' };
|
|
41
|
+
this.depth--;
|
|
42
|
+
this.current = undefined;
|
|
43
|
+
return frame;
|
|
44
|
+
}
|
|
45
|
+
}
|
package/dist/xml-ast/index.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { type XmlFrameRequest } from "./frames.js";
|
|
2
|
+
export type { XmlParserFrame, XmlFrameRequest } from "./frames.js";
|
|
3
|
+
export { normalizeXmlChunks } from "./characters.js";
|
|
4
|
+
import { type XmlSourceStep } from "./source.js";
|
|
5
|
+
export type { XmlSourceRead, XmlSourceStep } from "./source.js";
|
|
6
|
+
import type { XmlStreamEvent } from "./stream.js";
|
|
1
7
|
export { XmlLimitError } from "./errors.js";
|
|
2
8
|
export interface XmlName {
|
|
3
9
|
readonly name: string;
|
|
@@ -40,6 +46,26 @@ export interface XmlLimits {
|
|
|
40
46
|
readonly maxTextLength?: number;
|
|
41
47
|
readonly onElement?: (element: XmlName, parent: XmlName | undefined, depth: number) => void;
|
|
42
48
|
}
|
|
43
|
-
export
|
|
49
|
+
export interface XmlStepLimits extends XmlLimits {
|
|
50
|
+
/** Return root metadata without retaining descendants or text. */
|
|
51
|
+
readonly retainTree?: boolean;
|
|
52
|
+
/** Synchronous ordered events, including repaired closes. Requires retainTree:
|
|
53
|
+
* false. Drive the step iterator to await external consumers between windows. */
|
|
54
|
+
readonly events?: (event: XmlStreamEvent) => void;
|
|
55
|
+
}
|
|
56
|
+
export interface XmlSourceLimits extends XmlStepLimits {
|
|
57
|
+
/** Emit content bodies in bounded fragments, marking continuations of the same logical
|
|
58
|
+
* node. Requires retainTree: false; consumers must preserve fragment identity. */
|
|
59
|
+
readonly fragmentContent?: boolean;
|
|
60
|
+
/** Store active parser frames through host-serviced requests. Requires retainTree: false. */
|
|
61
|
+
readonly storeFrames?: boolean;
|
|
62
|
+
}
|
|
63
|
+
export type XmlParseStep = XmlSourceStep | XmlFrameRequest;
|
|
64
|
+
/** Parse a validated, BOM-free, line-normalized UTF-16 source via bounded read requests. */
|
|
65
|
+
export declare function parseXmlSourceSteps(length: number | undefined, limits?: XmlStepLimits & {
|
|
66
|
+
readonly storeFrames?: false;
|
|
67
|
+
}): Generator<XmlSourceStep, XmlElement, void>;
|
|
68
|
+
export declare function parseXmlSourceSteps(length: number | undefined, limits: XmlSourceLimits): Generator<XmlParseStep, XmlElement, void>;
|
|
69
|
+
export declare function parseXmlSteps(input: string, limits?: XmlStepLimits): Generator<number, XmlElement, void>;
|
|
44
70
|
export declare function parseXml(input: string, limits?: XmlLimits): XmlElement;
|
|
45
71
|
export { parseXmlStream, type XmlStreamLimits, type XmlStreamEvent } from "./stream.js";
|
package/dist/xml-ast/index.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import { XmlFrames } from "./frames.js";
|
|
2
|
+
import { validCharacter } from "./characters.js";
|
|
3
|
+
export { normalizeXmlChunks } from "./characters.js";
|
|
4
|
+
import { XmlSource } from "./source.js";
|
|
1
5
|
import { XmlLimitError } from "./errors.js";
|
|
2
6
|
export { XmlLimitError } from "./errors.js";
|
|
3
7
|
function* find(source, needle, start) {
|
|
4
|
-
const found = source.indexOf(needle, start);
|
|
8
|
+
const found = typeof source === "string" ? source.indexOf(needle, start) : yield* source.indexOf(needle, start);
|
|
5
9
|
const end = found < 0 ? source.length : found;
|
|
6
10
|
let remaining = end - start;
|
|
7
11
|
while (remaining >= 512) {
|
|
@@ -17,12 +21,6 @@ const xmlnsNamespace = "http://www.w3.org/2000/xmlns/";
|
|
|
17
21
|
function invalid(message) {
|
|
18
22
|
throw new SyntaxError(`Invalid XML: ${message}`);
|
|
19
23
|
}
|
|
20
|
-
function validCharacter(point) {
|
|
21
|
-
return point === 9 || point === 10 || point === 13
|
|
22
|
-
|| (point >= 0x20 && point <= 0xd7ff)
|
|
23
|
-
|| (point >= 0xe000 && point <= 0xfffd)
|
|
24
|
-
|| (point >= 0x10000 && point <= 0x10ffff);
|
|
25
|
-
}
|
|
26
24
|
function nameStart(point) {
|
|
27
25
|
return point === 95 || (point >= 65 && point <= 90) || (point >= 97 && point <= 122)
|
|
28
26
|
|| (point >= 0xc0 && point <= 0xd6) || (point >= 0xd8 && point <= 0xf6)
|
|
@@ -69,6 +67,95 @@ function qualifiedNameSync(name, cache) {
|
|
|
69
67
|
}
|
|
70
68
|
return parts;
|
|
71
69
|
}
|
|
70
|
+
function* entity(source, start, end, recover) {
|
|
71
|
+
// Inspect only a bounded prefix, even for arbitrarily long numeric references.
|
|
72
|
+
const prefix = typeof source === "string" ? source.slice(start, Math.min(end, start + 4))
|
|
73
|
+
: yield* source.slice(start, Math.min(end, start + 4));
|
|
74
|
+
const predefined = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
|
|
75
|
+
if (end - start <= 4 && Object.hasOwn(predefined, prefix))
|
|
76
|
+
return predefined[prefix];
|
|
77
|
+
const hexadecimal = prefix.startsWith("#x"), digits = start + (hexadecimal ? 2 : 1);
|
|
78
|
+
if (!prefix.startsWith("#") || digits >= end) {
|
|
79
|
+
if (!recover)
|
|
80
|
+
invalid("undeclared entity");
|
|
81
|
+
recover("undeclared entity");
|
|
82
|
+
return "";
|
|
83
|
+
}
|
|
84
|
+
let point = 0;
|
|
85
|
+
for (let index = digits; index < end; index++) {
|
|
86
|
+
const code = typeof source === "string" ? source.charCodeAt(index) : yield* source.charCodeAt(index);
|
|
87
|
+
const digit = code >= 48 && code <= 57 ? code - 48 : hexadecimal && code >= 65 && code <= 70 ? code - 55
|
|
88
|
+
: hexadecimal && code >= 97 && code <= 102 ? code - 87 : -1;
|
|
89
|
+
if (digit < 0)
|
|
90
|
+
invalid("undeclared entity");
|
|
91
|
+
point = point * (hexadecimal ? 16 : 10) + digit;
|
|
92
|
+
if (point > 0x10ffff)
|
|
93
|
+
invalid("invalid character reference");
|
|
94
|
+
if ((index - digits + 1) % 512 === 0)
|
|
95
|
+
yield 512;
|
|
96
|
+
}
|
|
97
|
+
if ((end - digits) % 512)
|
|
98
|
+
yield (end - digits) % 512;
|
|
99
|
+
if (!validCharacter(point))
|
|
100
|
+
invalid("invalid character reference");
|
|
101
|
+
return String.fromCodePoint(point);
|
|
102
|
+
}
|
|
103
|
+
/** Decoded text fragments; source read/work requests remain host-serviced. */
|
|
104
|
+
function* textParts(source, start, end, decode, recover) {
|
|
105
|
+
let cursor = start;
|
|
106
|
+
while (cursor < end) {
|
|
107
|
+
let finish = Math.min(end, cursor + 512);
|
|
108
|
+
if (finish < end) {
|
|
109
|
+
const last = yield* source.charCodeAt(finish - 1);
|
|
110
|
+
if (last >= 0xd800 && last <= 0xdbff)
|
|
111
|
+
finish--;
|
|
112
|
+
}
|
|
113
|
+
const raw = yield* source.slice(cursor, finish);
|
|
114
|
+
const found = decode ? raw.indexOf("&") : -1;
|
|
115
|
+
if (found < 0) {
|
|
116
|
+
yield raw;
|
|
117
|
+
yield raw.length;
|
|
118
|
+
cursor = finish;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const amp = cursor + found;
|
|
122
|
+
if (found) {
|
|
123
|
+
yield raw.slice(0, found);
|
|
124
|
+
yield found;
|
|
125
|
+
}
|
|
126
|
+
const semicolon = yield* source.indexOf(";", amp + 1, end);
|
|
127
|
+
if (semicolon < 0) {
|
|
128
|
+
if (!recover)
|
|
129
|
+
invalid("unterminated entity");
|
|
130
|
+
recover("unterminated entity");
|
|
131
|
+
cursor = amp + 1;
|
|
132
|
+
decode = false;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const part = yield* entity(source, amp + 1, semicolon, recover);
|
|
136
|
+
if (part)
|
|
137
|
+
yield part;
|
|
138
|
+
yield 1;
|
|
139
|
+
cursor = semicolon + 1;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function* textFragments(source, start, end, decode, recover) {
|
|
143
|
+
let pending = "";
|
|
144
|
+
for (const part of textParts(source, start, end, decode, recover)) {
|
|
145
|
+
if (typeof part !== "string") {
|
|
146
|
+
yield part;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
pending += part;
|
|
150
|
+
while (pending.length >= 512) {
|
|
151
|
+
const last = pending.charCodeAt(511), width = last >= 0xd800 && last <= 0xdbff ? 511 : 512;
|
|
152
|
+
yield pending.slice(0, width);
|
|
153
|
+
pending = pending.slice(width);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (pending)
|
|
157
|
+
yield pending;
|
|
158
|
+
}
|
|
72
159
|
function* entities(text, recover) {
|
|
73
160
|
let result = "";
|
|
74
161
|
let offset = 0;
|
|
@@ -84,39 +171,7 @@ function* entities(text, recover) {
|
|
|
84
171
|
recover("unterminated entity");
|
|
85
172
|
return result + text.slice(start + 1);
|
|
86
173
|
}
|
|
87
|
-
|
|
88
|
-
const predefined = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
|
|
89
|
-
if (Object.hasOwn(predefined, entity))
|
|
90
|
-
result += predefined[entity];
|
|
91
|
-
else {
|
|
92
|
-
const hexadecimal = entity.startsWith("#x");
|
|
93
|
-
const digits = entity.slice(hexadecimal ? 2 : 1);
|
|
94
|
-
if (!entity.startsWith("#") || !digits.length) {
|
|
95
|
-
if (!recover)
|
|
96
|
-
invalid("undeclared entity");
|
|
97
|
-
recover("undeclared entity");
|
|
98
|
-
offset = end + 1;
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
let point = 0;
|
|
102
|
-
for (let index = 0; index < digits.length; index++) {
|
|
103
|
-
const code = digits.charCodeAt(index);
|
|
104
|
-
const digit = code >= 48 && code <= 57 ? code - 48 : hexadecimal && code >= 65 && code <= 70 ? code - 55
|
|
105
|
-
: hexadecimal && code >= 97 && code <= 102 ? code - 87 : -1;
|
|
106
|
-
if (digit < 0)
|
|
107
|
-
invalid("undeclared entity");
|
|
108
|
-
point = point * (hexadecimal ? 16 : 10) + digit;
|
|
109
|
-
if (point > 0x10ffff)
|
|
110
|
-
invalid("invalid character reference");
|
|
111
|
-
if ((index + 1) % 512 === 0)
|
|
112
|
-
yield 512;
|
|
113
|
-
}
|
|
114
|
-
if (digits.length % 512)
|
|
115
|
-
yield digits.length % 512;
|
|
116
|
-
if (!validCharacter(point))
|
|
117
|
-
invalid("invalid character reference");
|
|
118
|
-
result += String.fromCodePoint(point);
|
|
119
|
-
}
|
|
174
|
+
result += yield* entity(text, start + 1, end, recover);
|
|
120
175
|
offset = end + 1;
|
|
121
176
|
}
|
|
122
177
|
return result;
|
|
@@ -182,68 +237,43 @@ function* validDeclaration(content, expectedEncoding) {
|
|
|
182
237
|
}
|
|
183
238
|
return offset === content.length;
|
|
184
239
|
}
|
|
185
|
-
|
|
240
|
+
function validateLimits(limits) {
|
|
241
|
+
for (const limit of [limits.maxDepth, limits.maxNodes, limits.maxAttributes, limits.maxContentNodes, limits.maxAttributesPerElement, limits.maxNamespaces]) {
|
|
242
|
+
if (limit !== undefined && ((limit !== Infinity && !Number.isSafeInteger(limit)) || limit < 1))
|
|
243
|
+
throw new RangeError("XML limits must be positive integers");
|
|
244
|
+
}
|
|
245
|
+
if (limits.maxTextLength !== undefined && ((limits.maxTextLength !== Infinity && !Number.isSafeInteger(limits.maxTextLength)) || limits.maxTextLength < 1))
|
|
246
|
+
throw new RangeError("XML limits must be positive integers");
|
|
247
|
+
if (limits.events && limits.retainTree !== false)
|
|
248
|
+
throw new TypeError("XML events require retainTree: false");
|
|
249
|
+
}
|
|
250
|
+
export function* parseXmlSourceSteps(length, limits = {}) {
|
|
251
|
+
validateLimits(limits);
|
|
252
|
+
if (limits.storeFrames && limits.retainTree !== false)
|
|
253
|
+
throw new TypeError("Stored XML frames require retainTree: false");
|
|
254
|
+
if (limits.fragmentContent && limits.retainTree !== false)
|
|
255
|
+
throw new TypeError("XML content fragments require retainTree: false");
|
|
256
|
+
const source = new XmlSource(length);
|
|
186
257
|
const maxDepth = limits.maxDepth ?? Infinity;
|
|
187
258
|
const maxNodes = limits.maxNodes ?? Infinity;
|
|
188
259
|
const maxAttributes = limits.maxAttributes ?? Infinity;
|
|
189
260
|
const retainContent = limits.retainContent !== false;
|
|
261
|
+
const retainTree = limits.retainTree !== false;
|
|
262
|
+
const retain = retainTree && retainContent;
|
|
190
263
|
const maxContentNodes = limits.maxContentNodes ?? Infinity;
|
|
191
264
|
const emptyContent = Object.freeze([]);
|
|
192
265
|
const emptyAttributes = Object.freeze([]);
|
|
193
266
|
const emptyNamespaces = new Map();
|
|
194
267
|
const maxAttributesPerElement = limits.maxAttributesPerElement ?? Infinity;
|
|
195
268
|
const maxNamespaces = limits.maxNamespaces ?? Infinity;
|
|
196
|
-
for (const limit of [limits.maxDepth, limits.maxNodes, limits.maxAttributes, limits.maxContentNodes, limits.maxAttributesPerElement, limits.maxNamespaces]) {
|
|
197
|
-
if (limit !== undefined && ((limit !== Infinity && !Number.isSafeInteger(limit)) || limit < 1))
|
|
198
|
-
throw new RangeError("XML limits must be positive integers");
|
|
199
|
-
}
|
|
200
269
|
const maxTextLength = limits.maxTextLength ?? Infinity;
|
|
201
|
-
if (limits.maxTextLength !== undefined && ((maxTextLength !== Infinity && !Number.isSafeInteger(maxTextLength)) || maxTextLength < 1))
|
|
202
|
-
throw new RangeError("XML limits must be positive integers");
|
|
203
270
|
let textLength = 0;
|
|
204
271
|
const admitText = (text) => {
|
|
205
272
|
if (text.length > maxTextLength - textLength)
|
|
206
273
|
throw new XmlLimitError("maxTextLength", "XML text limit exceeded");
|
|
207
274
|
textLength += text.length;
|
|
208
275
|
};
|
|
209
|
-
const
|
|
210
|
-
const start = input.charCodeAt(0) === 0xfeff ? 1 : 0;
|
|
211
|
-
let chunkStart = start;
|
|
212
|
-
let chunkLength = 0;
|
|
213
|
-
let normalizedChunk = "";
|
|
214
|
-
// Keep validated spans intact; only changed line endings need new strings.
|
|
215
|
-
for (let index = start; index < input.length; index++) {
|
|
216
|
-
const point = input.codePointAt(index);
|
|
217
|
-
if (!validCharacter(point))
|
|
218
|
-
invalid("invalid character");
|
|
219
|
-
if (point === 13) {
|
|
220
|
-
normalizedChunk += input.slice(chunkStart, index) + "\n";
|
|
221
|
-
if (input.charCodeAt(index + 1) === 10)
|
|
222
|
-
index++;
|
|
223
|
-
chunkStart = index + 1;
|
|
224
|
-
chunkLength++;
|
|
225
|
-
}
|
|
226
|
-
else {
|
|
227
|
-
chunkLength += point > 0xffff ? 2 : 1;
|
|
228
|
-
if (point > 0xffff)
|
|
229
|
-
index++;
|
|
230
|
-
}
|
|
231
|
-
if (chunkLength >= 512) {
|
|
232
|
-
if (normalizedChunk) {
|
|
233
|
-
chunks.push(normalizedChunk + input.slice(chunkStart, index + 1));
|
|
234
|
-
normalizedChunk = "";
|
|
235
|
-
chunkStart = index + 1;
|
|
236
|
-
}
|
|
237
|
-
chunkLength = 0;
|
|
238
|
-
yield 512;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
if (chunkLength)
|
|
242
|
-
yield chunkLength;
|
|
243
|
-
if (chunks.length || normalizedChunk)
|
|
244
|
-
chunks.push(normalizedChunk + input.slice(chunkStart));
|
|
245
|
-
const source = chunks.length ? chunks.join("") : input.slice(start);
|
|
246
|
-
const stack = [];
|
|
276
|
+
const stack = new XmlFrames(limits.storeFrames === true);
|
|
247
277
|
let root;
|
|
248
278
|
const prolog = [];
|
|
249
279
|
const epilog = [];
|
|
@@ -261,8 +291,8 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
261
291
|
let pendingWork = 0;
|
|
262
292
|
const skipWhitespace = function* () {
|
|
263
293
|
const start = offset;
|
|
264
|
-
while (
|
|
265
|
-
const c = source.charCodeAt(offset);
|
|
294
|
+
while (yield* source.has(offset)) {
|
|
295
|
+
const c = (yield* source.charCodeAt(offset));
|
|
266
296
|
if (c !== 32 && c !== 9 && c !== 10 && c !== 13)
|
|
267
297
|
break;
|
|
268
298
|
offset++;
|
|
@@ -276,8 +306,8 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
276
306
|
};
|
|
277
307
|
const scanName = function* () {
|
|
278
308
|
const start = offset;
|
|
279
|
-
while (
|
|
280
|
-
const c = source.charCodeAt(offset);
|
|
309
|
+
while (yield* source.has(offset)) {
|
|
310
|
+
const c = (yield* source.charCodeAt(offset));
|
|
281
311
|
if (c === 32 || c === 9 || c === 13 || c === 10 || c === 47 || c === 61 || c === 62 || c === 63)
|
|
282
312
|
break;
|
|
283
313
|
offset++;
|
|
@@ -287,7 +317,7 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
287
317
|
pendingWork -= 512;
|
|
288
318
|
}
|
|
289
319
|
}
|
|
290
|
-
const name = source.slice(start, offset);
|
|
320
|
+
const name = (yield* source.slice(start, offset));
|
|
291
321
|
pendingWork += offset - start;
|
|
292
322
|
while (pendingWork >= 512) {
|
|
293
323
|
yield 512;
|
|
@@ -296,7 +326,34 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
296
326
|
const [prefix, localName] = qualifiedNameSync(name, qualifiedNames);
|
|
297
327
|
return [name, prefix, localName];
|
|
298
328
|
};
|
|
299
|
-
|
|
329
|
+
const rawContent = function* (kind, start, end, parent, target = "") {
|
|
330
|
+
if (end - start > maxTextLength - textLength)
|
|
331
|
+
throw new XmlLimitError("maxTextLength", "XML text limit exceeded");
|
|
332
|
+
textLength += end - start;
|
|
333
|
+
admitContent();
|
|
334
|
+
let cursor = start, continuation = false;
|
|
335
|
+
do {
|
|
336
|
+
let finish = limits.fragmentContent ? Math.min(end, cursor + 512) : end;
|
|
337
|
+
if (finish < end) {
|
|
338
|
+
const last = yield* source.charCodeAt(finish - 1);
|
|
339
|
+
if (last >= 0xd800 && last <= 0xdbff)
|
|
340
|
+
finish--;
|
|
341
|
+
}
|
|
342
|
+
const text = yield* source.slice(cursor, finish);
|
|
343
|
+
const content = kind === "processing-instruction" ? { kind, target, text } : { kind, text };
|
|
344
|
+
if (kind === "cdata" && retainTree)
|
|
345
|
+
parent.element.text += text;
|
|
346
|
+
if (retain)
|
|
347
|
+
(parent?.content ?? (root ? epilog : prolog)).push(content);
|
|
348
|
+
limits.events?.({ type: "content", content, parent: parent?.element,
|
|
349
|
+
...(continuation ? { continuation: true } : {}) });
|
|
350
|
+
if (limits.fragmentContent)
|
|
351
|
+
yield Math.max(1, text.length);
|
|
352
|
+
continuation = true;
|
|
353
|
+
cursor = finish;
|
|
354
|
+
} while (cursor < end);
|
|
355
|
+
};
|
|
356
|
+
while (yield* source.has(offset)) {
|
|
300
357
|
pendingWork += 1;
|
|
301
358
|
if (pendingWork >= 512) {
|
|
302
359
|
while (pendingWork >= 512) {
|
|
@@ -304,10 +361,53 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
304
361
|
pendingWork -= 512;
|
|
305
362
|
}
|
|
306
363
|
}
|
|
307
|
-
if (source.charCodeAt(offset) !== 60) {
|
|
308
|
-
const next = source.indexOf("<", offset);
|
|
364
|
+
if ((yield* source.charCodeAt(offset)) !== 60) {
|
|
365
|
+
const next = (yield* source.indexOf("<", offset));
|
|
309
366
|
const endPos = next < 0 ? source.length : next;
|
|
310
|
-
|
|
367
|
+
if (limits.fragmentContent) {
|
|
368
|
+
if ((yield* source.indexOf("]]>", offset, endPos)) >= 0)
|
|
369
|
+
invalid("CDATA terminator in text");
|
|
370
|
+
const parent = yield* stack.peek();
|
|
371
|
+
let size = 0, outsideText = false;
|
|
372
|
+
// Validate before emitting any part of the logical node. This preserves
|
|
373
|
+
// entity/recovery diagnostics and limit precedence without retaining it.
|
|
374
|
+
for (const part of textFragments(source, offset, endPos, !!parent, limits.recover)) {
|
|
375
|
+
if (typeof part !== "string") {
|
|
376
|
+
yield part;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
size += part.length;
|
|
380
|
+
if (!parent)
|
|
381
|
+
for (const character of part) {
|
|
382
|
+
if (!" \t\r\n".includes(character))
|
|
383
|
+
outsideText = true;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (size > maxTextLength - textLength)
|
|
387
|
+
throw new XmlLimitError("maxTextLength", "XML text limit exceeded");
|
|
388
|
+
textLength += size;
|
|
389
|
+
if (outsideText)
|
|
390
|
+
invalid("text outside the root");
|
|
391
|
+
if (size) {
|
|
392
|
+
admitContent();
|
|
393
|
+
if (limits.events) {
|
|
394
|
+
let continuation = false;
|
|
395
|
+
for (const part of textFragments(source, offset, endPos, !!parent, limits.recover ? () => { } : undefined)) {
|
|
396
|
+
if (typeof part !== "string") {
|
|
397
|
+
yield part;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
limits.events({ type: "content", content: { kind: "text", text: part }, parent: parent?.element,
|
|
401
|
+
...(continuation ? { continuation: true } : {}) });
|
|
402
|
+
continuation = true;
|
|
403
|
+
yield 1;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
offset = endPos;
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
const text = (yield* source.slice(offset, endPos));
|
|
311
411
|
pendingWork += (endPos - offset) + text.length;
|
|
312
412
|
while (pendingWork >= 512) {
|
|
313
413
|
yield 512;
|
|
@@ -321,13 +421,15 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
321
421
|
pendingWork -= 512;
|
|
322
422
|
}
|
|
323
423
|
admitText(resolved);
|
|
324
|
-
const parent = stack.
|
|
424
|
+
const parent = (yield* stack.peek());
|
|
325
425
|
if (parent) {
|
|
326
|
-
|
|
426
|
+
if (retainTree)
|
|
427
|
+
parent.element.text += resolved;
|
|
327
428
|
if (resolved.length) {
|
|
328
429
|
admitContent();
|
|
329
|
-
if (
|
|
430
|
+
if (retain)
|
|
330
431
|
parent.content.push({ kind: "text", text: resolved });
|
|
432
|
+
limits.events?.({ type: "content", content: { kind: "text", text: resolved }, parent: parent.element });
|
|
331
433
|
}
|
|
332
434
|
}
|
|
333
435
|
else {
|
|
@@ -347,42 +449,31 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
347
449
|
}
|
|
348
450
|
if (resolved.length) {
|
|
349
451
|
admitContent();
|
|
350
|
-
if (
|
|
452
|
+
if (retain)
|
|
351
453
|
(root ? epilog : prolog).push({ kind: "text", text: resolved });
|
|
454
|
+
limits.events?.({ type: "content", content: { kind: "text", text: resolved }, parent: undefined });
|
|
352
455
|
}
|
|
353
456
|
}
|
|
354
457
|
offset = endPos;
|
|
355
458
|
}
|
|
356
|
-
else if (source.startsWith("<!--", offset)) {
|
|
459
|
+
else if ((yield* source.startsWith("<!--", offset))) {
|
|
357
460
|
const end = yield* find(source, "-->", offset + 4);
|
|
358
|
-
if (end < 0 || (yield*
|
|
461
|
+
if (end < 0 || (yield* source.indexOf("--", offset + 4, end)) >= 0
|
|
462
|
+
|| (end > offset + 4 && (yield* source.charCodeAt(end - 1)) === 45))
|
|
359
463
|
invalid("malformed comment");
|
|
360
|
-
|
|
361
|
-
const parent = stack.at(-1);
|
|
362
|
-
const text = source.slice(offset + 4, end);
|
|
363
|
-
admitText(text);
|
|
364
|
-
admitContent();
|
|
365
|
-
if (retainContent) {
|
|
366
|
-
(parent?.content ?? (root ? epilog : prolog)).push({ kind: "comment", text });
|
|
367
|
-
}
|
|
464
|
+
yield* rawContent("comment", offset + 4, end, yield* stack.peek());
|
|
368
465
|
offset = end + 3;
|
|
369
466
|
}
|
|
370
|
-
else if (source.startsWith("<![CDATA[", offset)) {
|
|
467
|
+
else if ((yield* source.startsWith("<![CDATA[", offset))) {
|
|
371
468
|
if (!stack.length)
|
|
372
469
|
invalid("CDATA outside root");
|
|
373
470
|
const end = yield* find(source, "]]>", offset + 9);
|
|
374
471
|
if (end < 0)
|
|
375
472
|
invalid("unterminated CDATA");
|
|
376
|
-
|
|
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 });
|
|
473
|
+
yield* rawContent("cdata", offset + 9, end, yield* stack.peek());
|
|
383
474
|
offset = end + 3;
|
|
384
475
|
}
|
|
385
|
-
else if (source.startsWith("<?", offset)) {
|
|
476
|
+
else if ((yield* source.startsWith("<?", offset))) {
|
|
386
477
|
const start = offset;
|
|
387
478
|
offset += 2;
|
|
388
479
|
const [target] = yield* scanName();
|
|
@@ -393,40 +484,38 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
393
484
|
const end = yield* find(source, "?>", offset);
|
|
394
485
|
if (end < 0)
|
|
395
486
|
invalid("unterminated processing instruction");
|
|
396
|
-
const content = source.slice(offset, end);
|
|
397
487
|
if (target.length === 3 && target.toLowerCase() === "xml") {
|
|
488
|
+
const content = yield* source.slice(offset, end);
|
|
398
489
|
if (start !== 0 || target !== "xml"
|
|
399
|
-
|| !(yield* validDeclaration(content, limits.expectedEncoding)))
|
|
490
|
+
|| !(yield* validDeclaration(content, limits.expectedEncoding)))
|
|
400
491
|
invalid("unsupported XML declaration");
|
|
401
|
-
}
|
|
402
492
|
if (retainContent)
|
|
403
|
-
declaration = source.slice(start, end + 2);
|
|
493
|
+
declaration = yield* source.slice(start, end + 2);
|
|
404
494
|
}
|
|
405
|
-
else
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
495
|
+
else {
|
|
496
|
+
if (offset < end && !" \t\n\r".includes(yield* source.slice(offset, offset + 1)))
|
|
497
|
+
invalid("invalid processing instruction");
|
|
498
|
+
let body = offset;
|
|
499
|
+
while (body < end && " \t\n\r".includes(yield* source.slice(body, body + 1))) {
|
|
500
|
+
body++;
|
|
501
|
+
pendingWork++;
|
|
502
|
+
if (pendingWork >= 512) {
|
|
503
|
+
yield 512;
|
|
504
|
+
pendingWork -= 512;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
413
507
|
while (pendingWork >= 512) {
|
|
414
508
|
yield 512;
|
|
415
509
|
pendingWork -= 512;
|
|
416
510
|
}
|
|
417
|
-
|
|
418
|
-
admitText(text);
|
|
419
|
-
admitContent();
|
|
420
|
-
if (retainContent) {
|
|
421
|
-
(parent?.content ?? (root ? epilog : prolog)).push({ kind: "processing-instruction", target, text });
|
|
422
|
-
}
|
|
511
|
+
yield* rawContent("processing-instruction", body, end, yield* stack.peek(), target);
|
|
423
512
|
}
|
|
424
513
|
offset = end + 2;
|
|
425
514
|
}
|
|
426
|
-
else if (source.startsWith("<!", offset)) {
|
|
515
|
+
else if ((yield* source.startsWith("<!", offset))) {
|
|
427
516
|
invalid("DTD and entity declarations are forbidden");
|
|
428
517
|
}
|
|
429
|
-
else if (source.charCodeAt(offset + 1) === 47) {
|
|
518
|
+
else if ((yield* source.charCodeAt(offset + 1)) === 47) {
|
|
430
519
|
offset += 2;
|
|
431
520
|
const [name] = yield* scanName();
|
|
432
521
|
yield* skipWhitespace();
|
|
@@ -434,15 +523,18 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
434
523
|
yield 512;
|
|
435
524
|
pendingWork -= 512;
|
|
436
525
|
}
|
|
437
|
-
|
|
526
|
+
const closed = (yield* source.slice(offset++, offset)) === ">" ? (yield* stack.pop()) : undefined;
|
|
527
|
+
if (closed?.name !== name) {
|
|
438
528
|
if (!limits.recover)
|
|
439
529
|
invalid("mismatched closing tag");
|
|
440
530
|
limits.recover("mismatched closing tag");
|
|
441
531
|
}
|
|
532
|
+
if (closed)
|
|
533
|
+
limits.events?.({ type: "close", element: closed.element, parent: (yield* stack.peek())?.element });
|
|
442
534
|
}
|
|
443
535
|
else {
|
|
444
536
|
offset++;
|
|
445
|
-
const repeated = previousEmpty && source.startsWith(previousEmpty.suffix, offset) ? previousEmpty : undefined;
|
|
537
|
+
const repeated = previousEmpty && (yield* source.startsWith(previousEmpty.suffix, offset)) ? previousEmpty : undefined;
|
|
446
538
|
const [name, prefix, localName] = repeated
|
|
447
539
|
? [repeated.name, repeated.prefix, repeated.localName]
|
|
448
540
|
: (yield* scanName());
|
|
@@ -457,7 +549,7 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
457
549
|
}
|
|
458
550
|
let attributes;
|
|
459
551
|
const attrMeta = [];
|
|
460
|
-
let namespaces = stack.
|
|
552
|
+
let namespaces = (yield* stack.peek())?.namespaces ?? new Map([["xml", xmlNamespace]]);
|
|
461
553
|
let ownsNamespaces = stack.length === 0;
|
|
462
554
|
while (!repeated) {
|
|
463
555
|
const ws = (yield* skipWhitespace());
|
|
@@ -465,7 +557,7 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
465
557
|
yield 512;
|
|
466
558
|
pendingWork -= 512;
|
|
467
559
|
}
|
|
468
|
-
const ch = source.charCodeAt(offset);
|
|
560
|
+
const ch = (yield* source.charCodeAt(offset));
|
|
469
561
|
if (ch === 47 || ch === 62 || limits.recover && offset === source.length)
|
|
470
562
|
break;
|
|
471
563
|
if (ws === 0)
|
|
@@ -482,13 +574,13 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
482
574
|
if ((attributes?.size ?? 0) >= maxAttributesPerElement)
|
|
483
575
|
throw new XmlLimitError("maxAttributesPerElement", "XML attribute limit exceeded");
|
|
484
576
|
yield* skipWhitespace();
|
|
485
|
-
if (source
|
|
577
|
+
if ((yield* source.slice(offset++, offset)) !== "=")
|
|
486
578
|
invalid("missing attribute equals");
|
|
487
579
|
yield* skipWhitespace();
|
|
488
|
-
const quote = source
|
|
580
|
+
const quote = (yield* source.slice(offset++, offset));
|
|
489
581
|
if (quote !== '"' && quote !== "'")
|
|
490
582
|
invalid("unquoted attribute");
|
|
491
|
-
const end = source.indexOf(quote, offset);
|
|
583
|
+
const end = (yield* source.indexOf(quote, offset));
|
|
492
584
|
const scanLen = (end < 0 ? source.length : end) - offset;
|
|
493
585
|
pendingWork += scanLen;
|
|
494
586
|
while (pendingWork >= 512) {
|
|
@@ -497,7 +589,7 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
497
589
|
}
|
|
498
590
|
if (end < 0)
|
|
499
591
|
invalid("unterminated attribute");
|
|
500
|
-
const raw = source.slice(offset, end);
|
|
592
|
+
const raw = (yield* source.slice(offset, end));
|
|
501
593
|
pendingWork += raw.length * 2;
|
|
502
594
|
while (pendingWork >= 512) {
|
|
503
595
|
yield 512;
|
|
@@ -564,7 +656,7 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
564
656
|
if (stack.length + 1 > maxDepth)
|
|
565
657
|
throw new XmlLimitError("maxDepth", "XML resource limit exceeded");
|
|
566
658
|
const namespace = namespaces.get(prefix) ?? "";
|
|
567
|
-
limits.onElement?.({ name, namespace, localName }, stack.
|
|
659
|
+
limits.onElement?.({ name, namespace, localName }, (yield* stack.peek())?.element, stack.length + 1);
|
|
568
660
|
admitContent();
|
|
569
661
|
const retainedAttributes = [];
|
|
570
662
|
if (retainContent && attributes) {
|
|
@@ -581,31 +673,36 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
581
673
|
yield 512;
|
|
582
674
|
pendingWork -= 512;
|
|
583
675
|
}
|
|
584
|
-
const content =
|
|
676
|
+
const content = retain ? [] : undefined;
|
|
585
677
|
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 }) } : {}) };
|
|
586
|
-
const parent = stack.
|
|
678
|
+
const parent = (yield* stack.peek());
|
|
587
679
|
if (parent) {
|
|
588
|
-
|
|
589
|
-
|
|
680
|
+
if (retainTree) {
|
|
681
|
+
parent.element.children.push(element);
|
|
682
|
+
parent.content?.push(element);
|
|
683
|
+
}
|
|
590
684
|
}
|
|
591
685
|
else if (root)
|
|
592
686
|
invalid("multiple root elements");
|
|
593
687
|
else
|
|
594
688
|
root = element;
|
|
595
|
-
const empty = source
|
|
689
|
+
const empty = (yield* source.slice(offset, offset + 1)) === "/";
|
|
596
690
|
if (empty)
|
|
597
691
|
offset++;
|
|
598
|
-
if (source
|
|
692
|
+
if ((yield* source.slice(offset++, offset)) !== ">") {
|
|
599
693
|
if (!limits.recover)
|
|
600
694
|
invalid("unterminated start tag");
|
|
601
695
|
limits.recover("unterminated start tag");
|
|
602
696
|
}
|
|
603
697
|
// Only cache compact, attribute-free spellings. Namespace resolution and
|
|
604
698
|
// every admission counter still run for each distinct physical element.
|
|
605
|
-
if (empty && !attributes && name.length <= 512 && source.slice(offset - name.length - 3, offset) === `<${name}/>`)
|
|
699
|
+
if (empty && !attributes && name.length <= 512 && (yield* source.slice(offset - name.length - 3, offset)) === `<${name}/>`)
|
|
606
700
|
previousEmpty = { suffix: name + "/>", name, prefix, localName };
|
|
701
|
+
limits.events?.({ type: "open", element, parent: parent?.element });
|
|
607
702
|
if (!empty)
|
|
608
|
-
stack.push({ element, content, name, namespaces });
|
|
703
|
+
yield* stack.push({ element, content, name, namespaces });
|
|
704
|
+
else
|
|
705
|
+
limits.events?.({ type: "close", element, parent: parent?.element });
|
|
609
706
|
}
|
|
610
707
|
}
|
|
611
708
|
if (pendingWork > 0) {
|
|
@@ -618,9 +715,73 @@ export function* parseXmlSteps(input, limits = {}) {
|
|
|
618
715
|
if (!limits.recover)
|
|
619
716
|
invalid("incomplete document");
|
|
620
717
|
limits.recover("incomplete document");
|
|
718
|
+
while (stack.length) {
|
|
719
|
+
const closed = (yield* stack.pop());
|
|
720
|
+
limits.events?.({ type: "close", element: closed.element, parent: (yield* stack.peek())?.element });
|
|
721
|
+
yield 1;
|
|
722
|
+
}
|
|
621
723
|
}
|
|
622
724
|
return root;
|
|
623
725
|
}
|
|
726
|
+
function* normalizeXmlSteps(input) {
|
|
727
|
+
const chunks = [];
|
|
728
|
+
const start = input.charCodeAt(0) === 0xfeff ? 1 : 0;
|
|
729
|
+
let chunkStart = start;
|
|
730
|
+
let chunkLength = 0;
|
|
731
|
+
let normalizedChunk = "";
|
|
732
|
+
// Keep validated spans intact; only changed line endings need new strings.
|
|
733
|
+
for (let index = start; index < input.length; index++) {
|
|
734
|
+
const point = input.codePointAt(index);
|
|
735
|
+
if (!validCharacter(point))
|
|
736
|
+
invalid("invalid character");
|
|
737
|
+
if (point === 13) {
|
|
738
|
+
normalizedChunk += input.slice(chunkStart, index) + "\n";
|
|
739
|
+
if (input.charCodeAt(index + 1) === 10)
|
|
740
|
+
index++;
|
|
741
|
+
chunkStart = index + 1;
|
|
742
|
+
chunkLength++;
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
chunkLength += point > 0xffff ? 2 : 1;
|
|
746
|
+
if (point > 0xffff)
|
|
747
|
+
index++;
|
|
748
|
+
}
|
|
749
|
+
if (chunkLength >= 512) {
|
|
750
|
+
if (normalizedChunk) {
|
|
751
|
+
chunks.push(normalizedChunk + input.slice(chunkStart, index + 1));
|
|
752
|
+
normalizedChunk = "";
|
|
753
|
+
chunkStart = index + 1;
|
|
754
|
+
}
|
|
755
|
+
chunkLength = 0;
|
|
756
|
+
yield 512;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
if (chunkLength)
|
|
760
|
+
yield chunkLength;
|
|
761
|
+
if (chunks.length || normalizedChunk)
|
|
762
|
+
chunks.push(normalizedChunk + input.slice(chunkStart));
|
|
763
|
+
return chunks.length ? chunks.join("") : input.slice(start);
|
|
764
|
+
}
|
|
765
|
+
export function* parseXmlSteps(input, limits = {}) {
|
|
766
|
+
validateLimits(limits);
|
|
767
|
+
const source = yield* normalizeXmlSteps(input);
|
|
768
|
+
const parser = parseXmlSourceSteps(source.length, limits);
|
|
769
|
+
let step = parser.next();
|
|
770
|
+
try {
|
|
771
|
+
while (!step.done) {
|
|
772
|
+
if (typeof step.value === "number")
|
|
773
|
+
yield step.value;
|
|
774
|
+
else
|
|
775
|
+
step.value.value = source.slice(step.value.offset, step.value.offset + step.value.length);
|
|
776
|
+
step = parser.next();
|
|
777
|
+
}
|
|
778
|
+
return step.value;
|
|
779
|
+
}
|
|
780
|
+
finally {
|
|
781
|
+
if (!step.done)
|
|
782
|
+
parser.return(undefined);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
624
785
|
export function parseXml(input, limits = {}) {
|
|
625
786
|
const parser = parseXmlSteps(input, limits);
|
|
626
787
|
let result = parser.next();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Fill value before advancing. Streaming reads may be short; mark complete at EOF. */
|
|
2
|
+
export interface XmlSourceRead {
|
|
3
|
+
readonly offset: number;
|
|
4
|
+
readonly length: number;
|
|
5
|
+
readonly streaming?: boolean;
|
|
6
|
+
value?: string;
|
|
7
|
+
complete?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export type XmlSourceStep = number | XmlSourceRead;
|
|
10
|
+
/** One fixed window; individual returned tokens still have their own size cost. */
|
|
11
|
+
export declare class XmlSource {
|
|
12
|
+
private window;
|
|
13
|
+
private start;
|
|
14
|
+
private end;
|
|
15
|
+
private readonly streaming;
|
|
16
|
+
constructor(length: number | undefined);
|
|
17
|
+
get length(): number;
|
|
18
|
+
private load;
|
|
19
|
+
has(offset: number): Generator<XmlSourceStep, boolean, void>;
|
|
20
|
+
slice(start: number, end?: number): Generator<XmlSourceStep, string, void>;
|
|
21
|
+
charCodeAt(offset: number): Generator<XmlSourceStep, number, void>;
|
|
22
|
+
startsWith(value: string, offset: number): Generator<XmlSourceStep, boolean, void>;
|
|
23
|
+
indexOf(value: string, offset: number, end?: number): Generator<XmlSourceStep, number, void>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** One fixed window; individual returned tokens still have their own size cost. */
|
|
2
|
+
export class XmlSource {
|
|
3
|
+
window = '';
|
|
4
|
+
start = -1;
|
|
5
|
+
end;
|
|
6
|
+
streaming;
|
|
7
|
+
constructor(length) {
|
|
8
|
+
if (length !== undefined && (!Number.isSafeInteger(length) || length < 0))
|
|
9
|
+
throw new RangeError('Invalid XML source length');
|
|
10
|
+
this.streaming = length === undefined;
|
|
11
|
+
this.end = length ?? Infinity;
|
|
12
|
+
}
|
|
13
|
+
get length() { return this.end; }
|
|
14
|
+
*load(offset) {
|
|
15
|
+
if (offset >= this.end || offset >= this.start && offset < this.start + this.window.length)
|
|
16
|
+
return;
|
|
17
|
+
const start = this.streaming ? offset : Math.floor(offset / 4096) * 4096;
|
|
18
|
+
const request = { offset: start, length: Math.min(this.streaming ? 512 : 4096, this.end - start),
|
|
19
|
+
...(this.streaming ? { streaming: true } : {}) };
|
|
20
|
+
yield request;
|
|
21
|
+
if (typeof request.value !== 'string' || request.value.length > request.length ||
|
|
22
|
+
(!this.streaming && request.value.length !== request.length) ||
|
|
23
|
+
(!request.value.length && !request.complete && this.streaming))
|
|
24
|
+
throw new TypeError('Incomplete XML source read');
|
|
25
|
+
if (this.streaming && request.complete)
|
|
26
|
+
this.end = start + request.value.length;
|
|
27
|
+
this.window = request.value;
|
|
28
|
+
this.start = start;
|
|
29
|
+
}
|
|
30
|
+
*has(offset) {
|
|
31
|
+
yield* this.load(offset);
|
|
32
|
+
return offset < this.end;
|
|
33
|
+
}
|
|
34
|
+
*slice(start, end = this.length) {
|
|
35
|
+
start = start < 0 ? Math.max(0, this.length + start) : Math.min(start, this.length);
|
|
36
|
+
end = end < 0 ? Math.max(0, this.length + end) : Math.min(end, this.length);
|
|
37
|
+
let result = '';
|
|
38
|
+
while (start < Math.min(end, this.length)) {
|
|
39
|
+
yield* this.load(start);
|
|
40
|
+
if (start >= this.end)
|
|
41
|
+
break;
|
|
42
|
+
const stop = Math.min(end, this.start + this.window.length);
|
|
43
|
+
result += this.window.slice(start - this.start, stop - this.start);
|
|
44
|
+
start = stop;
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
*charCodeAt(offset) {
|
|
49
|
+
return (yield* this.slice(offset, offset + 1)).charCodeAt(0);
|
|
50
|
+
}
|
|
51
|
+
*startsWith(value, offset) {
|
|
52
|
+
for (let index = 0; index < value.length; index++) {
|
|
53
|
+
if ((yield* this.charCodeAt(offset + index)) !== value.charCodeAt(index))
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
*indexOf(value, offset, end = Infinity) {
|
|
59
|
+
let tail = '';
|
|
60
|
+
while (offset < end && (yield* this.has(offset))) {
|
|
61
|
+
const stop = Math.min(end, this.start + this.window.length);
|
|
62
|
+
const part = tail + this.window.slice(offset - this.start, stop - this.start);
|
|
63
|
+
const found = part.indexOf(value);
|
|
64
|
+
if (found >= 0)
|
|
65
|
+
return offset - tail.length + found;
|
|
66
|
+
tail = value.length > 1 ? part.slice(-(value.length - 1)) : '';
|
|
67
|
+
offset = stop;
|
|
68
|
+
}
|
|
69
|
+
return -1;
|
|
70
|
+
}
|
|
71
|
+
}
|
package/dist/xml-ast/stream.d.ts
CHANGED