@poe-platform/safe-fs 0.1.753 → 0.1.755

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.
@@ -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);
@@ -1,45 +1 @@
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;
1
+ export * from "../xml-ast/index.js";
@@ -1,598 +1 @@
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
- }
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poe-platform/safe-fs",
3
- "version": "0.1.753",
3
+ "version": "0.1.755",
4
4
  "description": "Composable filesystem with a portable core and explicit Node adapters",
5
5
  "type": "module",
6
6
  "license": "MIT",