@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.
- package/.gitattributes +1 -1
- package/CHANGELOG.md +23 -5
- package/LICENSE +1 -1
- package/README.md +79 -21
- package/dist/accessors/abstract-accessor.d.ts +24 -10
- package/dist/accessors/abstract-accessor.js +21 -8
- package/dist/accessors/abstract-integration-accessor.d.ts +22 -0
- package/dist/accessors/abstract-integration-accessor.js +23 -0
- package/dist/accessors/formats/any-accessor.d.ts +10 -8
- package/dist/accessors/formats/any-accessor.js +9 -8
- package/dist/accessors/formats/array-accessor.d.ts +2 -0
- package/dist/accessors/formats/array-accessor.js +2 -0
- package/dist/accessors/formats/env-accessor.d.ts +2 -0
- package/dist/accessors/formats/env-accessor.js +2 -0
- package/dist/accessors/formats/ini-accessor.d.ts +2 -0
- package/dist/accessors/formats/ini-accessor.js +2 -0
- package/dist/accessors/formats/json-accessor.d.ts +2 -0
- package/dist/accessors/formats/json-accessor.js +2 -0
- package/dist/accessors/formats/ndjson-accessor.d.ts +2 -0
- package/dist/accessors/formats/ndjson-accessor.js +2 -0
- package/dist/accessors/formats/object-accessor.d.ts +2 -0
- package/dist/accessors/formats/object-accessor.js +2 -0
- package/dist/accessors/formats/xml-accessor.d.ts +2 -0
- package/dist/accessors/formats/xml-accessor.js +2 -0
- package/dist/accessors/formats/yaml-accessor.d.ts +3 -1
- package/dist/accessors/formats/yaml-accessor.js +4 -2
- package/dist/cache/simple-path-cache.d.ts +51 -0
- package/dist/cache/simple-path-cache.js +72 -0
- package/dist/contracts/accessors-interface.d.ts +2 -0
- package/dist/contracts/factory-accessors-interface.d.ts +2 -0
- package/dist/contracts/filter-evaluator-interface.d.ts +28 -0
- package/dist/contracts/filter-evaluator-interface.js +1 -0
- package/dist/contracts/parse-integration-interface.d.ts +2 -0
- package/dist/contracts/parser-interface.d.ts +92 -0
- package/dist/contracts/parser-interface.js +1 -0
- package/dist/contracts/path-cache-interface.d.ts +7 -6
- package/dist/contracts/readable-accessors-interface.d.ts +11 -6
- package/dist/contracts/security-guard-interface.d.ts +2 -0
- package/dist/contracts/security-parser-interface.d.ts +2 -0
- package/dist/contracts/validatable-parser-interface.d.ts +59 -0
- package/dist/contracts/validatable-parser-interface.js +1 -0
- package/dist/contracts/writable-accessors-interface.d.ts +5 -0
- package/dist/core/accessor-factory.d.ts +124 -0
- package/dist/core/accessor-factory.js +157 -0
- package/dist/core/dot-notation-parser.d.ts +34 -5
- package/dist/core/dot-notation-parser.js +51 -10
- package/dist/core/inline-builder-accessor.d.ts +82 -0
- package/dist/core/inline-builder-accessor.js +107 -0
- package/dist/exceptions/accessor-exception.d.ts +9 -0
- package/dist/exceptions/accessor-exception.js +9 -0
- package/dist/exceptions/invalid-format-exception.d.ts +5 -0
- package/dist/exceptions/invalid-format-exception.js +5 -0
- package/dist/exceptions/parser-exception.d.ts +4 -0
- package/dist/exceptions/parser-exception.js +4 -0
- package/dist/exceptions/path-not-found-exception.d.ts +4 -0
- package/dist/exceptions/path-not-found-exception.js +4 -0
- package/dist/exceptions/readonly-violation-exception.d.ts +4 -0
- package/dist/exceptions/readonly-violation-exception.js +4 -0
- package/dist/exceptions/security-exception.d.ts +6 -0
- package/dist/exceptions/security-exception.js +6 -0
- package/dist/exceptions/unsupported-type-exception.d.ts +4 -0
- package/dist/exceptions/unsupported-type-exception.js +4 -0
- package/dist/exceptions/yaml-parse-exception.d.ts +4 -0
- package/dist/exceptions/yaml-parse-exception.js +4 -0
- package/dist/index.js +2 -1
- package/dist/inline.d.ts +26 -56
- package/dist/inline.js +43 -111
- package/dist/parser/xml-parser.js +23 -10
- package/dist/parser/yaml-parser.d.ts +54 -7
- package/dist/parser/yaml-parser.js +268 -51
- package/dist/path-query/segment-filter-parser.d.ts +142 -0
- package/dist/path-query/segment-filter-parser.js +384 -0
- package/dist/path-query/segment-parser.d.ts +98 -0
- package/dist/path-query/segment-parser.js +283 -0
- package/dist/path-query/segment-path-resolver.d.ts +149 -0
- package/dist/path-query/segment-path-resolver.js +351 -0
- package/dist/path-query/segment-type.d.ts +85 -0
- package/dist/path-query/segment-type.js +35 -0
- package/dist/security/forbidden-keys.d.ts +2 -2
- package/dist/security/forbidden-keys.js +5 -5
- package/dist/security/security-guard.d.ts +4 -1
- package/dist/security/security-guard.js +7 -2
- package/dist/security/security-parser.d.ts +10 -1
- package/dist/security/security-parser.js +10 -1
- package/dist/type-format.d.ts +2 -0
- package/dist/type-format.js +2 -0
- package/package.json +11 -3
- package/src/accessors/abstract-accessor.ts +25 -19
- package/src/accessors/abstract-integration-accessor.ts +27 -0
- package/src/accessors/formats/any-accessor.ts +11 -11
- package/src/accessors/formats/array-accessor.ts +2 -0
- package/src/accessors/formats/env-accessor.ts +2 -0
- package/src/accessors/formats/ini-accessor.ts +2 -0
- package/src/accessors/formats/json-accessor.ts +2 -0
- package/src/accessors/formats/ndjson-accessor.ts +2 -0
- package/src/accessors/formats/object-accessor.ts +2 -0
- package/src/accessors/formats/xml-accessor.ts +2 -0
- package/src/accessors/formats/yaml-accessor.ts +4 -2
- package/src/cache/simple-path-cache.ts +77 -0
- package/src/contracts/accessors-interface.ts +2 -0
- package/src/contracts/factory-accessors-interface.ts +2 -0
- package/src/contracts/filter-evaluator-interface.ts +30 -0
- package/src/contracts/parse-integration-interface.ts +2 -0
- package/src/contracts/parser-interface.ts +114 -0
- package/src/contracts/path-cache-interface.ts +8 -6
- package/src/contracts/readable-accessors-interface.ts +11 -6
- package/src/contracts/security-guard-interface.ts +2 -0
- package/src/contracts/security-parser-interface.ts +2 -0
- package/src/contracts/validatable-parser-interface.ts +64 -0
- package/src/contracts/writable-accessors-interface.ts +5 -0
- package/src/core/accessor-factory.ts +173 -0
- package/src/core/dot-notation-parser.ts +74 -11
- package/src/core/inline-builder-accessor.ts +163 -0
- package/src/exceptions/accessor-exception.ts +9 -0
- package/src/exceptions/invalid-format-exception.ts +5 -0
- package/src/exceptions/parser-exception.ts +4 -0
- package/src/exceptions/path-not-found-exception.ts +4 -0
- package/src/exceptions/readonly-violation-exception.ts +4 -0
- package/src/exceptions/security-exception.ts +6 -0
- package/src/exceptions/unsupported-type-exception.ts +4 -0
- package/src/exceptions/yaml-parse-exception.ts +4 -0
- package/src/index.ts +3 -1
- package/src/inline.ts +46 -120
- package/src/parser/xml-parser.ts +31 -10
- package/src/parser/yaml-parser.ts +310 -45
- package/src/path-query/segment-filter-parser.ts +444 -0
- package/src/path-query/segment-parser.ts +321 -0
- package/src/path-query/segment-path-resolver.ts +521 -0
- package/src/path-query/segment-type.ts +82 -0
- package/src/security/forbidden-keys.ts +5 -5
- package/src/security/security-guard.ts +10 -2
- package/src/security/security-parser.ts +18 -3
- package/src/type-format.ts +2 -0
- package/stryker.config.json +8 -10
- package/tests/accessors/abstract-accessor.test.ts +217 -0
- package/tests/accessors/abstract-integration-accessor.test.ts +37 -0
- package/tests/accessors/formats/any-accessor.test.ts +57 -0
- package/tests/accessors/formats/array-accessor.test.ts +42 -0
- package/tests/accessors/formats/env-accessor.test.ts +103 -0
- package/tests/accessors/formats/ini-accessor.test.ts +186 -0
- package/tests/accessors/{json-accessor.test.ts → formats/json-accessor.test.ts} +6 -6
- package/tests/accessors/formats/ndjson-accessor.test.ts +49 -0
- package/tests/accessors/formats/object-accessor.test.ts +172 -0
- package/tests/accessors/formats/xml-accessor.test.ts +162 -0
- package/tests/accessors/formats/yaml-accessor.test.ts +36 -0
- package/tests/cache/simple-path-cache.test.ts +168 -0
- package/tests/core/accessor-factory.test.ts +157 -0
- package/tests/core/dot-notation-parser-edge-cases.test.ts +415 -0
- package/tests/core/dot-notation-parser.test.ts +0 -288
- package/tests/core/inline-builder-accessor.test.ts +114 -0
- package/tests/exceptions/accessor-exception.test.ts +28 -0
- package/tests/exceptions/invalid-format-exception.test.ts +31 -0
- package/tests/exceptions/path-not-found-exception.test.ts +33 -0
- package/tests/exceptions/readonly-violation-exception.test.ts +35 -0
- package/tests/exceptions/security-exception.test.ts +33 -0
- package/tests/exceptions/unsupported-type-exception.test.ts +33 -0
- package/tests/exceptions/yaml-parse-exception.test.ts +38 -0
- package/tests/mocks/fake-path-cache.ts +4 -3
- package/tests/parity-from.test.ts +118 -0
- package/tests/parity.test.ts +227 -10
- package/tests/parser/xml-parser-mutations.test.ts +579 -0
- package/tests/parser/xml-parser-scanner.test.ts +379 -0
- package/tests/parser/xml-parser.test.ts +17 -330
- package/tests/parser/yaml-parser-mutations.test.ts +750 -0
- package/tests/parser/yaml-parser.test.ts +844 -18
- package/tests/path-query/segment-filter-parser-mutations.test.ts +735 -0
- package/tests/path-query/segment-filter-parser.test.ts +1091 -0
- package/tests/path-query/segment-parser-mutations.test.ts +539 -0
- package/tests/path-query/segment-parser.test.ts +606 -0
- package/tests/path-query/segment-path-resolver-mutations.test.ts +626 -0
- package/tests/path-query/segment-path-resolver.test.ts +1009 -0
- package/tests/security/security-guard-advanced.test.ts +413 -0
- package/tests/security/security-guard-forbidden-keys.test.ts +87 -0
- package/tests/security/security-guard.test.ts +8 -479
- package/tests/security/security-parser.test.ts +18 -14
- package/vitest.config.ts +3 -3
- package/benchmarks/get.bench.ts +0 -26
- package/benchmarks/parse.bench.ts +0 -41
- package/tests/accessors/accessors.test.ts +0 -1017
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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>
|
|
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
|
|
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 '>'
|
|
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 '>'
|
|
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>'
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
});
|
|
@@ -333,286 +291,15 @@ describe(`${XmlParser.name} > manual parser edge cases`, () => {
|
|
|
333
291
|
const result = makeParser().parse('<root><item>hello <world</item></root>');
|
|
334
292
|
expect(result['item']).toBe('hello <world');
|
|
335
293
|
});
|
|
336
|
-
});
|
|
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
294
|
|
|
419
|
-
it('
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
expect(result['#text']).toBe('hello');
|
|
295
|
+
it('parses elements preceded by plain text in child content', () => {
|
|
296
|
+
const result = makeParser().parse('<root><wrap>prefix<a>v</a></wrap></root>');
|
|
297
|
+
const wrap = result['wrap'] as Record<string, unknown>;
|
|
298
|
+
expect(wrap['a']).toBe('v');
|
|
424
299
|
});
|
|
425
300
|
|
|
426
|
-
it('
|
|
427
|
-
const
|
|
428
|
-
|
|
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');
|
|
301
|
+
it('preserves child object with multiple keys without #text flattening', () => {
|
|
302
|
+
const result = makeParser().parse('<root><w><x>1</x><y>2</y></w></root>');
|
|
303
|
+
expect(result['w']).toEqual({ x: '1', y: '2' });
|
|
617
304
|
});
|
|
618
305
|
});
|