@poe-platform/safe-fs 0.1.812 → 0.1.814

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