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/hex/binary/octal), int64 (`L`/`LL`
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/hex/binary/octal), int64 (`L`/`LL`
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
- const INT_RE = /^[-+]?(0[xX][0-9A-Fa-f]+|0[bB][01]+|0[oO][0-7]+|[0-9]+)(LL?)?/;
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
- const c = this.src[this.pos];
98
- if (c === '{')
99
- return this.parseGroup();
100
- if (c === '[')
101
- return this.parseCollection('[', ']', 'array');
102
- if (c === '(')
103
- return this.parseCollection('(', ')', 'list');
104
- return this.parseScalar();
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 sign = lexeme[0] === '-' ? '-' : '';
185
- const unsigned = lexeme.replace(/^[-+]/, '');
186
- const suffix = /LL?$/i.exec(unsigned)?.[0] ?? '';
187
- const body = suffix ? unsigned.slice(0, -suffix.length) : unsigned;
188
- const prefix = body.slice(0, 2).toLowerCase();
189
- const radix = prefix === '0x' ? 16 : prefix === '0b' ? 2 : prefix === '0o' ? 8 : 10;
190
- const digits = radix === 10 ? body : body.slice(2);
191
- const big = BigInt(sign + (radix === 10 ? digits : body.slice(0, 2) + digits));
192
- const meta = { radix, suffix, digits: digits.length };
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
- out += String.fromCharCode(parseInt(this.src.slice(this.pos + 2, this.pos + 4), 16));
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 and all three comment forms. */
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
- const added = Object.keys(value ?? {}).filter((k) => !known.has(k));
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
- // The read-only raw carry of an untyped empty array: leave verbatim.
47
- if (typeof value === 'string' && value === (0, schema_1.raw)(node, src))
48
- return;
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: only replace on
68
- // a genuine mismatch with a non-raw value shape — otherwise leave verbatim.
69
- if (typeof value === 'string' && value === (0, schema_1.raw)(node, src))
70
- return;
71
- if (value !== undefined && typeof value !== 'string') {
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
- // The int64 string carry: exact digits back, with the original suffix.
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 carries a 64-bit integer as a string`);
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
- /** Integer in the source's radix and width, suffix preserved. */
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 = Math.abs(value).toString(meta.radix).toUpperCase().padStart(meta.digits, '0');
174
- return (value < 0 ? '-' : '') + prefix + digits + meta.suffix;
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 (documented beta limitation).
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
- return 'true'; // null/undefined have no libconfig literal; unreachable via typed forms
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}): libconfig gives no element type to edit by. Provide a JSON Schema to make it editable.`,
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.0",
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",