@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,45 @@
|
|
|
1
|
+
import { AbstractAccessor } from '../abstract-accessor.js';
|
|
2
|
+
import { InvalidFormatException } from '../../exceptions/invalid-format-exception.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Accessor for plain objects and arrays.
|
|
6
|
+
*
|
|
7
|
+
* Accepts a plain object or array directly. No string parsing is involved.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const accessor = new ArrayAccessor(parser).from({ key: 'value' });
|
|
11
|
+
* accessor.get('key'); // 'value'
|
|
12
|
+
*/
|
|
13
|
+
export class ArrayAccessor extends AbstractAccessor {
|
|
14
|
+
/**
|
|
15
|
+
* Hydrate from a plain object or array.
|
|
16
|
+
*
|
|
17
|
+
* @param data - Object or array input.
|
|
18
|
+
* @returns Populated accessor instance.
|
|
19
|
+
* @throws {InvalidFormatException} When input is neither an object nor an array.
|
|
20
|
+
* @throws {SecurityException} When data contains forbidden keys.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* accessor.from({ name: 'Alice' });
|
|
24
|
+
*/
|
|
25
|
+
from(data: unknown): this {
|
|
26
|
+
if (typeof data !== 'object' || data === null) {
|
|
27
|
+
/* Stryker disable StringLiteral -- error message content is cosmetic */
|
|
28
|
+
throw new InvalidFormatException(
|
|
29
|
+
`ArrayAccessor expects an object or array, got ${typeof data}`,
|
|
30
|
+
);
|
|
31
|
+
/* Stryker restore StringLiteral */
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const resolved: Record<string, unknown> = Array.isArray(data)
|
|
35
|
+
? Object.fromEntries(data.map((v, i) => [String(i), v]))
|
|
36
|
+
: (data as Record<string, unknown>);
|
|
37
|
+
|
|
38
|
+
return this.ingest(resolved);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** {@inheritDoc} */
|
|
42
|
+
protected parse(raw: unknown): Record<string, unknown> {
|
|
43
|
+
return raw as Record<string, unknown>;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { AbstractAccessor } from '../abstract-accessor.js';
|
|
2
|
+
import { InvalidFormatException } from '../../exceptions/invalid-format-exception.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Accessor for dotenv-formatted strings.
|
|
6
|
+
*
|
|
7
|
+
* Parses KEY=VALUE lines, skipping comments (#) and blank lines.
|
|
8
|
+
* Strips surrounding single and double quotes from values.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* const accessor = new EnvAccessor(parser).from('DB_HOST=localhost\nDEBUG=true');
|
|
12
|
+
* accessor.get('DB_HOST'); // 'localhost'
|
|
13
|
+
*/
|
|
14
|
+
export class EnvAccessor extends AbstractAccessor {
|
|
15
|
+
/**
|
|
16
|
+
* Hydrate from a dotenv-formatted string.
|
|
17
|
+
*
|
|
18
|
+
* @param data - Dotenv string input.
|
|
19
|
+
* @returns Populated accessor instance.
|
|
20
|
+
* @throws {InvalidFormatException} When input is not a string.
|
|
21
|
+
* @throws {SecurityException} When payload size exceeds limit.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* accessor.from('APP_ENV=production\nPORT=3000');
|
|
25
|
+
*/
|
|
26
|
+
from(data: unknown): this {
|
|
27
|
+
if (typeof data !== 'string') {
|
|
28
|
+
/* Stryker disable next-line StringLiteral -- error message content is cosmetic */
|
|
29
|
+
throw new InvalidFormatException(
|
|
30
|
+
`EnvAccessor expects an ENV string, got ${typeof data}`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return this.ingest(data);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** {@inheritDoc} */
|
|
38
|
+
protected parse(raw: unknown): Record<string, unknown> {
|
|
39
|
+
/* Stryker disable next-line ConditionalExpression,BlockStatement,StringLiteral -- unreachable: from() always validates string before ingest() */
|
|
40
|
+
/* c8 ignore start */
|
|
41
|
+
if (typeof raw !== 'string') {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
/* c8 ignore stop */
|
|
45
|
+
|
|
46
|
+
const result: Record<string, unknown> = {};
|
|
47
|
+
|
|
48
|
+
for (const rawLine of raw.split('\n')) {
|
|
49
|
+
/* Stryker disable next-line MethodExpression -- trim() on rawLine: whitespace-only lines still skip via empty check below */
|
|
50
|
+
const line = rawLine.trim();
|
|
51
|
+
|
|
52
|
+
/* Stryker disable next-line ConditionalExpression,LogicalOperator,StringLiteral,MethodExpression -- equivalent: blank lines not matching = are caught by eqPos === -1 guard; comment-skipping still works */
|
|
53
|
+
if (line === '' || line.startsWith('#')) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const eqPos = line.indexOf('=');
|
|
58
|
+
/* Stryker disable next-line ConditionalExpression,UnaryOperator,BlockStatement -- equivalent: lines without = produce no usable key=value pair */
|
|
59
|
+
if (eqPos === -1) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const key = line.slice(0, eqPos).trim();
|
|
64
|
+
let value = line.slice(eqPos + 1).trim();
|
|
65
|
+
|
|
66
|
+
// Strip surrounding quotes
|
|
67
|
+
if (
|
|
68
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
69
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
70
|
+
) {
|
|
71
|
+
value = value.slice(1, -1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
result[key] = value;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { AbstractAccessor } from '../abstract-accessor.js';
|
|
2
|
+
import { InvalidFormatException } from '../../exceptions/invalid-format-exception.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Accessor for INI-formatted strings.
|
|
6
|
+
*
|
|
7
|
+
* Parses sections (e.g. `[section]`) as nested keys.
|
|
8
|
+
* Type inference: numeric strings become numbers, `true`/`false` become booleans.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* const accessor = new IniAccessor(parser).from('[db]\nhost=localhost\nport=5432');
|
|
12
|
+
* accessor.get('db.host'); // 'localhost'
|
|
13
|
+
*/
|
|
14
|
+
export class IniAccessor extends AbstractAccessor {
|
|
15
|
+
/**
|
|
16
|
+
* Hydrate from an INI-formatted string.
|
|
17
|
+
*
|
|
18
|
+
* @param data - INI string input.
|
|
19
|
+
* @returns Populated accessor instance.
|
|
20
|
+
* @throws {InvalidFormatException} When input is not a string.
|
|
21
|
+
* @throws {SecurityException} When payload size exceeds limit.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* accessor.from('key=value\n[section]\nname=Alice');
|
|
25
|
+
*/
|
|
26
|
+
from(data: unknown): this {
|
|
27
|
+
if (typeof data !== 'string') {
|
|
28
|
+
/* Stryker disable next-line StringLiteral -- error message content is cosmetic */
|
|
29
|
+
throw new InvalidFormatException(
|
|
30
|
+
`IniAccessor expects an INI string, got ${typeof data}`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return this.ingest(data);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** {@inheritDoc} */
|
|
38
|
+
protected parse(raw: unknown): Record<string, unknown> {
|
|
39
|
+
/* Stryker disable next-line ConditionalExpression,BlockStatement,StringLiteral -- unreachable: from() always validates string before ingest() */
|
|
40
|
+
/* c8 ignore start */
|
|
41
|
+
if (typeof raw !== 'string') {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
/* c8 ignore stop */
|
|
45
|
+
|
|
46
|
+
return this.parseIni(raw);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parse an INI string into a nested record.
|
|
51
|
+
*
|
|
52
|
+
* @param input - Raw INI content.
|
|
53
|
+
* @returns Parsed key-value structure.
|
|
54
|
+
*/
|
|
55
|
+
private parseIni(input: string): Record<string, unknown> {
|
|
56
|
+
const result: Record<string, unknown> = {};
|
|
57
|
+
let currentSection: string | null = null;
|
|
58
|
+
|
|
59
|
+
for (const rawLine of input.split('\n')) {
|
|
60
|
+
/* Stryker disable next-line MethodExpression -- equivalent: untrimmed leading whitespace on keys/values handled by subsequent trim() calls */
|
|
61
|
+
const line = rawLine.trim();
|
|
62
|
+
|
|
63
|
+
/* Stryker disable next-line ConditionalExpression,LogicalOperator,StringLiteral,MethodExpression,BlockStatement -- fallthrough: blank/comment lines without = still skip via eqPos === -1 */
|
|
64
|
+
if (line === '' || line.startsWith('#') || line.startsWith(';')) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Section header
|
|
69
|
+
const sectionMatch = /^\[([^\]]+)\]/.exec(line);
|
|
70
|
+
if (sectionMatch !== null) {
|
|
71
|
+
currentSection = sectionMatch[1] as string;
|
|
72
|
+
if (!Object.prototype.hasOwnProperty.call(result, currentSection)) {
|
|
73
|
+
result[currentSection] = {};
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Key=Value
|
|
79
|
+
const eqPos = line.indexOf('=');
|
|
80
|
+
/* Stryker disable next-line ConditionalExpression,BlockStatement -- equivalent: lines without = produce no usable key=value pair */
|
|
81
|
+
if (eqPos === -1) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const key = line.slice(0, eqPos).trim();
|
|
86
|
+
const rawValue = line.slice(eqPos + 1).trim();
|
|
87
|
+
const value = this.castIniValue(rawValue);
|
|
88
|
+
|
|
89
|
+
if (currentSection !== null) {
|
|
90
|
+
(result[currentSection] as Record<string, unknown>)[key] = value;
|
|
91
|
+
} else {
|
|
92
|
+
result[key] = value;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Cast an INI string value to its native type.
|
|
101
|
+
*
|
|
102
|
+
* @param value - Raw string value from the INI file.
|
|
103
|
+
* @returns Typed value (boolean, null, number, or string).
|
|
104
|
+
*/
|
|
105
|
+
private castIniValue(value: string): unknown {
|
|
106
|
+
if (value === 'true' || value === 'yes' || value === 'on') return true;
|
|
107
|
+
if (value === 'false' || value === 'no' || value === 'off' || value === 'none')
|
|
108
|
+
return false;
|
|
109
|
+
if (value === 'null' || value === '') return null;
|
|
110
|
+
|
|
111
|
+
// Strip surrounding quotes
|
|
112
|
+
if (
|
|
113
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
114
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
115
|
+
) {
|
|
116
|
+
return value.slice(1, -1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (/^-?\d+$/.test(value)) return parseInt(value, 10);
|
|
120
|
+
if (/^-?\d+\.\d+$/.test(value)) return parseFloat(value);
|
|
121
|
+
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { AbstractAccessor } from '../abstract-accessor.js';
|
|
2
|
+
import { InvalidFormatException } from '../../exceptions/invalid-format-exception.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Accessor for JSON-encoded strings.
|
|
6
|
+
*
|
|
7
|
+
* Decodes JSON via `JSON.parse()`. Validates payload size before parsing.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const accessor = new JsonAccessor(parser).from('{"key":"value"}');
|
|
11
|
+
* accessor.get('key'); // 'value'
|
|
12
|
+
*/
|
|
13
|
+
export class JsonAccessor extends AbstractAccessor {
|
|
14
|
+
/**
|
|
15
|
+
* Hydrate from a JSON string.
|
|
16
|
+
*
|
|
17
|
+
* @param data - JSON string input.
|
|
18
|
+
* @returns Populated accessor instance.
|
|
19
|
+
* @throws {InvalidFormatException} When input is not a string or JSON is malformed.
|
|
20
|
+
* @throws {SecurityException} When payload size exceeds limit.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* accessor.from('{"name":"Alice"}');
|
|
24
|
+
*/
|
|
25
|
+
from(data: unknown): this {
|
|
26
|
+
if (typeof data !== 'string') {
|
|
27
|
+
/* Stryker disable StringLiteral -- error message content is cosmetic; mutation produces empty string which is still an InvalidFormatException */
|
|
28
|
+
throw new InvalidFormatException(
|
|
29
|
+
`JsonAccessor expects a JSON string, got ${typeof data}`,
|
|
30
|
+
);
|
|
31
|
+
/* Stryker restore StringLiteral */
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return this.ingest(data);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** {@inheritDoc} */
|
|
38
|
+
protected parse(raw: unknown): Record<string, unknown> {
|
|
39
|
+
/* Stryker disable next-line ConditionalExpression,BlockStatement,StringLiteral -- unreachable: from() always validates string before ingest() */
|
|
40
|
+
/* c8 ignore start */
|
|
41
|
+
if (typeof raw !== 'string') {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
/* c8 ignore stop */
|
|
45
|
+
|
|
46
|
+
let decoded: unknown;
|
|
47
|
+
try {
|
|
48
|
+
decoded = JSON.parse(raw);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
/* Stryker disable StringLiteral,ObjectLiteral -- error message cosmetic; cause object content not observable via public API */
|
|
51
|
+
/* c8 ignore start */
|
|
52
|
+
throw new InvalidFormatException(
|
|
53
|
+
`JsonAccessor failed to parse JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
54
|
+
{ cause: err instanceof Error ? err : undefined },
|
|
55
|
+
);
|
|
56
|
+
/* c8 ignore stop */
|
|
57
|
+
/* Stryker restore StringLiteral,ObjectLiteral */
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (typeof decoded !== 'object' || decoded === null) {
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return decoded as Record<string, unknown>;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { AbstractAccessor } from '../abstract-accessor.js';
|
|
2
|
+
import { InvalidFormatException } from '../../exceptions/invalid-format-exception.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Accessor for Newline-Delimited JSON (NDJSON) strings.
|
|
6
|
+
*
|
|
7
|
+
* Parses each non-empty line as a standalone JSON object,
|
|
8
|
+
* producing an indexed record of parsed entries.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* const ndjson = '{"id":1}\n{"id":2}';
|
|
12
|
+
* const accessor = new NdjsonAccessor(parser).from(ndjson);
|
|
13
|
+
* accessor.get('0.id'); // 1
|
|
14
|
+
*/
|
|
15
|
+
export class NdjsonAccessor extends AbstractAccessor {
|
|
16
|
+
/**
|
|
17
|
+
* Hydrate from an NDJSON string.
|
|
18
|
+
*
|
|
19
|
+
* @param data - NDJSON string input.
|
|
20
|
+
* @returns Populated accessor instance.
|
|
21
|
+
* @throws {InvalidFormatException} When input is not a string or any line is malformed.
|
|
22
|
+
* @throws {SecurityException} When payload size exceeds limit.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* accessor.from('{"name":"Alice"}\n{"name":"Bob"}');
|
|
26
|
+
*/
|
|
27
|
+
from(data: unknown): this {
|
|
28
|
+
if (typeof data !== 'string') {
|
|
29
|
+
/* Stryker disable StringLiteral -- error message content is cosmetic */
|
|
30
|
+
throw new InvalidFormatException(
|
|
31
|
+
`NdjsonAccessor expects an NDJSON string, got ${typeof data}`,
|
|
32
|
+
);
|
|
33
|
+
/* Stryker restore StringLiteral */
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return this.ingest(data);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** {@inheritDoc} */
|
|
40
|
+
protected parse(raw: unknown): Record<string, unknown> {
|
|
41
|
+
/* Stryker disable next-line ConditionalExpression,BlockStatement,StringLiteral -- unreachable: from() always validates string before ingest() */
|
|
42
|
+
/* c8 ignore start */
|
|
43
|
+
if (typeof raw !== 'string') {
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
/* c8 ignore stop */
|
|
47
|
+
|
|
48
|
+
const result: Record<string, unknown> = {};
|
|
49
|
+
const allLines = raw.split('\n');
|
|
50
|
+
const nonEmptyLines: Array<{ line: string; originalLine: number }> = [];
|
|
51
|
+
|
|
52
|
+
/* Stryker disable next-line EqualityOperator -- equivalent: extra undefined allLines[length] entry is safely handled by ?? '' */
|
|
53
|
+
for (let idx = 0; idx < allLines.length; idx++) {
|
|
54
|
+
/* Stryker disable next-line StringLiteral -- NoCoverage: allLines[idx] is always defined within valid loop bounds */
|
|
55
|
+
/* c8 ignore next */
|
|
56
|
+
const trimmed = (allLines[idx] ?? '').trim();
|
|
57
|
+
if (trimmed !== '') {
|
|
58
|
+
nonEmptyLines.push({ line: trimmed, originalLine: idx + 1 });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Stryker disable next-line ConditionalExpression,BlockStatement -- equivalent: empty nonEmptyLines causes loop to run 0 iterations and returns {} naturally */
|
|
63
|
+
if (nonEmptyLines.length === 0) {
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (let i = 0; i < nonEmptyLines.length; i++) {
|
|
68
|
+
const entry = nonEmptyLines[i]!;
|
|
69
|
+
let decoded: unknown;
|
|
70
|
+
try {
|
|
71
|
+
decoded = JSON.parse(entry.line);
|
|
72
|
+
} catch {
|
|
73
|
+
throw new InvalidFormatException(
|
|
74
|
+
`NdjsonAccessor failed to parse line ${entry.originalLine}: ${entry.line}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
result[String(i)] = decoded;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
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
|
+
/**
|
|
6
|
+
* Accessor for JavaScript objects, converting them to plain records recursively.
|
|
7
|
+
*
|
|
8
|
+
* Handles nested objects and arrays of objects without JSON roundtrip.
|
|
9
|
+
* Respects the configured max depth to prevent DoS from deeply nested structures.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const obj = { user: { name: 'Alice' } };
|
|
13
|
+
* const accessor = new ObjectAccessor(parser).from(obj);
|
|
14
|
+
* accessor.get('user.name'); // 'Alice'
|
|
15
|
+
*/
|
|
16
|
+
export class ObjectAccessor extends AbstractAccessor {
|
|
17
|
+
/**
|
|
18
|
+
* Hydrate from a JavaScript object.
|
|
19
|
+
*
|
|
20
|
+
* @param data - Object input.
|
|
21
|
+
* @returns Populated accessor instance.
|
|
22
|
+
* @throws {InvalidFormatException} When input is not an object.
|
|
23
|
+
* @throws {SecurityException} When data contains forbidden keys or exceeds depth limit.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* accessor.from({ name: 'Alice', age: 30 });
|
|
27
|
+
*/
|
|
28
|
+
from(data: unknown): this {
|
|
29
|
+
if (typeof data !== 'object' || data === null || Array.isArray(data)) {
|
|
30
|
+
throw new InvalidFormatException(
|
|
31
|
+
`ObjectAccessor expects an object, got ${Array.isArray(data) ? 'array' : typeof data}`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return this.ingest(data);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** {@inheritDoc} */
|
|
39
|
+
protected parse(raw: unknown): Record<string, unknown> {
|
|
40
|
+
return this.objectToRecord(raw as Record<string, unknown>, 0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Recursively convert a nested object into a plain record.
|
|
45
|
+
*
|
|
46
|
+
* @param value - Object to convert.
|
|
47
|
+
* @param depth - Current recursion depth.
|
|
48
|
+
* @returns Converted record.
|
|
49
|
+
*
|
|
50
|
+
* @throws {SecurityException} When recursion depth exceeds the configured maximum.
|
|
51
|
+
*/
|
|
52
|
+
private objectToRecord(value: Record<string, unknown>, depth: number): Record<string, unknown> {
|
|
53
|
+
const maxDepth = this.parser.getMaxDepth();
|
|
54
|
+
|
|
55
|
+
if (depth > maxDepth) {
|
|
56
|
+
throw new SecurityException(`Object depth ${depth} exceeds maximum of ${maxDepth}.`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const result: Record<string, unknown> = {};
|
|
60
|
+
|
|
61
|
+
for (const [key, val] of Object.entries(value)) {
|
|
62
|
+
if (typeof val === 'object' && val !== null && !Array.isArray(val)) {
|
|
63
|
+
result[key] = this.objectToRecord(val as Record<string, unknown>, depth + 1);
|
|
64
|
+
} else if (Array.isArray(val)) {
|
|
65
|
+
result[key] = this.convertArrayValues(val, depth + 1);
|
|
66
|
+
} else {
|
|
67
|
+
result[key] = val;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Recursively convert nested arrays containing objects.
|
|
76
|
+
*
|
|
77
|
+
* @param array - Array to process.
|
|
78
|
+
* @param depth - Current recursion depth.
|
|
79
|
+
* @returns Array with all objects converted.
|
|
80
|
+
*
|
|
81
|
+
* @throws {SecurityException} When recursion depth exceeds the configured maximum.
|
|
82
|
+
*/
|
|
83
|
+
private convertArrayValues(array: unknown[], depth: number): unknown[] {
|
|
84
|
+
const maxDepth = this.parser.getMaxDepth();
|
|
85
|
+
|
|
86
|
+
if (depth > maxDepth) {
|
|
87
|
+
/* Stryker disable next-line StringLiteral -- error message content is cosmetic; test asserts exception type only */
|
|
88
|
+
throw new SecurityException(`Object depth ${depth} exceeds maximum of ${maxDepth}.`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return array.map((val) => {
|
|
92
|
+
if (typeof val === 'object' && val !== null && !Array.isArray(val)) {
|
|
93
|
+
return this.objectToRecord(val as Record<string, unknown>, depth + 1);
|
|
94
|
+
} else if (Array.isArray(val)) {
|
|
95
|
+
return this.convertArrayValues(val, depth + 1);
|
|
96
|
+
}
|
|
97
|
+
return val;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
/**
|
|
7
|
+
* Accessor for XML strings.
|
|
8
|
+
*
|
|
9
|
+
* Parses XML using the DOM parser available in the current environment.
|
|
10
|
+
* Blocks DOCTYPE declarations to prevent XXE attacks.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* const accessor = new XmlAccessor(parser).from('<root><key>value</key></root>');
|
|
14
|
+
* accessor.get('key'); // 'value'
|
|
15
|
+
*/
|
|
16
|
+
export class XmlAccessor extends AbstractAccessor {
|
|
17
|
+
/**
|
|
18
|
+
* Hydrate from an XML string.
|
|
19
|
+
*
|
|
20
|
+
* @param data - XML string input.
|
|
21
|
+
* @returns Populated accessor instance.
|
|
22
|
+
* @throws {InvalidFormatException} When input is not a string.
|
|
23
|
+
* @throws {SecurityException} When DOCTYPE declaration is detected.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* accessor.from('<root><name>Alice</name></root>');
|
|
27
|
+
*/
|
|
28
|
+
from(data: unknown): this {
|
|
29
|
+
if (typeof data !== 'string') {
|
|
30
|
+
throw new InvalidFormatException(
|
|
31
|
+
/* Stryker disable next-line StringLiteral -- error message content is cosmetic */
|
|
32
|
+
`XmlAccessor expects an XML string, got ${typeof data}`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return this.ingest(data);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** {@inheritDoc} */
|
|
40
|
+
protected parse(raw: unknown): Record<string, unknown> {
|
|
41
|
+
/* Stryker disable next-line ConditionalExpression,BlockStatement,StringLiteral -- unreachable: from() always validates string before ingest() */
|
|
42
|
+
/* c8 ignore start */
|
|
43
|
+
if (typeof raw !== 'string') {
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
/* c8 ignore stop */
|
|
47
|
+
|
|
48
|
+
// Reject DOCTYPE to prevent XXE
|
|
49
|
+
if (/<!DOCTYPE/i.test(raw)) {
|
|
50
|
+
/* Stryker disable next-line StringLiteral -- error message content is the security message, tested functionally */
|
|
51
|
+
throw new SecurityException('XML DOCTYPE declarations are not allowed.');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Pass getMaxKeys() as the element-count cap: each XML opening tag maps to at most
|
|
55
|
+
// one output key, so maxKeys is a sound upper bound for the ReDoS guard.
|
|
56
|
+
return new XmlParser(this.parser.getMaxDepth(), this.parser.getMaxKeys()).parse(raw);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { AbstractAccessor } from '../abstract-accessor.js';
|
|
2
|
+
import { InvalidFormatException } from '../../exceptions/invalid-format-exception.js';
|
|
3
|
+
import { YamlParser } from '../../parser/yaml-parser.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Accessor for YAML-encoded strings.
|
|
7
|
+
*
|
|
8
|
+
* Uses the internal YamlParser for safe YAML parsing without
|
|
9
|
+
* depending on external YAML libraries. Tags, anchors, aliases, and
|
|
10
|
+
* merge keys are blocked as unsafe constructs.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* const accessor = new YamlAccessor(parser).from('key: value\nnested:\n a: 1');
|
|
14
|
+
* accessor.get('nested.a'); // 1
|
|
15
|
+
*/
|
|
16
|
+
export class YamlAccessor extends AbstractAccessor {
|
|
17
|
+
/**
|
|
18
|
+
* Hydrate from a YAML string.
|
|
19
|
+
*
|
|
20
|
+
* @param data - YAML string input.
|
|
21
|
+
* @returns Populated accessor instance.
|
|
22
|
+
* @throws {InvalidFormatException} When input is not a string or YAML is malformed.
|
|
23
|
+
* @throws {YamlParseException} When unsafe YAML constructs are present.
|
|
24
|
+
* @throws {SecurityException} When payload size exceeds limit.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* accessor.from('name: Alice\nage: 30');
|
|
28
|
+
*/
|
|
29
|
+
from(data: unknown): this {
|
|
30
|
+
if (typeof data !== 'string') {
|
|
31
|
+
/* Stryker disable StringLiteral -- error message content is cosmetic */
|
|
32
|
+
throw new InvalidFormatException(
|
|
33
|
+
`YamlAccessor expects a YAML string, got ${typeof data}`,
|
|
34
|
+
);
|
|
35
|
+
/* Stryker restore StringLiteral */
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return this.ingest(data);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** {@inheritDoc} */
|
|
42
|
+
protected parse(raw: unknown): Record<string, unknown> {
|
|
43
|
+
/* Stryker disable next-line ConditionalExpression,BlockStatement,StringLiteral -- unreachable: from() always validates string before ingest() */
|
|
44
|
+
/* c8 ignore start */
|
|
45
|
+
if (typeof raw !== 'string') {
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
/* c8 ignore stop */
|
|
49
|
+
|
|
50
|
+
return new YamlParser().parse(raw);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReadableAccessorsInterface } from './readable-accessors-interface.js';
|
|
2
|
+
import type { WritableAccessorsInterface } from './writable-accessors-interface.js';
|
|
3
|
+
import type { FactoryAccessorsInterface } from './factory-accessors-interface.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Unified contract combining read, write, and factory capabilities.
|
|
7
|
+
*
|
|
8
|
+
* Marker interface that aggregates all accessor responsibilities into
|
|
9
|
+
* a single type, used as the base contract for AbstractAccessor.
|
|
10
|
+
*/
|
|
11
|
+
export interface AccessorsInterface
|
|
12
|
+
extends ReadableAccessorsInterface, WritableAccessorsInterface, FactoryAccessorsInterface {}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract for creating accessor instances from raw input.
|
|
3
|
+
*
|
|
4
|
+
* Note: this `from(data)` is the per-accessor hydrator, not
|
|
5
|
+
* {@link Inline.from} which selects an accessor by TypeFormat.
|
|
6
|
+
*/
|
|
7
|
+
export interface FactoryAccessorsInterface {
|
|
8
|
+
/**
|
|
9
|
+
* Hydrate the accessor from raw input data.
|
|
10
|
+
*
|
|
11
|
+
* @param data - Raw input in the format expected by the accessor.
|
|
12
|
+
* @returns Populated accessor instance.
|
|
13
|
+
* @throws {InvalidFormatException} When the input cannot be parsed.
|
|
14
|
+
*/
|
|
15
|
+
from(data: unknown): this;
|
|
16
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract for custom format detection and parsing integration.
|
|
3
|
+
*
|
|
4
|
+
* Enables the {@link AnyAccessor} to accept arbitrary input by delegating
|
|
5
|
+
* format validation and parsing to a user-provided implementation.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* class CsvIntegration implements ParseIntegrationInterface {
|
|
9
|
+
* assertFormat(raw: unknown): boolean { return typeof raw === 'string' && raw.includes(','); }
|
|
10
|
+
* parse(raw: unknown): Record<string, unknown> { ... }
|
|
11
|
+
* }
|
|
12
|
+
* const accessor = Inline.withParserIntegration(new CsvIntegration()).fromAny(csvString);
|
|
13
|
+
*/
|
|
14
|
+
export interface ParseIntegrationInterface {
|
|
15
|
+
/**
|
|
16
|
+
* Determine whether the given raw input is in a supported format.
|
|
17
|
+
*
|
|
18
|
+
* @param raw - Raw input data to validate.
|
|
19
|
+
* @returns `true` if the input can be parsed by this integration.
|
|
20
|
+
*/
|
|
21
|
+
assertFormat(raw: unknown): boolean;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse raw input data into a normalized plain object.
|
|
25
|
+
*
|
|
26
|
+
* Called only after {@link assertFormat} returns `true`.
|
|
27
|
+
*
|
|
28
|
+
* @param raw - Raw input data previously validated by {@link assertFormat}.
|
|
29
|
+
* @returns Parsed data as a nested plain object.
|
|
30
|
+
*/
|
|
31
|
+
parse(raw: unknown): Record<string, unknown>;
|
|
32
|
+
}
|