@poe-platform/safe-fs 0.1.753 → 0.1.754
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/fs/memory/index.js +11 -11
- package/dist/safe-fs/xml.d.ts +1 -45
- package/dist/safe-fs/xml.js +1 -598
- package/dist/xml-ast/index.d.ts +45 -0
- package/dist/xml-ast/index.js +598 -0
- package/package.json +1 -1
|
@@ -1488,10 +1488,10 @@ export class MemoryFileSystem {
|
|
|
1488
1488
|
pooled.byteLength = length;
|
|
1489
1489
|
pooled.view = view;
|
|
1490
1490
|
pooled.allocation = allocation;
|
|
1491
|
-
pooled.sourceRef = data;
|
|
1491
|
+
pooled.sourceRef = pooled.data.subarray(0);
|
|
1492
1492
|
return pooled;
|
|
1493
1493
|
}
|
|
1494
|
-
return new MemoryFileNode(fileMode, this.nextInode++, now, length, allocation, view, data);
|
|
1494
|
+
return new MemoryFileNode(fileMode, this.nextInode++, now, length, allocation, view, (view ?? allocation.data).subarray(0, length));
|
|
1495
1495
|
}, syscall, path);
|
|
1496
1496
|
}
|
|
1497
1497
|
catch (error) {
|
|
@@ -1687,10 +1687,10 @@ export class MemoryFileSystem {
|
|
|
1687
1687
|
node.byteLength = length;
|
|
1688
1688
|
node.view = view;
|
|
1689
1689
|
node.allocation = allocation;
|
|
1690
|
-
node.sourceRef = data;
|
|
1690
|
+
node.sourceRef = node.data.subarray(0);
|
|
1691
1691
|
}
|
|
1692
1692
|
else {
|
|
1693
|
-
node = new MemoryFileNode(fileMode, this.nextInode++, now, length, allocation, view, data);
|
|
1693
|
+
node = new MemoryFileNode(fileMode, this.nextInode++, now, length, allocation, view, (view ?? allocation.data).subarray(0, length));
|
|
1694
1694
|
}
|
|
1695
1695
|
const prevNlink = parent.cachedNlinkRev === parent.revision ? parent.cachedNlink : (parent.entries.size === 0 ? 2 : undefined);
|
|
1696
1696
|
parent.entries.set(name, node);
|
|
@@ -1777,9 +1777,9 @@ export class MemoryFileSystem {
|
|
|
1777
1777
|
node.ctimeMs = now;
|
|
1778
1778
|
node.birthtimeMs = now;
|
|
1779
1779
|
node.byteLength = data.byteLength;
|
|
1780
|
-
node.view = data;
|
|
1780
|
+
node.view = new Uint8Array(data);
|
|
1781
1781
|
node.allocation = DUMMY_POOL_ALLOCATION;
|
|
1782
|
-
node.sourceRef = data;
|
|
1782
|
+
node.sourceRef = node.data.subarray(0);
|
|
1783
1783
|
const entryIdx = nextIdx++;
|
|
1784
1784
|
keys[entryIdx] = name;
|
|
1785
1785
|
vals[entryIdx] = node;
|
|
@@ -1834,9 +1834,9 @@ export class MemoryFileSystem {
|
|
|
1834
1834
|
node.ctimeMs = now;
|
|
1835
1835
|
node.birthtimeMs = now;
|
|
1836
1836
|
node.byteLength = length;
|
|
1837
|
-
node.view = data;
|
|
1837
|
+
node.view = new Uint8Array(data);
|
|
1838
1838
|
node.allocation = DUMMY_POOL_ALLOCATION;
|
|
1839
|
-
node.sourceRef = data;
|
|
1839
|
+
node.sourceRef = node.data.subarray(0);
|
|
1840
1840
|
const entryIdx = entries._next++;
|
|
1841
1841
|
entries._table[lastFastMapMiss.slot] = entryIdx;
|
|
1842
1842
|
entries._keys[entryIdx] = name;
|
|
@@ -1943,7 +1943,7 @@ export class MemoryFileSystem {
|
|
|
1943
1943
|
try {
|
|
1944
1944
|
const now = fastWriteCachedNow;
|
|
1945
1945
|
const fileMode = typeModes.file | mode;
|
|
1946
|
-
const view = zeroCopyConst ? data : (capacity === length ? allocation.data : undefined);
|
|
1946
|
+
const view = zeroCopyConst ? new Uint8Array(data) : (capacity === length ? allocation.data : undefined);
|
|
1947
1947
|
let node;
|
|
1948
1948
|
if (sharedFileNodePoolLen > 0) {
|
|
1949
1949
|
node = sharedFileNodePool[--sharedFileNodePoolLen];
|
|
@@ -1968,10 +1968,10 @@ export class MemoryFileSystem {
|
|
|
1968
1968
|
node.byteLength = length;
|
|
1969
1969
|
node.view = view;
|
|
1970
1970
|
node.allocation = allocation;
|
|
1971
|
-
node.sourceRef = data;
|
|
1971
|
+
node.sourceRef = node.data.subarray(0);
|
|
1972
1972
|
}
|
|
1973
1973
|
else {
|
|
1974
|
-
node = new MemoryFileNode(fileMode, this.nextInode++, now, length, allocation, view, data);
|
|
1974
|
+
node = new MemoryFileNode(fileMode, this.nextInode++, now, length, allocation, view, (view ?? allocation.data).subarray(0, length));
|
|
1975
1975
|
}
|
|
1976
1976
|
const prevNlink = parent.cachedNlinkRev === parent.revision ? parent.cachedNlink : (parent.entries.size === 0 ? 2 : undefined);
|
|
1977
1977
|
parent.entries.set(name, node);
|
package/dist/safe-fs/xml.d.ts
CHANGED
|
@@ -1,45 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
readonly name: string;
|
|
3
|
-
readonly namespace: string;
|
|
4
|
-
readonly localName: string;
|
|
5
|
-
}
|
|
6
|
-
export interface XmlAttribute extends XmlName {
|
|
7
|
-
readonly value: string;
|
|
8
|
-
}
|
|
9
|
-
export type XmlContent = XmlElement | {
|
|
10
|
-
readonly kind: "text" | "cdata" | "comment";
|
|
11
|
-
readonly text: string;
|
|
12
|
-
} | {
|
|
13
|
-
readonly kind: "processing-instruction";
|
|
14
|
-
readonly target: string;
|
|
15
|
-
readonly text: string;
|
|
16
|
-
};
|
|
17
|
-
export interface XmlElement extends XmlName {
|
|
18
|
-
readonly kind: "element";
|
|
19
|
-
readonly children: XmlElement[];
|
|
20
|
-
readonly content: readonly XmlContent[];
|
|
21
|
-
readonly attributes: readonly XmlAttribute[];
|
|
22
|
-
readonly namespaces: ReadonlyMap<string, string>;
|
|
23
|
-
text: string;
|
|
24
|
-
readonly declaration?: string;
|
|
25
|
-
readonly prolog?: readonly XmlContent[];
|
|
26
|
-
readonly epilog?: readonly XmlContent[];
|
|
27
|
-
}
|
|
28
|
-
export interface XmlLimits {
|
|
29
|
-
readonly expectedEncoding?: "UTF-8" | "UTF-16" | "UTF-16LE" | "UTF-16BE";
|
|
30
|
-
readonly retainContent?: boolean;
|
|
31
|
-
readonly maxDepth?: number;
|
|
32
|
-
readonly maxNodes?: number;
|
|
33
|
-
readonly maxAttributes?: number;
|
|
34
|
-
readonly maxAttributesPerElement?: number;
|
|
35
|
-
readonly maxNamespaces?: number;
|
|
36
|
-
readonly maxContentNodes?: number;
|
|
37
|
-
readonly maxTextLength?: number;
|
|
38
|
-
readonly onElement?: (element: XmlName, parent: XmlName | undefined, depth: number) => void;
|
|
39
|
-
}
|
|
40
|
-
export declare class XmlLimitError extends SyntaxError {
|
|
41
|
-
readonly limit: string;
|
|
42
|
-
constructor(limit: string, message: string);
|
|
43
|
-
}
|
|
44
|
-
export declare function parseXmlSteps(input: string, limits?: XmlLimits): Generator<number, XmlElement, void>;
|
|
45
|
-
export declare function parseXml(input: string, limits?: XmlLimits): XmlElement;
|
|
1
|
+
export * from "../xml-ast/index.js";
|
package/dist/safe-fs/xml.js
CHANGED
|
@@ -1,598 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
limit;
|
|
3
|
-
constructor(limit, message) {
|
|
4
|
-
super(message);
|
|
5
|
-
this.limit = limit;
|
|
6
|
-
}
|
|
7
|
-
}
|
|
8
|
-
function* find(source, needle, start) {
|
|
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;
|
|
15
|
-
}
|
|
16
|
-
if (remaining > 0)
|
|
17
|
-
yield remaining;
|
|
18
|
-
return found;
|
|
19
|
-
}
|
|
20
|
-
const xmlNamespace = "http://www.w3.org/XML/1998/namespace";
|
|
21
|
-
const xmlnsNamespace = "http://www.w3.org/2000/xmlns/";
|
|
22
|
-
function invalid(message) {
|
|
23
|
-
throw new SyntaxError(`Invalid XML: ${message}`);
|
|
24
|
-
}
|
|
25
|
-
function validCharacter(point) {
|
|
26
|
-
return point === 9 || point === 10 || point === 13
|
|
27
|
-
|| (point >= 0x20 && point <= 0xd7ff)
|
|
28
|
-
|| (point >= 0xe000 && point <= 0xfffd)
|
|
29
|
-
|| (point >= 0x10000 && point <= 0x10ffff);
|
|
30
|
-
}
|
|
31
|
-
function nameStart(point) {
|
|
32
|
-
return point === 95 || (point >= 65 && point <= 90) || (point >= 97 && point <= 122)
|
|
33
|
-
|| (point >= 0xc0 && point <= 0xd6) || (point >= 0xd8 && point <= 0xf6)
|
|
34
|
-
|| (point >= 0xf8 && point <= 0x2ff) || (point >= 0x370 && point <= 0x37d)
|
|
35
|
-
|| (point >= 0x37f && point <= 0x1fff) || (point >= 0x200c && point <= 0x200d)
|
|
36
|
-
|| (point >= 0x2070 && point <= 0x218f) || (point >= 0x2c00 && point <= 0x2fef)
|
|
37
|
-
|| (point >= 0x3001 && point <= 0xd7ff) || (point >= 0xf900 && point <= 0xfdcf)
|
|
38
|
-
|| (point >= 0xfdf0 && point <= 0xfffd) || (point >= 0x10000 && point <= 0xeffff);
|
|
39
|
-
}
|
|
40
|
-
function namePart(point) {
|
|
41
|
-
return nameStart(point) || point === 45 || point === 46 || point === 0xb7
|
|
42
|
-
|| (point >= 48 && point <= 57) || (point >= 0x300 && point <= 0x36f)
|
|
43
|
-
|| (point >= 0x203f && point <= 0x2040);
|
|
44
|
-
}
|
|
45
|
-
function qualifiedNameSync(name, cache) {
|
|
46
|
-
if (cache.name === name)
|
|
47
|
-
return cache.parts;
|
|
48
|
-
let prefix = "";
|
|
49
|
-
let start = 0;
|
|
50
|
-
let first = true;
|
|
51
|
-
for (let offset = 0; offset < name.length;) {
|
|
52
|
-
const point = name.codePointAt(offset);
|
|
53
|
-
if (point === 58) {
|
|
54
|
-
if (start !== 0 || first)
|
|
55
|
-
invalid("invalid qualified name");
|
|
56
|
-
prefix = name.slice(0, offset);
|
|
57
|
-
start = offset + 1;
|
|
58
|
-
first = true;
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
if (!(first ? nameStart(point) : namePart(point)))
|
|
62
|
-
invalid("invalid qualified name");
|
|
63
|
-
first = false;
|
|
64
|
-
}
|
|
65
|
-
offset += point > 0xffff ? 2 : 1;
|
|
66
|
-
}
|
|
67
|
-
if (first)
|
|
68
|
-
invalid("invalid qualified name");
|
|
69
|
-
const parts = [prefix, name.slice(start)];
|
|
70
|
-
// Retain only one short name; namespace resolution still happens per element.
|
|
71
|
-
if (name.length <= 512) {
|
|
72
|
-
cache.name = name;
|
|
73
|
-
cache.parts = parts;
|
|
74
|
-
}
|
|
75
|
-
return parts;
|
|
76
|
-
}
|
|
77
|
-
function* entities(text) {
|
|
78
|
-
let result = "";
|
|
79
|
-
let offset = 0;
|
|
80
|
-
while (offset < text.length) {
|
|
81
|
-
const start = yield* find(text, "&", offset);
|
|
82
|
-
if (start < 0)
|
|
83
|
-
return result + text.slice(offset);
|
|
84
|
-
result += text.slice(offset, start);
|
|
85
|
-
const end = yield* find(text, ";", start + 1);
|
|
86
|
-
if (end < 0)
|
|
87
|
-
invalid("unterminated entity");
|
|
88
|
-
const entity = text.slice(start + 1, end);
|
|
89
|
-
const predefined = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
|
|
90
|
-
if (Object.hasOwn(predefined, entity))
|
|
91
|
-
result += predefined[entity];
|
|
92
|
-
else {
|
|
93
|
-
const hexadecimal = entity.startsWith("#x");
|
|
94
|
-
const digits = entity.slice(hexadecimal ? 2 : 1);
|
|
95
|
-
if (!entity.startsWith("#") || !digits.length)
|
|
96
|
-
invalid("undeclared entity");
|
|
97
|
-
let point = 0;
|
|
98
|
-
for (let index = 0; index < digits.length; index++) {
|
|
99
|
-
const code = digits.charCodeAt(index);
|
|
100
|
-
const digit = code >= 48 && code <= 57 ? code - 48 : hexadecimal && code >= 65 && code <= 70 ? code - 55
|
|
101
|
-
: hexadecimal && code >= 97 && code <= 102 ? code - 87 : -1;
|
|
102
|
-
if (digit < 0)
|
|
103
|
-
invalid("undeclared entity");
|
|
104
|
-
point = point * (hexadecimal ? 16 : 10) + digit;
|
|
105
|
-
if (point > 0x10ffff)
|
|
106
|
-
invalid("invalid character reference");
|
|
107
|
-
if ((index + 1) % 512 === 0)
|
|
108
|
-
yield 512;
|
|
109
|
-
}
|
|
110
|
-
if (digits.length % 512)
|
|
111
|
-
yield digits.length % 512;
|
|
112
|
-
if (!validCharacter(point))
|
|
113
|
-
invalid("invalid character reference");
|
|
114
|
-
result += String.fromCodePoint(point);
|
|
115
|
-
}
|
|
116
|
-
offset = end + 1;
|
|
117
|
-
}
|
|
118
|
-
return result;
|
|
119
|
-
}
|
|
120
|
-
function* validDeclaration(content, expectedEncoding) {
|
|
121
|
-
let offset = 0;
|
|
122
|
-
const whitespace = function* () {
|
|
123
|
-
const start = offset;
|
|
124
|
-
while (offset < content.length && " \t\n\r".includes(content[offset])) {
|
|
125
|
-
offset++;
|
|
126
|
-
if ((offset - start) % 512 === 0)
|
|
127
|
-
yield 512;
|
|
128
|
-
}
|
|
129
|
-
if ((offset - start) % 512)
|
|
130
|
-
yield (offset - start) % 512;
|
|
131
|
-
return offset - start;
|
|
132
|
-
};
|
|
133
|
-
const field = function* (name) {
|
|
134
|
-
if (content.slice(offset, offset + name.length) !== name)
|
|
135
|
-
return undefined;
|
|
136
|
-
offset += name.length;
|
|
137
|
-
yield name.length;
|
|
138
|
-
yield* whitespace();
|
|
139
|
-
if (content[offset++] !== "=")
|
|
140
|
-
return undefined;
|
|
141
|
-
yield* whitespace();
|
|
142
|
-
const quote = content[offset++];
|
|
143
|
-
if (quote !== "'" && quote !== '"')
|
|
144
|
-
return undefined;
|
|
145
|
-
const start = offset;
|
|
146
|
-
while (offset < content.length && content[offset] !== quote) {
|
|
147
|
-
offset++;
|
|
148
|
-
if ((offset - start) % 512 === 0)
|
|
149
|
-
yield 512;
|
|
150
|
-
}
|
|
151
|
-
if ((offset - start) % 512)
|
|
152
|
-
yield (offset - start) % 512;
|
|
153
|
-
if (offset >= content.length)
|
|
154
|
-
return undefined;
|
|
155
|
-
return content.slice(start, offset++);
|
|
156
|
-
};
|
|
157
|
-
if (!(yield* whitespace()) || (yield* field("version")) !== "1.0")
|
|
158
|
-
return false;
|
|
159
|
-
let spacing = yield* whitespace();
|
|
160
|
-
if (content.slice(offset, offset + 8) === "encoding") {
|
|
161
|
-
if (!spacing)
|
|
162
|
-
return false;
|
|
163
|
-
const encoding = yield* field("encoding");
|
|
164
|
-
if (encoding === undefined || encoding.length > 8 || !["utf-8", "utf-16", "utf-16le", "utf-16be"].includes(encoding.toLowerCase()))
|
|
165
|
-
return false;
|
|
166
|
-
if (expectedEncoding !== undefined && encoding.toLowerCase() !== expectedEncoding.toLowerCase()
|
|
167
|
-
&& !(encoding.toLowerCase() === "utf-16" && (expectedEncoding === "UTF-16LE" || expectedEncoding === "UTF-16BE")))
|
|
168
|
-
return false;
|
|
169
|
-
spacing = yield* whitespace();
|
|
170
|
-
}
|
|
171
|
-
if (content.slice(offset, offset + 10) === "standalone") {
|
|
172
|
-
if (!spacing)
|
|
173
|
-
return false;
|
|
174
|
-
const standalone = yield* field("standalone");
|
|
175
|
-
if (standalone === undefined || standalone.length > 3 || !["yes", "no"].includes(standalone))
|
|
176
|
-
return false;
|
|
177
|
-
yield* whitespace();
|
|
178
|
-
}
|
|
179
|
-
return offset === content.length;
|
|
180
|
-
}
|
|
181
|
-
export function* parseXmlSteps(input, limits = {}) {
|
|
182
|
-
const maxDepth = limits.maxDepth ?? Infinity;
|
|
183
|
-
const maxNodes = limits.maxNodes ?? Infinity;
|
|
184
|
-
const maxAttributes = limits.maxAttributes ?? Infinity;
|
|
185
|
-
const retainContent = limits.retainContent !== false;
|
|
186
|
-
const maxContentNodes = limits.maxContentNodes ?? Infinity;
|
|
187
|
-
const emptyContent = Object.freeze([]);
|
|
188
|
-
const emptyAttributes = Object.freeze([]);
|
|
189
|
-
const emptyNamespaces = new Map();
|
|
190
|
-
const maxAttributesPerElement = limits.maxAttributesPerElement ?? Infinity;
|
|
191
|
-
const maxNamespaces = limits.maxNamespaces ?? Infinity;
|
|
192
|
-
for (const limit of [limits.maxDepth, limits.maxNodes, limits.maxAttributes, limits.maxContentNodes, limits.maxAttributesPerElement, limits.maxNamespaces]) {
|
|
193
|
-
if (limit !== undefined && ((limit !== Infinity && !Number.isSafeInteger(limit)) || limit < 1))
|
|
194
|
-
throw new RangeError("XML limits must be positive integers");
|
|
195
|
-
}
|
|
196
|
-
const maxTextLength = limits.maxTextLength ?? Infinity;
|
|
197
|
-
if (limits.maxTextLength !== undefined && ((maxTextLength !== Infinity && !Number.isSafeInteger(maxTextLength)) || maxTextLength < 1))
|
|
198
|
-
throw new RangeError("XML limits must be positive integers");
|
|
199
|
-
let textLength = 0;
|
|
200
|
-
const admitText = (text) => {
|
|
201
|
-
if (text.length > maxTextLength - textLength)
|
|
202
|
-
throw new XmlLimitError("maxTextLength", "XML text limit exceeded");
|
|
203
|
-
textLength += text.length;
|
|
204
|
-
};
|
|
205
|
-
const chunks = [];
|
|
206
|
-
const start = input.charCodeAt(0) === 0xfeff ? 1 : 0;
|
|
207
|
-
let chunkStart = start;
|
|
208
|
-
let chunkLength = 0;
|
|
209
|
-
let normalizedChunk = "";
|
|
210
|
-
// Keep validated spans intact; only changed line endings need new strings.
|
|
211
|
-
for (let index = start; index < input.length; index++) {
|
|
212
|
-
const point = input.codePointAt(index);
|
|
213
|
-
if (!validCharacter(point))
|
|
214
|
-
invalid("invalid character");
|
|
215
|
-
if (point === 13) {
|
|
216
|
-
normalizedChunk += input.slice(chunkStart, index) + "\n";
|
|
217
|
-
if (input.charCodeAt(index + 1) === 10)
|
|
218
|
-
index++;
|
|
219
|
-
chunkStart = index + 1;
|
|
220
|
-
chunkLength++;
|
|
221
|
-
}
|
|
222
|
-
else {
|
|
223
|
-
chunkLength += point > 0xffff ? 2 : 1;
|
|
224
|
-
if (point > 0xffff)
|
|
225
|
-
index++;
|
|
226
|
-
}
|
|
227
|
-
if (chunkLength >= 512) {
|
|
228
|
-
if (normalizedChunk) {
|
|
229
|
-
chunks.push(normalizedChunk + input.slice(chunkStart, index + 1));
|
|
230
|
-
normalizedChunk = "";
|
|
231
|
-
chunkStart = index + 1;
|
|
232
|
-
}
|
|
233
|
-
chunkLength = 0;
|
|
234
|
-
yield 512;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
if (chunkLength)
|
|
238
|
-
yield chunkLength;
|
|
239
|
-
if (chunks.length || normalizedChunk)
|
|
240
|
-
chunks.push(normalizedChunk + input.slice(chunkStart));
|
|
241
|
-
const source = chunks.length ? chunks.join("") : input.slice(start);
|
|
242
|
-
const stack = [];
|
|
243
|
-
let root;
|
|
244
|
-
const prolog = [];
|
|
245
|
-
const epilog = [];
|
|
246
|
-
let declaration;
|
|
247
|
-
let offset = 0;
|
|
248
|
-
let nodes = 0;
|
|
249
|
-
let attributeCount = 0;
|
|
250
|
-
let contentNodes = 0;
|
|
251
|
-
let previousEmpty;
|
|
252
|
-
const qualifiedNames = { name: undefined, parts: undefined };
|
|
253
|
-
const admitContent = () => {
|
|
254
|
-
if (++contentNodes > maxContentNodes)
|
|
255
|
-
throw new XmlLimitError("maxContentNodes", "XML content node limit exceeded");
|
|
256
|
-
};
|
|
257
|
-
let pendingWork = 0;
|
|
258
|
-
const skipWhitespace = () => {
|
|
259
|
-
const start = offset;
|
|
260
|
-
while (offset < source.length) {
|
|
261
|
-
const c = source.charCodeAt(offset);
|
|
262
|
-
if (c !== 32 && c !== 9 && c !== 10 && c !== 13)
|
|
263
|
-
break;
|
|
264
|
-
offset++;
|
|
265
|
-
}
|
|
266
|
-
return offset - start;
|
|
267
|
-
};
|
|
268
|
-
const scanName = () => {
|
|
269
|
-
const start = offset;
|
|
270
|
-
while (offset < source.length) {
|
|
271
|
-
const c = source.charCodeAt(offset);
|
|
272
|
-
if (c === 32 || c === 9 || c === 13 || c === 10 || c === 47 || c === 61 || c === 62 || c === 63)
|
|
273
|
-
break;
|
|
274
|
-
offset++;
|
|
275
|
-
}
|
|
276
|
-
const name = source.slice(start, offset);
|
|
277
|
-
const [prefix, localName] = qualifiedNameSync(name, qualifiedNames);
|
|
278
|
-
return [name, prefix, localName, (offset - start) * 2];
|
|
279
|
-
};
|
|
280
|
-
while (offset < source.length) {
|
|
281
|
-
pendingWork += 1;
|
|
282
|
-
if (pendingWork >= 512) {
|
|
283
|
-
while (pendingWork >= 512) {
|
|
284
|
-
yield 512;
|
|
285
|
-
pendingWork -= 512;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
if (source.charCodeAt(offset) !== 60) {
|
|
289
|
-
const next = source.indexOf("<", offset);
|
|
290
|
-
const endPos = next < 0 ? source.length : next;
|
|
291
|
-
const text = source.slice(offset, endPos);
|
|
292
|
-
pendingWork += (endPos - offset) + text.length;
|
|
293
|
-
while (pendingWork >= 512) {
|
|
294
|
-
yield 512;
|
|
295
|
-
pendingWork -= 512;
|
|
296
|
-
}
|
|
297
|
-
if (text.indexOf("]]>") >= 0)
|
|
298
|
-
invalid("CDATA terminator in text");
|
|
299
|
-
const resolved = stack.length ? (text.indexOf("&") < 0 ? (pendingWork += text.length, text) : yield* entities(text)) : text;
|
|
300
|
-
while (pendingWork >= 512) {
|
|
301
|
-
yield 512;
|
|
302
|
-
pendingWork -= 512;
|
|
303
|
-
}
|
|
304
|
-
admitText(resolved);
|
|
305
|
-
const parent = stack.at(-1);
|
|
306
|
-
if (parent) {
|
|
307
|
-
parent.element.text += resolved;
|
|
308
|
-
if (resolved.length) {
|
|
309
|
-
admitContent();
|
|
310
|
-
if (retainContent)
|
|
311
|
-
parent.content.push({ kind: "text", text: resolved });
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
else {
|
|
315
|
-
for (let index = 0; index < resolved.length; index++) {
|
|
316
|
-
const c = resolved.charCodeAt(index);
|
|
317
|
-
if (c !== 32 && c !== 9 && c !== 13 && c !== 10)
|
|
318
|
-
invalid("text outside the root");
|
|
319
|
-
}
|
|
320
|
-
pendingWork += resolved.length;
|
|
321
|
-
while (pendingWork >= 512) {
|
|
322
|
-
yield 512;
|
|
323
|
-
pendingWork -= 512;
|
|
324
|
-
}
|
|
325
|
-
if (resolved.length) {
|
|
326
|
-
admitContent();
|
|
327
|
-
if (retainContent)
|
|
328
|
-
(root ? epilog : prolog).push({ kind: "text", text: resolved });
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
offset = endPos;
|
|
332
|
-
}
|
|
333
|
-
else if (source.startsWith("<!--", offset)) {
|
|
334
|
-
const end = yield* find(source, "-->", offset + 4);
|
|
335
|
-
if (end < 0 || (yield* find(source.slice(offset + 4, end), "--", 0)) >= 0 || source.slice(offset + 4, end).endsWith("-")) {
|
|
336
|
-
invalid("malformed comment");
|
|
337
|
-
}
|
|
338
|
-
const parent = stack.at(-1);
|
|
339
|
-
const text = source.slice(offset + 4, end);
|
|
340
|
-
admitText(text);
|
|
341
|
-
admitContent();
|
|
342
|
-
if (retainContent) {
|
|
343
|
-
(parent?.content ?? (root ? epilog : prolog)).push({ kind: "comment", text });
|
|
344
|
-
}
|
|
345
|
-
offset = end + 3;
|
|
346
|
-
}
|
|
347
|
-
else if (source.startsWith("<![CDATA[", offset)) {
|
|
348
|
-
if (!stack.length)
|
|
349
|
-
invalid("CDATA outside root");
|
|
350
|
-
const end = yield* find(source, "]]>", offset + 9);
|
|
351
|
-
if (end < 0)
|
|
352
|
-
invalid("unterminated CDATA");
|
|
353
|
-
const cdataText = source.slice(offset + 9, end);
|
|
354
|
-
admitText(cdataText);
|
|
355
|
-
const parent = stack.at(-1);
|
|
356
|
-
parent.element.text += cdataText;
|
|
357
|
-
admitContent();
|
|
358
|
-
if (retainContent)
|
|
359
|
-
parent.content.push({ kind: "cdata", text: cdataText });
|
|
360
|
-
offset = end + 3;
|
|
361
|
-
}
|
|
362
|
-
else if (source.startsWith("<?", offset)) {
|
|
363
|
-
const start = offset;
|
|
364
|
-
offset += 2;
|
|
365
|
-
const [target, , , wName] = scanName();
|
|
366
|
-
pendingWork += wName;
|
|
367
|
-
while (pendingWork >= 512) {
|
|
368
|
-
yield 512;
|
|
369
|
-
pendingWork -= 512;
|
|
370
|
-
}
|
|
371
|
-
const end = yield* find(source, "?>", offset);
|
|
372
|
-
if (end < 0)
|
|
373
|
-
invalid("unterminated processing instruction");
|
|
374
|
-
const content = source.slice(offset, end);
|
|
375
|
-
if (target.length === 3 && target.toLowerCase() === "xml") {
|
|
376
|
-
if (start !== 0 || target !== "xml"
|
|
377
|
-
|| !(yield* validDeclaration(content, limits.expectedEncoding))) {
|
|
378
|
-
invalid("unsupported XML declaration");
|
|
379
|
-
}
|
|
380
|
-
if (retainContent)
|
|
381
|
-
declaration = source.slice(start, end + 2);
|
|
382
|
-
}
|
|
383
|
-
else if (content && !" \t\n\r".includes(content[0]))
|
|
384
|
-
invalid("invalid processing instruction");
|
|
385
|
-
if (!(target.length === 3 && target.toLowerCase() === "xml")) {
|
|
386
|
-
const parent = stack.at(-1);
|
|
387
|
-
let wsStart = 0;
|
|
388
|
-
while (wsStart < content.length && " \t\n\r".includes(content[wsStart]))
|
|
389
|
-
wsStart++;
|
|
390
|
-
pendingWork += wsStart;
|
|
391
|
-
while (pendingWork >= 512) {
|
|
392
|
-
yield 512;
|
|
393
|
-
pendingWork -= 512;
|
|
394
|
-
}
|
|
395
|
-
const text = content.slice(wsStart);
|
|
396
|
-
admitText(text);
|
|
397
|
-
admitContent();
|
|
398
|
-
if (retainContent) {
|
|
399
|
-
(parent?.content ?? (root ? epilog : prolog)).push({ kind: "processing-instruction", target, text });
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
offset = end + 2;
|
|
403
|
-
}
|
|
404
|
-
else if (source.startsWith("<!", offset)) {
|
|
405
|
-
invalid("DTD and entity declarations are forbidden");
|
|
406
|
-
}
|
|
407
|
-
else if (source.charCodeAt(offset + 1) === 47) {
|
|
408
|
-
offset += 2;
|
|
409
|
-
const [name, , , wName] = scanName();
|
|
410
|
-
pendingWork += wName + skipWhitespace();
|
|
411
|
-
while (pendingWork >= 512) {
|
|
412
|
-
yield 512;
|
|
413
|
-
pendingWork -= 512;
|
|
414
|
-
}
|
|
415
|
-
if (source[offset++] !== ">" || stack.pop()?.name !== name)
|
|
416
|
-
invalid("mismatched closing tag");
|
|
417
|
-
}
|
|
418
|
-
else {
|
|
419
|
-
offset++;
|
|
420
|
-
const repeated = previousEmpty && source.startsWith(previousEmpty.suffix, offset) ? previousEmpty : undefined;
|
|
421
|
-
const [name, prefix, localName, wName] = repeated
|
|
422
|
-
? [repeated.name, repeated.prefix, repeated.localName, repeated.name.length * 2]
|
|
423
|
-
: scanName();
|
|
424
|
-
if (repeated)
|
|
425
|
-
offset += name.length;
|
|
426
|
-
// Preserve scan/validation work while reusing the admitted spelling.
|
|
427
|
-
pendingWork += wName;
|
|
428
|
-
while (pendingWork >= 512) {
|
|
429
|
-
yield 512;
|
|
430
|
-
pendingWork -= 512;
|
|
431
|
-
}
|
|
432
|
-
let attributes;
|
|
433
|
-
const attrMeta = [];
|
|
434
|
-
let namespaces = stack.at(-1)?.namespaces ?? new Map([["xml", xmlNamespace]]);
|
|
435
|
-
let ownsNamespaces = stack.length === 0;
|
|
436
|
-
while (!repeated) {
|
|
437
|
-
const ws = skipWhitespace();
|
|
438
|
-
pendingWork += ws;
|
|
439
|
-
while (pendingWork >= 512) {
|
|
440
|
-
yield 512;
|
|
441
|
-
pendingWork -= 512;
|
|
442
|
-
}
|
|
443
|
-
const ch = source.charCodeAt(offset);
|
|
444
|
-
if (ch === 47 || ch === 62)
|
|
445
|
-
break;
|
|
446
|
-
if (ws === 0)
|
|
447
|
-
invalid("attributes require whitespace");
|
|
448
|
-
const [attribute, attrPrefix, attrLocal, wAttr] = scanName();
|
|
449
|
-
pendingWork += wAttr;
|
|
450
|
-
while (pendingWork >= 512) {
|
|
451
|
-
yield 512;
|
|
452
|
-
pendingWork -= 512;
|
|
453
|
-
}
|
|
454
|
-
if (attributes?.has(attribute))
|
|
455
|
-
invalid("duplicate attribute");
|
|
456
|
-
if (++attributeCount > maxAttributes)
|
|
457
|
-
throw new XmlLimitError("maxAttributes", "XML attribute limit exceeded");
|
|
458
|
-
if ((attributes?.size ?? 0) >= maxAttributesPerElement)
|
|
459
|
-
throw new XmlLimitError("maxAttributesPerElement", "XML attribute limit exceeded");
|
|
460
|
-
pendingWork += skipWhitespace();
|
|
461
|
-
if (source[offset++] !== "=")
|
|
462
|
-
invalid("missing attribute equals");
|
|
463
|
-
pendingWork += skipWhitespace();
|
|
464
|
-
const quote = source[offset++];
|
|
465
|
-
if (quote !== '"' && quote !== "'")
|
|
466
|
-
invalid("unquoted attribute");
|
|
467
|
-
const end = source.indexOf(quote, offset);
|
|
468
|
-
const scanLen = (end < 0 ? source.length : end) - offset;
|
|
469
|
-
pendingWork += scanLen;
|
|
470
|
-
while (pendingWork >= 512) {
|
|
471
|
-
yield 512;
|
|
472
|
-
pendingWork -= 512;
|
|
473
|
-
}
|
|
474
|
-
if (end < 0)
|
|
475
|
-
invalid("unterminated attribute");
|
|
476
|
-
const raw = source.slice(offset, end);
|
|
477
|
-
pendingWork += raw.length * 2;
|
|
478
|
-
while (pendingWork >= 512) {
|
|
479
|
-
yield 512;
|
|
480
|
-
pendingWork -= 512;
|
|
481
|
-
}
|
|
482
|
-
if (raw.indexOf("<") >= 0)
|
|
483
|
-
invalid("less-than in attribute");
|
|
484
|
-
const normalized = /[\t\n\r]/.test(raw) ? raw.replace(/[\t\n\r]/g, " ") : raw;
|
|
485
|
-
const value = normalized.indexOf("&") < 0 ? (pendingWork += normalized.length, normalized) : yield* entities(normalized);
|
|
486
|
-
while (pendingWork >= 512) {
|
|
487
|
-
yield 512;
|
|
488
|
-
pendingWork -= 512;
|
|
489
|
-
}
|
|
490
|
-
admitText(value);
|
|
491
|
-
(attributes ??= new Map()).set(attribute, value);
|
|
492
|
-
attrMeta.push([attribute, attrPrefix, attrLocal]);
|
|
493
|
-
offset = end + 1;
|
|
494
|
-
if (attribute === "xmlns" || attribute.startsWith("xmlns:")) {
|
|
495
|
-
const nsPrefix = attribute === "xmlns" ? "" : attribute.slice(6);
|
|
496
|
-
if (nsPrefix === "xmlns" || value === xmlnsNamespace
|
|
497
|
-
|| (nsPrefix === "xml") !== (value === xmlNamespace)
|
|
498
|
-
|| (nsPrefix !== "" && value === ""))
|
|
499
|
-
invalid("invalid namespace binding");
|
|
500
|
-
if (!ownsNamespaces) {
|
|
501
|
-
const copy = new Map();
|
|
502
|
-
for (const [key, uri] of namespaces) {
|
|
503
|
-
copy.set(key, uri);
|
|
504
|
-
pendingWork += 1;
|
|
505
|
-
}
|
|
506
|
-
namespaces = copy;
|
|
507
|
-
ownsNamespaces = true;
|
|
508
|
-
}
|
|
509
|
-
namespaces.set(nsPrefix, value);
|
|
510
|
-
if (namespaces.size > maxNamespaces)
|
|
511
|
-
throw new XmlLimitError("maxNamespaces", "XML namespace scope limit exceeded");
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
if (attrMeta.length > 0) {
|
|
515
|
-
const expanded = attrMeta.length > 1 ? new Set() : undefined;
|
|
516
|
-
for (let i = 0; i < attrMeta.length; i++) {
|
|
517
|
-
const [attribute, attrPrefix, attrLocal] = attrMeta[i];
|
|
518
|
-
if (attribute === "xmlns" || attribute.startsWith("xmlns:"))
|
|
519
|
-
continue;
|
|
520
|
-
pendingWork += attribute.length;
|
|
521
|
-
if (attrPrefix && !namespaces.has(attrPrefix))
|
|
522
|
-
invalid("unbound attribute prefix");
|
|
523
|
-
if (expanded) {
|
|
524
|
-
const key = `${attrPrefix ? namespaces.get(attrPrefix) : ""}\0${attrLocal}`;
|
|
525
|
-
if (expanded.has(key))
|
|
526
|
-
invalid("duplicate expanded attribute");
|
|
527
|
-
expanded.add(key);
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
pendingWork += name.length;
|
|
532
|
-
while (pendingWork >= 512) {
|
|
533
|
-
yield 512;
|
|
534
|
-
pendingWork -= 512;
|
|
535
|
-
}
|
|
536
|
-
if (prefix === "xmlns" || (prefix && !namespaces.has(prefix)))
|
|
537
|
-
invalid("unbound element prefix");
|
|
538
|
-
if (++nodes > maxNodes)
|
|
539
|
-
throw new XmlLimitError("maxNodes", "XML resource limit exceeded");
|
|
540
|
-
if (stack.length + 1 > maxDepth)
|
|
541
|
-
throw new XmlLimitError("maxDepth", "XML resource limit exceeded");
|
|
542
|
-
const namespace = namespaces.get(prefix) ?? "";
|
|
543
|
-
limits.onElement?.({ name, namespace, localName }, stack.at(-1)?.element, stack.length + 1);
|
|
544
|
-
admitContent();
|
|
545
|
-
const retainedAttributes = [];
|
|
546
|
-
if (retainContent && attributes) {
|
|
547
|
-
for (let i = 0; i < attrMeta.length; i++) {
|
|
548
|
-
const [attribute, attrPrefix, attrLocal] = attrMeta[i];
|
|
549
|
-
const value = attributes.get(attribute);
|
|
550
|
-
admitContent();
|
|
551
|
-
retainedAttributes.push({ name: attribute, localName: attrLocal,
|
|
552
|
-
namespace: attribute === "xmlns" || attrPrefix === "xmlns" ? xmlnsNamespace : attrPrefix ? namespaces.get(attrPrefix) : "", value });
|
|
553
|
-
pendingWork += attribute.length + 1;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
while (pendingWork >= 512) {
|
|
557
|
-
yield 512;
|
|
558
|
-
pendingWork -= 512;
|
|
559
|
-
}
|
|
560
|
-
const content = retainContent ? [] : undefined;
|
|
561
|
-
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 }) } : {}) };
|
|
562
|
-
const parent = stack.at(-1);
|
|
563
|
-
if (parent) {
|
|
564
|
-
parent.element.children.push(element);
|
|
565
|
-
parent.content?.push(element);
|
|
566
|
-
}
|
|
567
|
-
else if (root)
|
|
568
|
-
invalid("multiple root elements");
|
|
569
|
-
else
|
|
570
|
-
root = element;
|
|
571
|
-
const empty = source[offset] === "/";
|
|
572
|
-
if (empty)
|
|
573
|
-
offset++;
|
|
574
|
-
if (source[offset++] !== ">")
|
|
575
|
-
invalid("unterminated start tag");
|
|
576
|
-
// Only cache compact, attribute-free spellings. Namespace resolution and
|
|
577
|
-
// every admission counter still run for each distinct physical element.
|
|
578
|
-
if (empty && !attributes && name.length <= 512 && source.slice(offset - name.length - 3, offset) === `<${name}/>`)
|
|
579
|
-
previousEmpty = { suffix: name + "/>", name, prefix, localName };
|
|
580
|
-
if (!empty)
|
|
581
|
-
stack.push({ element, content, name, namespaces });
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
if (pendingWork > 0) {
|
|
585
|
-
yield pendingWork;
|
|
586
|
-
pendingWork = 0;
|
|
587
|
-
}
|
|
588
|
-
if (stack.length || !root)
|
|
589
|
-
invalid("incomplete document");
|
|
590
|
-
return root;
|
|
591
|
-
}
|
|
592
|
-
export function parseXml(input, limits = {}) {
|
|
593
|
-
const parser = parseXmlSteps(input, limits);
|
|
594
|
-
let result = parser.next();
|
|
595
|
-
while (!result.done)
|
|
596
|
-
result = parser.next();
|
|
597
|
-
return result.value;
|
|
598
|
-
}
|
|
1
|
+
export * from "../xml-ast/index.js";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface XmlName {
|
|
2
|
+
readonly name: string;
|
|
3
|
+
readonly namespace: string;
|
|
4
|
+
readonly localName: string;
|
|
5
|
+
}
|
|
6
|
+
export interface XmlAttribute extends XmlName {
|
|
7
|
+
readonly value: string;
|
|
8
|
+
}
|
|
9
|
+
export type XmlContent = XmlElement | {
|
|
10
|
+
readonly kind: "text" | "cdata" | "comment";
|
|
11
|
+
readonly text: string;
|
|
12
|
+
} | {
|
|
13
|
+
readonly kind: "processing-instruction";
|
|
14
|
+
readonly target: string;
|
|
15
|
+
readonly text: string;
|
|
16
|
+
};
|
|
17
|
+
export interface XmlElement extends XmlName {
|
|
18
|
+
readonly kind: "element";
|
|
19
|
+
readonly children: XmlElement[];
|
|
20
|
+
readonly content: readonly XmlContent[];
|
|
21
|
+
readonly attributes: readonly XmlAttribute[];
|
|
22
|
+
readonly namespaces: ReadonlyMap<string, string>;
|
|
23
|
+
text: string;
|
|
24
|
+
readonly declaration?: string;
|
|
25
|
+
readonly prolog?: readonly XmlContent[];
|
|
26
|
+
readonly epilog?: readonly XmlContent[];
|
|
27
|
+
}
|
|
28
|
+
export interface XmlLimits {
|
|
29
|
+
readonly expectedEncoding?: "UTF-8" | "UTF-16" | "UTF-16LE" | "UTF-16BE";
|
|
30
|
+
readonly retainContent?: boolean;
|
|
31
|
+
readonly maxDepth?: number;
|
|
32
|
+
readonly maxNodes?: number;
|
|
33
|
+
readonly maxAttributes?: number;
|
|
34
|
+
readonly maxAttributesPerElement?: number;
|
|
35
|
+
readonly maxNamespaces?: number;
|
|
36
|
+
readonly maxContentNodes?: number;
|
|
37
|
+
readonly maxTextLength?: number;
|
|
38
|
+
readonly onElement?: (element: XmlName, parent: XmlName | undefined, depth: number) => void;
|
|
39
|
+
}
|
|
40
|
+
export declare class XmlLimitError extends SyntaxError {
|
|
41
|
+
readonly limit: string;
|
|
42
|
+
constructor(limit: string, message: string);
|
|
43
|
+
}
|
|
44
|
+
export declare function parseXmlSteps(input: string, limits?: XmlLimits): Generator<number, XmlElement, void>;
|
|
45
|
+
export declare function parseXml(input: string, limits?: XmlLimits): XmlElement;
|
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
export class XmlLimitError extends SyntaxError {
|
|
2
|
+
limit;
|
|
3
|
+
constructor(limit, message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.limit = limit;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
function* find(source, needle, start) {
|
|
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;
|
|
15
|
+
}
|
|
16
|
+
if (remaining > 0)
|
|
17
|
+
yield remaining;
|
|
18
|
+
return found;
|
|
19
|
+
}
|
|
20
|
+
const xmlNamespace = "http://www.w3.org/XML/1998/namespace";
|
|
21
|
+
const xmlnsNamespace = "http://www.w3.org/2000/xmlns/";
|
|
22
|
+
function invalid(message) {
|
|
23
|
+
throw new SyntaxError(`Invalid XML: ${message}`);
|
|
24
|
+
}
|
|
25
|
+
function validCharacter(point) {
|
|
26
|
+
return point === 9 || point === 10 || point === 13
|
|
27
|
+
|| (point >= 0x20 && point <= 0xd7ff)
|
|
28
|
+
|| (point >= 0xe000 && point <= 0xfffd)
|
|
29
|
+
|| (point >= 0x10000 && point <= 0x10ffff);
|
|
30
|
+
}
|
|
31
|
+
function nameStart(point) {
|
|
32
|
+
return point === 95 || (point >= 65 && point <= 90) || (point >= 97 && point <= 122)
|
|
33
|
+
|| (point >= 0xc0 && point <= 0xd6) || (point >= 0xd8 && point <= 0xf6)
|
|
34
|
+
|| (point >= 0xf8 && point <= 0x2ff) || (point >= 0x370 && point <= 0x37d)
|
|
35
|
+
|| (point >= 0x37f && point <= 0x1fff) || (point >= 0x200c && point <= 0x200d)
|
|
36
|
+
|| (point >= 0x2070 && point <= 0x218f) || (point >= 0x2c00 && point <= 0x2fef)
|
|
37
|
+
|| (point >= 0x3001 && point <= 0xd7ff) || (point >= 0xf900 && point <= 0xfdcf)
|
|
38
|
+
|| (point >= 0xfdf0 && point <= 0xfffd) || (point >= 0x10000 && point <= 0xeffff);
|
|
39
|
+
}
|
|
40
|
+
function namePart(point) {
|
|
41
|
+
return nameStart(point) || point === 45 || point === 46 || point === 0xb7
|
|
42
|
+
|| (point >= 48 && point <= 57) || (point >= 0x300 && point <= 0x36f)
|
|
43
|
+
|| (point >= 0x203f && point <= 0x2040);
|
|
44
|
+
}
|
|
45
|
+
function qualifiedNameSync(name, cache) {
|
|
46
|
+
if (cache.name === name)
|
|
47
|
+
return cache.parts;
|
|
48
|
+
let prefix = "";
|
|
49
|
+
let start = 0;
|
|
50
|
+
let first = true;
|
|
51
|
+
for (let offset = 0; offset < name.length;) {
|
|
52
|
+
const point = name.codePointAt(offset);
|
|
53
|
+
if (point === 58) {
|
|
54
|
+
if (start !== 0 || first)
|
|
55
|
+
invalid("invalid qualified name");
|
|
56
|
+
prefix = name.slice(0, offset);
|
|
57
|
+
start = offset + 1;
|
|
58
|
+
first = true;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
if (!(first ? nameStart(point) : namePart(point)))
|
|
62
|
+
invalid("invalid qualified name");
|
|
63
|
+
first = false;
|
|
64
|
+
}
|
|
65
|
+
offset += point > 0xffff ? 2 : 1;
|
|
66
|
+
}
|
|
67
|
+
if (first)
|
|
68
|
+
invalid("invalid qualified name");
|
|
69
|
+
const parts = [prefix, name.slice(start)];
|
|
70
|
+
// Retain only one short name; namespace resolution still happens per element.
|
|
71
|
+
if (name.length <= 512) {
|
|
72
|
+
cache.name = name;
|
|
73
|
+
cache.parts = parts;
|
|
74
|
+
}
|
|
75
|
+
return parts;
|
|
76
|
+
}
|
|
77
|
+
function* entities(text) {
|
|
78
|
+
let result = "";
|
|
79
|
+
let offset = 0;
|
|
80
|
+
while (offset < text.length) {
|
|
81
|
+
const start = yield* find(text, "&", offset);
|
|
82
|
+
if (start < 0)
|
|
83
|
+
return result + text.slice(offset);
|
|
84
|
+
result += text.slice(offset, start);
|
|
85
|
+
const end = yield* find(text, ";", start + 1);
|
|
86
|
+
if (end < 0)
|
|
87
|
+
invalid("unterminated entity");
|
|
88
|
+
const entity = text.slice(start + 1, end);
|
|
89
|
+
const predefined = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
|
|
90
|
+
if (Object.hasOwn(predefined, entity))
|
|
91
|
+
result += predefined[entity];
|
|
92
|
+
else {
|
|
93
|
+
const hexadecimal = entity.startsWith("#x");
|
|
94
|
+
const digits = entity.slice(hexadecimal ? 2 : 1);
|
|
95
|
+
if (!entity.startsWith("#") || !digits.length)
|
|
96
|
+
invalid("undeclared entity");
|
|
97
|
+
let point = 0;
|
|
98
|
+
for (let index = 0; index < digits.length; index++) {
|
|
99
|
+
const code = digits.charCodeAt(index);
|
|
100
|
+
const digit = code >= 48 && code <= 57 ? code - 48 : hexadecimal && code >= 65 && code <= 70 ? code - 55
|
|
101
|
+
: hexadecimal && code >= 97 && code <= 102 ? code - 87 : -1;
|
|
102
|
+
if (digit < 0)
|
|
103
|
+
invalid("undeclared entity");
|
|
104
|
+
point = point * (hexadecimal ? 16 : 10) + digit;
|
|
105
|
+
if (point > 0x10ffff)
|
|
106
|
+
invalid("invalid character reference");
|
|
107
|
+
if ((index + 1) % 512 === 0)
|
|
108
|
+
yield 512;
|
|
109
|
+
}
|
|
110
|
+
if (digits.length % 512)
|
|
111
|
+
yield digits.length % 512;
|
|
112
|
+
if (!validCharacter(point))
|
|
113
|
+
invalid("invalid character reference");
|
|
114
|
+
result += String.fromCodePoint(point);
|
|
115
|
+
}
|
|
116
|
+
offset = end + 1;
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
function* validDeclaration(content, expectedEncoding) {
|
|
121
|
+
let offset = 0;
|
|
122
|
+
const whitespace = function* () {
|
|
123
|
+
const start = offset;
|
|
124
|
+
while (offset < content.length && " \t\n\r".includes(content[offset])) {
|
|
125
|
+
offset++;
|
|
126
|
+
if ((offset - start) % 512 === 0)
|
|
127
|
+
yield 512;
|
|
128
|
+
}
|
|
129
|
+
if ((offset - start) % 512)
|
|
130
|
+
yield (offset - start) % 512;
|
|
131
|
+
return offset - start;
|
|
132
|
+
};
|
|
133
|
+
const field = function* (name) {
|
|
134
|
+
if (content.slice(offset, offset + name.length) !== name)
|
|
135
|
+
return undefined;
|
|
136
|
+
offset += name.length;
|
|
137
|
+
yield name.length;
|
|
138
|
+
yield* whitespace();
|
|
139
|
+
if (content[offset++] !== "=")
|
|
140
|
+
return undefined;
|
|
141
|
+
yield* whitespace();
|
|
142
|
+
const quote = content[offset++];
|
|
143
|
+
if (quote !== "'" && quote !== '"')
|
|
144
|
+
return undefined;
|
|
145
|
+
const start = offset;
|
|
146
|
+
while (offset < content.length && content[offset] !== quote) {
|
|
147
|
+
offset++;
|
|
148
|
+
if ((offset - start) % 512 === 0)
|
|
149
|
+
yield 512;
|
|
150
|
+
}
|
|
151
|
+
if ((offset - start) % 512)
|
|
152
|
+
yield (offset - start) % 512;
|
|
153
|
+
if (offset >= content.length)
|
|
154
|
+
return undefined;
|
|
155
|
+
return content.slice(start, offset++);
|
|
156
|
+
};
|
|
157
|
+
if (!(yield* whitespace()) || (yield* field("version")) !== "1.0")
|
|
158
|
+
return false;
|
|
159
|
+
let spacing = yield* whitespace();
|
|
160
|
+
if (content.slice(offset, offset + 8) === "encoding") {
|
|
161
|
+
if (!spacing)
|
|
162
|
+
return false;
|
|
163
|
+
const encoding = yield* field("encoding");
|
|
164
|
+
if (encoding === undefined || encoding.length > 8 || !["utf-8", "utf-16", "utf-16le", "utf-16be"].includes(encoding.toLowerCase()))
|
|
165
|
+
return false;
|
|
166
|
+
if (expectedEncoding !== undefined && encoding.toLowerCase() !== expectedEncoding.toLowerCase()
|
|
167
|
+
&& !(encoding.toLowerCase() === "utf-16" && (expectedEncoding === "UTF-16LE" || expectedEncoding === "UTF-16BE")))
|
|
168
|
+
return false;
|
|
169
|
+
spacing = yield* whitespace();
|
|
170
|
+
}
|
|
171
|
+
if (content.slice(offset, offset + 10) === "standalone") {
|
|
172
|
+
if (!spacing)
|
|
173
|
+
return false;
|
|
174
|
+
const standalone = yield* field("standalone");
|
|
175
|
+
if (standalone === undefined || standalone.length > 3 || !["yes", "no"].includes(standalone))
|
|
176
|
+
return false;
|
|
177
|
+
yield* whitespace();
|
|
178
|
+
}
|
|
179
|
+
return offset === content.length;
|
|
180
|
+
}
|
|
181
|
+
export function* parseXmlSteps(input, limits = {}) {
|
|
182
|
+
const maxDepth = limits.maxDepth ?? Infinity;
|
|
183
|
+
const maxNodes = limits.maxNodes ?? Infinity;
|
|
184
|
+
const maxAttributes = limits.maxAttributes ?? Infinity;
|
|
185
|
+
const retainContent = limits.retainContent !== false;
|
|
186
|
+
const maxContentNodes = limits.maxContentNodes ?? Infinity;
|
|
187
|
+
const emptyContent = Object.freeze([]);
|
|
188
|
+
const emptyAttributes = Object.freeze([]);
|
|
189
|
+
const emptyNamespaces = new Map();
|
|
190
|
+
const maxAttributesPerElement = limits.maxAttributesPerElement ?? Infinity;
|
|
191
|
+
const maxNamespaces = limits.maxNamespaces ?? Infinity;
|
|
192
|
+
for (const limit of [limits.maxDepth, limits.maxNodes, limits.maxAttributes, limits.maxContentNodes, limits.maxAttributesPerElement, limits.maxNamespaces]) {
|
|
193
|
+
if (limit !== undefined && ((limit !== Infinity && !Number.isSafeInteger(limit)) || limit < 1))
|
|
194
|
+
throw new RangeError("XML limits must be positive integers");
|
|
195
|
+
}
|
|
196
|
+
const maxTextLength = limits.maxTextLength ?? Infinity;
|
|
197
|
+
if (limits.maxTextLength !== undefined && ((maxTextLength !== Infinity && !Number.isSafeInteger(maxTextLength)) || maxTextLength < 1))
|
|
198
|
+
throw new RangeError("XML limits must be positive integers");
|
|
199
|
+
let textLength = 0;
|
|
200
|
+
const admitText = (text) => {
|
|
201
|
+
if (text.length > maxTextLength - textLength)
|
|
202
|
+
throw new XmlLimitError("maxTextLength", "XML text limit exceeded");
|
|
203
|
+
textLength += text.length;
|
|
204
|
+
};
|
|
205
|
+
const chunks = [];
|
|
206
|
+
const start = input.charCodeAt(0) === 0xfeff ? 1 : 0;
|
|
207
|
+
let chunkStart = start;
|
|
208
|
+
let chunkLength = 0;
|
|
209
|
+
let normalizedChunk = "";
|
|
210
|
+
// Keep validated spans intact; only changed line endings need new strings.
|
|
211
|
+
for (let index = start; index < input.length; index++) {
|
|
212
|
+
const point = input.codePointAt(index);
|
|
213
|
+
if (!validCharacter(point))
|
|
214
|
+
invalid("invalid character");
|
|
215
|
+
if (point === 13) {
|
|
216
|
+
normalizedChunk += input.slice(chunkStart, index) + "\n";
|
|
217
|
+
if (input.charCodeAt(index + 1) === 10)
|
|
218
|
+
index++;
|
|
219
|
+
chunkStart = index + 1;
|
|
220
|
+
chunkLength++;
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
chunkLength += point > 0xffff ? 2 : 1;
|
|
224
|
+
if (point > 0xffff)
|
|
225
|
+
index++;
|
|
226
|
+
}
|
|
227
|
+
if (chunkLength >= 512) {
|
|
228
|
+
if (normalizedChunk) {
|
|
229
|
+
chunks.push(normalizedChunk + input.slice(chunkStart, index + 1));
|
|
230
|
+
normalizedChunk = "";
|
|
231
|
+
chunkStart = index + 1;
|
|
232
|
+
}
|
|
233
|
+
chunkLength = 0;
|
|
234
|
+
yield 512;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (chunkLength)
|
|
238
|
+
yield chunkLength;
|
|
239
|
+
if (chunks.length || normalizedChunk)
|
|
240
|
+
chunks.push(normalizedChunk + input.slice(chunkStart));
|
|
241
|
+
const source = chunks.length ? chunks.join("") : input.slice(start);
|
|
242
|
+
const stack = [];
|
|
243
|
+
let root;
|
|
244
|
+
const prolog = [];
|
|
245
|
+
const epilog = [];
|
|
246
|
+
let declaration;
|
|
247
|
+
let offset = 0;
|
|
248
|
+
let nodes = 0;
|
|
249
|
+
let attributeCount = 0;
|
|
250
|
+
let contentNodes = 0;
|
|
251
|
+
let previousEmpty;
|
|
252
|
+
const qualifiedNames = { name: undefined, parts: undefined };
|
|
253
|
+
const admitContent = () => {
|
|
254
|
+
if (++contentNodes > maxContentNodes)
|
|
255
|
+
throw new XmlLimitError("maxContentNodes", "XML content node limit exceeded");
|
|
256
|
+
};
|
|
257
|
+
let pendingWork = 0;
|
|
258
|
+
const skipWhitespace = () => {
|
|
259
|
+
const start = offset;
|
|
260
|
+
while (offset < source.length) {
|
|
261
|
+
const c = source.charCodeAt(offset);
|
|
262
|
+
if (c !== 32 && c !== 9 && c !== 10 && c !== 13)
|
|
263
|
+
break;
|
|
264
|
+
offset++;
|
|
265
|
+
}
|
|
266
|
+
return offset - start;
|
|
267
|
+
};
|
|
268
|
+
const scanName = () => {
|
|
269
|
+
const start = offset;
|
|
270
|
+
while (offset < source.length) {
|
|
271
|
+
const c = source.charCodeAt(offset);
|
|
272
|
+
if (c === 32 || c === 9 || c === 13 || c === 10 || c === 47 || c === 61 || c === 62 || c === 63)
|
|
273
|
+
break;
|
|
274
|
+
offset++;
|
|
275
|
+
}
|
|
276
|
+
const name = source.slice(start, offset);
|
|
277
|
+
const [prefix, localName] = qualifiedNameSync(name, qualifiedNames);
|
|
278
|
+
return [name, prefix, localName, (offset - start) * 2];
|
|
279
|
+
};
|
|
280
|
+
while (offset < source.length) {
|
|
281
|
+
pendingWork += 1;
|
|
282
|
+
if (pendingWork >= 512) {
|
|
283
|
+
while (pendingWork >= 512) {
|
|
284
|
+
yield 512;
|
|
285
|
+
pendingWork -= 512;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (source.charCodeAt(offset) !== 60) {
|
|
289
|
+
const next = source.indexOf("<", offset);
|
|
290
|
+
const endPos = next < 0 ? source.length : next;
|
|
291
|
+
const text = source.slice(offset, endPos);
|
|
292
|
+
pendingWork += (endPos - offset) + text.length;
|
|
293
|
+
while (pendingWork >= 512) {
|
|
294
|
+
yield 512;
|
|
295
|
+
pendingWork -= 512;
|
|
296
|
+
}
|
|
297
|
+
if (text.indexOf("]]>") >= 0)
|
|
298
|
+
invalid("CDATA terminator in text");
|
|
299
|
+
const resolved = stack.length ? (text.indexOf("&") < 0 ? (pendingWork += text.length, text) : yield* entities(text)) : text;
|
|
300
|
+
while (pendingWork >= 512) {
|
|
301
|
+
yield 512;
|
|
302
|
+
pendingWork -= 512;
|
|
303
|
+
}
|
|
304
|
+
admitText(resolved);
|
|
305
|
+
const parent = stack.at(-1);
|
|
306
|
+
if (parent) {
|
|
307
|
+
parent.element.text += resolved;
|
|
308
|
+
if (resolved.length) {
|
|
309
|
+
admitContent();
|
|
310
|
+
if (retainContent)
|
|
311
|
+
parent.content.push({ kind: "text", text: resolved });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
for (let index = 0; index < resolved.length; index++) {
|
|
316
|
+
const c = resolved.charCodeAt(index);
|
|
317
|
+
if (c !== 32 && c !== 9 && c !== 13 && c !== 10)
|
|
318
|
+
invalid("text outside the root");
|
|
319
|
+
}
|
|
320
|
+
pendingWork += resolved.length;
|
|
321
|
+
while (pendingWork >= 512) {
|
|
322
|
+
yield 512;
|
|
323
|
+
pendingWork -= 512;
|
|
324
|
+
}
|
|
325
|
+
if (resolved.length) {
|
|
326
|
+
admitContent();
|
|
327
|
+
if (retainContent)
|
|
328
|
+
(root ? epilog : prolog).push({ kind: "text", text: resolved });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
offset = endPos;
|
|
332
|
+
}
|
|
333
|
+
else if (source.startsWith("<!--", offset)) {
|
|
334
|
+
const end = yield* find(source, "-->", offset + 4);
|
|
335
|
+
if (end < 0 || (yield* find(source.slice(offset + 4, end), "--", 0)) >= 0 || source.slice(offset + 4, end).endsWith("-")) {
|
|
336
|
+
invalid("malformed comment");
|
|
337
|
+
}
|
|
338
|
+
const parent = stack.at(-1);
|
|
339
|
+
const text = source.slice(offset + 4, end);
|
|
340
|
+
admitText(text);
|
|
341
|
+
admitContent();
|
|
342
|
+
if (retainContent) {
|
|
343
|
+
(parent?.content ?? (root ? epilog : prolog)).push({ kind: "comment", text });
|
|
344
|
+
}
|
|
345
|
+
offset = end + 3;
|
|
346
|
+
}
|
|
347
|
+
else if (source.startsWith("<![CDATA[", offset)) {
|
|
348
|
+
if (!stack.length)
|
|
349
|
+
invalid("CDATA outside root");
|
|
350
|
+
const end = yield* find(source, "]]>", offset + 9);
|
|
351
|
+
if (end < 0)
|
|
352
|
+
invalid("unterminated CDATA");
|
|
353
|
+
const cdataText = source.slice(offset + 9, end);
|
|
354
|
+
admitText(cdataText);
|
|
355
|
+
const parent = stack.at(-1);
|
|
356
|
+
parent.element.text += cdataText;
|
|
357
|
+
admitContent();
|
|
358
|
+
if (retainContent)
|
|
359
|
+
parent.content.push({ kind: "cdata", text: cdataText });
|
|
360
|
+
offset = end + 3;
|
|
361
|
+
}
|
|
362
|
+
else if (source.startsWith("<?", offset)) {
|
|
363
|
+
const start = offset;
|
|
364
|
+
offset += 2;
|
|
365
|
+
const [target, , , wName] = scanName();
|
|
366
|
+
pendingWork += wName;
|
|
367
|
+
while (pendingWork >= 512) {
|
|
368
|
+
yield 512;
|
|
369
|
+
pendingWork -= 512;
|
|
370
|
+
}
|
|
371
|
+
const end = yield* find(source, "?>", offset);
|
|
372
|
+
if (end < 0)
|
|
373
|
+
invalid("unterminated processing instruction");
|
|
374
|
+
const content = source.slice(offset, end);
|
|
375
|
+
if (target.length === 3 && target.toLowerCase() === "xml") {
|
|
376
|
+
if (start !== 0 || target !== "xml"
|
|
377
|
+
|| !(yield* validDeclaration(content, limits.expectedEncoding))) {
|
|
378
|
+
invalid("unsupported XML declaration");
|
|
379
|
+
}
|
|
380
|
+
if (retainContent)
|
|
381
|
+
declaration = source.slice(start, end + 2);
|
|
382
|
+
}
|
|
383
|
+
else if (content && !" \t\n\r".includes(content[0]))
|
|
384
|
+
invalid("invalid processing instruction");
|
|
385
|
+
if (!(target.length === 3 && target.toLowerCase() === "xml")) {
|
|
386
|
+
const parent = stack.at(-1);
|
|
387
|
+
let wsStart = 0;
|
|
388
|
+
while (wsStart < content.length && " \t\n\r".includes(content[wsStart]))
|
|
389
|
+
wsStart++;
|
|
390
|
+
pendingWork += wsStart;
|
|
391
|
+
while (pendingWork >= 512) {
|
|
392
|
+
yield 512;
|
|
393
|
+
pendingWork -= 512;
|
|
394
|
+
}
|
|
395
|
+
const text = content.slice(wsStart);
|
|
396
|
+
admitText(text);
|
|
397
|
+
admitContent();
|
|
398
|
+
if (retainContent) {
|
|
399
|
+
(parent?.content ?? (root ? epilog : prolog)).push({ kind: "processing-instruction", target, text });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
offset = end + 2;
|
|
403
|
+
}
|
|
404
|
+
else if (source.startsWith("<!", offset)) {
|
|
405
|
+
invalid("DTD and entity declarations are forbidden");
|
|
406
|
+
}
|
|
407
|
+
else if (source.charCodeAt(offset + 1) === 47) {
|
|
408
|
+
offset += 2;
|
|
409
|
+
const [name, , , wName] = scanName();
|
|
410
|
+
pendingWork += wName + skipWhitespace();
|
|
411
|
+
while (pendingWork >= 512) {
|
|
412
|
+
yield 512;
|
|
413
|
+
pendingWork -= 512;
|
|
414
|
+
}
|
|
415
|
+
if (source[offset++] !== ">" || stack.pop()?.name !== name)
|
|
416
|
+
invalid("mismatched closing tag");
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
offset++;
|
|
420
|
+
const repeated = previousEmpty && source.startsWith(previousEmpty.suffix, offset) ? previousEmpty : undefined;
|
|
421
|
+
const [name, prefix, localName, wName] = repeated
|
|
422
|
+
? [repeated.name, repeated.prefix, repeated.localName, repeated.name.length * 2]
|
|
423
|
+
: scanName();
|
|
424
|
+
if (repeated)
|
|
425
|
+
offset += name.length;
|
|
426
|
+
// Preserve scan/validation work while reusing the admitted spelling.
|
|
427
|
+
pendingWork += wName;
|
|
428
|
+
while (pendingWork >= 512) {
|
|
429
|
+
yield 512;
|
|
430
|
+
pendingWork -= 512;
|
|
431
|
+
}
|
|
432
|
+
let attributes;
|
|
433
|
+
const attrMeta = [];
|
|
434
|
+
let namespaces = stack.at(-1)?.namespaces ?? new Map([["xml", xmlNamespace]]);
|
|
435
|
+
let ownsNamespaces = stack.length === 0;
|
|
436
|
+
while (!repeated) {
|
|
437
|
+
const ws = skipWhitespace();
|
|
438
|
+
pendingWork += ws;
|
|
439
|
+
while (pendingWork >= 512) {
|
|
440
|
+
yield 512;
|
|
441
|
+
pendingWork -= 512;
|
|
442
|
+
}
|
|
443
|
+
const ch = source.charCodeAt(offset);
|
|
444
|
+
if (ch === 47 || ch === 62)
|
|
445
|
+
break;
|
|
446
|
+
if (ws === 0)
|
|
447
|
+
invalid("attributes require whitespace");
|
|
448
|
+
const [attribute, attrPrefix, attrLocal, wAttr] = scanName();
|
|
449
|
+
pendingWork += wAttr;
|
|
450
|
+
while (pendingWork >= 512) {
|
|
451
|
+
yield 512;
|
|
452
|
+
pendingWork -= 512;
|
|
453
|
+
}
|
|
454
|
+
if (attributes?.has(attribute))
|
|
455
|
+
invalid("duplicate attribute");
|
|
456
|
+
if (++attributeCount > maxAttributes)
|
|
457
|
+
throw new XmlLimitError("maxAttributes", "XML attribute limit exceeded");
|
|
458
|
+
if ((attributes?.size ?? 0) >= maxAttributesPerElement)
|
|
459
|
+
throw new XmlLimitError("maxAttributesPerElement", "XML attribute limit exceeded");
|
|
460
|
+
pendingWork += skipWhitespace();
|
|
461
|
+
if (source[offset++] !== "=")
|
|
462
|
+
invalid("missing attribute equals");
|
|
463
|
+
pendingWork += skipWhitespace();
|
|
464
|
+
const quote = source[offset++];
|
|
465
|
+
if (quote !== '"' && quote !== "'")
|
|
466
|
+
invalid("unquoted attribute");
|
|
467
|
+
const end = source.indexOf(quote, offset);
|
|
468
|
+
const scanLen = (end < 0 ? source.length : end) - offset;
|
|
469
|
+
pendingWork += scanLen;
|
|
470
|
+
while (pendingWork >= 512) {
|
|
471
|
+
yield 512;
|
|
472
|
+
pendingWork -= 512;
|
|
473
|
+
}
|
|
474
|
+
if (end < 0)
|
|
475
|
+
invalid("unterminated attribute");
|
|
476
|
+
const raw = source.slice(offset, end);
|
|
477
|
+
pendingWork += raw.length * 2;
|
|
478
|
+
while (pendingWork >= 512) {
|
|
479
|
+
yield 512;
|
|
480
|
+
pendingWork -= 512;
|
|
481
|
+
}
|
|
482
|
+
if (raw.indexOf("<") >= 0)
|
|
483
|
+
invalid("less-than in attribute");
|
|
484
|
+
const normalized = /[\t\n\r]/.test(raw) ? raw.replace(/[\t\n\r]/g, " ") : raw;
|
|
485
|
+
const value = normalized.indexOf("&") < 0 ? (pendingWork += normalized.length, normalized) : yield* entities(normalized);
|
|
486
|
+
while (pendingWork >= 512) {
|
|
487
|
+
yield 512;
|
|
488
|
+
pendingWork -= 512;
|
|
489
|
+
}
|
|
490
|
+
admitText(value);
|
|
491
|
+
(attributes ??= new Map()).set(attribute, value);
|
|
492
|
+
attrMeta.push([attribute, attrPrefix, attrLocal]);
|
|
493
|
+
offset = end + 1;
|
|
494
|
+
if (attribute === "xmlns" || attribute.startsWith("xmlns:")) {
|
|
495
|
+
const nsPrefix = attribute === "xmlns" ? "" : attribute.slice(6);
|
|
496
|
+
if (nsPrefix === "xmlns" || value === xmlnsNamespace
|
|
497
|
+
|| (nsPrefix === "xml") !== (value === xmlNamespace)
|
|
498
|
+
|| (nsPrefix !== "" && value === ""))
|
|
499
|
+
invalid("invalid namespace binding");
|
|
500
|
+
if (!ownsNamespaces) {
|
|
501
|
+
const copy = new Map();
|
|
502
|
+
for (const [key, uri] of namespaces) {
|
|
503
|
+
copy.set(key, uri);
|
|
504
|
+
pendingWork += 1;
|
|
505
|
+
}
|
|
506
|
+
namespaces = copy;
|
|
507
|
+
ownsNamespaces = true;
|
|
508
|
+
}
|
|
509
|
+
namespaces.set(nsPrefix, value);
|
|
510
|
+
if (namespaces.size > maxNamespaces)
|
|
511
|
+
throw new XmlLimitError("maxNamespaces", "XML namespace scope limit exceeded");
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (attrMeta.length > 0) {
|
|
515
|
+
const expanded = attrMeta.length > 1 ? new Set() : undefined;
|
|
516
|
+
for (let i = 0; i < attrMeta.length; i++) {
|
|
517
|
+
const [attribute, attrPrefix, attrLocal] = attrMeta[i];
|
|
518
|
+
if (attribute === "xmlns" || attribute.startsWith("xmlns:"))
|
|
519
|
+
continue;
|
|
520
|
+
pendingWork += attribute.length;
|
|
521
|
+
if (attrPrefix && !namespaces.has(attrPrefix))
|
|
522
|
+
invalid("unbound attribute prefix");
|
|
523
|
+
if (expanded) {
|
|
524
|
+
const key = `${attrPrefix ? namespaces.get(attrPrefix) : ""}\0${attrLocal}`;
|
|
525
|
+
if (expanded.has(key))
|
|
526
|
+
invalid("duplicate expanded attribute");
|
|
527
|
+
expanded.add(key);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
pendingWork += name.length;
|
|
532
|
+
while (pendingWork >= 512) {
|
|
533
|
+
yield 512;
|
|
534
|
+
pendingWork -= 512;
|
|
535
|
+
}
|
|
536
|
+
if (prefix === "xmlns" || (prefix && !namespaces.has(prefix)))
|
|
537
|
+
invalid("unbound element prefix");
|
|
538
|
+
if (++nodes > maxNodes)
|
|
539
|
+
throw new XmlLimitError("maxNodes", "XML resource limit exceeded");
|
|
540
|
+
if (stack.length + 1 > maxDepth)
|
|
541
|
+
throw new XmlLimitError("maxDepth", "XML resource limit exceeded");
|
|
542
|
+
const namespace = namespaces.get(prefix) ?? "";
|
|
543
|
+
limits.onElement?.({ name, namespace, localName }, stack.at(-1)?.element, stack.length + 1);
|
|
544
|
+
admitContent();
|
|
545
|
+
const retainedAttributes = [];
|
|
546
|
+
if (retainContent && attributes) {
|
|
547
|
+
for (let i = 0; i < attrMeta.length; i++) {
|
|
548
|
+
const [attribute, attrPrefix, attrLocal] = attrMeta[i];
|
|
549
|
+
const value = attributes.get(attribute);
|
|
550
|
+
admitContent();
|
|
551
|
+
retainedAttributes.push({ name: attribute, localName: attrLocal,
|
|
552
|
+
namespace: attribute === "xmlns" || attrPrefix === "xmlns" ? xmlnsNamespace : attrPrefix ? namespaces.get(attrPrefix) : "", value });
|
|
553
|
+
pendingWork += attribute.length + 1;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
while (pendingWork >= 512) {
|
|
557
|
+
yield 512;
|
|
558
|
+
pendingWork -= 512;
|
|
559
|
+
}
|
|
560
|
+
const content = retainContent ? [] : undefined;
|
|
561
|
+
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 }) } : {}) };
|
|
562
|
+
const parent = stack.at(-1);
|
|
563
|
+
if (parent) {
|
|
564
|
+
parent.element.children.push(element);
|
|
565
|
+
parent.content?.push(element);
|
|
566
|
+
}
|
|
567
|
+
else if (root)
|
|
568
|
+
invalid("multiple root elements");
|
|
569
|
+
else
|
|
570
|
+
root = element;
|
|
571
|
+
const empty = source[offset] === "/";
|
|
572
|
+
if (empty)
|
|
573
|
+
offset++;
|
|
574
|
+
if (source[offset++] !== ">")
|
|
575
|
+
invalid("unterminated start tag");
|
|
576
|
+
// Only cache compact, attribute-free spellings. Namespace resolution and
|
|
577
|
+
// every admission counter still run for each distinct physical element.
|
|
578
|
+
if (empty && !attributes && name.length <= 512 && source.slice(offset - name.length - 3, offset) === `<${name}/>`)
|
|
579
|
+
previousEmpty = { suffix: name + "/>", name, prefix, localName };
|
|
580
|
+
if (!empty)
|
|
581
|
+
stack.push({ element, content, name, namespaces });
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (pendingWork > 0) {
|
|
585
|
+
yield pendingWork;
|
|
586
|
+
pendingWork = 0;
|
|
587
|
+
}
|
|
588
|
+
if (stack.length || !root)
|
|
589
|
+
invalid("incomplete document");
|
|
590
|
+
return root;
|
|
591
|
+
}
|
|
592
|
+
export function parseXml(input, limits = {}) {
|
|
593
|
+
const parser = parseXmlSteps(input, limits);
|
|
594
|
+
let result = parser.next();
|
|
595
|
+
while (!result.done)
|
|
596
|
+
result = parser.next();
|
|
597
|
+
return result.value;
|
|
598
|
+
}
|