ng-form-foundry-transformers 0.4.0 → 0.4.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/README.md
CHANGED
|
@@ -294,6 +294,15 @@ npm test # node:test on the compiled output (no Python needed)
|
|
|
294
294
|
|
|
295
295
|
## Status
|
|
296
296
|
|
|
297
|
+
`0.4.1` — libconfig hardening after a conformance battle test against the C
|
|
298
|
+
library's own scanner, test corpus, and real srsRAN/OAI configs: `0q`/`0Q`
|
|
299
|
+
octal, verbatim radix-prefix preservation on edits, sign accepted on decimal
|
|
300
|
+
literals only, negative edits into hex/bin/oct slots emit decimal, exact
|
|
301
|
+
round-trips for collections mixing safe and beyond-2^53 integers, `null`
|
|
302
|
+
rejected instead of written as `true`, a 256-level nesting cap, strict `\x`
|
|
303
|
+
escapes, `@include` legal in value positions, and modified read-only carries
|
|
304
|
+
throw instead of corrupting.
|
|
305
|
+
|
|
297
306
|
`0.4.0` — version-alignment release with the workspace (the paired
|
|
298
307
|
`ng-form-foundry` 0.4.0 adds appearance-driven field layout); no transformer
|
|
299
308
|
changes.
|
|
@@ -13,9 +13,13 @@
|
|
|
13
13
|
* (https://hyperrealm.github.io/libconfig/libconfig_manual.html); the grammar
|
|
14
14
|
* covers: groups `{}`, arrays `[]` (homogeneous scalars), lists `()`
|
|
15
15
|
* (heterogeneous), settings `name = value;` / `name : value` with an optional
|
|
16
|
-
* `;`/`,` terminator, int (decimal
|
|
16
|
+
* `;`/`,` terminator, int (decimal, `0x` hex, `0b` binary, `0o`/`0q` octal —
|
|
17
|
+
* a sign is legal on decimal only, as in the C scanner), int64 (`L`/`LL`
|
|
17
18
|
* suffix), float, bool (case-insensitive), strings with escapes and adjacent
|
|
18
|
-
* literal concatenation, `#`/`//`/`/* */` comments, and `@include` lines
|
|
19
|
+
* literal concatenation, `#`/`//`/`/* */` comments, and `@include` lines
|
|
20
|
+
* (handled as trivia, so they are legal anywhere the C scanner accepts them).
|
|
21
|
+
* Nesting is capped at {@link MAX_DEPTH} levels so hostile documents fail
|
|
22
|
+
* with a parse error instead of exhausting the JS stack.
|
|
19
23
|
*/
|
|
20
24
|
/** Half-open source range of a node, in code-unit offsets. */
|
|
21
25
|
export interface Span {
|
|
@@ -29,6 +33,8 @@ export interface IntMeta {
|
|
|
29
33
|
suffix: string;
|
|
30
34
|
/** Hex/binary/octal digit count of the literal, for width-preserving edits. */
|
|
31
35
|
digits: number;
|
|
36
|
+
/** Radix prefix verbatim (`0x`, `0B`, `0q`, …), empty for decimal. */
|
|
37
|
+
prefix: string;
|
|
32
38
|
}
|
|
33
39
|
export type CfgValue = CfgScalar | CfgGroup | CfgArray | CfgList;
|
|
34
40
|
export interface CfgScalar {
|
|
@@ -14,9 +14,13 @@
|
|
|
14
14
|
* (https://hyperrealm.github.io/libconfig/libconfig_manual.html); the grammar
|
|
15
15
|
* covers: groups `{}`, arrays `[]` (homogeneous scalars), lists `()`
|
|
16
16
|
* (heterogeneous), settings `name = value;` / `name : value` with an optional
|
|
17
|
-
* `;`/`,` terminator, int (decimal
|
|
17
|
+
* `;`/`,` terminator, int (decimal, `0x` hex, `0b` binary, `0o`/`0q` octal —
|
|
18
|
+
* a sign is legal on decimal only, as in the C scanner), int64 (`L`/`LL`
|
|
18
19
|
* suffix), float, bool (case-insensitive), strings with escapes and adjacent
|
|
19
|
-
* literal concatenation, `#`/`//`/`/* */` comments, and `@include` lines
|
|
20
|
+
* literal concatenation, `#`/`//`/`/* */` comments, and `@include` lines
|
|
21
|
+
* (handled as trivia, so they are legal anywhere the C scanner accepts them).
|
|
22
|
+
* Nesting is capped at {@link MAX_DEPTH} levels so hostile documents fail
|
|
23
|
+
* with a parse error instead of exhausting the JS stack.
|
|
20
24
|
*/
|
|
21
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
26
|
exports.LibconfigParseError = void 0;
|
|
@@ -33,18 +37,22 @@ class LibconfigParseError extends Error {
|
|
|
33
37
|
}
|
|
34
38
|
exports.LibconfigParseError = LibconfigParseError;
|
|
35
39
|
const NAME_RE = /^[A-Za-z*][-A-Za-z0-9_*.]*/;
|
|
36
|
-
|
|
40
|
+
// Like the C scanner: octal is `0o` or `0q`, and only decimal takes a sign.
|
|
41
|
+
const INT_RE = /^(0[xX][0-9A-Fa-f]+|0[bB][01]+|0[oOqQ][0-7]+|[-+]?[0-9]+)(LL?)?/;
|
|
37
42
|
const FLOAT_RE = /^[-+]?(([0-9]*\.[0-9]+|[0-9]+\.[0-9]*)([eE][-+]?[0-9]+)?|[0-9]+[eE][-+]?[0-9]+)/;
|
|
38
43
|
const BOOL_RE = /^(true|false)(?![-A-Za-z0-9_*.])/i;
|
|
39
44
|
/** Parse a libconfig document into its root group (spans index into `source`). */
|
|
40
45
|
function parseLibconfig(source, options) {
|
|
41
46
|
return new Parser(source, options?.includes ?? 'reject').parseRoot();
|
|
42
47
|
}
|
|
48
|
+
/** Deepest legal value nesting — far past real configs, well inside JS stack. */
|
|
49
|
+
const MAX_DEPTH = 256;
|
|
43
50
|
class Parser {
|
|
44
51
|
constructor(src, includes) {
|
|
45
52
|
this.src = src;
|
|
46
53
|
this.includes = includes;
|
|
47
54
|
this.pos = 0;
|
|
55
|
+
this.depth = 0;
|
|
48
56
|
}
|
|
49
57
|
parseRoot() {
|
|
50
58
|
const settings = this.parseSettings(undefined);
|
|
@@ -67,10 +75,6 @@ class Parser {
|
|
|
67
75
|
}
|
|
68
76
|
if (closer && this.src[this.pos] === closer)
|
|
69
77
|
return settings;
|
|
70
|
-
if (this.src[this.pos] === '@') {
|
|
71
|
-
this.handleInclude();
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
78
|
settings.push(this.parseSetting(settings));
|
|
75
79
|
}
|
|
76
80
|
}
|
|
@@ -94,14 +98,21 @@ class Parser {
|
|
|
94
98
|
return { name, span: { start, end: this.pos }, value };
|
|
95
99
|
}
|
|
96
100
|
parseValue() {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
if (++this.depth > MAX_DEPTH)
|
|
102
|
+
this.fail(`nesting deeper than ${MAX_DEPTH} levels`);
|
|
103
|
+
try {
|
|
104
|
+
const c = this.src[this.pos];
|
|
105
|
+
if (c === '{')
|
|
106
|
+
return this.parseGroup();
|
|
107
|
+
if (c === '[')
|
|
108
|
+
return this.parseCollection('[', ']', 'array');
|
|
109
|
+
if (c === '(')
|
|
110
|
+
return this.parseCollection('(', ')', 'list');
|
|
111
|
+
return this.parseScalar();
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
this.depth--;
|
|
115
|
+
}
|
|
105
116
|
}
|
|
106
117
|
parseGroup() {
|
|
107
118
|
const start = this.pos++;
|
|
@@ -181,15 +192,16 @@ class Parser {
|
|
|
181
192
|
return this.fail('expected a value');
|
|
182
193
|
}
|
|
183
194
|
intScalar(lexeme, span) {
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
const
|
|
195
|
+
const suffix = /LL?$/i.exec(lexeme)?.[0] ?? '';
|
|
196
|
+
const body = suffix ? lexeme.slice(0, -suffix.length) : lexeme;
|
|
197
|
+
const prefix = /^0[xXbBoOqQ]/.test(body) ? body.slice(0, 2) : '';
|
|
198
|
+
const p = prefix.toLowerCase();
|
|
199
|
+
const radix = p === '0x' ? 16 : p === '0b' ? 2 : p === '' ? 10 : 8;
|
|
200
|
+
const digits = prefix ? body.slice(2) : body.replace(/^[-+]/, '');
|
|
201
|
+
const sign = body[0] === '-' ? '-' : '';
|
|
202
|
+
// `0q`/`0O` normalize to a prefix BigInt understands; decimal keeps its sign.
|
|
203
|
+
const big = prefix ? BigInt((radix === 8 ? '0o' : p) + digits) : BigInt(sign + digits);
|
|
204
|
+
const meta = { radix, suffix, digits: digits.length, prefix };
|
|
193
205
|
// Values past 2^53 ride as exact decimal strings (the core bigint strategy);
|
|
194
206
|
// the suffix and radix stay in the metadata for the write-back.
|
|
195
207
|
const safe = big >= BigInt(Number.MIN_SAFE_INTEGER) && big <= BigInt(Number.MAX_SAFE_INTEGER);
|
|
@@ -228,8 +240,11 @@ class Parser {
|
|
|
228
240
|
}
|
|
229
241
|
if (c === '\\') {
|
|
230
242
|
const esc = this.src[this.pos + 1];
|
|
231
|
-
if (esc === 'x') {
|
|
232
|
-
|
|
243
|
+
if (esc === 'x' || esc === 'X') {
|
|
244
|
+
const hex = this.src.slice(this.pos + 2, this.pos + 4);
|
|
245
|
+
if (!/^[0-9A-Fa-f]{2}$/.test(hex))
|
|
246
|
+
this.fail('\\x escape needs two hex digits');
|
|
247
|
+
out += String.fromCharCode(parseInt(hex, 16));
|
|
233
248
|
this.pos += 4;
|
|
234
249
|
continue;
|
|
235
250
|
}
|
|
@@ -253,7 +268,7 @@ class Parser {
|
|
|
253
268
|
}
|
|
254
269
|
this.pos = lineEnd === -1 ? this.src.length : lineEnd + 1;
|
|
255
270
|
}
|
|
256
|
-
/** Skip whitespace
|
|
271
|
+
/** Skip whitespace, all three comment forms, and `@include` lines. */
|
|
257
272
|
skipTrivia() {
|
|
258
273
|
for (;;) {
|
|
259
274
|
const c = this.src[this.pos];
|
|
@@ -261,6 +276,12 @@ class Parser {
|
|
|
261
276
|
this.pos++;
|
|
262
277
|
continue;
|
|
263
278
|
}
|
|
279
|
+
// The C scanner handles `@include` at token level, so it is legal in any
|
|
280
|
+
// trivia position (even between list elements), not just between settings.
|
|
281
|
+
if (c === '@' && this.src.startsWith('@include', this.pos)) {
|
|
282
|
+
this.handleInclude();
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
264
285
|
if (c === '#' || (c === '/' && this.src[this.pos + 1] === '/')) {
|
|
265
286
|
const nl = this.src.indexOf('\n', this.pos);
|
|
266
287
|
this.pos = nl === -1 ? this.src.length : nl;
|
|
@@ -21,7 +21,8 @@ function patchGroup(src, group, value, edits) {
|
|
|
21
21
|
patchValue(src, setting.value, value[setting.name], edits);
|
|
22
22
|
}
|
|
23
23
|
const known = new Set(group.settings.map((s) => s.name));
|
|
24
|
-
|
|
24
|
+
// null/undefined means "absent", never a value: such keys are not added.
|
|
25
|
+
const added = Object.keys(value ?? {}).filter((k) => !known.has(k) && value[k] != null);
|
|
25
26
|
if (added.length) {
|
|
26
27
|
edits.push(insertionEdit(src, group, added.map((k) => `${k} = ${serialize(value[k], '')};`)));
|
|
27
28
|
}
|
|
@@ -31,6 +32,10 @@ function patchValue(src, node, value, edits) {
|
|
|
31
32
|
case 'scalar': {
|
|
32
33
|
if (node.value === value)
|
|
33
34
|
return;
|
|
35
|
+
// The collection-wide bigint carry hands safe elements back as digit
|
|
36
|
+
// strings; a string spelling the same value is not an edit.
|
|
37
|
+
if (typeof value === 'string' && typeof node.value === 'number' && value === String(node.value))
|
|
38
|
+
return;
|
|
34
39
|
edits.push({ ...node.span, text: emitScalar(value, node) });
|
|
35
40
|
return;
|
|
36
41
|
}
|
|
@@ -43,9 +48,13 @@ function patchValue(src, node, value, edits) {
|
|
|
43
48
|
case 'array': {
|
|
44
49
|
if (Array.isArray(value))
|
|
45
50
|
return patchElements(src, node.elements, node, value, edits);
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
// A string here is the read-only raw carry of an untyped empty array:
|
|
52
|
+
// unchanged it stays verbatim, edited it is an error, never a splice.
|
|
53
|
+
if (typeof value === 'string') {
|
|
54
|
+
if (value === (0, schema_1.raw)(node, src))
|
|
55
|
+
return;
|
|
56
|
+
throw new Error('this collection is read-only (no element type to edit by): the edited text was not applied');
|
|
57
|
+
}
|
|
49
58
|
edits.push({ ...node.span, text: serialize(value, '') });
|
|
50
59
|
return;
|
|
51
60
|
}
|
|
@@ -64,11 +73,15 @@ function patchValue(src, node, value, edits) {
|
|
|
64
73
|
}
|
|
65
74
|
return;
|
|
66
75
|
}
|
|
67
|
-
// Heterogeneous (read-only raw carry) or unchanged empty:
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
76
|
+
// Heterogeneous (read-only raw carry) or unchanged empty: the carry
|
|
77
|
+
// stays verbatim, an edited carry is an error, a non-string value is a
|
|
78
|
+
// genuine shape change and regenerates.
|
|
79
|
+
if (typeof value === 'string') {
|
|
80
|
+
if (value === (0, schema_1.raw)(node, src))
|
|
81
|
+
return;
|
|
82
|
+
throw new Error('this list is read-only (heterogeneous): the edited text was not applied');
|
|
83
|
+
}
|
|
84
|
+
if (value !== undefined) {
|
|
72
85
|
edits.push({ ...node.span, text: serialize(value, '') });
|
|
73
86
|
}
|
|
74
87
|
return;
|
|
@@ -146,11 +159,17 @@ function emitScalar(value, node) {
|
|
|
146
159
|
if (typeof value === 'boolean')
|
|
147
160
|
return value ? 'true' : 'false';
|
|
148
161
|
if (typeof value === 'string') {
|
|
149
|
-
|
|
150
|
-
if (node.type === 'int64' && typeof node.value === 'string') {
|
|
162
|
+
if (node.type === 'int' || node.type === 'int64') {
|
|
151
163
|
if (!/^[-+]?[0-9]+$/.test(value)) {
|
|
152
|
-
throw new Error(`'${value}' is not an integer: this setting
|
|
164
|
+
throw new Error(`'${value}' is not an integer: this setting is integer-typed`);
|
|
153
165
|
}
|
|
166
|
+
// The int64 string carry: exact digits back, with the original suffix.
|
|
167
|
+
if (typeof node.value === 'string')
|
|
168
|
+
return value + (node.int?.suffix ?? '');
|
|
169
|
+
// A safe element of a carried collection: back to its own literal style.
|
|
170
|
+
const n = Number(value);
|
|
171
|
+
if (Number.isSafeInteger(n))
|
|
172
|
+
return intLiteral(n, node.int);
|
|
154
173
|
return value + (node.int?.suffix ?? '');
|
|
155
174
|
}
|
|
156
175
|
return quote(value);
|
|
@@ -165,13 +184,17 @@ function emitScalar(value, node) {
|
|
|
165
184
|
}
|
|
166
185
|
return serialize(value, '');
|
|
167
186
|
}
|
|
168
|
-
/**
|
|
187
|
+
/**
|
|
188
|
+
* Integer in the source's radix, width, and prefix spelling, suffix preserved.
|
|
189
|
+
* Negatives always emit decimal: the C scanner accepts no sign on a
|
|
190
|
+
* hex/binary/octal literal, so `-0x1A` would not load.
|
|
191
|
+
*/
|
|
169
192
|
function intLiteral(value, meta) {
|
|
170
|
-
if (!meta || meta.radix === 10)
|
|
193
|
+
if (!meta || meta.radix === 10 || value < 0)
|
|
171
194
|
return String(value) + (meta?.suffix ?? '');
|
|
172
|
-
const prefix = meta.radix === 16 ? '0x' : meta.radix === 2 ? '0b' : '0o';
|
|
173
|
-
const digits =
|
|
174
|
-
return
|
|
195
|
+
const prefix = meta.prefix || (meta.radix === 16 ? '0x' : meta.radix === 2 ? '0b' : '0o');
|
|
196
|
+
const digits = value.toString(meta.radix).toUpperCase().padStart(meta.digits, '0');
|
|
197
|
+
return prefix + digits + meta.suffix;
|
|
175
198
|
}
|
|
176
199
|
/** A float literal that stays a float: integral values gain `.0`. */
|
|
177
200
|
function floatLiteral(value) {
|
|
@@ -206,9 +229,13 @@ function quote(value) {
|
|
|
206
229
|
* grown collection). Typing is value-driven here: integral numbers emit as
|
|
207
230
|
* ints, fractional as floats — a host that needs a float-typed `21.0` for a
|
|
208
231
|
* fresh setting should send `21.0`-producing edits through an existing float
|
|
209
|
-
* slot or accept the int typing
|
|
232
|
+
* slot or accept the int typing. null/undefined have no libconfig literal
|
|
233
|
+
* and throw.
|
|
210
234
|
*/
|
|
211
235
|
function serialize(value, indent) {
|
|
236
|
+
if (value == null) {
|
|
237
|
+
throw new Error('null/undefined has no libconfig representation: omit the setting instead');
|
|
238
|
+
}
|
|
212
239
|
if (typeof value === 'boolean')
|
|
213
240
|
return value ? 'true' : 'false';
|
|
214
241
|
if (typeof value === 'number')
|
|
@@ -226,7 +253,7 @@ function serialize(value, indent) {
|
|
|
226
253
|
.join(' ');
|
|
227
254
|
return `{ ${body} }`;
|
|
228
255
|
}
|
|
229
|
-
|
|
256
|
+
throw new Error(`a ${typeof value} value has no libconfig representation`);
|
|
230
257
|
}
|
|
231
258
|
/** An integer string that exceeds the double-safe range (the bigint carry). */
|
|
232
259
|
function isBigIntString(value) {
|
|
@@ -149,12 +149,15 @@ function listShape(list) {
|
|
|
149
149
|
}
|
|
150
150
|
/** Read-only leaf carrying the collection's verbatim source text. */
|
|
151
151
|
function rawLeaf(name, why) {
|
|
152
|
+
const hint = why === 'empty collection'
|
|
153
|
+
? 'Provide a JSON Schema to type its elements and make it editable.'
|
|
154
|
+
: 'It stays read-only: no single element type exists to edit by.';
|
|
152
155
|
return {
|
|
153
156
|
kind: 'leaf',
|
|
154
157
|
name,
|
|
155
158
|
type: 'string',
|
|
156
159
|
readOnly: true,
|
|
157
|
-
description: `Shown verbatim (${why})
|
|
160
|
+
description: `Shown verbatim (${why}). ${hint}`,
|
|
158
161
|
};
|
|
159
162
|
}
|
|
160
163
|
/** The verbatim source slice of a node. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ng-form-foundry-transformers",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Framework-agnostic Node + TypeScript catalog of source-format transformers: turn a YANG model (and more) into an ng-form-foundry schema and revert the edited form value back to the source format.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ng-form-foundry",
|