@poe-platform/safe-fs 0.1.818 → 0.1.820

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,18 @@
1
+ import type { XmlSourceSpan } from "./source.js";
2
+ import type { XmlNamespaceScope } from "./frames.js";
1
3
  export interface XmlAttributeRecord {
2
4
  name: string;
3
5
  prefix: string;
4
6
  localName: string;
5
7
  value: string;
6
8
  next: number;
9
+ nameSource?: XmlSourceSpan;
10
+ localNameSource?: XmlSourceSpan;
11
+ prefixSource?: XmlSourceSpan;
12
+ source?: {
13
+ start: number;
14
+ end: number;
15
+ };
7
16
  }
8
17
  export interface XmlAttributeState {
9
18
  reference: number;
@@ -15,7 +24,13 @@ export type XmlAttributeRequest = {
15
24
  attributeOperation: 'has' | 'expanded';
16
25
  state: XmlAttributeState;
17
26
  name: string;
27
+ nameSource?: XmlSourceSpan;
18
28
  found?: boolean;
29
+ namespace?: {
30
+ scope: XmlNamespaceScope;
31
+ prefix: string;
32
+ prefixSource?: XmlSourceSpan;
33
+ };
19
34
  } | {
20
35
  attributeOperation: 'append';
21
36
  state: XmlAttributeState;
@@ -40,8 +55,15 @@ export declare class XmlAttributes {
40
55
  get length(): number;
41
56
  get size(): number;
42
57
  get first(): number;
43
- has(name: string): Generator<XmlAttributeRequest, boolean, void>;
44
- append(name: string, prefix: string, localName: string, value: string): Generator<XmlAttributeRequest, void, void>;
58
+ has(name: string, nameSource?: XmlSourceSpan): Generator<XmlAttributeRequest, boolean, void>;
59
+ append(name: string, prefix: string, localName: string, value: string, source?: {
60
+ start: number;
61
+ end: number;
62
+ }, nameSource?: XmlSourceSpan, localNameSource?: XmlSourceSpan, prefixSource?: XmlSourceSpan): Generator<XmlAttributeRequest, void, void>;
45
63
  read(reference: number, values: boolean): Generator<XmlAttributeRequest, XmlAttributeRecord, void>;
46
- expanded(name: string): Generator<XmlAttributeRequest, boolean, void>;
64
+ expanded(name: string, namespace?: {
65
+ scope: XmlNamespaceScope;
66
+ prefix: string;
67
+ prefixSource?: XmlSourceSpan;
68
+ }, nameSource?: XmlSourceSpan): Generator<XmlAttributeRequest, boolean, void>;
47
69
  }
@@ -12,25 +12,25 @@ export class XmlAttributes {
12
12
  get length() { return this.state.length; }
13
13
  get size() { return this.state.size; }
14
14
  get first() { return this.state.first; }
15
- *has(name) {
15
+ *has(name, nameSource) {
16
16
  if (!this.length)
17
17
  return false;
18
18
  if (!this.external)
19
19
  return this.names.has(name);
20
- const request = { attributeOperation: 'has', state: this.state, name };
20
+ const request = { attributeOperation: 'has', state: this.state, name, ...(nameSource ? { nameSource } : {}) };
21
21
  yield request;
22
22
  if (request.found === undefined)
23
23
  throw new TypeError('Incomplete XML attribute lookup');
24
24
  return request.found;
25
25
  }
26
- *append(name, prefix, localName, value) {
26
+ *append(name, prefix, localName, value, source, nameSource, localNameSource, prefixSource) {
27
27
  if (!this.external) {
28
28
  this.names.set(name, value);
29
29
  this.records.push({ name, prefix, localName });
30
30
  this.state = { reference: 0, first: 1, length: this.records.length, size: this.names.size };
31
31
  return;
32
32
  }
33
- const request = { attributeOperation: 'append', state: this.state, attribute: { name, prefix, localName, value } };
33
+ const request = { attributeOperation: 'append', state: this.state, attribute: { name, prefix, localName, value, ...(source ? { source } : {}), ...(nameSource ? { nameSource } : {}), ...(localNameSource ? { localNameSource } : {}), ...(prefixSource ? { prefixSource } : {}) } };
34
34
  yield request;
35
35
  if (!request.result)
36
36
  throw new TypeError('Incomplete XML attribute append');
@@ -47,13 +47,13 @@ export class XmlAttributes {
47
47
  throw new TypeError('Incomplete XML attribute read');
48
48
  return request.result;
49
49
  }
50
- *expanded(name) {
50
+ *expanded(name, namespace, nameSource) {
51
51
  if (!this.external) {
52
52
  const found = this.expandedNames.has(name);
53
53
  this.expandedNames.add(name);
54
54
  return found;
55
55
  }
56
- const request = { attributeOperation: 'expanded', state: this.state, name };
56
+ const request = { attributeOperation: 'expanded', state: this.state, name, ...(namespace ? { namespace } : {}), ...(nameSource ? { nameSource } : {}) };
57
57
  yield request;
58
58
  if (request.found === undefined)
59
59
  throw new TypeError('Incomplete XML expanded attribute lookup');
@@ -1,4 +1,5 @@
1
- import type { XmlContent, XmlElement } from './index.js';
1
+ import type { XmlSourceStep, XmlSourceSpan } from './source.js';
2
+ import type { XmlContent, XmlElement, XmlName } from './index.js';
2
3
  export interface XmlParserFrame {
3
4
  element: XmlElement;
4
5
  content: XmlContent[] | undefined;
@@ -33,17 +34,34 @@ export interface XmlNamespaceScope {
33
34
  readonly size: number;
34
35
  }
35
36
  export type XmlNamespaceRequest = {
37
+ readonly namespaceOperation: 'reference';
38
+ readonly scope: XmlNamespaceScope;
39
+ readonly prefix: string;
40
+ readonly prefixSource?: XmlSourceSpan;
41
+ reference?: number;
42
+ complete?: true;
43
+ } | {
44
+ readonly namespaceOperation: 'has';
45
+ readonly scope: XmlNamespaceScope;
46
+ readonly prefix: string;
47
+ readonly prefixSource?: XmlSourceSpan;
48
+ found?: boolean;
49
+ } | {
36
50
  readonly namespaceOperation: 'get';
37
51
  readonly scope: XmlNamespaceScope;
38
52
  readonly prefix: string;
53
+ readonly prefixSource?: XmlSourceSpan;
39
54
  value?: string;
40
55
  complete?: true;
41
56
  } | {
42
57
  readonly namespaceOperation: 'set';
43
58
  readonly scope: XmlNamespaceScope;
44
59
  readonly prefix: string;
45
- readonly value: string;
60
+ readonly prefixSource?: XmlSourceSpan;
61
+ readonly value: string | Generator<XmlSourceStep | string, void, void>;
46
62
  result?: XmlNamespaceScope;
47
63
  };
48
- export declare function namespaceValue(scope: Map<string, string> | XmlNamespaceScope, prefix: string): Generator<XmlNamespaceRequest, string | undefined, void>;
49
- export declare function bindNamespace(scope: Map<string, string> | XmlNamespaceScope, prefix: string, value: string): Generator<XmlNamespaceRequest, Map<string, string> | XmlNamespaceScope, void>;
64
+ export declare function namespaceValue(scope: Map<string, string> | XmlNamespaceScope, prefix: string, prefixSource?: XmlSourceSpan): Generator<XmlNamespaceRequest, string | undefined, void>;
65
+ export declare function namespaceMetadata(scope: Map<string, string> | XmlNamespaceScope, prefix: string, deferred?: boolean, prefixSource?: XmlSourceSpan): Generator<XmlNamespaceRequest, Pick<XmlName, 'namespace' | 'namespaceReference'>, void>;
66
+ export declare function hasNamespace(scope: Map<string, string> | XmlNamespaceScope, prefix: string, prefixSource?: XmlSourceSpan): Generator<XmlNamespaceRequest, boolean, void>;
67
+ export declare function bindNamespace(scope: Map<string, string> | XmlNamespaceScope, prefix: string, value: string | Generator<XmlSourceStep | string, void, void>, prefixSource?: XmlSourceSpan): Generator<XmlNamespaceRequest, Map<string, string> | XmlNamespaceScope, void>;
@@ -43,21 +43,43 @@ export class XmlFrames {
43
43
  return frame;
44
44
  }
45
45
  }
46
- export function* namespaceValue(scope, prefix) {
46
+ export function* namespaceValue(scope, prefix, prefixSource) {
47
47
  if (scope instanceof Map)
48
48
  return scope.get(prefix);
49
- const request = { namespaceOperation: 'get', scope, prefix };
49
+ const request = { namespaceOperation: 'get', scope, prefix, ...(prefixSource ? { prefixSource } : {}) };
50
50
  yield request;
51
51
  if (!request.complete)
52
52
  throw new TypeError('Incomplete XML namespace read');
53
53
  return request.value;
54
54
  }
55
- export function* bindNamespace(scope, prefix, value) {
55
+ export function* namespaceMetadata(scope, prefix, deferred = false, prefixSource) {
56
+ if (!deferred || scope instanceof Map)
57
+ return { namespace: (yield* namespaceValue(scope, prefix, prefixSource)) ?? '' };
58
+ if (prefix === 'xml')
59
+ return { namespace: 'http://www.w3.org/XML/1998/namespace' };
60
+ const request = { namespaceOperation: 'reference', scope, prefix, ...(prefixSource ? { prefixSource } : {}) };
61
+ yield request;
62
+ if (!request.complete)
63
+ throw new TypeError('Incomplete XML namespace reference lookup');
64
+ return { namespace: '', ...(request.reference === undefined ? {} : { namespaceReference: request.reference }) };
65
+ }
66
+ export function* hasNamespace(scope, prefix, prefixSource) {
67
+ if (scope instanceof Map)
68
+ return scope.has(prefix);
69
+ const request = { namespaceOperation: 'has', scope, prefix, ...(prefixSource ? { prefixSource } : {}) };
70
+ yield request;
71
+ if (request.found === undefined)
72
+ throw new TypeError('Incomplete XML namespace membership lookup');
73
+ return request.found;
74
+ }
75
+ export function* bindNamespace(scope, prefix, value, prefixSource) {
56
76
  if (scope instanceof Map) {
77
+ if (typeof value !== 'string')
78
+ throw new TypeError('Streamed XML namespace values require external scopes');
57
79
  scope.set(prefix, value);
58
80
  return scope;
59
81
  }
60
- const request = { namespaceOperation: 'set', scope, prefix, value };
82
+ const request = { namespaceOperation: 'set', scope, prefix, value, ...(prefixSource ? { prefixSource } : {}) };
61
83
  yield request;
62
84
  if (!request.result)
63
85
  throw new TypeError('Incomplete XML namespace update');
@@ -3,13 +3,21 @@ export type { XmlAttributeRequest, XmlAttributeRecord, XmlAttributeState } from
3
3
  import { type XmlNamespaceRequest, type XmlFrameRequest } from "./frames.js";
4
4
  export type { XmlParserFrame, XmlFrameRequest, XmlNamespaceScope, XmlNamespaceRequest } from "./frames.js";
5
5
  export { normalizeXmlChunks } from "./characters.js";
6
- import { type XmlSourceStep } from "./source.js";
7
- export type { XmlSourceRead, XmlSourceStep } from "./source.js";
6
+ import { type XmlSourceSpan, type XmlSourceStep } from "./source.js";
7
+ export type { XmlSourceRead, XmlSourceSpan, XmlSourceStep } from "./source.js";
8
8
  import type { XmlStreamEvent } from "./stream.js";
9
9
  export { XmlLimitError } from "./errors.js";
10
10
  export interface XmlName {
11
11
  readonly name: string;
12
+ readonly nameSource?: XmlSourceSpan;
13
+ readonly nameReference?: number;
14
+ readonly localNameSource?: XmlSourceSpan;
15
+ readonly localNameReference?: number;
16
+ readonly prefix?: string;
17
+ readonly prefixSource?: XmlSourceSpan;
18
+ readonly prefixReference?: number;
12
19
  readonly namespace: string;
20
+ readonly namespaceReference?: number;
13
21
  readonly localName: string;
14
22
  }
15
23
  export interface XmlAttribute extends XmlName {
@@ -21,6 +29,8 @@ export type XmlContent = XmlElement | {
21
29
  } | {
22
30
  readonly kind: "processing-instruction";
23
31
  readonly target: string;
32
+ readonly targetSource?: XmlSourceSpan;
33
+ readonly targetReference?: number;
24
34
  readonly text: string;
25
35
  };
26
36
  export interface XmlElement extends XmlName {
@@ -65,9 +75,18 @@ export interface XmlSourceLimits extends XmlStepLimits {
65
75
  readonly storeFrames?: boolean;
66
76
  /** Resolve namespace scopes through host-serviced requests. Requires retainTree: false. */
67
77
  readonly storeNamespaces?: boolean;
78
+ /** Carry host namespace token references in event metadata. Requires stored namespaces. */
79
+ readonly deferNamespaces?: boolean;
80
+ /** Emit long processing-instruction targets as source spans. Requires fragmentContent. */
81
+ readonly deferContentNames?: boolean;
82
+ /** Defer long element names and, with external namespaces, their prefixes. */
83
+ readonly deferElementNames?: boolean;
84
+ /** Defer attribute names and namespace declaration prefixes to source spans. */
85
+ readonly deferAttributeNames?: boolean;
68
86
  /** Store attribute collections through host requests and emit resolved attributes individually. */
69
87
  readonly storeAttributes?: boolean;
70
- readonly onAttribute?: (attribute: XmlAttribute, element: XmlElement) => void;
88
+ readonly fragmentAttributes?: boolean;
89
+ readonly onAttribute?: (attribute: XmlAttribute, element: XmlElement, continuation?: boolean) => void;
71
90
  }
72
91
  export type XmlParseStep = XmlSourceStep | XmlFrameRequest | XmlNamespaceRequest | XmlAttributeRequest;
73
92
  /** Parse a validated, BOM-free, line-normalized UTF-16 source via bounded read requests. */
@@ -1,5 +1,5 @@
1
1
  import { XmlAttributes } from "./attributes.js";
2
- import { XmlFrames, bindNamespace, namespaceValue } from "./frames.js";
2
+ import { XmlFrames, bindNamespace, namespaceValue, namespaceMetadata, hasNamespace } from "./frames.js";
3
3
  import { validCharacter } from "./characters.js";
4
4
  export { normalizeXmlChunks } from "./characters.js";
5
5
  import { XmlSource } from "./source.js";
@@ -68,6 +68,41 @@ function qualifiedNameSync(name, cache) {
68
68
  }
69
69
  return parts;
70
70
  }
71
+ /** Validate a name span without retaining its spelling. */
72
+ function* validateNameSpan(source, start, end) {
73
+ let first = true, colon = -1, work = 0;
74
+ for (let offset = start; offset < end;) {
75
+ let point = yield* source.charCodeAt(offset);
76
+ if (point >= 0xd800 && point <= 0xdbff && offset + 1 < end) {
77
+ const low = yield* source.charCodeAt(offset + 1);
78
+ if (low >= 0xdc00 && low <= 0xdfff)
79
+ point = 0x10000 + (point - 0xd800) * 1024 + low - 0xdc00;
80
+ }
81
+ if (point === 58) {
82
+ if (colon >= 0 || first)
83
+ invalid("invalid qualified name");
84
+ colon = offset;
85
+ first = true;
86
+ }
87
+ else {
88
+ if (!(first ? nameStart(point) : namePart(point)))
89
+ invalid("invalid qualified name");
90
+ first = false;
91
+ }
92
+ const size = point > 0xffff ? 2 : 1;
93
+ offset += size;
94
+ work += size;
95
+ if (work >= 512) {
96
+ yield 512;
97
+ work -= 512;
98
+ }
99
+ }
100
+ if (first)
101
+ invalid("invalid qualified name");
102
+ if (work)
103
+ yield work;
104
+ return colon;
105
+ }
71
106
  function* entity(source, start, end, recover) {
72
107
  // Inspect only a bounded prefix, even for arbitrarily long numeric references.
73
108
  const prefix = typeof source === "string" ? source.slice(start, Math.min(end, start + 4))
@@ -102,7 +137,7 @@ function* entity(source, start, end, recover) {
102
137
  return String.fromCodePoint(point);
103
138
  }
104
139
  /** Decoded text fragments; source read/work requests remain host-serviced. */
105
- function* textParts(source, start, end, decode, recover) {
140
+ function* textParts(source, start, end, decode, recover, attribute = false) {
106
141
  let cursor = start;
107
142
  while (cursor < end) {
108
143
  let finish = Math.min(end, cursor + 512);
@@ -111,7 +146,13 @@ function* textParts(source, start, end, decode, recover) {
111
146
  if (last >= 0xd800 && last <= 0xdbff)
112
147
  finish--;
113
148
  }
114
- const raw = yield* source.slice(cursor, finish);
149
+ let raw = yield* source.slice(cursor, finish);
150
+ if (attribute) {
151
+ let normalized = "";
152
+ for (const character of raw)
153
+ normalized += "\t\n\r".includes(character) ? " " : character;
154
+ raw = normalized;
155
+ }
115
156
  const found = decode ? raw.indexOf("&") : -1;
116
157
  if (found < 0) {
117
158
  yield raw;
@@ -140,9 +181,9 @@ function* textParts(source, start, end, decode, recover) {
140
181
  cursor = semicolon + 1;
141
182
  }
142
183
  }
143
- function* textFragments(source, start, end, decode, recover) {
184
+ function* textFragments(source, start, end, decode, recover, attribute = false) {
144
185
  let pending = "";
145
- for (const part of textParts(source, start, end, decode, recover)) {
186
+ for (const part of textParts(source, start, end, decode, recover, attribute)) {
146
187
  if (typeof part !== "string") {
147
188
  yield part;
148
189
  continue;
@@ -254,10 +295,20 @@ function validateLimits(limits) {
254
295
  }
255
296
  export function* parseXmlSourceSteps(length, limits = {}) {
256
297
  validateLimits(limits);
298
+ if (limits.deferAttributeNames && (!limits.storeAttributes || !limits.storeNamespaces))
299
+ throw new TypeError("Deferred attribute names require stored attributes and namespaces");
300
+ if (limits.deferElementNames && limits.retainTree !== false)
301
+ throw new TypeError("Deferred element names require retainTree: false");
302
+ if (limits.deferContentNames && !limits.fragmentContent)
303
+ throw new TypeError("Deferred XML content names require fragmentContent: true");
304
+ if (limits.fragmentAttributes && !limits.storeAttributes)
305
+ throw new TypeError("XML attribute fragments require storeAttributes: true");
257
306
  if (limits.storeFrames && limits.retainTree !== false)
258
307
  throw new TypeError("Stored XML frames require retainTree: false");
259
308
  if (limits.storeAttributes && limits.retainTree !== false)
260
309
  throw new TypeError("Stored XML attributes require retainTree: false");
310
+ if (limits.deferNamespaces && !limits.storeNamespaces)
311
+ throw new TypeError("Deferred XML namespaces require stored namespaces");
261
312
  if (limits.storeNamespaces && limits.retainTree !== false)
262
313
  throw new TypeError("Stored XML namespaces require retainTree: false");
263
314
  if (limits.fragmentContent && limits.retainTree !== false)
@@ -314,7 +365,24 @@ export function* parseXmlSourceSteps(length, limits = {}) {
314
365
  }
315
366
  return offset - start;
316
367
  };
317
- const scanName = function* () {
368
+ const sameName = function* (left, right, span) {
369
+ if (!left.nameSource && !span)
370
+ return left.name === right;
371
+ const leftLength = left.nameSource ? left.nameSource.end - left.nameSource.start : left.name.length;
372
+ const rightLength = span ? span.end - span.start : right.length;
373
+ if (leftLength !== rightLength)
374
+ return false;
375
+ for (let index = 0; index < leftLength; index += 512) {
376
+ const end = Math.min(leftLength, index + 512);
377
+ const a = left.nameSource ? yield* source.slice(left.nameSource.start + index, left.nameSource.start + end) : left.name.slice(index, end);
378
+ const b = span ? yield* source.slice(span.start + index, span.start + end) : right.slice(index, end);
379
+ yield end - index;
380
+ if (a !== b)
381
+ return false;
382
+ }
383
+ return true;
384
+ };
385
+ const scanName = function* (defer) {
318
386
  const start = offset;
319
387
  while (yield* source.has(offset)) {
320
388
  const c = (yield* source.charCodeAt(offset));
@@ -327,6 +395,21 @@ export function* parseXmlSourceSteps(length, limits = {}) {
327
395
  pendingWork -= 512;
328
396
  }
329
397
  }
398
+ if (defer && offset - start > 512) {
399
+ const colon = yield* validateNameSpan(source, start, offset);
400
+ if (defer === "content")
401
+ return ["", "", "", { start, end: offset }];
402
+ if (colon - start > 512 && !limits.storeNamespaces) {
403
+ const name = yield* source.slice(start, offset);
404
+ return [name, name.slice(0, colon - start), name.slice(colon - start + 1)];
405
+ }
406
+ const localStart = colon < 0 ? start : colon + 1;
407
+ const prefixSource = colon - start > 512 ? { start, end: colon } : undefined;
408
+ const prefix = colon < 0 || prefixSource ? "" : yield* source.slice(start, colon);
409
+ const localNameSource = offset - localStart > 512 ? { start: localStart, end: offset } : undefined;
410
+ const local = localNameSource ? "" : yield* source.slice(localStart, offset);
411
+ return ["", prefix, local, { start, end: offset }, localNameSource, prefixSource];
412
+ }
330
413
  const name = (yield* source.slice(start, offset));
331
414
  pendingWork += offset - start;
332
415
  while (pendingWork >= 512) {
@@ -336,7 +419,7 @@ export function* parseXmlSourceSteps(length, limits = {}) {
336
419
  const [prefix, localName] = qualifiedNameSync(name, qualifiedNames);
337
420
  return [name, prefix, localName];
338
421
  };
339
- const rawContent = function* (kind, start, end, parent, target = "") {
422
+ const rawContent = function* (kind, start, end, parent, target = "", targetSource) {
340
423
  if (end - start > maxTextLength - textLength)
341
424
  throw new XmlLimitError("maxTextLength", "XML text limit exceeded");
342
425
  textLength += end - start;
@@ -350,7 +433,7 @@ export function* parseXmlSourceSteps(length, limits = {}) {
350
433
  finish--;
351
434
  }
352
435
  const text = yield* source.slice(cursor, finish);
353
- const content = kind === "processing-instruction" ? { kind, target, text } : { kind, text };
436
+ const content = kind === "processing-instruction" ? { kind, target, text, ...(targetSource ? { targetSource } : {}) } : { kind, text };
354
437
  if (kind === "cdata" && retainTree)
355
438
  parent.element.text += text;
356
439
  if (retain)
@@ -486,7 +569,7 @@ export function* parseXmlSourceSteps(length, limits = {}) {
486
569
  else if ((yield* source.startsWith("<?", offset))) {
487
570
  const start = offset;
488
571
  offset += 2;
489
- const [target] = yield* scanName();
572
+ const [target, , , targetSource] = yield* scanName(limits.deferContentNames ? "content" : undefined);
490
573
  while (pendingWork >= 512) {
491
574
  yield 512;
492
575
  pendingWork -= 512;
@@ -519,7 +602,7 @@ export function* parseXmlSourceSteps(length, limits = {}) {
519
602
  yield 512;
520
603
  pendingWork -= 512;
521
604
  }
522
- yield* rawContent("processing-instruction", body, end, yield* stack.peek(), target);
605
+ yield* rawContent("processing-instruction", body, end, yield* stack.peek(), target, targetSource);
523
606
  }
524
607
  offset = end + 2;
525
608
  }
@@ -528,14 +611,14 @@ export function* parseXmlSourceSteps(length, limits = {}) {
528
611
  }
529
612
  else if ((yield* source.charCodeAt(offset + 1)) === 47) {
530
613
  offset += 2;
531
- const [name] = yield* scanName();
614
+ const [name, , , nameSource] = yield* scanName(limits.deferElementNames ? "element" : undefined);
532
615
  yield* skipWhitespace();
533
616
  while (pendingWork >= 512) {
534
617
  yield 512;
535
618
  pendingWork -= 512;
536
619
  }
537
620
  const closed = (yield* source.slice(offset++, offset)) === ">" ? (yield* stack.pop()) : undefined;
538
- if (closed?.name !== name) {
621
+ if (!closed || !(yield* sameName(closed.element, name, nameSource))) {
539
622
  if (!limits.recover)
540
623
  invalid("mismatched closing tag");
541
624
  limits.recover("mismatched closing tag");
@@ -546,9 +629,9 @@ export function* parseXmlSourceSteps(length, limits = {}) {
546
629
  else {
547
630
  offset++;
548
631
  const repeated = previousEmpty && (yield* source.startsWith(previousEmpty.suffix, offset)) ? previousEmpty : undefined;
549
- const [name, prefix, localName] = repeated
632
+ const [name, prefix, localName, nameSource, localNameSource, prefixSource] = repeated
550
633
  ? [repeated.name, repeated.prefix, repeated.localName]
551
- : (yield* scanName());
634
+ : (yield* scanName(limits.deferElementNames ? "element" : undefined));
552
635
  if (repeated)
553
636
  offset += name.length;
554
637
  // Preserve scan/validation work while reusing the admitted spelling.
@@ -573,12 +656,12 @@ export function* parseXmlSourceSteps(length, limits = {}) {
573
656
  break;
574
657
  if (ws === 0)
575
658
  invalid("attributes require whitespace");
576
- const [attribute, attrPrefix, attrLocal] = yield* scanName();
659
+ const [attribute, attrPrefix, attrLocal, nameSource, localNameSource, attrPrefixSource] = yield* scanName(limits.deferAttributeNames ? "attribute" : undefined);
577
660
  while (pendingWork >= 512) {
578
661
  yield 512;
579
662
  pendingWork -= 512;
580
663
  }
581
- if ((yield* attributes.has(attribute)))
664
+ if ((yield* attributes.has(attribute, nameSource)))
582
665
  invalid("duplicate attribute");
583
666
  if (++attributeCount > maxAttributes)
584
667
  throw new XmlLimitError("maxAttributes", "XML attribute limit exceeded");
@@ -600,28 +683,57 @@ export function* parseXmlSourceSteps(length, limits = {}) {
600
683
  }
601
684
  if (end < 0)
602
685
  invalid("unterminated attribute");
603
- const raw = (yield* source.slice(offset, end));
604
- pendingWork += raw.length * 2;
605
- while (pendingWork >= 512) {
606
- yield 512;
607
- pendingWork -= 512;
686
+ const fragmented = limits.fragmentAttributes && (limits.storeNamespaces || attribute !== "xmlns" && attrPrefix !== "xmlns");
687
+ const valueStart = offset;
688
+ let value = "", size = 0, matchesXml = true, matchesXmlns = true;
689
+ if (fragmented) {
690
+ if ((yield* source.indexOf("<", offset, end)) >= 0)
691
+ invalid("less-than in attribute");
692
+ for (const part of textFragments(source, offset, end, true, limits.recover, true)) {
693
+ if (typeof part !== "string")
694
+ yield part;
695
+ else {
696
+ matchesXml = matchesXml && part === xmlNamespace.slice(size, size + part.length);
697
+ matchesXmlns = matchesXmlns && part === xmlnsNamespace.slice(size, size + part.length);
698
+ size += part.length;
699
+ }
700
+ }
701
+ matchesXml = matchesXml && size === xmlNamespace.length;
702
+ matchesXmlns = matchesXmlns && size === xmlnsNamespace.length;
703
+ if (size > maxTextLength - textLength)
704
+ throw new XmlLimitError("maxTextLength", "XML text limit exceeded");
705
+ textLength += size;
608
706
  }
609
- if (raw.indexOf("<") >= 0)
610
- invalid("less-than in attribute");
611
- const normalized = /[\t\n\r]/.test(raw) ? raw.replace(/[\t\n\r]/g, " ") : raw;
612
- const value = normalized.indexOf("&") < 0 ? (pendingWork += normalized.length, normalized) : yield* entities(normalized, limits.recover);
613
- while (pendingWork >= 512) {
614
- yield 512;
615
- pendingWork -= 512;
707
+ else {
708
+ const raw = (yield* source.slice(offset, end));
709
+ pendingWork += raw.length * 2;
710
+ while (pendingWork >= 512) {
711
+ yield 512;
712
+ pendingWork -= 512;
713
+ }
714
+ if (raw.indexOf("<") >= 0)
715
+ invalid("less-than in attribute");
716
+ let normalized = raw;
717
+ if (raw.includes("\t") || raw.includes("\n") || raw.includes("\r")) {
718
+ normalized = "";
719
+ for (const character of raw)
720
+ normalized += "\t\n\r".includes(character) ? " " : character;
721
+ }
722
+ value = normalized.indexOf("&") < 0 ? (pendingWork += normalized.length, normalized) : yield* entities(normalized, limits.recover);
723
+ while (pendingWork >= 512) {
724
+ yield 512;
725
+ pendingWork -= 512;
726
+ }
727
+ admitText(value);
616
728
  }
617
- admitText(value);
618
- yield* attributes.append(attribute, attrPrefix, attrLocal, value);
729
+ yield* attributes.append(attribute, attrPrefix, attrLocal, value, limits.fragmentAttributes ? { start: offset, end } : undefined, nameSource, localNameSource, attrPrefixSource);
619
730
  offset = end + 1;
620
- if (attribute === "xmlns" || attribute.startsWith("xmlns:")) {
621
- const nsPrefix = attribute === "xmlns" ? "" : attribute.slice(6);
622
- if (nsPrefix === "xmlns" || value === xmlnsNamespace
623
- || (nsPrefix === "xml") !== (value === xmlNamespace)
624
- || (nsPrefix !== "" && value === ""))
731
+ if (attribute === "xmlns" || attrPrefix === "xmlns") {
732
+ const nsPrefix = attribute === "xmlns" ? "" : attrLocal;
733
+ const nsPrefixSource = attrPrefix === "xmlns" ? localNameSource : undefined;
734
+ if (nsPrefix === "xmlns" || (fragmented ? matchesXmlns : value === xmlnsNamespace)
735
+ || (nsPrefix === "xml") !== (fragmented ? matchesXml : value === xmlNamespace)
736
+ || ((nsPrefix !== "" || nsPrefixSource) && (fragmented ? size === 0 : value === "")))
625
737
  invalid("invalid namespace binding");
626
738
  if (!ownsNamespaces && namespaces instanceof Map) {
627
739
  const copy = new Map();
@@ -632,7 +744,8 @@ export function* parseXmlSourceSteps(length, limits = {}) {
632
744
  namespaces = copy;
633
745
  ownsNamespaces = true;
634
746
  }
635
- namespaces = yield* bindNamespace(namespaces, nsPrefix, value);
747
+ namespaces = yield* bindNamespace(namespaces, nsPrefix, fragmented
748
+ ? textFragments(source, valueStart, end, true, limits.recover ? () => { } : undefined, true) : value, nsPrefixSource);
636
749
  if (namespaces.size > maxNamespaces)
637
750
  throw new XmlLimitError("maxNamespaces", "XML namespace scope limit exceeded");
638
751
  }
@@ -642,14 +755,15 @@ export function* parseXmlSourceSteps(length, limits = {}) {
642
755
  const record = yield* attributes.read(cursor, false);
643
756
  cursor = record.next;
644
757
  const { name: attribute, prefix: attrPrefix, localName: attrLocal } = record;
645
- if (attribute === "xmlns" || attribute.startsWith("xmlns:"))
758
+ if (attribute === "xmlns" || attrPrefix === "xmlns")
646
759
  continue;
647
760
  pendingWork += attribute.length;
648
- if (attrPrefix && (yield* namespaceValue(namespaces, attrPrefix)) === undefined)
761
+ if ((attrPrefix || record.prefixSource) && !(yield* hasNamespace(namespaces, attrPrefix, record.prefixSource)))
649
762
  invalid("unbound attribute prefix");
650
763
  if (attributes.length > 1) {
651
- const key = `${attrPrefix ? (yield* namespaceValue(namespaces, attrPrefix)) : ""}\0${attrLocal}`;
652
- if (yield* attributes.expanded(key))
764
+ const backed = limits.storeAttributes && !(namespaces instanceof Map) ? namespaces : undefined;
765
+ const key = backed ? attrLocal : `${attrPrefix ? (yield* namespaceValue(namespaces, attrPrefix)) : ""}\0${attrLocal}`;
766
+ if (yield* attributes.expanded(key, backed ? { scope: backed, prefix: attrPrefix, ...(record.prefixSource ? { prefixSource: record.prefixSource } : {}) } : undefined, record.localNameSource))
653
767
  invalid("duplicate expanded attribute");
654
768
  }
655
769
  }
@@ -659,14 +773,15 @@ export function* parseXmlSourceSteps(length, limits = {}) {
659
773
  yield 512;
660
774
  pendingWork -= 512;
661
775
  }
662
- if (prefix === "xmlns" || (prefix && (yield* namespaceValue(namespaces, prefix)) === undefined))
776
+ if (prefix === "xmlns" || ((prefix || prefixSource) && !(yield* hasNamespace(namespaces, prefix, prefixSource))))
663
777
  invalid("unbound element prefix");
664
778
  if (++nodes > maxNodes)
665
779
  throw new XmlLimitError("maxNodes", "XML resource limit exceeded");
666
780
  if (stack.length + 1 > maxDepth)
667
781
  throw new XmlLimitError("maxDepth", "XML resource limit exceeded");
668
- const namespace = (yield* namespaceValue(namespaces, prefix)) ?? "";
669
- limits.onElement?.({ name, namespace, localName }, (yield* stack.peek())?.element, stack.length + 1);
782
+ const namespace = yield* namespaceMetadata(namespaces, prefix, limits.deferNamespaces, prefixSource);
783
+ const elementName = { name, localName, ...(nameSource ? { nameSource, prefix } : {}), ...(localNameSource ? { localNameSource } : {}), ...(prefixSource ? { prefixSource } : {}) };
784
+ limits.onElement?.({ ...elementName, ...namespace }, (yield* stack.peek())?.element, stack.length + 1);
670
785
  admitContent();
671
786
  const retainedAttributes = [];
672
787
  if (retainContent) {
@@ -686,7 +801,7 @@ export function* parseXmlSourceSteps(length, limits = {}) {
686
801
  pendingWork -= 512;
687
802
  }
688
803
  const content = retain ? [] : undefined;
689
- const element = { kind: "element", name, namespace, localName, children: [], text: "", content: content ?? emptyContent, attributes: retainContent ? retainedAttributes : emptyAttributes, namespaces: retainContent && namespaces instanceof Map ? namespaces : emptyNamespaces, ...(root === undefined && retainContent ? { prolog, epilog, ...(declaration === undefined ? {} : { declaration }) } : {}) };
804
+ const element = { kind: "element", ...elementName, ...namespace, children: [], text: "", content: content ?? emptyContent, attributes: retainContent ? retainedAttributes : emptyAttributes, namespaces: retainContent && namespaces instanceof Map ? namespaces : emptyNamespaces, ...(root === undefined && retainContent ? { prolog, epilog, ...(declaration === undefined ? {} : { declaration }) } : {}) };
690
805
  const parent = (yield* stack.peek());
691
806
  if (parent) {
692
807
  if (retainTree) {
@@ -708,7 +823,7 @@ export function* parseXmlSourceSteps(length, limits = {}) {
708
823
  }
709
824
  // Only cache compact, attribute-free spellings. Namespace resolution and
710
825
  // every admission counter still run for each distinct physical element.
711
- if (empty && !attributes.length && name.length <= 512 && (yield* source.slice(offset - name.length - 3, offset)) === `<${name}/>`)
826
+ if (empty && !nameSource && !attributes.length && name.length <= 512 && (yield* source.slice(offset - name.length - 3, offset)) === `<${name}/>`)
712
827
  previousEmpty = { suffix: name + "/>", name, prefix, localName };
713
828
  limits.events?.({ type: "open", element, parent: parent?.element });
714
829
  if (limits.storeAttributes && limits.onAttribute && retainContent) {
@@ -716,10 +831,25 @@ export function* parseXmlSourceSteps(length, limits = {}) {
716
831
  for (let cursor = attributes.first; cursor;) {
717
832
  const attribute = yield* attributes.read(cursor, true);
718
833
  cursor = attribute.next;
719
- const namespace = attribute.name === "xmlns" || attribute.prefix === "xmlns" ? xmlnsNamespace
720
- : attribute.prefix ? (yield* namespaceValue(namespaces, attribute.prefix)) : "";
721
- limits.onAttribute({ name: attribute.name, localName: attribute.localName, namespace, value: attribute.value }, element);
722
- yield 1;
834
+ const namespace = attribute.name === "xmlns" || attribute.prefix === "xmlns" ? { namespace: xmlnsNamespace }
835
+ : attribute.prefix || attribute.prefixSource ? yield* namespaceMetadata(namespaces, attribute.prefix, limits.deferNamespaces, attribute.prefixSource) : { namespace: "" };
836
+ const nameMetadata = { name: attribute.name, localName: attribute.localName, ...(attribute.nameSource ? { nameSource: attribute.nameSource, prefix: attribute.prefix } : {}), ...(attribute.localNameSource ? { localNameSource: attribute.localNameSource } : {}), ...(attribute.prefixSource ? { prefixSource: attribute.prefixSource } : {}) };
837
+ let continuation = false;
838
+ if (attribute.source) {
839
+ for (const part of textFragments(source, attribute.source.start, attribute.source.end, true, limits.recover ? () => { } : undefined, true)) {
840
+ if (typeof part !== "string") {
841
+ yield part;
842
+ continue;
843
+ }
844
+ limits.onAttribute({ ...nameMetadata, ...namespace, value: part }, element, continuation);
845
+ continuation = true;
846
+ yield 1;
847
+ }
848
+ }
849
+ if (!continuation) {
850
+ limits.onAttribute({ ...nameMetadata, ...namespace, value: attribute.value }, element);
851
+ yield 1;
852
+ }
723
853
  }
724
854
  }
725
855
  if (!empty)