@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
@@ -0,0 +1,332 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { XmlParser } from '../../src/parser/xml-parser.js';
3
+ import { InvalidFormatException } from '../../src/exceptions/invalid-format-exception.js';
4
+ import { SecurityException } from '../../src/exceptions/security-exception.js';
5
+
6
+ function makeParser(maxDepth = 10): XmlParser {
7
+ return new XmlParser(maxDepth);
8
+ }
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
+ describe(`${XmlParser.name} > browser DOMParser path`, () => {
53
+ afterEach(() => {
54
+ vi.unstubAllGlobals();
55
+ });
56
+
57
+ it('delegates to DOMParser when available', () => {
58
+ const root = makeElement('root', [makeElement('name', [makeTextNode('Alice')])]);
59
+ stubDomParser(root);
60
+ expect(makeParser().parse('<root><name>Alice</name></root>')).toEqual({
61
+ name: { '#text': 'Alice' },
62
+ });
63
+ });
64
+
65
+ it('passes application/xml as MIME type to DOMParser', () => {
66
+ let capturedType = '';
67
+ vi.stubGlobal(
68
+ 'DOMParser',
69
+ class {
70
+ parseFromString(_xml: string, type: string): unknown {
71
+ capturedType = type;
72
+ return {
73
+ querySelector: () => null,
74
+ documentElement: makeElement('root', []),
75
+ };
76
+ }
77
+ },
78
+ );
79
+ makeParser().parse('<root/>');
80
+ expect(capturedType).toBe('application/xml');
81
+ });
82
+
83
+ it('throws InvalidFormatException when DOMParser reports parsererror', () => {
84
+ stubDomParser(null, true);
85
+ expect(() => makeParser().parse('<root/>')).toThrow(InvalidFormatException);
86
+ });
87
+
88
+ it('includes the parseerror detail in the exception message', () => {
89
+ stubDomParser(null, true);
90
+ expect(() => makeParser().parse('<root/>')).toThrow(/parse failed/);
91
+ });
92
+
93
+ it('uses Unknown error when parseerror textContent is null', () => {
94
+ vi.stubGlobal(
95
+ 'DOMParser',
96
+ class {
97
+ parseFromString(): unknown {
98
+ return {
99
+ querySelector: (sel: string) =>
100
+ sel === 'parsererror' ? { textContent: null } : null,
101
+ documentElement: null,
102
+ };
103
+ }
104
+ },
105
+ );
106
+ expect(() => makeParser().parse('<root/>')).toThrow(/Unknown error/);
107
+ });
108
+
109
+ it('returns empty object when documentElement is null', () => {
110
+ stubDomParser(null, false);
111
+ expect(makeParser().parse('<root/>')).toEqual({});
112
+ });
113
+
114
+ it('maps element attributes with @ prefix', () => {
115
+ const root = makeElement('root', [], [{ name: 'id', value: '42' }]);
116
+ stubDomParser(root);
117
+ const result = makeParser().parse('<root id="42"/>');
118
+ expect(result['@id']).toBe('42');
119
+ });
120
+
121
+ it('ignores undefined attribute slots', () => {
122
+ const attrs: FakeAttrs = { length: 2, 0: { name: 'a', value: '1' }, 1: undefined };
123
+ const root: FakeNode = {
124
+ nodeType: 1,
125
+ nodeName: 'root',
126
+ attributes: attrs,
127
+ childNodes: { length: 0 },
128
+ };
129
+ stubDomParser(root);
130
+ expect(makeParser().parse('<root a="1"/>')).toEqual({ '@a': '1' });
131
+ });
132
+
133
+ it('captures non-empty text nodes as #text', () => {
134
+ const root = makeElement('root', [makeTextNode(' hello ')]);
135
+ stubDomParser(root);
136
+ const result = makeParser().parse('<root>hello</root>');
137
+ expect(result['#text']).toBe('hello');
138
+ });
139
+
140
+ it('ignores whitespace-only text nodes', () => {
141
+ const root = makeElement('root', [makeTextNode(' ')]);
142
+ stubDomParser(root);
143
+ expect(makeParser().parse('<root> </root>')).toEqual({});
144
+ });
145
+
146
+ it('handles text node with null textContent gracefully', () => {
147
+ const nullTextNode: FakeNode = { nodeType: 3, textContent: null as unknown as string };
148
+ const root = makeElement('root', [nullTextNode]);
149
+ stubDomParser(root);
150
+ expect(makeParser().parse('<root/>')).toEqual({});
151
+ });
152
+
153
+ it('ignores undefined child node slots', () => {
154
+ const undef: FakeNode = undefined as unknown as FakeNode;
155
+ const childNodes: FakeChildNodes = { length: 2, 0: makeTextNode('hello'), 1: undef };
156
+ const root: FakeNode = {
157
+ nodeType: 1,
158
+ nodeName: 'root',
159
+ attributes: { length: 0 },
160
+ childNodes,
161
+ };
162
+ stubDomParser(root);
163
+ const result = makeParser().parse('<root/>');
164
+ expect(result['#text']).toBe('hello');
165
+ });
166
+
167
+ it('merges duplicate child elements into an array via DOMParser', () => {
168
+ const root = makeElement('root', [
169
+ makeElement('item', [makeTextNode('a')]),
170
+ makeElement('item', [makeTextNode('b')]),
171
+ ]);
172
+ stubDomParser(root);
173
+ const result = makeParser().parse('<root><item>a</item><item>b</item></root>');
174
+ expect(result['item']).toEqual([{ '#text': 'a' }, { '#text': 'b' }]);
175
+ });
176
+
177
+ it('pushes into existing array when third duplicate appears', () => {
178
+ const root = makeElement('root', [
179
+ makeElement('item', [makeTextNode('a')]),
180
+ makeElement('item', [makeTextNode('b')]),
181
+ makeElement('item', [makeTextNode('c')]),
182
+ ]);
183
+ stubDomParser(root);
184
+ const result = makeParser().parse(
185
+ '<root><item>a</item><item>b</item><item>c</item></root>',
186
+ );
187
+ expect(Array.isArray(result['item'])).toBe(true);
188
+ expect((result['item'] as unknown[]).length).toBe(3);
189
+ });
190
+
191
+ it('returns element with only #text key as-is (not flattened)', () => {
192
+ const root = makeElement('root', [makeTextNode('only-text')]);
193
+ stubDomParser(root);
194
+ const result = makeParser().parse('<root>only-text</root>');
195
+ expect(result['#text']).toBe('only-text');
196
+ expect(Object.keys(result).length).toBe(1);
197
+ });
198
+
199
+ it('does not throw when DOM element depth exactly equals maxDepth', () => {
200
+ const child = makeElement('child', [makeTextNode('v')]);
201
+ const root = makeElement('root', [child]);
202
+ stubDomParser(root);
203
+ expect(() => new XmlParser(1).parse('<root/>')).not.toThrow();
204
+ });
205
+
206
+ it('throws SecurityException when DOM element depth exceeds maxDepth', () => {
207
+ const deep = makeElement('c', [makeTextNode('v')]);
208
+ const mid = makeElement('b', [deep]);
209
+ const root = makeElement('root', [makeElement('a', [mid])]);
210
+ stubDomParser(root);
211
+ expect(() => new XmlParser(1).parse('<root/>')).toThrow(SecurityException);
212
+ });
213
+
214
+ it('includes depth information in the SecurityException message', () => {
215
+ const deep = makeElement('c', [makeTextNode('v')]);
216
+ const mid = makeElement('b', [deep]);
217
+ const root = makeElement('root', [makeElement('a', [mid])]);
218
+ stubDomParser(root);
219
+ expect(() => new XmlParser(1).parse('<root/>')).toThrow(/exceed/i);
220
+ });
221
+
222
+ it('ignores child nodes with nodeType other than 1 or 3', () => {
223
+ const unknownNode: FakeNode = { nodeType: 8 };
224
+ const root = makeElement('root', [unknownNode, makeElement('name', [makeTextNode('Bob')])]);
225
+ stubDomParser(root);
226
+ const result = makeParser().parse('<root/>');
227
+ expect(result['name']).toEqual({ '#text': 'Bob' });
228
+ });
229
+ });
230
+
231
+ describe(`${XmlParser.name} > linear scanner - nesting counter`, () => {
232
+ it('extracts outer element when same-name elements nest (kills nestDepth++ mutant)', () => {
233
+ // nestDepth must be incremented at inner <a> so the first </a> does not
234
+ // prematurely close the outer element
235
+ const result = makeParser().parse('<root><a><a>inner</a>rest</a></root>');
236
+ const a = result['a'] as Record<string, unknown>;
237
+ expect(a['a']).toBe('inner');
238
+ });
239
+
240
+ it('resolves 3-deep same-name nesting (kills off-by-one in nestDepth-- condition)', () => {
241
+ // nestDepth starts at 1, increments twice, decrements 3× - only
242
+ // when it hits exactly 0 should inner content be collected
243
+ const result = makeParser().parse('<root><a><a><a>deep</a></a></a></root>');
244
+ const a1 = result['a'] as Record<string, unknown>;
245
+ const a2 = a1['a'] as Record<string, unknown>;
246
+ expect(a2['a']).toBe('deep');
247
+ });
248
+
249
+ it('does not count a self-closing same-name tag as open nestDepth (kills self-closing increment mutant)', () => {
250
+ // <a/> inside <a>…</a> must NOT increment nestDepth; if it did, the first
251
+ // </a> would only bring nestDepth to 1 and the parser would scan past it
252
+ const result = makeParser().parse('<root><a><a/>text</a></root>');
253
+ const a = result['a'] as Record<string, unknown>;
254
+ expect(a['a']).toBe('');
255
+ });
256
+ });
257
+
258
+ describe(`${XmlParser.name} > linear scanner - self-closing detection`, () => {
259
+ it('treats <tag /> (spaces before />) as self-closing (kills trimEnd mutant)', () => {
260
+ const result = makeParser().parse('<root><empty /></root>');
261
+ expect(result['empty']).toBe('');
262
+ });
263
+
264
+ it('treats <tag attr="v" /> as self-closing (attribute + space + /)', () => {
265
+ const result = makeParser().parse('<root><flag enabled="true" /></root>');
266
+ expect(result['flag']).toBe('');
267
+ });
268
+ });
269
+
270
+ describe(`${XmlParser.name} > linear scanner - skip non-element tokens`, () => {
271
+ it('skips XML comment nodes inside children (kills nextChar === "!" mutant)', () => {
272
+ const result = makeParser().parse('<root><!-- comment --><name>Alice</name></root>');
273
+ expect(result['name']).toBe('Alice');
274
+ expect(Object.keys(result)).toEqual(['name']);
275
+ });
276
+
277
+ it('skips processing instructions inside children (kills nextChar === "?" mutant)', () => {
278
+ const result = makeParser().parse('<root><?pi data?><name>Bob</name></root>');
279
+ expect(result['name']).toBe('Bob');
280
+ expect(Object.keys(result)).toEqual(['name']);
281
+ });
282
+
283
+ it('skips stray closing tags inside children (kills nextChar === "/" mutant)', () => {
284
+ const result = makeParser().parse('<root></stray><name>Charlie</name></root>');
285
+ expect(result['name']).toBe('Charlie');
286
+ expect(Object.keys(result)).toEqual(['name']);
287
+ });
288
+
289
+ it('handles comment-like token with no closing > (gt === -1 ternary branch)', () => {
290
+ // '<!no close tag' in inner content - no '>' found, so i is set to
291
+ // content.length terminating the loop; content falls through as #text
292
+ const result = makeParser().parse('<root><!no close tag</root>');
293
+ expect(result['#text']).toBe('<!no close tag');
294
+ });
295
+
296
+ it('skips child tag whose name starts with a digit (kills !\\[a-zA-Z_\\] continue branch)', () => {
297
+ // <1tag> - nextChar is '1', fails [a-zA-Z_] test; loop advances past it
298
+ // and the content falls through as #text
299
+ const result = makeParser().parse('<root><1tag>value</root>');
300
+ expect(result['#text']).toBe('<1tag>value');
301
+ });
302
+ });
303
+
304
+ describe(`${XmlParser.name} > linear scanner - unclosed and malformed tags`, () => {
305
+ it('skips an unclosed child and continues parsing siblings (kills innerEnd === -1 check mutant)', () => {
306
+ // <unclosed> has no </unclosed> - parser must skip it and continue
307
+ const result = makeParser().parse('<root><unclosed><name>Bob</name></root>');
308
+ expect(result['name']).toBe('Bob');
309
+ });
310
+
311
+ it('accepts closing tag at end of string with no trailing > (c === undefined branch)', () => {
312
+ // </a is the last token with no > - charAfter is undefined; the undefined
313
+ // branch must accept this as the close tag or the inner value is lost
314
+ const result = makeParser().parse('<root><a>1</a</root>');
315
+ expect(result['a']).toBe('1');
316
+ });
317
+
318
+ it('skips close-tag prefix that matches a longer tag name', () => {
319
+ // </a matches the prefix of </ab> - the char after </a is 'b', not
320
+ // a delimiter, so the scanner must skip it and keep looking for </a>
321
+ const result = makeParser().parse('<root><a><ab>inner</ab>rest</a></root>');
322
+ const a = result['a'] as Record<string, unknown>;
323
+ expect(a['ab']).toBe('inner');
324
+ });
325
+
326
+ it('handles a trailing bare < in inner content gracefully (nextChar === undefined break)', () => {
327
+ // Bare < at the very end of inner content - nextChar is undefined, the
328
+ // outer loop must terminate without crashing
329
+ const result = makeParser().parse('<root><name>Alice</name><</root>');
330
+ expect(result['name']).toBe('Alice');
331
+ });
332
+ });