@safeaccess/inline 0.1.1 → 0.1.2

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.
Files changed (179) hide show
  1. package/.gitattributes +1 -1
  2. package/CHANGELOG.md +10 -5
  3. package/LICENSE +1 -1
  4. package/README.md +56 -14
  5. package/dist/accessors/abstract-accessor.d.ts +22 -10
  6. package/dist/accessors/abstract-accessor.js +21 -8
  7. package/dist/accessors/abstract-integration-accessor.d.ts +22 -0
  8. package/dist/accessors/abstract-integration-accessor.js +23 -0
  9. package/dist/accessors/formats/any-accessor.d.ts +10 -8
  10. package/dist/accessors/formats/any-accessor.js +9 -8
  11. package/dist/accessors/formats/array-accessor.d.ts +2 -0
  12. package/dist/accessors/formats/array-accessor.js +2 -0
  13. package/dist/accessors/formats/env-accessor.d.ts +2 -0
  14. package/dist/accessors/formats/env-accessor.js +2 -0
  15. package/dist/accessors/formats/ini-accessor.d.ts +2 -0
  16. package/dist/accessors/formats/ini-accessor.js +2 -0
  17. package/dist/accessors/formats/json-accessor.d.ts +2 -0
  18. package/dist/accessors/formats/json-accessor.js +2 -0
  19. package/dist/accessors/formats/ndjson-accessor.d.ts +2 -0
  20. package/dist/accessors/formats/ndjson-accessor.js +2 -0
  21. package/dist/accessors/formats/object-accessor.d.ts +2 -0
  22. package/dist/accessors/formats/object-accessor.js +2 -0
  23. package/dist/accessors/formats/xml-accessor.d.ts +2 -0
  24. package/dist/accessors/formats/xml-accessor.js +2 -0
  25. package/dist/accessors/formats/yaml-accessor.d.ts +3 -1
  26. package/dist/accessors/formats/yaml-accessor.js +4 -2
  27. package/dist/cache/simple-path-cache.d.ts +51 -0
  28. package/dist/cache/simple-path-cache.js +72 -0
  29. package/dist/contracts/accessors-interface.d.ts +2 -0
  30. package/dist/contracts/factory-accessors-interface.d.ts +2 -0
  31. package/dist/contracts/filter-evaluator-interface.d.ts +28 -0
  32. package/dist/contracts/filter-evaluator-interface.js +1 -0
  33. package/dist/contracts/parse-integration-interface.d.ts +2 -0
  34. package/dist/contracts/parser-interface.d.ts +92 -0
  35. package/dist/contracts/parser-interface.js +1 -0
  36. package/dist/contracts/path-cache-interface.d.ts +7 -6
  37. package/dist/contracts/readable-accessors-interface.d.ts +11 -6
  38. package/dist/contracts/security-guard-interface.d.ts +2 -0
  39. package/dist/contracts/security-parser-interface.d.ts +2 -0
  40. package/dist/contracts/validatable-parser-interface.d.ts +59 -0
  41. package/dist/contracts/validatable-parser-interface.js +1 -0
  42. package/dist/contracts/writable-accessors-interface.d.ts +5 -0
  43. package/dist/core/accessor-factory.d.ts +124 -0
  44. package/dist/core/accessor-factory.js +157 -0
  45. package/dist/core/dot-notation-parser.d.ts +34 -5
  46. package/dist/core/dot-notation-parser.js +51 -10
  47. package/dist/core/inline-builder-accessor.d.ts +82 -0
  48. package/dist/core/inline-builder-accessor.js +107 -0
  49. package/dist/exceptions/accessor-exception.d.ts +9 -0
  50. package/dist/exceptions/accessor-exception.js +9 -0
  51. package/dist/exceptions/invalid-format-exception.d.ts +5 -0
  52. package/dist/exceptions/invalid-format-exception.js +5 -0
  53. package/dist/exceptions/parser-exception.d.ts +4 -0
  54. package/dist/exceptions/parser-exception.js +4 -0
  55. package/dist/exceptions/path-not-found-exception.d.ts +4 -0
  56. package/dist/exceptions/path-not-found-exception.js +4 -0
  57. package/dist/exceptions/readonly-violation-exception.d.ts +4 -0
  58. package/dist/exceptions/readonly-violation-exception.js +4 -0
  59. package/dist/exceptions/security-exception.d.ts +6 -0
  60. package/dist/exceptions/security-exception.js +6 -0
  61. package/dist/exceptions/unsupported-type-exception.d.ts +4 -0
  62. package/dist/exceptions/unsupported-type-exception.js +4 -0
  63. package/dist/exceptions/yaml-parse-exception.d.ts +4 -0
  64. package/dist/exceptions/yaml-parse-exception.js +4 -0
  65. package/dist/index.js +2 -1
  66. package/dist/inline.d.ts +22 -56
  67. package/dist/inline.js +39 -111
  68. package/dist/parser/xml-parser.js +23 -10
  69. package/dist/parser/yaml-parser.d.ts +54 -7
  70. package/dist/parser/yaml-parser.js +268 -51
  71. package/dist/path-query/segment-filter-parser.d.ts +142 -0
  72. package/dist/path-query/segment-filter-parser.js +384 -0
  73. package/dist/path-query/segment-parser.d.ts +98 -0
  74. package/dist/path-query/segment-parser.js +283 -0
  75. package/dist/path-query/segment-path-resolver.d.ts +149 -0
  76. package/dist/path-query/segment-path-resolver.js +351 -0
  77. package/dist/path-query/segment-type.d.ts +85 -0
  78. package/dist/path-query/segment-type.js +35 -0
  79. package/dist/security/forbidden-keys.d.ts +2 -2
  80. package/dist/security/forbidden-keys.js +5 -5
  81. package/dist/security/security-guard.d.ts +3 -1
  82. package/dist/security/security-guard.js +5 -2
  83. package/dist/security/security-parser.d.ts +10 -1
  84. package/dist/security/security-parser.js +10 -1
  85. package/dist/type-format.d.ts +2 -0
  86. package/dist/type-format.js +2 -0
  87. package/package.json +11 -3
  88. package/src/accessors/abstract-accessor.ts +23 -19
  89. package/src/accessors/abstract-integration-accessor.ts +27 -0
  90. package/src/accessors/formats/any-accessor.ts +11 -11
  91. package/src/accessors/formats/array-accessor.ts +2 -0
  92. package/src/accessors/formats/env-accessor.ts +2 -0
  93. package/src/accessors/formats/ini-accessor.ts +2 -0
  94. package/src/accessors/formats/json-accessor.ts +2 -0
  95. package/src/accessors/formats/ndjson-accessor.ts +2 -0
  96. package/src/accessors/formats/object-accessor.ts +2 -0
  97. package/src/accessors/formats/xml-accessor.ts +2 -0
  98. package/src/accessors/formats/yaml-accessor.ts +4 -2
  99. package/src/cache/simple-path-cache.ts +77 -0
  100. package/src/contracts/accessors-interface.ts +2 -0
  101. package/src/contracts/factory-accessors-interface.ts +2 -0
  102. package/src/contracts/filter-evaluator-interface.ts +30 -0
  103. package/src/contracts/parse-integration-interface.ts +2 -0
  104. package/src/contracts/parser-interface.ts +114 -0
  105. package/src/contracts/path-cache-interface.ts +8 -6
  106. package/src/contracts/readable-accessors-interface.ts +11 -6
  107. package/src/contracts/security-guard-interface.ts +2 -0
  108. package/src/contracts/security-parser-interface.ts +2 -0
  109. package/src/contracts/validatable-parser-interface.ts +64 -0
  110. package/src/contracts/writable-accessors-interface.ts +5 -0
  111. package/src/core/accessor-factory.ts +173 -0
  112. package/src/core/dot-notation-parser.ts +74 -11
  113. package/src/core/inline-builder-accessor.ts +163 -0
  114. package/src/exceptions/accessor-exception.ts +9 -0
  115. package/src/exceptions/invalid-format-exception.ts +5 -0
  116. package/src/exceptions/parser-exception.ts +4 -0
  117. package/src/exceptions/path-not-found-exception.ts +4 -0
  118. package/src/exceptions/readonly-violation-exception.ts +4 -0
  119. package/src/exceptions/security-exception.ts +6 -0
  120. package/src/exceptions/unsupported-type-exception.ts +4 -0
  121. package/src/exceptions/yaml-parse-exception.ts +4 -0
  122. package/src/index.ts +3 -1
  123. package/src/inline.ts +42 -120
  124. package/src/parser/xml-parser.ts +31 -10
  125. package/src/parser/yaml-parser.ts +310 -45
  126. package/src/path-query/segment-filter-parser.ts +444 -0
  127. package/src/path-query/segment-parser.ts +321 -0
  128. package/src/path-query/segment-path-resolver.ts +521 -0
  129. package/src/path-query/segment-type.ts +82 -0
  130. package/src/security/forbidden-keys.ts +5 -5
  131. package/src/security/security-guard.ts +7 -2
  132. package/src/security/security-parser.ts +18 -3
  133. package/src/type-format.ts +2 -0
  134. package/stryker.config.json +8 -10
  135. package/tests/accessors/abstract-accessor.test.ts +217 -0
  136. package/tests/accessors/abstract-integration-accessor.test.ts +37 -0
  137. package/tests/accessors/formats/any-accessor.test.ts +57 -0
  138. package/tests/accessors/formats/array-accessor.test.ts +42 -0
  139. package/tests/accessors/formats/env-accessor.test.ts +103 -0
  140. package/tests/accessors/formats/ini-accessor.test.ts +186 -0
  141. package/tests/accessors/{json-accessor.test.ts → formats/json-accessor.test.ts} +6 -6
  142. package/tests/accessors/formats/ndjson-accessor.test.ts +49 -0
  143. package/tests/accessors/formats/object-accessor.test.ts +172 -0
  144. package/tests/accessors/formats/xml-accessor.test.ts +162 -0
  145. package/tests/accessors/formats/yaml-accessor.test.ts +36 -0
  146. package/tests/cache/simple-path-cache.test.ts +168 -0
  147. package/tests/core/accessor-factory.test.ts +157 -0
  148. package/tests/core/dot-notation-parser-edge-cases.test.ts +415 -0
  149. package/tests/core/dot-notation-parser.test.ts +0 -288
  150. package/tests/core/inline-builder-accessor.test.ts +114 -0
  151. package/tests/exceptions/accessor-exception.test.ts +28 -0
  152. package/tests/exceptions/invalid-format-exception.test.ts +31 -0
  153. package/tests/exceptions/path-not-found-exception.test.ts +33 -0
  154. package/tests/exceptions/readonly-violation-exception.test.ts +35 -0
  155. package/tests/exceptions/security-exception.test.ts +33 -0
  156. package/tests/exceptions/unsupported-type-exception.test.ts +33 -0
  157. package/tests/exceptions/yaml-parse-exception.test.ts +38 -0
  158. package/tests/mocks/fake-path-cache.ts +4 -3
  159. package/tests/parity-from.test.ts +118 -0
  160. package/tests/parity.test.ts +227 -10
  161. package/tests/parser/xml-parser-mutations.test.ts +579 -0
  162. package/tests/parser/xml-parser-scanner.test.ts +332 -0
  163. package/tests/parser/xml-parser.test.ts +10 -334
  164. package/tests/parser/yaml-parser-mutations.test.ts +750 -0
  165. package/tests/parser/yaml-parser.test.ts +844 -18
  166. package/tests/path-query/segment-filter-parser-mutations.test.ts +735 -0
  167. package/tests/path-query/segment-filter-parser.test.ts +1091 -0
  168. package/tests/path-query/segment-parser-mutations.test.ts +539 -0
  169. package/tests/path-query/segment-parser.test.ts +606 -0
  170. package/tests/path-query/segment-path-resolver-mutations.test.ts +626 -0
  171. package/tests/path-query/segment-path-resolver.test.ts +1009 -0
  172. package/tests/security/security-guard-advanced.test.ts +413 -0
  173. package/tests/security/security-guard-forbidden-keys.test.ts +87 -0
  174. package/tests/security/security-guard.test.ts +3 -484
  175. package/tests/security/security-parser.test.ts +18 -14
  176. package/vitest.config.ts +3 -3
  177. package/benchmarks/get.bench.ts +0 -26
  178. package/benchmarks/parse.bench.ts +0 -41
  179. package/tests/accessors/accessors.test.ts +0 -1017
