@safeaccess/inline 0.1.1
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 +16 -0
- package/.gitkeep +0 -0
- package/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/README.md +454 -0
- package/benchmarks/get.bench.ts +26 -0
- package/benchmarks/parse.bench.ts +41 -0
- package/dist/accessors/abstract-accessor.d.ts +213 -0
- package/dist/accessors/abstract-accessor.js +294 -0
- package/dist/accessors/formats/any-accessor.d.ts +35 -0
- package/dist/accessors/formats/any-accessor.js +44 -0
- package/dist/accessors/formats/array-accessor.d.ts +26 -0
- package/dist/accessors/formats/array-accessor.js +39 -0
- package/dist/accessors/formats/env-accessor.d.ts +27 -0
- package/dist/accessors/formats/env-accessor.js +64 -0
- package/dist/accessors/formats/ini-accessor.d.ts +41 -0
- package/dist/accessors/formats/ini-accessor.js +109 -0
- package/dist/accessors/formats/json-accessor.d.ts +26 -0
- package/dist/accessors/formats/json-accessor.js +56 -0
- package/dist/accessors/formats/ndjson-accessor.d.ts +28 -0
- package/dist/accessors/formats/ndjson-accessor.js +71 -0
- package/dist/accessors/formats/object-accessor.d.ts +48 -0
- package/dist/accessors/formats/object-accessor.js +90 -0
- package/dist/accessors/formats/xml-accessor.d.ts +27 -0
- package/dist/accessors/formats/xml-accessor.js +52 -0
- package/dist/accessors/formats/yaml-accessor.d.ts +29 -0
- package/dist/accessors/formats/yaml-accessor.js +46 -0
- package/dist/contracts/accessors-interface.d.ts +11 -0
- package/dist/contracts/accessors-interface.js +1 -0
- package/dist/contracts/factory-accessors-interface.d.ts +16 -0
- package/dist/contracts/factory-accessors-interface.js +1 -0
- package/dist/contracts/parse-integration-interface.d.ts +31 -0
- package/dist/contracts/parse-integration-interface.js +1 -0
- package/dist/contracts/path-cache-interface.d.ts +40 -0
- package/dist/contracts/path-cache-interface.js +1 -0
- package/dist/contracts/readable-accessors-interface.d.ts +79 -0
- package/dist/contracts/readable-accessors-interface.js +1 -0
- package/dist/contracts/security-guard-interface.d.ts +40 -0
- package/dist/contracts/security-guard-interface.js +1 -0
- package/dist/contracts/security-parser-interface.d.ts +67 -0
- package/dist/contracts/security-parser-interface.js +1 -0
- package/dist/contracts/writable-accessors-interface.d.ts +65 -0
- package/dist/contracts/writable-accessors-interface.js +1 -0
- package/dist/core/dot-notation-parser.d.ts +204 -0
- package/dist/core/dot-notation-parser.js +343 -0
- package/dist/exceptions/accessor-exception.d.ts +13 -0
- package/dist/exceptions/accessor-exception.js +16 -0
- package/dist/exceptions/invalid-format-exception.d.ts +14 -0
- package/dist/exceptions/invalid-format-exception.js +17 -0
- package/dist/exceptions/parser-exception.d.ts +14 -0
- package/dist/exceptions/parser-exception.js +17 -0
- package/dist/exceptions/path-not-found-exception.d.ts +14 -0
- package/dist/exceptions/path-not-found-exception.js +17 -0
- package/dist/exceptions/readonly-violation-exception.d.ts +15 -0
- package/dist/exceptions/readonly-violation-exception.js +18 -0
- package/dist/exceptions/security-exception.d.ts +18 -0
- package/dist/exceptions/security-exception.js +21 -0
- package/dist/exceptions/unsupported-type-exception.d.ts +14 -0
- package/dist/exceptions/unsupported-type-exception.js +17 -0
- package/dist/exceptions/yaml-parse-exception.d.ts +17 -0
- package/dist/exceptions/yaml-parse-exception.js +20 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +30 -0
- package/dist/inline.d.ts +402 -0
- package/dist/inline.js +512 -0
- package/dist/parser/xml-parser.d.ts +46 -0
- package/dist/parser/xml-parser.js +288 -0
- package/dist/parser/yaml-parser.d.ts +94 -0
- package/dist/parser/yaml-parser.js +286 -0
- package/dist/security/forbidden-keys.d.ts +34 -0
- package/dist/security/forbidden-keys.js +80 -0
- package/dist/security/security-guard.d.ts +94 -0
- package/dist/security/security-guard.js +172 -0
- package/dist/security/security-parser.d.ts +130 -0
- package/dist/security/security-parser.js +192 -0
- package/dist/type-format.d.ts +28 -0
- package/dist/type-format.js +29 -0
- package/eslint.config.js +1 -0
- package/package.json +39 -0
- package/src/accessors/abstract-accessor.ts +353 -0
- package/src/accessors/formats/any-accessor.ts +51 -0
- package/src/accessors/formats/array-accessor.ts +45 -0
- package/src/accessors/formats/env-accessor.ts +79 -0
- package/src/accessors/formats/ini-accessor.ts +124 -0
- package/src/accessors/formats/json-accessor.ts +66 -0
- package/src/accessors/formats/ndjson-accessor.ts +82 -0
- package/src/accessors/formats/object-accessor.ts +100 -0
- package/src/accessors/formats/xml-accessor.ts +58 -0
- package/src/accessors/formats/yaml-accessor.ts +52 -0
- package/src/contracts/accessors-interface.ts +12 -0
- package/src/contracts/factory-accessors-interface.ts +16 -0
- package/src/contracts/parse-integration-interface.ts +32 -0
- package/src/contracts/path-cache-interface.ts +43 -0
- package/src/contracts/readable-accessors-interface.ts +88 -0
- package/src/contracts/security-guard-interface.ts +43 -0
- package/src/contracts/security-parser-interface.ts +74 -0
- package/src/contracts/writable-accessors-interface.ts +70 -0
- package/src/core/dot-notation-parser.ts +419 -0
- package/src/exceptions/accessor-exception.ts +16 -0
- package/src/exceptions/invalid-format-exception.ts +18 -0
- package/src/exceptions/parser-exception.ts +18 -0
- package/src/exceptions/path-not-found-exception.ts +18 -0
- package/src/exceptions/readonly-violation-exception.ts +19 -0
- package/src/exceptions/security-exception.ts +22 -0
- package/src/exceptions/unsupported-type-exception.ts +18 -0
- package/src/exceptions/yaml-parse-exception.ts +21 -0
- package/src/index.ts +46 -0
- package/src/inline.ts +570 -0
- package/src/parser/xml-parser.ts +334 -0
- package/src/parser/yaml-parser.ts +368 -0
- package/src/security/forbidden-keys.ts +81 -0
- package/src/security/security-guard.ts +195 -0
- package/src/security/security-parser.ts +233 -0
- package/src/type-format.ts +28 -0
- package/stryker.config.json +24 -0
- package/tests/accessors/accessors.test.ts +1017 -0
- package/tests/accessors/json-accessor.test.ts +171 -0
- package/tests/core/dot-notation-parser.test.ts +587 -0
- package/tests/exceptions/parser-exception.test.ts +31 -0
- package/tests/inline.test.ts +445 -0
- package/tests/mocks/fake-parse-integration.ts +24 -0
- package/tests/mocks/fake-path-cache.ts +31 -0
- package/tests/parity.test.ts +164 -0
- package/tests/parser/xml-parser.test.ts +618 -0
- package/tests/parser/yaml-parser.test.ts +463 -0
- package/tests/security/security-guard.test.ts +646 -0
- package/tests/security/security-parser.test.ts +391 -0
- package/tsconfig.json +16 -0
- package/vitest.config.ts +19 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { AbstractAccessor } from '../abstract-accessor.js';
|
|
2
|
+
import { InvalidFormatException } from '../../exceptions/invalid-format-exception.js';
|
|
3
|
+
/**
|
|
4
|
+
* Accessor for dotenv-formatted strings.
|
|
5
|
+
*
|
|
6
|
+
* Parses KEY=VALUE lines, skipping comments (#) and blank lines.
|
|
7
|
+
* Strips surrounding single and double quotes from values.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const accessor = new EnvAccessor(parser).from('DB_HOST=localhost\nDEBUG=true');
|
|
11
|
+
* accessor.get('DB_HOST'); // 'localhost'
|
|
12
|
+
*/
|
|
13
|
+
export class EnvAccessor extends AbstractAccessor {
|
|
14
|
+
/**
|
|
15
|
+
* Hydrate from a dotenv-formatted string.
|
|
16
|
+
*
|
|
17
|
+
* @param data - Dotenv string input.
|
|
18
|
+
* @returns Populated accessor instance.
|
|
19
|
+
* @throws {InvalidFormatException} When input is not a string.
|
|
20
|
+
* @throws {SecurityException} When payload size exceeds limit.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* accessor.from('APP_ENV=production\nPORT=3000');
|
|
24
|
+
*/
|
|
25
|
+
from(data) {
|
|
26
|
+
if (typeof data !== 'string') {
|
|
27
|
+
/* Stryker disable next-line StringLiteral -- error message content is cosmetic */
|
|
28
|
+
throw new InvalidFormatException(`EnvAccessor expects an ENV string, got ${typeof data}`);
|
|
29
|
+
}
|
|
30
|
+
return this.ingest(data);
|
|
31
|
+
}
|
|
32
|
+
/** {@inheritDoc} */
|
|
33
|
+
parse(raw) {
|
|
34
|
+
/* Stryker disable next-line ConditionalExpression,BlockStatement,StringLiteral -- unreachable: from() always validates string before ingest() */
|
|
35
|
+
/* c8 ignore start */
|
|
36
|
+
if (typeof raw !== 'string') {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
/* c8 ignore stop */
|
|
40
|
+
const result = {};
|
|
41
|
+
for (const rawLine of raw.split('\n')) {
|
|
42
|
+
/* Stryker disable next-line MethodExpression -- trim() on rawLine: whitespace-only lines still skip via empty check below */
|
|
43
|
+
const line = rawLine.trim();
|
|
44
|
+
/* Stryker disable next-line ConditionalExpression,LogicalOperator,StringLiteral,MethodExpression -- equivalent: blank lines not matching = are caught by eqPos === -1 guard; comment-skipping still works */
|
|
45
|
+
if (line === '' || line.startsWith('#')) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const eqPos = line.indexOf('=');
|
|
49
|
+
/* Stryker disable next-line ConditionalExpression,UnaryOperator,BlockStatement -- equivalent: lines without = produce no usable key=value pair */
|
|
50
|
+
if (eqPos === -1) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const key = line.slice(0, eqPos).trim();
|
|
54
|
+
let value = line.slice(eqPos + 1).trim();
|
|
55
|
+
// Strip surrounding quotes
|
|
56
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
57
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
58
|
+
value = value.slice(1, -1);
|
|
59
|
+
}
|
|
60
|
+
result[key] = value;
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { AbstractAccessor } from '../abstract-accessor.js';
|
|
2
|
+
/**
|
|
3
|
+
* Accessor for INI-formatted strings.
|
|
4
|
+
*
|
|
5
|
+
* Parses sections (e.g. `[section]`) as nested keys.
|
|
6
|
+
* Type inference: numeric strings become numbers, `true`/`false` become booleans.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const accessor = new IniAccessor(parser).from('[db]\nhost=localhost\nport=5432');
|
|
10
|
+
* accessor.get('db.host'); // 'localhost'
|
|
11
|
+
*/
|
|
12
|
+
export declare class IniAccessor extends AbstractAccessor {
|
|
13
|
+
/**
|
|
14
|
+
* Hydrate from an INI-formatted string.
|
|
15
|
+
*
|
|
16
|
+
* @param data - INI string input.
|
|
17
|
+
* @returns Populated accessor instance.
|
|
18
|
+
* @throws {InvalidFormatException} When input is not a string.
|
|
19
|
+
* @throws {SecurityException} When payload size exceeds limit.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* accessor.from('key=value\n[section]\nname=Alice');
|
|
23
|
+
*/
|
|
24
|
+
from(data: unknown): this;
|
|
25
|
+
/** {@inheritDoc} */
|
|
26
|
+
protected parse(raw: unknown): Record<string, unknown>;
|
|
27
|
+
/**
|
|
28
|
+
* Parse an INI string into a nested record.
|
|
29
|
+
*
|
|
30
|
+
* @param input - Raw INI content.
|
|
31
|
+
* @returns Parsed key-value structure.
|
|
32
|
+
*/
|
|
33
|
+
private parseIni;
|
|
34
|
+
/**
|
|
35
|
+
* Cast an INI string value to its native type.
|
|
36
|
+
*
|
|
37
|
+
* @param value - Raw string value from the INI file.
|
|
38
|
+
* @returns Typed value (boolean, null, number, or string).
|
|
39
|
+
*/
|
|
40
|
+
private castIniValue;
|
|
41
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { AbstractAccessor } from '../abstract-accessor.js';
|
|
2
|
+
import { InvalidFormatException } from '../../exceptions/invalid-format-exception.js';
|
|
3
|
+
/**
|
|
4
|
+
* Accessor for INI-formatted strings.
|
|
5
|
+
*
|
|
6
|
+
* Parses sections (e.g. `[section]`) as nested keys.
|
|
7
|
+
* Type inference: numeric strings become numbers, `true`/`false` become booleans.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const accessor = new IniAccessor(parser).from('[db]\nhost=localhost\nport=5432');
|
|
11
|
+
* accessor.get('db.host'); // 'localhost'
|
|
12
|
+
*/
|
|
13
|
+
export class IniAccessor extends AbstractAccessor {
|
|
14
|
+
/**
|
|
15
|
+
* Hydrate from an INI-formatted string.
|
|
16
|
+
*
|
|
17
|
+
* @param data - INI string input.
|
|
18
|
+
* @returns Populated accessor instance.
|
|
19
|
+
* @throws {InvalidFormatException} When input is not a string.
|
|
20
|
+
* @throws {SecurityException} When payload size exceeds limit.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* accessor.from('key=value\n[section]\nname=Alice');
|
|
24
|
+
*/
|
|
25
|
+
from(data) {
|
|
26
|
+
if (typeof data !== 'string') {
|
|
27
|
+
/* Stryker disable next-line StringLiteral -- error message content is cosmetic */
|
|
28
|
+
throw new InvalidFormatException(`IniAccessor expects an INI string, got ${typeof data}`);
|
|
29
|
+
}
|
|
30
|
+
return this.ingest(data);
|
|
31
|
+
}
|
|
32
|
+
/** {@inheritDoc} */
|
|
33
|
+
parse(raw) {
|
|
34
|
+
/* Stryker disable next-line ConditionalExpression,BlockStatement,StringLiteral -- unreachable: from() always validates string before ingest() */
|
|
35
|
+
/* c8 ignore start */
|
|
36
|
+
if (typeof raw !== 'string') {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
/* c8 ignore stop */
|
|
40
|
+
return this.parseIni(raw);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Parse an INI string into a nested record.
|
|
44
|
+
*
|
|
45
|
+
* @param input - Raw INI content.
|
|
46
|
+
* @returns Parsed key-value structure.
|
|
47
|
+
*/
|
|
48
|
+
parseIni(input) {
|
|
49
|
+
const result = {};
|
|
50
|
+
let currentSection = null;
|
|
51
|
+
for (const rawLine of input.split('\n')) {
|
|
52
|
+
/* Stryker disable next-line MethodExpression -- equivalent: untrimmed leading whitespace on keys/values handled by subsequent trim() calls */
|
|
53
|
+
const line = rawLine.trim();
|
|
54
|
+
/* Stryker disable next-line ConditionalExpression,LogicalOperator,StringLiteral,MethodExpression,BlockStatement -- fallthrough: blank/comment lines without = still skip via eqPos === -1 */
|
|
55
|
+
if (line === '' || line.startsWith('#') || line.startsWith(';')) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
// Section header
|
|
59
|
+
const sectionMatch = /^\[([^\]]+)\]/.exec(line);
|
|
60
|
+
if (sectionMatch !== null) {
|
|
61
|
+
currentSection = sectionMatch[1];
|
|
62
|
+
if (!Object.prototype.hasOwnProperty.call(result, currentSection)) {
|
|
63
|
+
result[currentSection] = {};
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
// Key=Value
|
|
68
|
+
const eqPos = line.indexOf('=');
|
|
69
|
+
/* Stryker disable next-line ConditionalExpression,BlockStatement -- equivalent: lines without = produce no usable key=value pair */
|
|
70
|
+
if (eqPos === -1) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const key = line.slice(0, eqPos).trim();
|
|
74
|
+
const rawValue = line.slice(eqPos + 1).trim();
|
|
75
|
+
const value = this.castIniValue(rawValue);
|
|
76
|
+
if (currentSection !== null) {
|
|
77
|
+
result[currentSection][key] = value;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
result[key] = value;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Cast an INI string value to its native type.
|
|
87
|
+
*
|
|
88
|
+
* @param value - Raw string value from the INI file.
|
|
89
|
+
* @returns Typed value (boolean, null, number, or string).
|
|
90
|
+
*/
|
|
91
|
+
castIniValue(value) {
|
|
92
|
+
if (value === 'true' || value === 'yes' || value === 'on')
|
|
93
|
+
return true;
|
|
94
|
+
if (value === 'false' || value === 'no' || value === 'off' || value === 'none')
|
|
95
|
+
return false;
|
|
96
|
+
if (value === 'null' || value === '')
|
|
97
|
+
return null;
|
|
98
|
+
// Strip surrounding quotes
|
|
99
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
100
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
101
|
+
return value.slice(1, -1);
|
|
102
|
+
}
|
|
103
|
+
if (/^-?\d+$/.test(value))
|
|
104
|
+
return parseInt(value, 10);
|
|
105
|
+
if (/^-?\d+\.\d+$/.test(value))
|
|
106
|
+
return parseFloat(value);
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { AbstractAccessor } from '../abstract-accessor.js';
|
|
2
|
+
/**
|
|
3
|
+
* Accessor for JSON-encoded strings.
|
|
4
|
+
*
|
|
5
|
+
* Decodes JSON via `JSON.parse()`. Validates payload size before parsing.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const accessor = new JsonAccessor(parser).from('{"key":"value"}');
|
|
9
|
+
* accessor.get('key'); // 'value'
|
|
10
|
+
*/
|
|
11
|
+
export declare class JsonAccessor extends AbstractAccessor {
|
|
12
|
+
/**
|
|
13
|
+
* Hydrate from a JSON string.
|
|
14
|
+
*
|
|
15
|
+
* @param data - JSON string input.
|
|
16
|
+
* @returns Populated accessor instance.
|
|
17
|
+
* @throws {InvalidFormatException} When input is not a string or JSON is malformed.
|
|
18
|
+
* @throws {SecurityException} When payload size exceeds limit.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* accessor.from('{"name":"Alice"}');
|
|
22
|
+
*/
|
|
23
|
+
from(data: unknown): this;
|
|
24
|
+
/** {@inheritDoc} */
|
|
25
|
+
protected parse(raw: unknown): Record<string, unknown>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { AbstractAccessor } from '../abstract-accessor.js';
|
|
2
|
+
import { InvalidFormatException } from '../../exceptions/invalid-format-exception.js';
|
|
3
|
+
/**
|
|
4
|
+
* Accessor for JSON-encoded strings.
|
|
5
|
+
*
|
|
6
|
+
* Decodes JSON via `JSON.parse()`. Validates payload size before parsing.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const accessor = new JsonAccessor(parser).from('{"key":"value"}');
|
|
10
|
+
* accessor.get('key'); // 'value'
|
|
11
|
+
*/
|
|
12
|
+
export class JsonAccessor extends AbstractAccessor {
|
|
13
|
+
/**
|
|
14
|
+
* Hydrate from a JSON string.
|
|
15
|
+
*
|
|
16
|
+
* @param data - JSON string input.
|
|
17
|
+
* @returns Populated accessor instance.
|
|
18
|
+
* @throws {InvalidFormatException} When input is not a string or JSON is malformed.
|
|
19
|
+
* @throws {SecurityException} When payload size exceeds limit.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* accessor.from('{"name":"Alice"}');
|
|
23
|
+
*/
|
|
24
|
+
from(data) {
|
|
25
|
+
if (typeof data !== 'string') {
|
|
26
|
+
/* Stryker disable StringLiteral -- error message content is cosmetic; mutation produces empty string which is still an InvalidFormatException */
|
|
27
|
+
throw new InvalidFormatException(`JsonAccessor expects a JSON string, got ${typeof data}`);
|
|
28
|
+
/* Stryker restore StringLiteral */
|
|
29
|
+
}
|
|
30
|
+
return this.ingest(data);
|
|
31
|
+
}
|
|
32
|
+
/** {@inheritDoc} */
|
|
33
|
+
parse(raw) {
|
|
34
|
+
/* Stryker disable next-line ConditionalExpression,BlockStatement,StringLiteral -- unreachable: from() always validates string before ingest() */
|
|
35
|
+
/* c8 ignore start */
|
|
36
|
+
if (typeof raw !== 'string') {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
/* c8 ignore stop */
|
|
40
|
+
let decoded;
|
|
41
|
+
try {
|
|
42
|
+
decoded = JSON.parse(raw);
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
/* Stryker disable StringLiteral,ObjectLiteral -- error message cosmetic; cause object content not observable via public API */
|
|
46
|
+
/* c8 ignore start */
|
|
47
|
+
throw new InvalidFormatException(`JsonAccessor failed to parse JSON: ${err instanceof Error ? err.message : String(err)}`, { cause: err instanceof Error ? err : undefined });
|
|
48
|
+
/* c8 ignore stop */
|
|
49
|
+
/* Stryker restore StringLiteral,ObjectLiteral */
|
|
50
|
+
}
|
|
51
|
+
if (typeof decoded !== 'object' || decoded === null) {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
return decoded;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { AbstractAccessor } from '../abstract-accessor.js';
|
|
2
|
+
/**
|
|
3
|
+
* Accessor for Newline-Delimited JSON (NDJSON) strings.
|
|
4
|
+
*
|
|
5
|
+
* Parses each non-empty line as a standalone JSON object,
|
|
6
|
+
* producing an indexed record of parsed entries.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const ndjson = '{"id":1}\n{"id":2}';
|
|
10
|
+
* const accessor = new NdjsonAccessor(parser).from(ndjson);
|
|
11
|
+
* accessor.get('0.id'); // 1
|
|
12
|
+
*/
|
|
13
|
+
export declare class NdjsonAccessor extends AbstractAccessor {
|
|
14
|
+
/**
|
|
15
|
+
* Hydrate from an NDJSON string.
|
|
16
|
+
*
|
|
17
|
+
* @param data - NDJSON string input.
|
|
18
|
+
* @returns Populated accessor instance.
|
|
19
|
+
* @throws {InvalidFormatException} When input is not a string or any line is malformed.
|
|
20
|
+
* @throws {SecurityException} When payload size exceeds limit.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* accessor.from('{"name":"Alice"}\n{"name":"Bob"}');
|
|
24
|
+
*/
|
|
25
|
+
from(data: unknown): this;
|
|
26
|
+
/** {@inheritDoc} */
|
|
27
|
+
protected parse(raw: unknown): Record<string, unknown>;
|
|
28
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { AbstractAccessor } from '../abstract-accessor.js';
|
|
2
|
+
import { InvalidFormatException } from '../../exceptions/invalid-format-exception.js';
|
|
3
|
+
/**
|
|
4
|
+
* Accessor for Newline-Delimited JSON (NDJSON) strings.
|
|
5
|
+
*
|
|
6
|
+
* Parses each non-empty line as a standalone JSON object,
|
|
7
|
+
* producing an indexed record of parsed entries.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const ndjson = '{"id":1}\n{"id":2}';
|
|
11
|
+
* const accessor = new NdjsonAccessor(parser).from(ndjson);
|
|
12
|
+
* accessor.get('0.id'); // 1
|
|
13
|
+
*/
|
|
14
|
+
export class NdjsonAccessor extends AbstractAccessor {
|
|
15
|
+
/**
|
|
16
|
+
* Hydrate from an NDJSON string.
|
|
17
|
+
*
|
|
18
|
+
* @param data - NDJSON string input.
|
|
19
|
+
* @returns Populated accessor instance.
|
|
20
|
+
* @throws {InvalidFormatException} When input is not a string or any line is malformed.
|
|
21
|
+
* @throws {SecurityException} When payload size exceeds limit.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* accessor.from('{"name":"Alice"}\n{"name":"Bob"}');
|
|
25
|
+
*/
|
|
26
|
+
from(data) {
|
|
27
|
+
if (typeof data !== 'string') {
|
|
28
|
+
/* Stryker disable StringLiteral -- error message content is cosmetic */
|
|
29
|
+
throw new InvalidFormatException(`NdjsonAccessor expects an NDJSON string, got ${typeof data}`);
|
|
30
|
+
/* Stryker restore StringLiteral */
|
|
31
|
+
}
|
|
32
|
+
return this.ingest(data);
|
|
33
|
+
}
|
|
34
|
+
/** {@inheritDoc} */
|
|
35
|
+
parse(raw) {
|
|
36
|
+
/* Stryker disable next-line ConditionalExpression,BlockStatement,StringLiteral -- unreachable: from() always validates string before ingest() */
|
|
37
|
+
/* c8 ignore start */
|
|
38
|
+
if (typeof raw !== 'string') {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
/* c8 ignore stop */
|
|
42
|
+
const result = {};
|
|
43
|
+
const allLines = raw.split('\n');
|
|
44
|
+
const nonEmptyLines = [];
|
|
45
|
+
/* Stryker disable next-line EqualityOperator -- equivalent: extra undefined allLines[length] entry is safely handled by ?? '' */
|
|
46
|
+
for (let idx = 0; idx < allLines.length; idx++) {
|
|
47
|
+
/* Stryker disable next-line StringLiteral -- NoCoverage: allLines[idx] is always defined within valid loop bounds */
|
|
48
|
+
/* c8 ignore next */
|
|
49
|
+
const trimmed = (allLines[idx] ?? '').trim();
|
|
50
|
+
if (trimmed !== '') {
|
|
51
|
+
nonEmptyLines.push({ line: trimmed, originalLine: idx + 1 });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/* Stryker disable next-line ConditionalExpression,BlockStatement -- equivalent: empty nonEmptyLines causes loop to run 0 iterations and returns {} naturally */
|
|
55
|
+
if (nonEmptyLines.length === 0) {
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
for (let i = 0; i < nonEmptyLines.length; i++) {
|
|
59
|
+
const entry = nonEmptyLines[i];
|
|
60
|
+
let decoded;
|
|
61
|
+
try {
|
|
62
|
+
decoded = JSON.parse(entry.line);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
throw new InvalidFormatException(`NdjsonAccessor failed to parse line ${entry.originalLine}: ${entry.line}`);
|
|
66
|
+
}
|
|
67
|
+
result[String(i)] = decoded;
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { AbstractAccessor } from '../abstract-accessor.js';
|
|
2
|
+
/**
|
|
3
|
+
* Accessor for JavaScript objects, converting them to plain records recursively.
|
|
4
|
+
*
|
|
5
|
+
* Handles nested objects and arrays of objects without JSON roundtrip.
|
|
6
|
+
* Respects the configured max depth to prevent DoS from deeply nested structures.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const obj = { user: { name: 'Alice' } };
|
|
10
|
+
* const accessor = new ObjectAccessor(parser).from(obj);
|
|
11
|
+
* accessor.get('user.name'); // 'Alice'
|
|
12
|
+
*/
|
|
13
|
+
export declare class ObjectAccessor extends AbstractAccessor {
|
|
14
|
+
/**
|
|
15
|
+
* Hydrate from a JavaScript object.
|
|
16
|
+
*
|
|
17
|
+
* @param data - Object input.
|
|
18
|
+
* @returns Populated accessor instance.
|
|
19
|
+
* @throws {InvalidFormatException} When input is not an object.
|
|
20
|
+
* @throws {SecurityException} When data contains forbidden keys or exceeds depth limit.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* accessor.from({ name: 'Alice', age: 30 });
|
|
24
|
+
*/
|
|
25
|
+
from(data: unknown): this;
|
|
26
|
+
/** {@inheritDoc} */
|
|
27
|
+
protected parse(raw: unknown): Record<string, unknown>;
|
|
28
|
+
/**
|
|
29
|
+
* Recursively convert a nested object into a plain record.
|
|
30
|
+
*
|
|
31
|
+
* @param value - Object to convert.
|
|
32
|
+
* @param depth - Current recursion depth.
|
|
33
|
+
* @returns Converted record.
|
|
34
|
+
*
|
|
35
|
+
* @throws {SecurityException} When recursion depth exceeds the configured maximum.
|
|
36
|
+
*/
|
|
37
|
+
private objectToRecord;
|
|
38
|
+
/**
|
|
39
|
+
* Recursively convert nested arrays containing objects.
|
|
40
|
+
*
|
|
41
|
+
* @param array - Array to process.
|
|
42
|
+
* @param depth - Current recursion depth.
|
|
43
|
+
* @returns Array with all objects converted.
|
|
44
|
+
*
|
|
45
|
+
* @throws {SecurityException} When recursion depth exceeds the configured maximum.
|
|
46
|
+
*/
|
|
47
|
+
private convertArrayValues;
|
|
48
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { AbstractAccessor } from '../abstract-accessor.js';
|
|
2
|
+
import { InvalidFormatException } from '../../exceptions/invalid-format-exception.js';
|
|
3
|
+
import { SecurityException } from '../../exceptions/security-exception.js';
|
|
4
|
+
/**
|
|
5
|
+
* Accessor for JavaScript objects, converting them to plain records recursively.
|
|
6
|
+
*
|
|
7
|
+
* Handles nested objects and arrays of objects without JSON roundtrip.
|
|
8
|
+
* Respects the configured max depth to prevent DoS from deeply nested structures.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* const obj = { user: { name: 'Alice' } };
|
|
12
|
+
* const accessor = new ObjectAccessor(parser).from(obj);
|
|
13
|
+
* accessor.get('user.name'); // 'Alice'
|
|
14
|
+
*/
|
|
15
|
+
export class ObjectAccessor extends AbstractAccessor {
|
|
16
|
+
/**
|
|
17
|
+
* Hydrate from a JavaScript object.
|
|
18
|
+
*
|
|
19
|
+
* @param data - Object input.
|
|
20
|
+
* @returns Populated accessor instance.
|
|
21
|
+
* @throws {InvalidFormatException} When input is not an object.
|
|
22
|
+
* @throws {SecurityException} When data contains forbidden keys or exceeds depth limit.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* accessor.from({ name: 'Alice', age: 30 });
|
|
26
|
+
*/
|
|
27
|
+
from(data) {
|
|
28
|
+
if (typeof data !== 'object' || data === null || Array.isArray(data)) {
|
|
29
|
+
throw new InvalidFormatException(`ObjectAccessor expects an object, got ${Array.isArray(data) ? 'array' : typeof data}`);
|
|
30
|
+
}
|
|
31
|
+
return this.ingest(data);
|
|
32
|
+
}
|
|
33
|
+
/** {@inheritDoc} */
|
|
34
|
+
parse(raw) {
|
|
35
|
+
return this.objectToRecord(raw, 0);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Recursively convert a nested object into a plain record.
|
|
39
|
+
*
|
|
40
|
+
* @param value - Object to convert.
|
|
41
|
+
* @param depth - Current recursion depth.
|
|
42
|
+
* @returns Converted record.
|
|
43
|
+
*
|
|
44
|
+
* @throws {SecurityException} When recursion depth exceeds the configured maximum.
|
|
45
|
+
*/
|
|
46
|
+
objectToRecord(value, depth) {
|
|
47
|
+
const maxDepth = this.parser.getMaxDepth();
|
|
48
|
+
if (depth > maxDepth) {
|
|
49
|
+
throw new SecurityException(`Object depth ${depth} exceeds maximum of ${maxDepth}.`);
|
|
50
|
+
}
|
|
51
|
+
const result = {};
|
|
52
|
+
for (const [key, val] of Object.entries(value)) {
|
|
53
|
+
if (typeof val === 'object' && val !== null && !Array.isArray(val)) {
|
|
54
|
+
result[key] = this.objectToRecord(val, depth + 1);
|
|
55
|
+
}
|
|
56
|
+
else if (Array.isArray(val)) {
|
|
57
|
+
result[key] = this.convertArrayValues(val, depth + 1);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
result[key] = val;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Recursively convert nested arrays containing objects.
|
|
67
|
+
*
|
|
68
|
+
* @param array - Array to process.
|
|
69
|
+
* @param depth - Current recursion depth.
|
|
70
|
+
* @returns Array with all objects converted.
|
|
71
|
+
*
|
|
72
|
+
* @throws {SecurityException} When recursion depth exceeds the configured maximum.
|
|
73
|
+
*/
|
|
74
|
+
convertArrayValues(array, depth) {
|
|
75
|
+
const maxDepth = this.parser.getMaxDepth();
|
|
76
|
+
if (depth > maxDepth) {
|
|
77
|
+
/* Stryker disable next-line StringLiteral -- error message content is cosmetic; test asserts exception type only */
|
|
78
|
+
throw new SecurityException(`Object depth ${depth} exceeds maximum of ${maxDepth}.`);
|
|
79
|
+
}
|
|
80
|
+
return array.map((val) => {
|
|
81
|
+
if (typeof val === 'object' && val !== null && !Array.isArray(val)) {
|
|
82
|
+
return this.objectToRecord(val, depth + 1);
|
|
83
|
+
}
|
|
84
|
+
else if (Array.isArray(val)) {
|
|
85
|
+
return this.convertArrayValues(val, depth + 1);
|
|
86
|
+
}
|
|
87
|
+
return val;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { AbstractAccessor } from '../abstract-accessor.js';
|
|
2
|
+
/**
|
|
3
|
+
* Accessor for XML strings.
|
|
4
|
+
*
|
|
5
|
+
* Parses XML using the DOM parser available in the current environment.
|
|
6
|
+
* Blocks DOCTYPE declarations to prevent XXE attacks.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const accessor = new XmlAccessor(parser).from('<root><key>value</key></root>');
|
|
10
|
+
* accessor.get('key'); // 'value'
|
|
11
|
+
*/
|
|
12
|
+
export declare class XmlAccessor extends AbstractAccessor {
|
|
13
|
+
/**
|
|
14
|
+
* Hydrate from an XML string.
|
|
15
|
+
*
|
|
16
|
+
* @param data - XML string input.
|
|
17
|
+
* @returns Populated accessor instance.
|
|
18
|
+
* @throws {InvalidFormatException} When input is not a string.
|
|
19
|
+
* @throws {SecurityException} When DOCTYPE declaration is detected.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* accessor.from('<root><name>Alice</name></root>');
|
|
23
|
+
*/
|
|
24
|
+
from(data: unknown): this;
|
|
25
|
+
/** {@inheritDoc} */
|
|
26
|
+
protected parse(raw: unknown): Record<string, unknown>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { AbstractAccessor } from '../abstract-accessor.js';
|
|
2
|
+
import { InvalidFormatException } from '../../exceptions/invalid-format-exception.js';
|
|
3
|
+
import { SecurityException } from '../../exceptions/security-exception.js';
|
|
4
|
+
import { XmlParser } from '../../parser/xml-parser.js';
|
|
5
|
+
/**
|
|
6
|
+
* Accessor for XML strings.
|
|
7
|
+
*
|
|
8
|
+
* Parses XML using the DOM parser available in the current environment.
|
|
9
|
+
* Blocks DOCTYPE declarations to prevent XXE attacks.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const accessor = new XmlAccessor(parser).from('<root><key>value</key></root>');
|
|
13
|
+
* accessor.get('key'); // 'value'
|
|
14
|
+
*/
|
|
15
|
+
export class XmlAccessor extends AbstractAccessor {
|
|
16
|
+
/**
|
|
17
|
+
* Hydrate from an XML string.
|
|
18
|
+
*
|
|
19
|
+
* @param data - XML string input.
|
|
20
|
+
* @returns Populated accessor instance.
|
|
21
|
+
* @throws {InvalidFormatException} When input is not a string.
|
|
22
|
+
* @throws {SecurityException} When DOCTYPE declaration is detected.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* accessor.from('<root><name>Alice</name></root>');
|
|
26
|
+
*/
|
|
27
|
+
from(data) {
|
|
28
|
+
if (typeof data !== 'string') {
|
|
29
|
+
throw new InvalidFormatException(
|
|
30
|
+
/* Stryker disable next-line StringLiteral -- error message content is cosmetic */
|
|
31
|
+
`XmlAccessor expects an XML string, got ${typeof data}`);
|
|
32
|
+
}
|
|
33
|
+
return this.ingest(data);
|
|
34
|
+
}
|
|
35
|
+
/** {@inheritDoc} */
|
|
36
|
+
parse(raw) {
|
|
37
|
+
/* Stryker disable next-line ConditionalExpression,BlockStatement,StringLiteral -- unreachable: from() always validates string before ingest() */
|
|
38
|
+
/* c8 ignore start */
|
|
39
|
+
if (typeof raw !== 'string') {
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
/* c8 ignore stop */
|
|
43
|
+
// Reject DOCTYPE to prevent XXE
|
|
44
|
+
if (/<!DOCTYPE/i.test(raw)) {
|
|
45
|
+
/* Stryker disable next-line StringLiteral -- error message content is the security message, tested functionally */
|
|
46
|
+
throw new SecurityException('XML DOCTYPE declarations are not allowed.');
|
|
47
|
+
}
|
|
48
|
+
// Pass getMaxKeys() as the element-count cap: each XML opening tag maps to at most
|
|
49
|
+
// one output key, so maxKeys is a sound upper bound for the ReDoS guard.
|
|
50
|
+
return new XmlParser(this.parser.getMaxDepth(), this.parser.getMaxKeys()).parse(raw);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { AbstractAccessor } from '../abstract-accessor.js';
|
|
2
|
+
/**
|
|
3
|
+
* Accessor for YAML-encoded strings.
|
|
4
|
+
*
|
|
5
|
+
* Uses the internal YamlParser for safe YAML parsing without
|
|
6
|
+
* depending on external YAML libraries. Tags, anchors, aliases, and
|
|
7
|
+
* merge keys are blocked as unsafe constructs.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const accessor = new YamlAccessor(parser).from('key: value\nnested:\n a: 1');
|
|
11
|
+
* accessor.get('nested.a'); // 1
|
|
12
|
+
*/
|
|
13
|
+
export declare class YamlAccessor extends AbstractAccessor {
|
|
14
|
+
/**
|
|
15
|
+
* Hydrate from a YAML string.
|
|
16
|
+
*
|
|
17
|
+
* @param data - YAML string input.
|
|
18
|
+
* @returns Populated accessor instance.
|
|
19
|
+
* @throws {InvalidFormatException} When input is not a string or YAML is malformed.
|
|
20
|
+
* @throws {YamlParseException} When unsafe YAML constructs are present.
|
|
21
|
+
* @throws {SecurityException} When payload size exceeds limit.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* accessor.from('name: Alice\nage: 30');
|
|
25
|
+
*/
|
|
26
|
+
from(data: unknown): this;
|
|
27
|
+
/** {@inheritDoc} */
|
|
28
|
+
protected parse(raw: unknown): Record<string, unknown>;
|
|
29
|
+
}
|