@safeaccess/inline 0.1.1 → 0.1.3

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 +23 -5
  3. package/LICENSE +1 -1
  4. package/README.md +79 -21
  5. package/dist/accessors/abstract-accessor.d.ts +24 -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 +26 -56
  67. package/dist/inline.js +43 -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 +4 -1
  82. package/dist/security/security-guard.js +7 -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 +25 -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 +46 -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 +10 -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 +379 -0
  163. package/tests/parser/xml-parser.test.ts +17 -330
  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 +8 -479
  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,579 @@
1
+ import { describe, expect, it } 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
+ describe(`${XmlParser.name} > mutation killing - constructor clamping`, () => {
11
+ it('enforces maxElements=1 so 2-element document throws', () => {
12
+ expect(() => new XmlParser(10, 1).parse('<root><a/></root>')).toThrow(SecurityException);
13
+ });
14
+
15
+ it('allows single-element self-closing document when maxElements=1', () => {
16
+ expect(new XmlParser(10, 1).parse('<a/>')).toEqual({});
17
+ });
18
+
19
+ it('enforces maxElements=1 with nested element document', () => {
20
+ expect(() => new XmlParser(10, 1).parse('<r><a>v</a></r>')).toThrow(SecurityException);
21
+ });
22
+
23
+ it('clamps maxElements=0 to 10000 instead of rejecting all documents', () => {
24
+ // With 0, if not clamped, `elementCount > 0` would reject every document
25
+ expect(() => new XmlParser(10, 0).parse('<r><a>v</a></r>')).not.toThrow();
26
+ });
27
+
28
+ it('clamps maxElements=-1 to 10000', () => {
29
+ expect(() => new XmlParser(10, -1).parse('<r><a>v</a></r>')).not.toThrow();
30
+ });
31
+
32
+ it('clamps maxElements=NaN to 10000', () => {
33
+ expect(() => new XmlParser(10, NaN).parse('<r><a>v</a></r>')).not.toThrow();
34
+ });
35
+
36
+ it('clamps maxElements=Infinity to 10000', () => {
37
+ const parser = new XmlParser(10, Infinity);
38
+ // Would need > 10000 elements to throw if clamped; verify small doc works
39
+ expect(() => parser.parse('<r><a>v</a></r>')).not.toThrow();
40
+ });
41
+ });
42
+
43
+ describe(`${XmlParser.name} > mutation killing - element count regex`, () => {
44
+ it('counts only opening tags (not closing) for element limit', () => {
45
+ expect(() => new XmlParser(10, 2).parse('<root><a/><b/></root>')).toThrow(
46
+ SecurityException,
47
+ );
48
+ });
49
+
50
+ it('allows document when opening tag count equals maxElements', () => {
51
+ expect(() => new XmlParser(10, 2).parse('<root><a/></root>')).not.toThrow();
52
+ });
53
+ });
54
+
55
+ describe(`${XmlParser.name} > mutation killing - extractRootContent validation`, () => {
56
+ it('rejects root tag starting with a digit', () => {
57
+ expect(() => makeParser().parse('<1>data</1>')).toThrow(InvalidFormatException);
58
+ });
59
+
60
+ it('rejects root tag starting with a hyphen', () => {
61
+ expect(() => makeParser().parse('<->x</->')).toThrow(InvalidFormatException);
62
+ });
63
+
64
+ it('includes XmlAccessor in error message for missing closing >', () => {
65
+ expect(() => makeParser().parse('<root')).toThrow(/XmlAccessor/);
66
+ });
67
+
68
+ it('includes XmlAccessor in error message for self-closing with trailing content', () => {
69
+ expect(() => makeParser().parse('<root/><extra>')).toThrow(/XmlAccessor/);
70
+ });
71
+
72
+ it('includes XmlAccessor in error message for non-self-closing root detected as self-closing', () => {
73
+ expect(() => makeParser().parse('<root/ ><extra>')).toThrow(/XmlAccessor/);
74
+ });
75
+
76
+ it('includes XmlAccessor in error message for mismatched closing tag', () => {
77
+ expect(() => makeParser().parse('<root>val</wrong>')).toThrow(/XmlAccessor/);
78
+ });
79
+
80
+ it('includes XmlAccessor in error message when closing lacks </ prefix', () => {
81
+ expect(() => makeParser().parse('<root>text root>')).toThrow(/XmlAccessor/);
82
+ });
83
+
84
+ it('includes XmlAccessor in error message for close tag overlapping open tag', () => {
85
+ expect(() => makeParser().parse('<abc</abc>')).toThrow(/XmlAccessor/);
86
+ });
87
+
88
+ it('includes XmlAccessor in error message for truncated document not ending with >', () => {
89
+ expect(() => makeParser().parse('<root>text')).toThrow(/XmlAccessor/);
90
+ });
91
+ });
92
+
93
+ describe(`${XmlParser.name} > mutation killing - backward whitespace walk in root close tag`, () => {
94
+ it('handles space before > in closing root tag', () => {
95
+ expect(makeParser().parse('<root><k>v</k></root >')).toEqual({ k: 'v' });
96
+ });
97
+
98
+ it('handles tab before > in closing root tag', () => {
99
+ expect(makeParser().parse('<root><k>v</k></root\t>')).toEqual({ k: 'v' });
100
+ });
101
+
102
+ it('handles newline before > in closing root tag', () => {
103
+ expect(makeParser().parse('<root><k>v</k></root\n>')).toEqual({ k: 'v' });
104
+ });
105
+
106
+ it('handles carriage return before > in closing root tag', () => {
107
+ expect(makeParser().parse('<root><k>v</k></root\r>')).toEqual({ k: 'v' });
108
+ });
109
+
110
+ it('handles mixed whitespace before > in closing root tag', () => {
111
+ expect(makeParser().parse('<root><k>v</k></root \t\n\r>')).toEqual({ k: 'v' });
112
+ });
113
+
114
+ it('handles pos reaching 0 during backward whitespace walk', () => {
115
+ expect(() => makeParser().parse('<r>v</r\t\t\t\t\t\t\t\t\t\t\t\t\t>')).not.toThrow();
116
+ });
117
+ });
118
+
119
+ describe(`${XmlParser.name} > mutation killing - close tag delimiter matching in children`, () => {
120
+ it('matches child close tag followed by space', () => {
121
+ expect(makeParser().parse('<root><item>val</item ></root>')).toEqual({ item: 'val' });
122
+ });
123
+
124
+ it('matches child close tag followed by tab', () => {
125
+ expect(makeParser().parse('<root><item>val</item\t></root>')).toEqual({ item: 'val' });
126
+ });
127
+
128
+ it('matches child close tag followed by newline', () => {
129
+ expect(makeParser().parse('<root><item>val</item\n></root>')).toEqual({ item: 'val' });
130
+ });
131
+
132
+ it('matches child close tag followed by carriage return', () => {
133
+ expect(makeParser().parse('<root><item>val</item\r></root>')).toEqual({ item: 'val' });
134
+ });
135
+
136
+ it('matches child close tag at end of content (undefined after tag name)', () => {
137
+ const result = makeParser().parse('<root><a>1</a</root>');
138
+ expect(result['a']).toBe('1');
139
+ });
140
+ });
141
+
142
+ describe(`${XmlParser.name} > mutation killing - open prefix nesting delimiter matching`, () => {
143
+ it('increments nestDepth for opening tag followed by >', () => {
144
+ const result = makeParser().parse('<root><a><a>inner</a>rest</a></root>');
145
+ const a = result['a'] as Record<string, unknown>;
146
+ expect(a['a']).toBe('inner');
147
+ });
148
+
149
+ it('increments nestDepth for opening tag followed by space', () => {
150
+ const result = makeParser().parse('<root><a ><a>inner</a>rest</a></root>');
151
+ const a = result['a'] as Record<string, unknown>;
152
+ expect(a['a']).toBe('inner');
153
+ });
154
+
155
+ it('increments nestDepth for opening tag followed by tab', () => {
156
+ const result = makeParser().parse('<root><a\t><a>inner</a>rest</a></root>');
157
+ const a = result['a'] as Record<string, unknown>;
158
+ expect(a['a']).toBe('inner');
159
+ });
160
+
161
+ it('increments nestDepth for opening tag followed by newline', () => {
162
+ const result = makeParser().parse('<root><a\n><a>inner</a>rest</a></root>');
163
+ const a = result['a'] as Record<string, unknown>;
164
+ expect(a['a']).toBe('inner');
165
+ });
166
+
167
+ it('increments nestDepth for opening tag followed by carriage return', () => {
168
+ const result = makeParser().parse('<root><a\r><a>inner</a>rest</a></root>');
169
+ const a = result['a'] as Record<string, unknown>;
170
+ expect(a['a']).toBe('inner');
171
+ });
172
+
173
+ it('does not increment nestDepth for self-closing same-name tag', () => {
174
+ const result = makeParser().parse('<root><a><a/>rest</a></root>');
175
+ const a = result['a'] as Record<string, unknown>;
176
+ expect(a['a']).toBe('');
177
+ });
178
+
179
+ it('does not increment nestDepth for self-closing tag with attributes', () => {
180
+ const result = makeParser().parse('<root><a><a id="1"/>rest</a></root>');
181
+ const a = result['a'] as Record<string, unknown>;
182
+ expect(a['a']).toBe('');
183
+ });
184
+ });
185
+
186
+ describe(`${XmlParser.name} > mutation killing - self-closing child trimEnd`, () => {
187
+ it('uses trimEnd for self-closing detection (spaces before /)', () => {
188
+ const result = makeParser().parse('<root><empty /></root>');
189
+ expect(result['empty']).toBe('');
190
+ });
191
+
192
+ it('trimStart would fail for self-closing with leading space before /', () => {
193
+ const result = makeParser().parse('<root><tag attr="v" /></root>');
194
+ expect(result['tag']).toBe('');
195
+ });
196
+ });
197
+
198
+ describe(`${XmlParser.name} > mutation killing - parseXmlChildren loop boundaries`, () => {
199
+ it('processes child elements without overshooting content boundary', () => {
200
+ const result = makeParser().parse('<root><a>x</a></root>');
201
+ expect(result['a']).toBe('x');
202
+ });
203
+
204
+ it('handles bare < at end of inner content', () => {
205
+ const result = makeParser().parse('<root><name>Alice</name><</root>');
206
+ expect(result['name']).toBe('Alice');
207
+ });
208
+
209
+ it('skips all three non-element token types (/, !, ?) in children', () => {
210
+ const xml = '<root></stray><!-- comment --><?pi data?><a>v</a></root>';
211
+ expect(makeParser().parse(xml)).toEqual({ a: 'v' });
212
+ });
213
+
214
+ it('advances past > correctly for skipped tokens (gt+1)', () => {
215
+ const result = makeParser().parse('<root><!-- c1 --><!-- c2 --><a>v</a></root>');
216
+ expect(result['a']).toBe('v');
217
+ });
218
+
219
+ it('skips tag name starting with digit in child content', () => {
220
+ const result = makeParser().parse('<root><1bad>v</root>');
221
+ expect(result['#text']).toBe('<1bad>v');
222
+ });
223
+
224
+ it('advances correctly after self-closing child (gt+1)', () => {
225
+ const result = makeParser().parse('<root><a/><b>v</b></root>');
226
+ expect(result['a']).toBe('');
227
+ expect(result['b']).toBe('v');
228
+ });
229
+
230
+ it('advances correctly for nesting counter start position (gt+1)', () => {
231
+ const result = makeParser().parse('<root><a><b>v</b></a></root>');
232
+ expect((result['a'] as Record<string, unknown>)['b']).toBe('v');
233
+ });
234
+
235
+ it('terminates nesting loop when close tag is found at nestDepth=0', () => {
236
+ const result = makeParser().parse('<root><a>text</a></root>');
237
+ expect(result['a']).toBe('text');
238
+ });
239
+
240
+ it('correctly calculates inner end position for close tag', () => {
241
+ const result = makeParser().parse('<root><item>content</item></root>');
242
+ expect(result['item']).toBe('content');
243
+ });
244
+
245
+ it('correctly positions after close tag > for sequential siblings', () => {
246
+ const result = makeParser().parse('<root><a>1</a><b>2</b></root>');
247
+ expect(result['a']).toBe('1');
248
+ expect(result['b']).toBe('2');
249
+ });
250
+
251
+ it('advances past unclosed inner tag to parse next sibling', () => {
252
+ const result = makeParser().parse('<root><unclosed><b>v</b></root>');
253
+ expect(result['b']).toBe('v');
254
+ });
255
+ });
256
+
257
+ describe(`${XmlParser.name} > mutation killing - text content detection`, () => {
258
+ it('treats text-only inner content as plain string', () => {
259
+ const result = makeParser().parse('<root><item>plain text</item></root>');
260
+ expect(result['item']).toBe('plain text');
261
+ expect(typeof result['item']).toBe('string');
262
+ });
263
+
264
+ it('stores non-empty text as #text when no child elements', () => {
265
+ const result = makeParser().parse('<root>just text</root>');
266
+ expect(result['#text']).toBe('just text');
267
+ });
268
+
269
+ it('returns empty object when inner content has no text and no elements', () => {
270
+ expect(makeParser().parse('<root> </root>')).toEqual({});
271
+ });
272
+
273
+ it('parses child elements in inner content when elements are present', () => {
274
+ const result = makeParser().parse('<root><item>no-elements-here</item></root>');
275
+ expect(result['item']).toBe('no-elements-here');
276
+ });
277
+
278
+ it('flattens child result with only #text key to string', () => {
279
+ const result = makeParser().parse('<root><wrap><inner>val</inner></wrap></root>');
280
+ const wrap = result['wrap'] as Record<string, unknown>;
281
+ expect(wrap['inner']).toBe('val');
282
+ });
283
+
284
+ it('does not flatten child result with multiple keys', () => {
285
+ const result = makeParser().parse('<root><wrap><a>1</a><b>2</b></wrap></root>');
286
+ const wrap = result['wrap'] as Record<string, unknown>;
287
+ expect(wrap['a']).toBe('1');
288
+ expect(wrap['b']).toBe('2');
289
+ });
290
+
291
+ it('handles empty inner content between open and close tag', () => {
292
+ const result = makeParser().parse('<root><item></item></root>');
293
+ expect(result['item']).toBe('');
294
+ });
295
+ });
296
+
297
+ describe(`${XmlParser.name} > mutation killing - close tag length arithmetic`, () => {
298
+ it('computes close tag search position correctly (nextLt + closeTag.length)', () => {
299
+ const result = makeParser().parse('<root><abc>value</abc></root>');
300
+ expect(result['abc']).toBe('value');
301
+ });
302
+
303
+ it('computes position after close tag > correctly (cgt + 1)', () => {
304
+ const result = makeParser().parse('<root><x>1</x><y>2</y><z>3</z></root>');
305
+ expect(result['x']).toBe('1');
306
+ expect(result['y']).toBe('2');
307
+ expect(result['z']).toBe('3');
308
+ });
309
+ });
310
+
311
+ describe(`${XmlParser.name} > mutation killing - root extraction edge cases`, () => {
312
+ it('handles root tag name of exactly 1 character', () => {
313
+ expect(makeParser().parse('<r><a>v</a></r>')).toEqual({ a: 'v' });
314
+ });
315
+
316
+ it('handles root tag with dot in name', () => {
317
+ expect(makeParser().parse('<r.x><a>v</a></r.x>')).toEqual({ a: 'v' });
318
+ });
319
+
320
+ it('handles root tag with hyphen in name', () => {
321
+ expect(makeParser().parse('<r-x><a>v</a></r-x>')).toEqual({ a: 'v' });
322
+ });
323
+
324
+ it('handles root tag with underscore prefix', () => {
325
+ expect(makeParser().parse('<_r><a>v</a></_r>')).toEqual({ a: 'v' });
326
+ });
327
+
328
+ it('handles close tag where nameStart is exactly 2', () => {
329
+ expect(makeParser().parse('<a>v</a>')).toEqual({ '#text': 'v' });
330
+ });
331
+
332
+ it('handles close tag where nameStart calculation is at boundary', () => {
333
+ expect(makeParser().parse('<ab>v</ab>')).toEqual({ '#text': 'v' });
334
+ });
335
+ });
336
+
337
+ describe(`${XmlParser.name} > mutation killing - nesting counter boundary conditions`, () => {
338
+ it('does not match close tag prefix of longer name (nestDepth boundary)', () => {
339
+ const result = makeParser().parse('<root><a><ab>inner</ab>rest</a></root>');
340
+ const a = result['a'] as Record<string, unknown>;
341
+ expect(a['ab']).toBe('inner');
342
+ });
343
+
344
+ it('three-deep same-name nesting counts correctly', () => {
345
+ const result = makeParser().parse('<root><a><a><a>deep</a></a></a></root>');
346
+ const a1 = result['a'] as Record<string, unknown>;
347
+ const a2 = a1['a'] as Record<string, unknown>;
348
+ expect(a2['a']).toBe('deep');
349
+ });
350
+
351
+ it('handles no inner > found during nesting search (ogt === -1)', () => {
352
+ const result = makeParser().parse('<root><a>text</a></root>');
353
+ expect(result['a']).toBe('text');
354
+ });
355
+
356
+ it('handles tag name scanning up to content.length boundary', () => {
357
+ const result = makeParser().parse('<root><longtagname>value</longtagname></root>');
358
+ expect(result['longtagname']).toBe('value');
359
+ });
360
+ });
361
+
362
+ describe(`${XmlParser.name} > mutation killing - error paths in extractRootContent`, () => {
363
+ it('throws and includes error for empty-looking document', () => {
364
+ expect(() => makeParser().parse('#')).toThrow(/XmlAccessor/);
365
+ });
366
+
367
+ it('throws for document starting with < but second char is not alpha', () => {
368
+ expect(() => makeParser().parse('<!')).toThrow(InvalidFormatException);
369
+ });
370
+
371
+ it('throws for root with wrong closing tag name', () => {
372
+ expect(() => makeParser().parse('<abc>text</xyz>')).toThrow(InvalidFormatException);
373
+ });
374
+
375
+ it('throws when close tag < is not preceded by /', () => {
376
+ expect(() => makeParser().parse('<root>val<root>')).toThrow(InvalidFormatException);
377
+ });
378
+ });
379
+
380
+ describe(`${XmlParser.name} > mutation killing - L139 doc.length boundary`, () => {
381
+ it('rejects single-char document that is just <', () => {
382
+ expect(() => makeParser().parse('<')).toThrow(InvalidFormatException);
383
+ });
384
+
385
+ it('rejects two-char document <! (no valid root)', () => {
386
+ expect(() => makeParser().parse('<!')).toThrow(InvalidFormatException);
387
+ });
388
+
389
+ it('accepts shortest valid self-closing: <a/>', () => {
390
+ expect(makeParser().parse('<a/>')).toEqual({});
391
+ });
392
+ });
393
+
394
+ describe(`${XmlParser.name} > mutation killing - self-closing root edge cases`, () => {
395
+ it('rejects self-closing root with trailing content after >', () => {
396
+ expect(() => makeParser().parse('<root/>extra')).toThrow(InvalidFormatException);
397
+ });
398
+
399
+ it('root self-closing with attributes', () => {
400
+ // Self-closing produces empty object even with attributes (manual parser)
401
+ expect(makeParser().parse('<root attr="v"/>')).toEqual({});
402
+ });
403
+
404
+ it('root self-closing with space before slash', () => {
405
+ expect(makeParser().parse('<root />')).toEqual({});
406
+ });
407
+ });
408
+
409
+ describe(`${XmlParser.name} > mutation killing - L173 backward walk boundary`, () => {
410
+ it('root close tag with no whitespace before >', () => {
411
+ expect(makeParser().parse('<root>text</root>')).toEqual({ '#text': 'text' });
412
+ });
413
+
414
+ it('handles single-char root tag in close tag backward walk', () => {
415
+ expect(makeParser().parse('<r>t</r>')).toEqual({ '#text': 't' });
416
+ });
417
+ });
418
+
419
+ describe(`${XmlParser.name} > mutation killing - L208-218 parseXmlChildren inner loop`, () => {
420
+ it('stops scanning when no more < found in children content', () => {
421
+ expect(makeParser().parse('<root>plain text only</root>')).toEqual({
422
+ '#text': 'plain text only',
423
+ });
424
+ });
425
+
426
+ it('handles child content that ends right at a close tag', () => {
427
+ const r = makeParser().parse('<root><a>1</a></root>');
428
+ expect(r['a']).toBe('1');
429
+ });
430
+
431
+ it('handles consecutive comments then element', () => {
432
+ const r = makeParser().parse('<root><!-- c1 --><!-- c2 --><!-- c3 --><a>v</a></root>');
433
+ expect(r['a']).toBe('v');
434
+ });
435
+
436
+ it('handles processing instruction in children', () => {
437
+ const r = makeParser().parse('<root><?pi data?><a>v</a></root>');
438
+ expect(r['a']).toBe('v');
439
+ });
440
+
441
+ it('handles closing tag appearing in children (skipped via /)', () => {
442
+ const r = makeParser().parse('<root></stray><a>v</a></root>');
443
+ expect(r['a']).toBe('v');
444
+ });
445
+ });
446
+
447
+ describe(`${XmlParser.name} > mutation killing - L255 nesting loop boundaries`, () => {
448
+ it('correctly matches close tag when nestDepth transitions from 1 to 0', () => {
449
+ const r = makeParser().parse('<root><item>value</item></root>');
450
+ expect(r['item']).toBe('value');
451
+ });
452
+
453
+ it('handles two-level same-name nesting', () => {
454
+ const r = makeParser().parse('<root><x><x>inner</x></x></root>');
455
+ expect((r['x'] as Record<string, unknown>)['x']).toBe('inner');
456
+ });
457
+
458
+ it('handles three-level same-name nesting to stress nestDepth counter', () => {
459
+ const r = makeParser().parse('<root><n><n><n>d</n></n></n></root>');
460
+ const n1 = r['n'] as Record<string, unknown>;
461
+ const n2 = n1['n'] as Record<string, unknown>;
462
+ expect(n2['n']).toBe('d');
463
+ });
464
+ });
465
+
466
+ describe(`${XmlParser.name} > mutation killing - L265-266 close tag arithmetic`, () => {
467
+ it('handles multi-char tag name in close tag position calculation', () => {
468
+ const r = makeParser().parse('<root><abcdef>val</abcdef></root>');
469
+ expect(r['abcdef']).toBe('val');
470
+ });
471
+
472
+ it('handles after-close position for two siblings with long names', () => {
473
+ const r = makeParser().parse('<root><alpha>1</alpha><beta>2</beta></root>');
474
+ expect(r['alpha']).toBe('1');
475
+ expect(r['beta']).toBe('2');
476
+ });
477
+ });
478
+
479
+ describe(`${XmlParser.name} > mutation killing - L276 close delimiter chars`, () => {
480
+ it('close tag followed by > is recognized', () => {
481
+ const r = makeParser().parse('<root><k>v</k></root>');
482
+ expect(r['k']).toBe('v');
483
+ });
484
+
485
+ it('close tag with space before > in child', () => {
486
+ const r = makeParser().parse('<root><k>v</k ></root>');
487
+ expect(r['k']).toBe('v');
488
+ });
489
+
490
+ it('close tag with tab before > in child', () => {
491
+ const r = makeParser().parse('<root><k>v</k\t></root>');
492
+ expect(r['k']).toBe('v');
493
+ });
494
+
495
+ it('close tag with newline before > in child', () => {
496
+ const r = makeParser().parse('<root><k>v</k\n></root>');
497
+ expect(r['k']).toBe('v');
498
+ });
499
+
500
+ it('close tag with CR before > in child', () => {
501
+ const r = makeParser().parse('<root><k>v</k\r></root>');
502
+ expect(r['k']).toBe('v');
503
+ });
504
+ });
505
+
506
+ describe(`${XmlParser.name} > mutation killing - L278 open prefix matching`, () => {
507
+ it('does not count non-self-closing same-name open tag in nesting', () => {
508
+ const r = makeParser().parse('<root><a><a>deep</a>rest</a></root>');
509
+ const a = r['a'] as Record<string, unknown>;
510
+ expect(a['a']).toBe('deep');
511
+ });
512
+
513
+ it('detects self-closing same-name tag without incrementing nestDepth', () => {
514
+ const r = makeParser().parse('<root><a><a />after</a></root>');
515
+ const a = r['a'] as Record<string, unknown>;
516
+ expect(a['a']).toBe('');
517
+ });
518
+
519
+ it('handles open prefix with attributes (space after tag name)', () => {
520
+ const r = makeParser().parse('<root><a><a id="1">deep</a>rest</a></root>');
521
+ const a = r['a'] as Record<string, unknown>;
522
+ expect(a['a']).toBe('deep');
523
+ });
524
+ });
525
+
526
+ describe(`${XmlParser.name} > mutation killing - L297-300 trimmedInner detection`, () => {
527
+ it('treats empty inner content as empty string value (not child parse)', () => {
528
+ const r = makeParser().parse('<root><e></e></root>');
529
+ expect(r['e']).toBe('');
530
+ });
531
+
532
+ it('treats whitespace-only inner content as empty string value', () => {
533
+ const r = makeParser().parse('<root><e> </e></root>');
534
+ expect(r['e']).toBe('');
535
+ });
536
+
537
+ it('treats non-empty text without elements as string (not record)', () => {
538
+ const r = makeParser().parse('<root><e>text</e></root>');
539
+ expect(typeof r['e']).toBe('string');
540
+ expect(r['e']).toBe('text');
541
+ });
542
+
543
+ it('parses inner content with elements as record', () => {
544
+ const r = makeParser().parse('<root><e><f>v</f></e></root>');
545
+ expect(typeof r['e']).toBe('object');
546
+ expect((r['e'] as Record<string, unknown>)['f']).toBe('v');
547
+ });
548
+
549
+ it('flattens single #text child to string', () => {
550
+ const r = makeParser().parse('<root><wrap><inner>val</inner></wrap></root>');
551
+ expect((r['wrap'] as Record<string, unknown>)['inner']).toBe('val');
552
+ });
553
+
554
+ it('does not flatten multi-key child', () => {
555
+ const r = makeParser().parse('<root><wrap><a>1</a><b>2</b></wrap></root>');
556
+ const w = r['wrap'] as Record<string, unknown>;
557
+ expect(w['a']).toBe('1');
558
+ expect(w['b']).toBe('2');
559
+ });
560
+ });
561
+
562
+ describe(`${XmlParser.name} > mutation killing - L239 attribute trimming`, () => {
563
+ it('extracts attribute content with trimEnd on attributes string', () => {
564
+ const r = makeParser().parse('<root><item key="val" >text</item></root>');
565
+ expect(r['item']).toBe('text');
566
+ });
567
+
568
+ it('parses tag with attribute and no space before >', () => {
569
+ const r = makeParser().parse('<root><item key="val">text</item></root>');
570
+ expect(r['item']).toBe('text');
571
+ });
572
+ });
573
+
574
+ describe(`${XmlParser.name} > mutation killing - L189 closeTagStart boundary`, () => {
575
+ it('rejects when close tag start equals open tag end', () => {
576
+ // <a></a> has closeTagStart=3, openGt=2 → closeTagStart > openGt → valid
577
+ expect(makeParser().parse('<a></a>')).toEqual({});
578
+ });
579
+ });