@@ -1,4 +1,4 @@
1
- import { afterEach, describe, expect, it, vi } from 'vitest';
1
+ import { describe, expect, it } from 'vitest';
2
2
  import { XmlParser } from '../../src/parser/xml-parser.js';
3
3
  import { InvalidFormatException } from '../../src/exceptions/invalid-format-exception.js';
4
4
  import { SecurityException } from '../../src/exceptions/security-exception.js';
@@ -7,48 +7,6 @@ function makeParser(maxDepth = 10): XmlParser {
7
7
  return new XmlParser(maxDepth);
8
8
  }
9
9
 
10
- type FakeAttr = { name: string; value: string };
11
- type FakeNode = {
12
- nodeType: number;
13
- textContent?: string;
14
- nodeName?: string;
15
- attributes?: FakeAttrs;
16
- childNodes?: FakeChildNodes;
17
- };
18
- type FakeAttrs = { length: number; [index: number]: FakeAttr | undefined };
19
- type FakeChildNodes = { length: number; [index: number]: FakeNode | undefined };
20
-
21
- function makeTextNode(text: string): FakeNode {
22
- return { nodeType: 3, textContent: text };
23
- }
24
-
25
- function makeElement(name: string, children: FakeNode[] = [], attrs: FakeAttr[] = []): FakeNode {
26
- const attributes: FakeAttrs = { length: attrs.length };
27
- attrs.forEach((a, i) => {
28
- attributes[i] = a;
29
- });
30
- const childNodes: FakeChildNodes = { length: children.length };
31
- children.forEach((c, i) => {
32
- childNodes[i] = c;
33
- });
34
- return { nodeType: 1, nodeName: name, attributes, childNodes };
35
- }
36
-
37
- function stubDomParser(root: FakeNode | null, hasParserError = false): void {
38
- const parserErrorEl = hasParserError ? { textContent: 'parse failed' } : null;
39
- vi.stubGlobal(
40
- 'DOMParser',
41
- class {
42
- parseFromString(): unknown {
43
- return {
44
- querySelector: (sel: string) => (sel === 'parsererror' ? parserErrorEl : null),
45
- documentElement: root,
46
- };
47
- }
48
- },
49
- );
50
- }
51
-
52
10
  describe(XmlParser.name, () => {
53
11
  it('parses a single child element', () => {
54
12
  expect(makeParser().parse('<root><name>Alice</name></root>')).toEqual({
@@ -140,25 +98,25 @@ describe(XmlParser.name, () => {
140
98
  });
141
99
 
142
100
  it('throws InvalidFormatException when closing tag is embedded inside opening tag body (closeTagStart <= openGt)', () => {
143
- // <abc</abc> backward scan finds 'abc' at the end, confirms '</abc',
101
+ // <abc</abc> - backward scan finds 'abc' at the end, confirms '</abc',
144
102
  // but closeTagStart (4) is <= openGt (9), meaning the close marker is
145
- // inside the opening-tag span structurally impossible, must throw
103
+ // inside the opening-tag span - structurally impossible, must throw
146
104
  expect(() => makeParser().parse('<abc</abc>')).toThrow(InvalidFormatException);
147
105
  });
148
106
 
149
107
  it('throws InvalidFormatException when document does not end with > (no closing tag)', () => {
150
- // '<root>unclosed text' ends with 't', not '>' triggers the
108
+ // '<root>unclosed text' ends with 't', not '>' - triggers the
151
109
  // doc[doc.length - 1] !== '>' guard in extractRootContent
152
110
  expect(() => makeParser().parse('<root>unclosed text')).toThrow(InvalidFormatException);
153
111
  });
154
112
 
155
113
  it('throws InvalidFormatException when opening tag has no closing > at all', () => {
156
- // '<root' has no '>' openGt === -1 guard in extractRootContent
114
+ // '<root' has no '>' - openGt === -1 guard in extractRootContent
157
115
  expect(() => makeParser().parse('<root')).toThrow(InvalidFormatException);
158
116
  });
159
117
 
160
118
  it('throws InvalidFormatException when tag name found at end but not preceded by </ (space before tag name)', () => {
161
- // '<root>text root>' backward scan finds 'root' at the end but
119
+ // '<root>text root>' - backward scan finds 'root' at the end but
162
120
  // the preceding char is ' ' not '/', triggering the </ guard
163
121
  expect(() => makeParser().parse('<root>text root>')).toThrow(InvalidFormatException);
164
122
  });
@@ -200,7 +158,7 @@ describe(`${XmlParser.name} > duplicate sibling elements`, () => {
200
158
  });
201
159
  });
202
160
 
203
- describe(`${XmlParser.name} > security depth limit`, () => {
161
+ describe(`${XmlParser.name} > security - depth limit`, () => {
204
162
  it('parses successfully when depth equals maxDepth', () => {
205
163
  const result = new XmlParser(1).parse('<root><a>value</a></root>');
206
164
  expect(result['a']).toBe('value');
@@ -234,7 +192,7 @@ describe(`${XmlParser.name} > security — depth limit`, () => {
234
192
  });
235
193
  });
236
194
 
237
- describe(`${XmlParser.name} > security element count limit (maxElements)`, () => {
195
+ describe(`${XmlParser.name} > security - element count limit (maxElements)`, () => {
238
196
  it('throws SecurityException when element count exceeds custom maxElements', () => {
239
197
  const xml = '<root>' + '<item>x</item>'.repeat(3) + '</root>';
240
198
  expect(() => new XmlParser(10, 2).parse(xml)).toThrow(SecurityException);
@@ -254,7 +212,7 @@ describe(`${XmlParser.name} > security — element count limit (maxElements)`, (
254
212
  });
255
213
  });
256
214
 
257
- describe(`${XmlParser.name} > constructor maxElements clamping (SEC-017)`, () => {
215
+ describe(`${XmlParser.name} > constructor - maxElements clamping (SEC-017)`, () => {
258
216
  it('clamps NaN to 10 000 so the element guard still fires at 10 000', () => {
259
217
  const xml = '<root>' + '<item>x</item>'.repeat(10_001) + '</root>';
260
218
  expect(() => new XmlParser(100, NaN).parse(xml)).toThrow(SecurityException);
@@ -285,7 +243,7 @@ describe(`${XmlParser.name} > constructor — maxElements clamping (SEC-017)`, (
285
243
  expect(() => new XmlParser(100, 4).parse(xml)).toThrow(SecurityException);
286
244
  });
287
245
 
288
- it('uses the provided positive finite maxElements when within limit no exception', () => {
246
+ it('uses the provided positive finite maxElements when within limit - no exception', () => {
289
247
  const xml = '<root>' + '<item>x</item>'.repeat(3) + '</root>';
290
248
  expect(() => new XmlParser(100, 10).parse(xml)).not.toThrow();
291
249
  });
@@ -334,285 +292,3 @@ describe(`${XmlParser.name} > manual parser edge cases`, () => {
334
292
  expect(result['item']).toBe('hello <world');
335
293
  });
336
294
  });
337
-
338
- describe(`${XmlParser.name} > browser DOMParser path`, () => {
339
- afterEach(() => {
340
- vi.unstubAllGlobals();
341
- });
342
-
343
- it('delegates to DOMParser when available', () => {
344
- const root = makeElement('root', [makeElement('name', [makeTextNode('Alice')])]);
345
- stubDomParser(root);
346
- expect(makeParser().parse('<root><name>Alice</name></root>')).toEqual({
347
- name: { '#text': 'Alice' },
348
- });
349
- });
350
-
351
- it('passes application/xml as MIME type to DOMParser', () => {
352
- let capturedType = '';
353
- vi.stubGlobal(
354
- 'DOMParser',
355
- class {
356
- parseFromString(_xml: string, type: string): unknown {
357
- capturedType = type;
358
- return {
359
- querySelector: () => null,
360
- documentElement: makeElement('root', []),
361
- };
362
- }
363
- },
364
- );
365
- makeParser().parse('<root/>');
366
- expect(capturedType).toBe('application/xml');
367
- });
368
-
369
- it('throws InvalidFormatException when DOMParser reports parsererror', () => {
370
- stubDomParser(null, true);
371
- expect(() => makeParser().parse('<root/>')).toThrow(InvalidFormatException);
372
- });
373
-
374
- it('includes the parseerror detail in the exception message', () => {
375
- stubDomParser(null, true);
376
- expect(() => makeParser().parse('<root/>')).toThrow(/parse failed/);
377
- });
378
-
379
- it('uses Unknown error when parseerror textContent is null', () => {
380
- vi.stubGlobal(
381
- 'DOMParser',
382
- class {
383
- parseFromString(): unknown {
384
- return {
385
- querySelector: (sel: string) =>
386
- sel === 'parsererror' ? { textContent: null } : null,
387
- documentElement: null,
388
- };
389
- }
390
- },
391
- );
392
- expect(() => makeParser().parse('<root/>')).toThrow(/Unknown error/);
393
- });
394
-
395
- it('returns empty object when documentElement is null', () => {
396
- stubDomParser(null, false);
397
- expect(makeParser().parse('<root/>')).toEqual({});
398
- });
399
-
400
- it('maps element attributes with @ prefix', () => {
401
- const root = makeElement('root', [], [{ name: 'id', value: '42' }]);
402
- stubDomParser(root);
403
- const result = makeParser().parse('<root id="42"/>');
404
- expect(result['@id']).toBe('42');
405
- });
406
-
407
- it('ignores undefined attribute slots', () => {
408
- const attrs: FakeAttrs = { length: 2, 0: { name: 'a', value: '1' }, 1: undefined };
409
- const root: FakeNode = {
410
- nodeType: 1,
411
- nodeName: 'root',
412
- attributes: attrs,
413
- childNodes: { length: 0 },
414
- };
415
- stubDomParser(root);
416
- expect(makeParser().parse('<root a="1"/>')).toEqual({ '@a': '1' });
417
- });
418
-
419
- it('captures non-empty text nodes as #text', () => {
420
- const root = makeElement('root', [makeTextNode(' hello ')]);
421
- stubDomParser(root);
422
- const result = makeParser().parse('<root>hello</root>');
423
- expect(result['#text']).toBe('hello');
424
- });
425
-
426
- it('ignores whitespace-only text nodes', () => {
427
- const root = makeElement('root', [makeTextNode(' ')]);
428
- stubDomParser(root);
429
- expect(makeParser().parse('<root> </root>')).toEqual({});
430
- });
431
-
432
- it('handles text node with null textContent gracefully', () => {
433
- const nullTextNode: FakeNode = { nodeType: 3, textContent: null as unknown as string };
434
- const root = makeElement('root', [nullTextNode]);
435
- stubDomParser(root);
436
- expect(makeParser().parse('<root/>')).toEqual({});
437
- });
438
-
439
- it('ignores undefined child node slots', () => {
440
- const undef: FakeNode = undefined as unknown as FakeNode;
441
- const childNodes: FakeChildNodes = { length: 2, 0: makeTextNode('hello'), 1: undef };
442
- const root: FakeNode = {
443
- nodeType: 1,
444
- nodeName: 'root',
445
- attributes: { length: 0 },
446
- childNodes,
447
- };
448
- stubDomParser(root);
449
- const result = makeParser().parse('<root/>');
450
- expect(result['#text']).toBe('hello');
451
- });
452
-
453
- it('merges duplicate child elements into an array via DOMParser', () => {
454
- const root = makeElement('root', [
455
- makeElement('item', [makeTextNode('a')]),
456
- makeElement('item', [makeTextNode('b')]),
457
- ]);
458
- stubDomParser(root);
459
- const result = makeParser().parse('<root><item>a</item><item>b</item></root>');
460
- expect(result['item']).toEqual([{ '#text': 'a' }, { '#text': 'b' }]);
461
- });
462
-
463
- it('pushes into existing array when third duplicate appears', () => {
464
- const root = makeElement('root', [
465
- makeElement('item', [makeTextNode('a')]),
466
- makeElement('item', [makeTextNode('b')]),
467
- makeElement('item', [makeTextNode('c')]),
468
- ]);
469
- stubDomParser(root);
470
- const result = makeParser().parse(
471
- '<root><item>a</item><item>b</item><item>c</item></root>',
472
- );
473
- expect(Array.isArray(result['item'])).toBe(true);
474
- expect((result['item'] as unknown[]).length).toBe(3);
475
- });
476
-
477
- it('returns element with only #text key as-is (not flattened)', () => {
478
- const root = makeElement('root', [makeTextNode('only-text')]);
479
- stubDomParser(root);
480
- const result = makeParser().parse('<root>only-text</root>');
481
- expect(result['#text']).toBe('only-text');
482
- expect(Object.keys(result).length).toBe(1);
483
- });
484
-
485
- it('does not throw when DOM element depth exactly equals maxDepth', () => {
486
- const child = makeElement('child', [makeTextNode('v')]);
487
- const root = makeElement('root', [child]);
488
- stubDomParser(root);
489
- expect(() => new XmlParser(1).parse('<root/>')).not.toThrow();
490
- });
491
-
492
- it('throws SecurityException when DOM element depth exceeds maxDepth', () => {
493
- const deep = makeElement('c', [makeTextNode('v')]);
494
- const mid = makeElement('b', [deep]);
495
- const root = makeElement('root', [makeElement('a', [mid])]);
496
- stubDomParser(root);
497
- expect(() => new XmlParser(1).parse('<root/>')).toThrow(SecurityException);
498
- });
499
-
500
- it('includes depth information in the SecurityException message', () => {
501
- const deep = makeElement('c', [makeTextNode('v')]);
502
- const mid = makeElement('b', [deep]);
503
- const root = makeElement('root', [makeElement('a', [mid])]);
504
- stubDomParser(root);
505
- expect(() => new XmlParser(1).parse('<root/>')).toThrow(/exceed/i);
506
- });
507
-
508
- it('ignores child nodes with nodeType other than 1 or 3', () => {
509
- const unknownNode: FakeNode = { nodeType: 8 };
510
- const root = makeElement('root', [unknownNode, makeElement('name', [makeTextNode('Bob')])]);
511
- stubDomParser(root);
512
- const result = makeParser().parse('<root/>');
513
- expect(result['name']).toEqual({ '#text': 'Bob' });
514
- });
515
- });
516
-
517
- describe(`${XmlParser.name} > linear scanner — nesting counter`, () => {
518
- it('extracts outer element when same-name elements nest (kills nestDepth++ mutant)', () => {
519
- // nestDepth must be incremented at inner <a> so the first </a> does not
520
- // prematurely close the outer element
521
- const result = makeParser().parse('<root><a><a>inner</a>rest</a></root>');
522
- const a = result['a'] as Record<string, unknown>;
523
- expect(a['a']).toBe('inner');
524
- });
525
-
526
- it('resolves 3-deep same-name nesting (kills off-by-one in nestDepth-- condition)', () => {
527
- // nestDepth starts at 1, increments twice, decrements 3× — only
528
- // when it hits exactly 0 should inner content be collected
529
- const result = makeParser().parse('<root><a><a><a>deep</a></a></a></root>');
530
- const a1 = result['a'] as Record<string, unknown>;
531
- const a2 = a1['a'] as Record<string, unknown>;
532
- expect(a2['a']).toBe('deep');
533
- });
534
-
535
- it('does not count a self-closing same-name tag as open nestDepth (kills self-closing increment mutant)', () => {
536
- // <a/> inside <a>…</a> must NOT increment nestDepth; if it did, the first
537
- // </a> would only bring nestDepth to 1 and the parser would scan past it
538
- const result = makeParser().parse('<root><a><a/>text</a></root>');
539
- const a = result['a'] as Record<string, unknown>;
540
- expect(a['a']).toBe('');
541
- });
542
- });
543
-
544
- describe(`${XmlParser.name} > linear scanner — self-closing detection`, () => {
545
- it('treats <tag /> (spaces before />) as self-closing (kills trimEnd mutant)', () => {
546
- const result = makeParser().parse('<root><empty /></root>');
547
- expect(result['empty']).toBe('');
548
- });
549
-
550
- it('treats <tag attr="v" /> as self-closing (attribute + space + /)', () => {
551
- const result = makeParser().parse('<root><flag enabled="true" /></root>');
552
- expect(result['flag']).toBe('');
553
- });
554
- });
555
-
556
- describe(`${XmlParser.name} > linear scanner — skip non-element tokens`, () => {
557
- it('skips XML comment nodes inside children (kills nextChar === "!" mutant)', () => {
558
- const result = makeParser().parse('<root><!-- comment --><name>Alice</name></root>');
559
- expect(result['name']).toBe('Alice');
560
- expect(Object.keys(result)).toEqual(['name']);
561
- });
562
-
563
- it('skips processing instructions inside children (kills nextChar === "?" mutant)', () => {
564
- const result = makeParser().parse('<root><?pi data?><name>Bob</name></root>');
565
- expect(result['name']).toBe('Bob');
566
- expect(Object.keys(result)).toEqual(['name']);
567
- });
568
-
569
- it('skips stray closing tags inside children (kills nextChar === "/" mutant)', () => {
570
- const result = makeParser().parse('<root></stray><name>Charlie</name></root>');
571
- expect(result['name']).toBe('Charlie');
572
- expect(Object.keys(result)).toEqual(['name']);
573
- });
574
-
575
- it('handles comment-like token with no closing > (gt === -1 ternary branch)', () => {
576
- // '<!no close tag' in inner content — no '>' found, so i is set to
577
- // content.length terminating the loop; content falls through as #text
578
- const result = makeParser().parse('<root><!no close tag</root>');
579
- expect(result['#text']).toBe('<!no close tag');
580
- });
581
-
582
- it('skips child tag whose name starts with a digit (kills !\\[a-zA-Z_\\] continue branch)', () => {
583
- // <1tag> — nextChar is '1', fails [a-zA-Z_] test; loop advances past it
584
- // and the content falls through as #text
585
- const result = makeParser().parse('<root><1tag>value</root>');
586
- expect(result['#text']).toBe('<1tag>value');
587
- });
588
- });
589
-
590
- describe(`${XmlParser.name} > linear scanner — unclosed and malformed tags`, () => {
591
- it('skips an unclosed child and continues parsing siblings (kills innerEnd === -1 check mutant)', () => {
592
- // <unclosed> has no </unclosed> — parser must skip it and continue
593
- const result = makeParser().parse('<root><unclosed><name>Bob</name></root>');
594
- expect(result['name']).toBe('Bob');
595
- });
596
-
597
- it('accepts closing tag at end of string with no trailing > (c === undefined branch)', () => {
598
- // </a is the last token with no > — charAfter is undefined; the undefined
599
- // branch must accept this as the close tag or the inner value is lost
600
- const result = makeParser().parse('<root><a>1</a</root>');
601
- expect(result['a']).toBe('1');
602
- });
603
-
604
- it('skips close-tag prefix that matches a longer tag name', () => {
605
- // </a matches the prefix of </ab> — the char after </a is 'b', not
606
- // a delimiter, so the scanner must skip it and keep looking for </a>
607
- const result = makeParser().parse('<root><a><ab>inner</ab>rest</a></root>');
608
- const a = result['a'] as Record<string, unknown>;
609
- expect(a['ab']).toBe('inner');
610
- });
611
-
612
- it('handles a trailing bare < in inner content gracefully (nextChar === undefined break)', () => {
613
- // Bare < at the very end of inner content — nextChar is undefined, the
614
- // outer loop must terminate without crashing
615
- const result = makeParser().parse('<root><name>Alice</name><</root>');
616
- expect(result['name']).toBe('Alice');
617
- });
618
- });