@o3co/ts.hocon 1.11.0 → 1.12.0

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
@@ -265,12 +265,12 @@ Supported byte units: `B`, `KB`/`KiB`, `MB`/`MiB`, `GB`/`GiB`, `TB`/`TiB` (and l
265
265
 
266
266
  ## Spec Compliance
267
267
 
268
- Conformance against the [Lightbend HOCON specification](https://github.com/lightbend/config/blob/main/HOCON.md) is tracked at item granularity in [`docs/spec-compliance.md`](docs/spec-compliance.md). The table below is a snapshot as of 2026-05-13; see [`xx.hocon/docs/compliance-matrix.md`](https://github.com/o3co/xx.hocon/blob/main/docs/compliance-matrix.md) for live cross-impl values.
268
+ Conformance against the [Lightbend HOCON specification](https://github.com/lightbend/config/blob/main/HOCON.md) is tracked at item granularity in [`docs/spec-compliance.md`](docs/spec-compliance.md), which is the source these rates are computed from — `tests/docs.test.ts` recomputes them and fails the build if this table drifts. See [`xx.hocon/docs/compliance-matrix.md`](https://github.com/o3co/xx.hocon/blob/main/docs/compliance-matrix.md) for the cross-implementation roll-up.
269
269
 
270
270
  | Metric | Status |
271
271
  | ------------------------------------- | ------------- |
272
- | Spec total (incl. out-of-scope) | **74.2%** |
273
- | In-scope only | **83.3%** |
272
+ | Spec total (incl. out-of-scope) | **89.3%** |
273
+ | In-scope only | **99.2%** |
274
274
  | Lightbend `test01`–`test13` suite | 13/13 passing |
275
275
 
276
276
  **Extra-spec conventions (E-series) — implementation status:**
@@ -289,10 +289,6 @@ Supported since v0.2.0 (P1):
289
289
 
290
290
  - `.properties` file parsing
291
291
 
292
- ### Stricter than Lightbend
293
-
294
- - **S8.6 leading-hyphen rejection** (Unreleased): `a = -foo`, `a = -bar`, `a = -` etc. now raise a lex error per HOCON.md L270–276, where Lightbend silently falls back to unquoted strings. Mitigation: quote the value (`a = "-foo"`). See [CHANGELOG](CHANGELOG.md#unreleased) and [`docs/spec-compliance.md`](docs/spec-compliance.md) §S8.6.
295
-
296
292
  ## Performance
297
293
 
298
294
  ### ts.hocon Parsing Cost
@@ -325,7 +321,7 @@ For typical application configs (loaded once at startup), the parsing cost is ne
325
321
 
326
322
  ### Feature Comparison with node-config
327
323
 
328
- ts.hocon provides significantly richer configuration capabilities compared to [node-config](https://github.com/node-config/node-config) (JSON):
324
+ Feature-level comparison with [node-config](https://github.com/node-config/node-config) **v4.4.2** reading JSON config files, as of 2026-07-26:
329
325
 
330
326
  | Feature | ts.hocon | node-config (JSON) |
331
327
  |---|---|---|
@@ -498,6 +494,15 @@ When parsing untrusted HOCON input, be aware of:
498
494
  - **Path traversal in includes:** a relative `include` path resolves against `baseDir` and can climb out of it with `..` segments to reach sensitive files such as `/etc/passwd`. Use a custom `readFileSync`/`readFile` that validates paths if parsing untrusted input.
499
495
  - **Input size:** The parser has no built-in input size limit. For untrusted input, validate size before calling `parse()`.
500
496
  - **Include depth:** Limited to 50 levels to prevent stack overflow from deep include chains.
497
+ - **Mapped path depth:** an environment variable's `__` segments and a
498
+ `.properties` dotted key are limited to 64 segments. One name produces one
499
+ arbitrarily deep chain, so without the cap a single long variable name was
500
+ enough to exhaust the stack. rs.hocon and py.hocon cap the same mapping at 64.
501
+ - **Document nesting depth:** not capped. A document nested past what the
502
+ engine's stack holds fails as `ParseError` or `ConfigError` — never as a bare
503
+ `RangeError` — but the depth at which that happens depends on how deep the
504
+ calling code already is, so validate size before parsing untrusted input (see
505
+ above) rather than relying on a fixed level.
501
506
  - **Prototype pollution:** keys named `__proto__`, `constructor` or `prototype`
502
507
  are **kept as ordinary keys** — a `.properties` file or environment variable
503
508
  may legitimately use them, and dropping them would be silent data loss.
@@ -557,6 +557,31 @@ function stripBom(input) {
557
557
  return input.charCodeAt(0) === 65279 ? input.slice(1) : input;
558
558
  }
559
559
 
560
+ // src/internal/depth.ts
561
+ var MAX_PATH_SEGMENTS = 64;
562
+ function tooDeep(segments) {
563
+ return segments > MAX_PATH_SEGMENTS;
564
+ }
565
+ function guardStackDepth(fn, wrap) {
566
+ try {
567
+ return fn();
568
+ } catch (e) {
569
+ if (!isStackOverflow(e)) throw e;
570
+ throw wrap(STACK_MESSAGE);
571
+ }
572
+ }
573
+ var STACK_MESSAGE = "input is nested too deeply for this engine's stack, or contains a cycle (flatten the document, or break the cycle)";
574
+ function isStackOverflow(e) {
575
+ if (!(e instanceof RangeError)) return false;
576
+ const m = e.message.toLowerCase();
577
+ return m.includes("call stack") || // V8: "Maximum call stack size exceeded"
578
+ m.includes("recursion") || // SpiderMonkey: "too much recursion"
579
+ m.includes("stack overflow");
580
+ }
581
+ function depthError(message) {
582
+ return new ConfigError(message, "");
583
+ }
584
+
560
585
  // src/internal/properties/properties.ts
561
586
  function nestPairs(pairs) {
562
587
  const root = /* @__PURE__ */ Object.create(null);
@@ -1570,7 +1595,10 @@ function normalizeLeadingZeroNumber(raw) {
1570
1595
 
1571
1596
  // src/value-factory.ts
1572
1597
  function fromMap(values, originDescription) {
1573
- const fields = coerceObject(values, "");
1598
+ const fields = guardStackDepth(
1599
+ () => coerceObject(values, ""),
1600
+ (msg) => depthError(`fromMap: ${msg}`)
1601
+ );
1574
1602
  const root = { kind: "object", fields };
1575
1603
  return Config._fromResolvedValue(root, { originDescription });
1576
1604
  }
@@ -1670,13 +1698,14 @@ function parseDotEnv(input, opts = {}) {
1670
1698
  for (let i = 0; i < lines.length; i++) {
1671
1699
  const raw = (lines[i] ?? "").trim();
1672
1700
  if (raw === "" || raw.startsWith("#")) continue;
1673
- const line = raw.startsWith("export ") ? raw.slice("export ".length) : raw;
1701
+ const line = stripExport(raw);
1674
1702
  const eq = line.indexOf("=");
1675
1703
  if (eq === -1) throw new ConfigError(`${origin}:${i + 1}: expected NAME=value`, "");
1676
1704
  const name = line.slice(0, eq).trim();
1677
1705
  if (name === "") throw new ConfigError(`${origin}:${i + 1}: empty variable name`, "");
1678
- const value = dotEnvValue(line.slice(eq + 1).replace(/^[ \t]+/, ""), origin, i + 1, name);
1679
1706
  if (!name.startsWith(prefix)) continue;
1707
+ checkName(name, origin, i + 1);
1708
+ const value = dotEnvValue(line.slice(eq + 1).replace(/^[ \t]+/, ""), origin, i + 1, name);
1680
1709
  pairs.push([toPath(name.slice(prefix.length)), value]);
1681
1710
  }
1682
1711
  return fromMap(nestPairs(pairs), origin);
@@ -1686,6 +1715,12 @@ function toPath(name) {
1686
1715
  if (segs.some((s) => s === "")) {
1687
1716
  throw new ConfigError(`env: "${name}" produces an empty path segment`, name);
1688
1717
  }
1718
+ if (tooDeep(segs.length)) {
1719
+ throw new ConfigError(
1720
+ `env: "${name}" maps to a path ${segs.length} segments deep, over the limit of ${MAX_PATH_SEGMENTS}`,
1721
+ name
1722
+ );
1723
+ }
1689
1724
  return segs;
1690
1725
  }
1691
1726
  function asciiLower(s) {
@@ -1731,6 +1766,20 @@ function dotEnvValue(v, origin, line, name) {
1731
1766
  }
1732
1767
  return trimmed;
1733
1768
  }
1769
+ function stripExport(line) {
1770
+ const m = /^export[ \t]+/.exec(line);
1771
+ return m ? line.slice(m[0].length) : line;
1772
+ }
1773
+ function checkName(name, origin, line) {
1774
+ const bad = /\p{White_Space}|#/u.exec(name);
1775
+ if (bad) {
1776
+ const what = bad[0] === "#" ? "'#'" : "whitespace";
1777
+ throw new ConfigError(
1778
+ `${origin}:${line}: variable name "${name}" contains ${what}; the line is not NAME=value (spec F1.7)`,
1779
+ name
1780
+ );
1781
+ }
1782
+ }
1734
1783
  // Annotate the CommonJS export names for ESM import in node:
1735
1784
  0 && (module.exports = {
1736
1785
  loadEnv,
@@ -520,6 +520,31 @@ function stripBom(input) {
520
520
  return input.charCodeAt(0) === 65279 ? input.slice(1) : input;
521
521
  }
522
522
 
523
+ // src/internal/depth.ts
524
+ var MAX_PATH_SEGMENTS = 64;
525
+ function tooDeep(segments) {
526
+ return segments > MAX_PATH_SEGMENTS;
527
+ }
528
+ function guardStackDepth(fn, wrap) {
529
+ try {
530
+ return fn();
531
+ } catch (e) {
532
+ if (!isStackOverflow(e)) throw e;
533
+ throw wrap(STACK_MESSAGE);
534
+ }
535
+ }
536
+ var STACK_MESSAGE = "input is nested too deeply for this engine's stack, or contains a cycle (flatten the document, or break the cycle)";
537
+ function isStackOverflow(e) {
538
+ if (!(e instanceof RangeError)) return false;
539
+ const m = e.message.toLowerCase();
540
+ return m.includes("call stack") || // V8: "Maximum call stack size exceeded"
541
+ m.includes("recursion") || // SpiderMonkey: "too much recursion"
542
+ m.includes("stack overflow");
543
+ }
544
+ function depthError(message) {
545
+ return new ConfigError(message, "");
546
+ }
547
+
523
548
  // src/internal/properties/properties.ts
524
549
  function nestPairs(pairs) {
525
550
  const root = /* @__PURE__ */ Object.create(null);
@@ -1533,7 +1558,10 @@ function normalizeLeadingZeroNumber(raw) {
1533
1558
 
1534
1559
  // src/value-factory.ts
1535
1560
  function fromMap(values, originDescription) {
1536
- const fields = coerceObject(values, "");
1561
+ const fields = guardStackDepth(
1562
+ () => coerceObject(values, ""),
1563
+ (msg) => depthError(`fromMap: ${msg}`)
1564
+ );
1537
1565
  const root = { kind: "object", fields };
1538
1566
  return Config._fromResolvedValue(root, { originDescription });
1539
1567
  }
@@ -1633,13 +1661,14 @@ function parseDotEnv(input, opts = {}) {
1633
1661
  for (let i = 0; i < lines.length; i++) {
1634
1662
  const raw = (lines[i] ?? "").trim();
1635
1663
  if (raw === "" || raw.startsWith("#")) continue;
1636
- const line = raw.startsWith("export ") ? raw.slice("export ".length) : raw;
1664
+ const line = stripExport(raw);
1637
1665
  const eq = line.indexOf("=");
1638
1666
  if (eq === -1) throw new ConfigError(`${origin}:${i + 1}: expected NAME=value`, "");
1639
1667
  const name = line.slice(0, eq).trim();
1640
1668
  if (name === "") throw new ConfigError(`${origin}:${i + 1}: empty variable name`, "");
1641
- const value = dotEnvValue(line.slice(eq + 1).replace(/^[ \t]+/, ""), origin, i + 1, name);
1642
1669
  if (!name.startsWith(prefix)) continue;
1670
+ checkName(name, origin, i + 1);
1671
+ const value = dotEnvValue(line.slice(eq + 1).replace(/^[ \t]+/, ""), origin, i + 1, name);
1643
1672
  pairs.push([toPath(name.slice(prefix.length)), value]);
1644
1673
  }
1645
1674
  return fromMap(nestPairs(pairs), origin);
@@ -1649,6 +1678,12 @@ function toPath(name) {
1649
1678
  if (segs.some((s) => s === "")) {
1650
1679
  throw new ConfigError(`env: "${name}" produces an empty path segment`, name);
1651
1680
  }
1681
+ if (tooDeep(segs.length)) {
1682
+ throw new ConfigError(
1683
+ `env: "${name}" maps to a path ${segs.length} segments deep, over the limit of ${MAX_PATH_SEGMENTS}`,
1684
+ name
1685
+ );
1686
+ }
1652
1687
  return segs;
1653
1688
  }
1654
1689
  function asciiLower(s) {
@@ -1694,6 +1729,20 @@ function dotEnvValue(v, origin, line, name) {
1694
1729
  }
1695
1730
  return trimmed;
1696
1731
  }
1732
+ function stripExport(line) {
1733
+ const m = /^export[ \t]+/.exec(line);
1734
+ return m ? line.slice(m[0].length) : line;
1735
+ }
1736
+ function checkName(name, origin, line) {
1737
+ const bad = /\p{White_Space}|#/u.exec(name);
1738
+ if (bad) {
1739
+ const what = bad[0] === "#" ? "'#'" : "whitespace";
1740
+ throw new ConfigError(
1741
+ `${origin}:${line}: variable name "${name}" contains ${what}; the line is not NAME=value (spec F1.7)`,
1742
+ name
1743
+ );
1744
+ }
1745
+ }
1697
1746
  export {
1698
1747
  loadEnv,
1699
1748
  parseDotEnv
@@ -557,6 +557,27 @@ function stripBom(input) {
557
557
  return input.charCodeAt(0) === 65279 ? input.slice(1) : input;
558
558
  }
559
559
 
560
+ // src/internal/depth.ts
561
+ function guardStackDepth(fn, wrap) {
562
+ try {
563
+ return fn();
564
+ } catch (e) {
565
+ if (!isStackOverflow(e)) throw e;
566
+ throw wrap(STACK_MESSAGE);
567
+ }
568
+ }
569
+ var STACK_MESSAGE = "input is nested too deeply for this engine's stack, or contains a cycle (flatten the document, or break the cycle)";
570
+ function isStackOverflow(e) {
571
+ if (!(e instanceof RangeError)) return false;
572
+ const m = e.message.toLowerCase();
573
+ return m.includes("call stack") || // V8: "Maximum call stack size exceeded"
574
+ m.includes("recursion") || // SpiderMonkey: "too much recursion"
575
+ m.includes("stack overflow");
576
+ }
577
+ function depthError(message) {
578
+ return new ConfigError(message, "");
579
+ }
580
+
560
581
  // src/value/numeric-array.ts
561
582
  function numericObjectToArray(value) {
562
583
  if (value.kind !== "object") return null;
@@ -1535,7 +1556,10 @@ function normalizeLeadingZeroNumber(raw) {
1535
1556
 
1536
1557
  // src/value-factory.ts
1537
1558
  function fromMap(values, originDescription) {
1538
- const fields = coerceObject(values, "");
1559
+ const fields = guardStackDepth(
1560
+ () => coerceObject(values, ""),
1561
+ (msg) => depthError(`fromMap: ${msg}`)
1562
+ );
1539
1563
  const root = { kind: "object", fields };
1540
1564
  return Config._fromResolvedValue(root, { originDescription });
1541
1565
  }
@@ -1600,7 +1624,10 @@ function coerceValue(v, atPath) {
1600
1624
 
1601
1625
  // src/adapters/jsonc.ts
1602
1626
  function parseJsonc(input, originDescription) {
1603
- const doc = JSON.parse(stripTrailingCommas(stripComments(stripBom(input))), bigIntReviver);
1627
+ const doc = guardStackDepth(
1628
+ () => JSON.parse(stripTrailingCommas(stripComments(stripBom(input))), bigIntReviver),
1629
+ (msg) => depthError(`jsonc: ${msg}`)
1630
+ );
1604
1631
  if (doc === null || typeof doc !== "object" || Array.isArray(doc)) {
1605
1632
  throw new ConfigError(
1606
1633
  `jsonc: document root is ${Array.isArray(doc) ? "an array" : typeof doc}, but a config root must be an object (spec F0.3)`,
@@ -20,7 +20,18 @@ import { C as Config } from '../config-Dlq6rF3w.cjs';
20
20
  * the JS number model, as they do for HOCON's own literals, so read large
21
21
  * identifiers with `getString`.
22
22
  *
23
- * See docs/specs/format-ingestion-mapping.md items F3.x in the hocon scope.
23
+ * **An unpaired `\uXXXX` surrogate is accepted here and refused by go.hocon,
24
+ * py.hocon and rs.hocon** (F3.5). That is deliberate, and the same divergence
25
+ * the properties adapter already carries under F2.8: a JavaScript string is
26
+ * UTF-16, like Java's, so it holds a lone surrogate natively and round-trips it.
27
+ * A Go or Rust string cannot hold one at all — Go silently substituted U+FFFD
28
+ * until F3.5 — and a Python `str` can hold one but cannot encode it as UTF-8,
29
+ * so all three refuse rather than defer the failure. Refusing here would mean
30
+ * this spec overriding the host language rather than protecting the user; see
31
+ * S1.2.6 for the class.
32
+ *
33
+ * See the F3.x items in the format-ingestion mapping spec:
34
+ * https://github.com/o3co/xx.hocon/blob/main/docs/format-ingestion-mapping.md
24
35
  */
25
36
  declare function parseJsonc(input: string, originDescription?: string): Config;
26
37
  /**
@@ -20,7 +20,18 @@ import { C as Config } from '../config-Dlq6rF3w.js';
20
20
  * the JS number model, as they do for HOCON's own literals, so read large
21
21
  * identifiers with `getString`.
22
22
  *
23
- * See docs/specs/format-ingestion-mapping.md items F3.x in the hocon scope.
23
+ * **An unpaired `\uXXXX` surrogate is accepted here and refused by go.hocon,
24
+ * py.hocon and rs.hocon** (F3.5). That is deliberate, and the same divergence
25
+ * the properties adapter already carries under F2.8: a JavaScript string is
26
+ * UTF-16, like Java's, so it holds a lone surrogate natively and round-trips it.
27
+ * A Go or Rust string cannot hold one at all — Go silently substituted U+FFFD
28
+ * until F3.5 — and a Python `str` can hold one but cannot encode it as UTF-8,
29
+ * so all three refuse rather than defer the failure. Refusing here would mean
30
+ * this spec overriding the host language rather than protecting the user; see
31
+ * S1.2.6 for the class.
32
+ *
33
+ * See the F3.x items in the format-ingestion mapping spec:
34
+ * https://github.com/o3co/xx.hocon/blob/main/docs/format-ingestion-mapping.md
24
35
  */
25
36
  declare function parseJsonc(input: string, originDescription?: string): Config;
26
37
  /**
@@ -520,6 +520,27 @@ function stripBom(input) {
520
520
  return input.charCodeAt(0) === 65279 ? input.slice(1) : input;
521
521
  }
522
522
 
523
+ // src/internal/depth.ts
524
+ function guardStackDepth(fn, wrap) {
525
+ try {
526
+ return fn();
527
+ } catch (e) {
528
+ if (!isStackOverflow(e)) throw e;
529
+ throw wrap(STACK_MESSAGE);
530
+ }
531
+ }
532
+ var STACK_MESSAGE = "input is nested too deeply for this engine's stack, or contains a cycle (flatten the document, or break the cycle)";
533
+ function isStackOverflow(e) {
534
+ if (!(e instanceof RangeError)) return false;
535
+ const m = e.message.toLowerCase();
536
+ return m.includes("call stack") || // V8: "Maximum call stack size exceeded"
537
+ m.includes("recursion") || // SpiderMonkey: "too much recursion"
538
+ m.includes("stack overflow");
539
+ }
540
+ function depthError(message) {
541
+ return new ConfigError(message, "");
542
+ }
543
+
523
544
  // src/value/numeric-array.ts
524
545
  function numericObjectToArray(value) {
525
546
  if (value.kind !== "object") return null;
@@ -1498,7 +1519,10 @@ function normalizeLeadingZeroNumber(raw) {
1498
1519
 
1499
1520
  // src/value-factory.ts
1500
1521
  function fromMap(values, originDescription) {
1501
- const fields = coerceObject(values, "");
1522
+ const fields = guardStackDepth(
1523
+ () => coerceObject(values, ""),
1524
+ (msg) => depthError(`fromMap: ${msg}`)
1525
+ );
1502
1526
  const root = { kind: "object", fields };
1503
1527
  return Config._fromResolvedValue(root, { originDescription });
1504
1528
  }
@@ -1563,7 +1587,10 @@ function coerceValue(v, atPath) {
1563
1587
 
1564
1588
  // src/adapters/jsonc.ts
1565
1589
  function parseJsonc(input, originDescription) {
1566
- const doc = JSON.parse(stripTrailingCommas(stripComments(stripBom(input))), bigIntReviver);
1590
+ const doc = guardStackDepth(
1591
+ () => JSON.parse(stripTrailingCommas(stripComments(stripBom(input))), bigIntReviver),
1592
+ (msg) => depthError(`jsonc: ${msg}`)
1593
+ );
1567
1594
  if (doc === null || typeof doc !== "object" || Array.isArray(doc)) {
1568
1595
  throw new ConfigError(
1569
1596
  `jsonc: document root is ${Array.isArray(doc) ? "an array" : typeof doc}, but a config root must be an object (spec F0.3)`,
@@ -565,6 +565,31 @@ function stripBom(input) {
565
565
  return input.charCodeAt(0) === 65279 ? input.slice(1) : input;
566
566
  }
567
567
 
568
+ // src/internal/depth.ts
569
+ var MAX_PATH_SEGMENTS = 64;
570
+ function tooDeep(segments) {
571
+ return segments > MAX_PATH_SEGMENTS;
572
+ }
573
+ function guardStackDepth(fn, wrap) {
574
+ try {
575
+ return fn();
576
+ } catch (e) {
577
+ if (!isStackOverflow(e)) throw e;
578
+ throw wrap(STACK_MESSAGE);
579
+ }
580
+ }
581
+ var STACK_MESSAGE = "input is nested too deeply for this engine's stack, or contains a cycle (flatten the document, or break the cycle)";
582
+ function isStackOverflow(e) {
583
+ if (!(e instanceof RangeError)) return false;
584
+ const m = e.message.toLowerCase();
585
+ return m.includes("call stack") || // V8: "Maximum call stack size exceeded"
586
+ m.includes("recursion") || // SpiderMonkey: "too much recursion"
587
+ m.includes("stack overflow");
588
+ }
589
+ function depthError(message) {
590
+ return new ConfigError(message, "");
591
+ }
592
+
568
593
  // src/internal/properties/properties.ts
569
594
  function parseProperties(input) {
570
595
  const pairs = [];
@@ -572,7 +597,15 @@ function parseProperties(input) {
572
597
  const [rawKey, rawValue] = splitKeyValue(text);
573
598
  const key = unescapeProps(rawKey, line);
574
599
  if (key === "") continue;
575
- pairs.push([key.split("."), unescapeProps(rawValue, line)]);
600
+ const segments = key.split(".");
601
+ if (tooDeep(segments.length)) {
602
+ throw new ParseError(
603
+ `key maps to a path ${segments.length} segments deep, over the limit of ${MAX_PATH_SEGMENTS}`,
604
+ line,
605
+ 1
606
+ );
607
+ }
608
+ pairs.push([segments, unescapeProps(rawValue, line)]);
576
609
  }
577
610
  return nestPairs(pairs);
578
611
  }
@@ -1671,7 +1704,10 @@ function normalizeLeadingZeroNumber(raw) {
1671
1704
 
1672
1705
  // src/value-factory.ts
1673
1706
  function fromMap(values, originDescription) {
1674
- const fields = coerceObject(values, "");
1707
+ const fields = guardStackDepth(
1708
+ () => coerceObject(values, ""),
1709
+ (msg) => depthError(`fromMap: ${msg}`)
1710
+ );
1675
1711
  const root = { kind: "object", fields };
1676
1712
  return Config._fromResolvedValue(root, { originDescription });
1677
1713
  }
@@ -529,6 +529,31 @@ function stripBom(input) {
529
529
  return input.charCodeAt(0) === 65279 ? input.slice(1) : input;
530
530
  }
531
531
 
532
+ // src/internal/depth.ts
533
+ var MAX_PATH_SEGMENTS = 64;
534
+ function tooDeep(segments) {
535
+ return segments > MAX_PATH_SEGMENTS;
536
+ }
537
+ function guardStackDepth(fn, wrap) {
538
+ try {
539
+ return fn();
540
+ } catch (e) {
541
+ if (!isStackOverflow(e)) throw e;
542
+ throw wrap(STACK_MESSAGE);
543
+ }
544
+ }
545
+ var STACK_MESSAGE = "input is nested too deeply for this engine's stack, or contains a cycle (flatten the document, or break the cycle)";
546
+ function isStackOverflow(e) {
547
+ if (!(e instanceof RangeError)) return false;
548
+ const m = e.message.toLowerCase();
549
+ return m.includes("call stack") || // V8: "Maximum call stack size exceeded"
550
+ m.includes("recursion") || // SpiderMonkey: "too much recursion"
551
+ m.includes("stack overflow");
552
+ }
553
+ function depthError(message) {
554
+ return new ConfigError(message, "");
555
+ }
556
+
532
557
  // src/internal/properties/properties.ts
533
558
  function parseProperties(input) {
534
559
  const pairs = [];
@@ -536,7 +561,15 @@ function parseProperties(input) {
536
561
  const [rawKey, rawValue] = splitKeyValue(text);
537
562
  const key = unescapeProps(rawKey, line);
538
563
  if (key === "") continue;
539
- pairs.push([key.split("."), unescapeProps(rawValue, line)]);
564
+ const segments = key.split(".");
565
+ if (tooDeep(segments.length)) {
566
+ throw new ParseError(
567
+ `key maps to a path ${segments.length} segments deep, over the limit of ${MAX_PATH_SEGMENTS}`,
568
+ line,
569
+ 1
570
+ );
571
+ }
572
+ pairs.push([segments, unescapeProps(rawValue, line)]);
540
573
  }
541
574
  return nestPairs(pairs);
542
575
  }
@@ -1635,7 +1668,10 @@ function normalizeLeadingZeroNumber(raw) {
1635
1668
 
1636
1669
  // src/value-factory.ts
1637
1670
  function fromMap(values, originDescription) {
1638
- const fields = coerceObject(values, "");
1671
+ const fields = guardStackDepth(
1672
+ () => coerceObject(values, ""),
1673
+ (msg) => depthError(`fromMap: ${msg}`)
1674
+ );
1639
1675
  const root = { kind: "object", fields };
1640
1676
  return Config._fromResolvedValue(root, { originDescription });
1641
1677
  }
@@ -557,6 +557,27 @@ function stripBom(input) {
557
557
  return input.charCodeAt(0) === 65279 ? input.slice(1) : input;
558
558
  }
559
559
 
560
+ // src/internal/depth.ts
561
+ function guardStackDepth(fn, wrap) {
562
+ try {
563
+ return fn();
564
+ } catch (e) {
565
+ if (!isStackOverflow(e)) throw e;
566
+ throw wrap(STACK_MESSAGE);
567
+ }
568
+ }
569
+ var STACK_MESSAGE = "input is nested too deeply for this engine's stack, or contains a cycle (flatten the document, or break the cycle)";
570
+ function isStackOverflow(e) {
571
+ if (!(e instanceof RangeError)) return false;
572
+ const m = e.message.toLowerCase();
573
+ return m.includes("call stack") || // V8: "Maximum call stack size exceeded"
574
+ m.includes("recursion") || // SpiderMonkey: "too much recursion"
575
+ m.includes("stack overflow");
576
+ }
577
+ function depthError(message) {
578
+ return new ConfigError(message, "");
579
+ }
580
+
560
581
  // src/value/numeric-array.ts
561
582
  function numericObjectToArray(value) {
562
583
  if (value.kind !== "object") return null;
@@ -1535,7 +1556,10 @@ function normalizeLeadingZeroNumber(raw) {
1535
1556
 
1536
1557
  // src/value-factory.ts
1537
1558
  function fromMap(values, originDescription) {
1538
- const fields = coerceObject(values, "");
1559
+ const fields = guardStackDepth(
1560
+ () => coerceObject(values, ""),
1561
+ (msg) => depthError(`fromMap: ${msg}`)
1562
+ );
1539
1563
  const root = { kind: "object", fields };
1540
1564
  return Config._fromResolvedValue(root, { originDescription });
1541
1565
  }
@@ -1601,7 +1625,13 @@ function coerceValue(v, atPath) {
1601
1625
  // src/adapters/toml.ts
1602
1626
  function parseTomlConfig(input, originDescription) {
1603
1627
  const doc = (0, import_smol_toml.parse)(stripBom(input));
1604
- return fromMap(convert(doc, ""), originDescription);
1628
+ return fromMap(
1629
+ guardStackDepth(
1630
+ () => convert(doc, ""),
1631
+ (msg) => depthError(`toml: ${msg}`)
1632
+ ),
1633
+ originDescription
1634
+ );
1605
1635
  }
1606
1636
  function convert(v, atPath) {
1607
1637
  if (Array.isArray(v)) return v.map((e, i) => convert(e, `${atPath}[${i}]`));
@@ -11,7 +11,8 @@ import { C as Config } from '../config-Dlq6rF3w.cjs';
11
11
  * the safe-integer range is an error here rather than silently rounded — go's
12
12
  * adapter accepts those, its int64 being wide enough (spec F0.5).
13
13
  *
14
- * See docs/specs/format-ingestion-mapping.md items F4.x in the hocon scope.
14
+ * See the F4.x items in the format-ingestion mapping spec:
15
+ * https://github.com/o3co/xx.hocon/blob/main/docs/format-ingestion-mapping.md
15
16
  */
16
17
  declare function parseTomlConfig(input: string, originDescription?: string): Config;
17
18
 
@@ -11,7 +11,8 @@ import { C as Config } from '../config-Dlq6rF3w.js';
11
11
  * the safe-integer range is an error here rather than silently rounded — go's
12
12
  * adapter accepts those, its int64 being wide enough (spec F0.5).
13
13
  *
14
- * See docs/specs/format-ingestion-mapping.md items F4.x in the hocon scope.
14
+ * See the F4.x items in the format-ingestion mapping spec:
15
+ * https://github.com/o3co/xx.hocon/blob/main/docs/format-ingestion-mapping.md
15
16
  */
16
17
  declare function parseTomlConfig(input: string, originDescription?: string): Config;
17
18
 
@@ -523,6 +523,27 @@ function stripBom(input) {
523
523
  return input.charCodeAt(0) === 65279 ? input.slice(1) : input;
524
524
  }
525
525
 
526
+ // src/internal/depth.ts
527
+ function guardStackDepth(fn, wrap) {
528
+ try {
529
+ return fn();
530
+ } catch (e) {
531
+ if (!isStackOverflow(e)) throw e;
532
+ throw wrap(STACK_MESSAGE);
533
+ }
534
+ }
535
+ var STACK_MESSAGE = "input is nested too deeply for this engine's stack, or contains a cycle (flatten the document, or break the cycle)";
536
+ function isStackOverflow(e) {
537
+ if (!(e instanceof RangeError)) return false;
538
+ const m = e.message.toLowerCase();
539
+ return m.includes("call stack") || // V8: "Maximum call stack size exceeded"
540
+ m.includes("recursion") || // SpiderMonkey: "too much recursion"
541
+ m.includes("stack overflow");
542
+ }
543
+ function depthError(message) {
544
+ return new ConfigError(message, "");
545
+ }
546
+
526
547
  // src/value/numeric-array.ts
527
548
  function numericObjectToArray(value) {
528
549
  if (value.kind !== "object") return null;
@@ -1501,7 +1522,10 @@ function normalizeLeadingZeroNumber(raw) {
1501
1522
 
1502
1523
  // src/value-factory.ts
1503
1524
  function fromMap(values, originDescription) {
1504
- const fields = coerceObject(values, "");
1525
+ const fields = guardStackDepth(
1526
+ () => coerceObject(values, ""),
1527
+ (msg) => depthError(`fromMap: ${msg}`)
1528
+ );
1505
1529
  const root = { kind: "object", fields };
1506
1530
  return Config._fromResolvedValue(root, { originDescription });
1507
1531
  }
@@ -1567,7 +1591,13 @@ function coerceValue(v, atPath) {
1567
1591
  // src/adapters/toml.ts
1568
1592
  function parseTomlConfig(input, originDescription) {
1569
1593
  const doc = parseToml(stripBom(input));
1570
- return fromMap(convert(doc, ""), originDescription);
1594
+ return fromMap(
1595
+ guardStackDepth(
1596
+ () => convert(doc, ""),
1597
+ (msg) => depthError(`toml: ${msg}`)
1598
+ ),
1599
+ originDescription
1600
+ );
1571
1601
  }
1572
1602
  function convert(v, atPath) {
1573
1603
  if (Array.isArray(v)) return v.map((e, i) => convert(e, `${atPath}[${i}]`));
@@ -558,6 +558,27 @@ function stripBom(input) {
558
558
  return input.charCodeAt(0) === 65279 ? input.slice(1) : input;
559
559
  }
560
560
 
561
+ // src/internal/depth.ts
562
+ function guardStackDepth(fn, wrap) {
563
+ try {
564
+ return fn();
565
+ } catch (e) {
566
+ if (!isStackOverflow(e)) throw e;
567
+ throw wrap(STACK_MESSAGE);
568
+ }
569
+ }
570
+ var STACK_MESSAGE = "input is nested too deeply for this engine's stack, or contains a cycle (flatten the document, or break the cycle)";
571
+ function isStackOverflow(e) {
572
+ if (!(e instanceof RangeError)) return false;
573
+ const m = e.message.toLowerCase();
574
+ return m.includes("call stack") || // V8: "Maximum call stack size exceeded"
575
+ m.includes("recursion") || // SpiderMonkey: "too much recursion"
576
+ m.includes("stack overflow");
577
+ }
578
+ function depthError(message) {
579
+ return new ConfigError(message, "");
580
+ }
581
+
561
582
  // src/value/numeric-array.ts
562
583
  function numericObjectToArray(value) {
563
584
  if (value.kind !== "object") return null;
@@ -1536,7 +1557,10 @@ function normalizeLeadingZeroNumber(raw) {
1536
1557
 
1537
1558
  // src/value-factory.ts
1538
1559
  function fromMap(values, originDescription) {
1539
- const fields = coerceObject(values, "");
1560
+ const fields = guardStackDepth(
1561
+ () => coerceObject(values, ""),
1562
+ (msg) => depthError(`fromMap: ${msg}`)
1563
+ );
1540
1564
  const root = { kind: "object", fields };
1541
1565
  return Config._fromResolvedValue(root, { originDescription });
1542
1566
  }
@@ -1613,11 +1637,14 @@ function parseYaml(input, originDescription) {
1613
1637
  if (doc.errors.length > 0) {
1614
1638
  throw new ConfigError(`yaml: ${doc.errors[0]?.message ?? "parse error"}`, "");
1615
1639
  }
1616
- return fromYamlValue(doc.toJS({ mapAsMap: false }), originDescription);
1640
+ return fromYamlValue(doc.toJS({ mapAsMap: true }), originDescription);
1617
1641
  }
1618
1642
  function fromYamlValue(value, originDescription) {
1619
1643
  if (value === null || value === void 0) return fromMap({}, originDescription);
1620
- const normalized = convert(value, "");
1644
+ const normalized = guardStackDepth(
1645
+ () => convert(value, ""),
1646
+ (msg) => depthError(`yaml: ${msg}`)
1647
+ );
1621
1648
  if (typeof normalized !== "object" || normalized === null || Array.isArray(normalized)) {
1622
1649
  throw new ConfigError(
1623
1650
  `yaml: document root is ${Array.isArray(normalized) ? "an array" : typeof normalized}, but a config root must be an object (spec F0.3)`,
@@ -1633,6 +1660,7 @@ function convert(v, atPath) {
1633
1660
  }
1634
1661
  if (v instanceof Map) {
1635
1662
  const entries = [];
1663
+ const seen = /* @__PURE__ */ new Map();
1636
1664
  for (const [k, e] of v) {
1637
1665
  if (k !== null && typeof k === "object") {
1638
1666
  throw new ConfigError(
@@ -1641,6 +1669,13 @@ function convert(v, atPath) {
1641
1669
  );
1642
1670
  }
1643
1671
  const key = String(k);
1672
+ if (seen.has(key)) {
1673
+ throw new ConfigError(
1674
+ `yaml: sibling mapping keys ${keyForm(seen.get(key))} and ${keyForm(k)} both give the key "${key}"${atPath === "" ? "" : ` at "${atPath}"`}; rename one of them, because one of the two values would otherwise be lost. Quoting a non-string key helps only where that changes the key text, as 0x10 does and 1 does not (spec F5.3)`,
1675
+ atPath
1676
+ );
1677
+ }
1678
+ seen.set(key, k);
1644
1679
  entries.push([key, convert(e, atPath === "" ? key : `${atPath}.${key}`)]);
1645
1680
  }
1646
1681
  return Object.fromEntries(entries);
@@ -1674,6 +1709,10 @@ function bytesToBase64(bytes) {
1674
1709
  }
1675
1710
  return btoa(binary);
1676
1711
  }
1712
+ function keyForm(k) {
1713
+ if (typeof k === "string") return JSON.stringify(k);
1714
+ return `${String(k)} (${k === null ? "null" : typeof k})`;
1715
+ }
1677
1716
  var MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
1678
1717
  var MIN_SAFE = BigInt(Number.MIN_SAFE_INTEGER);
1679
1718
  function isSafeIntegerBigInt(v) {
@@ -29,7 +29,8 @@ import { C as Config } from '../config-Dlq6rF3w.cjs';
29
29
  * than rounded. `getNumber` and `toObject` still apply the JS number model, so
30
30
  * read large identifiers with `getString`.
31
31
  *
32
- * See docs/specs/format-ingestion-mapping.md items F5.x in the hocon scope.
32
+ * See the F5.x items in the format-ingestion mapping spec:
33
+ * https://github.com/o3co/xx.hocon/blob/main/docs/format-ingestion-mapping.md
33
34
  */
34
35
  declare function parseYaml(input: string, originDescription?: string): Config;
35
36
  /**
@@ -29,7 +29,8 @@ import { C as Config } from '../config-Dlq6rF3w.js';
29
29
  * than rounded. `getNumber` and `toObject` still apply the JS number model, so
30
30
  * read large identifiers with `getString`.
31
31
  *
32
- * See docs/specs/format-ingestion-mapping.md items F5.x in the hocon scope.
32
+ * See the F5.x items in the format-ingestion mapping spec:
33
+ * https://github.com/o3co/xx.hocon/blob/main/docs/format-ingestion-mapping.md
33
34
  */
34
35
  declare function parseYaml(input: string, originDescription?: string): Config;
35
36
  /**
@@ -523,6 +523,27 @@ function stripBom(input) {
523
523
  return input.charCodeAt(0) === 65279 ? input.slice(1) : input;
524
524
  }
525
525
 
526
+ // src/internal/depth.ts
527
+ function guardStackDepth(fn, wrap) {
528
+ try {
529
+ return fn();
530
+ } catch (e) {
531
+ if (!isStackOverflow(e)) throw e;
532
+ throw wrap(STACK_MESSAGE);
533
+ }
534
+ }
535
+ var STACK_MESSAGE = "input is nested too deeply for this engine's stack, or contains a cycle (flatten the document, or break the cycle)";
536
+ function isStackOverflow(e) {
537
+ if (!(e instanceof RangeError)) return false;
538
+ const m = e.message.toLowerCase();
539
+ return m.includes("call stack") || // V8: "Maximum call stack size exceeded"
540
+ m.includes("recursion") || // SpiderMonkey: "too much recursion"
541
+ m.includes("stack overflow");
542
+ }
543
+ function depthError(message) {
544
+ return new ConfigError(message, "");
545
+ }
546
+
526
547
  // src/value/numeric-array.ts
527
548
  function numericObjectToArray(value) {
528
549
  if (value.kind !== "object") return null;
@@ -1501,7 +1522,10 @@ function normalizeLeadingZeroNumber(raw) {
1501
1522
 
1502
1523
  // src/value-factory.ts
1503
1524
  function fromMap(values, originDescription) {
1504
- const fields = coerceObject(values, "");
1525
+ const fields = guardStackDepth(
1526
+ () => coerceObject(values, ""),
1527
+ (msg) => depthError(`fromMap: ${msg}`)
1528
+ );
1505
1529
  const root = { kind: "object", fields };
1506
1530
  return Config._fromResolvedValue(root, { originDescription });
1507
1531
  }
@@ -1578,11 +1602,14 @@ function parseYaml(input, originDescription) {
1578
1602
  if (doc.errors.length > 0) {
1579
1603
  throw new ConfigError(`yaml: ${doc.errors[0]?.message ?? "parse error"}`, "");
1580
1604
  }
1581
- return fromYamlValue(doc.toJS({ mapAsMap: false }), originDescription);
1605
+ return fromYamlValue(doc.toJS({ mapAsMap: true }), originDescription);
1582
1606
  }
1583
1607
  function fromYamlValue(value, originDescription) {
1584
1608
  if (value === null || value === void 0) return fromMap({}, originDescription);
1585
- const normalized = convert(value, "");
1609
+ const normalized = guardStackDepth(
1610
+ () => convert(value, ""),
1611
+ (msg) => depthError(`yaml: ${msg}`)
1612
+ );
1586
1613
  if (typeof normalized !== "object" || normalized === null || Array.isArray(normalized)) {
1587
1614
  throw new ConfigError(
1588
1615
  `yaml: document root is ${Array.isArray(normalized) ? "an array" : typeof normalized}, but a config root must be an object (spec F0.3)`,
@@ -1598,6 +1625,7 @@ function convert(v, atPath) {
1598
1625
  }
1599
1626
  if (v instanceof Map) {
1600
1627
  const entries = [];
1628
+ const seen = /* @__PURE__ */ new Map();
1601
1629
  for (const [k, e] of v) {
1602
1630
  if (k !== null && typeof k === "object") {
1603
1631
  throw new ConfigError(
@@ -1606,6 +1634,13 @@ function convert(v, atPath) {
1606
1634
  );
1607
1635
  }
1608
1636
  const key = String(k);
1637
+ if (seen.has(key)) {
1638
+ throw new ConfigError(
1639
+ `yaml: sibling mapping keys ${keyForm(seen.get(key))} and ${keyForm(k)} both give the key "${key}"${atPath === "" ? "" : ` at "${atPath}"`}; rename one of them, because one of the two values would otherwise be lost. Quoting a non-string key helps only where that changes the key text, as 0x10 does and 1 does not (spec F5.3)`,
1640
+ atPath
1641
+ );
1642
+ }
1643
+ seen.set(key, k);
1609
1644
  entries.push([key, convert(e, atPath === "" ? key : `${atPath}.${key}`)]);
1610
1645
  }
1611
1646
  return Object.fromEntries(entries);
@@ -1639,6 +1674,10 @@ function bytesToBase64(bytes) {
1639
1674
  }
1640
1675
  return btoa(binary);
1641
1676
  }
1677
+ function keyForm(k) {
1678
+ if (typeof k === "string") return JSON.stringify(k);
1679
+ return `${String(k)} (${k === null ? "null" : typeof k})`;
1680
+ }
1642
1681
  var MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
1643
1682
  var MIN_SAFE = BigInt(Number.MIN_SAFE_INTEGER);
1644
1683
  function isSafeIntegerBigInt(v) {
package/dist/index.cjs CHANGED
@@ -1548,6 +1548,39 @@ function stripBom(input) {
1548
1548
  return input.charCodeAt(0) === 65279 ? input.slice(1) : input;
1549
1549
  }
1550
1550
 
1551
+ // src/internal/depth.ts
1552
+ var MAX_PATH_SEGMENTS = 64;
1553
+ function tooDeep(segments) {
1554
+ return segments > MAX_PATH_SEGMENTS;
1555
+ }
1556
+ function guardStackDepth(fn, wrap) {
1557
+ try {
1558
+ return fn();
1559
+ } catch (e) {
1560
+ if (!isStackOverflow(e)) throw e;
1561
+ throw wrap(STACK_MESSAGE);
1562
+ }
1563
+ }
1564
+ var STACK_MESSAGE = "input is nested too deeply for this engine's stack, or contains a cycle (flatten the document, or break the cycle)";
1565
+ function isStackOverflow(e) {
1566
+ if (!(e instanceof RangeError)) return false;
1567
+ const m = e.message.toLowerCase();
1568
+ return m.includes("call stack") || // V8: "Maximum call stack size exceeded"
1569
+ m.includes("recursion") || // SpiderMonkey: "too much recursion"
1570
+ m.includes("stack overflow");
1571
+ }
1572
+ function depthError(message) {
1573
+ return new ConfigError(message, "");
1574
+ }
1575
+ async function guardStackDepthAsync(fn, wrap) {
1576
+ try {
1577
+ return await fn();
1578
+ } catch (e) {
1579
+ if (!isStackOverflow(e)) throw e;
1580
+ throw wrap(STACK_MESSAGE);
1581
+ }
1582
+ }
1583
+
1551
1584
  // src/internal/properties/properties.ts
1552
1585
  function parseProperties(input) {
1553
1586
  const pairs = [];
@@ -1555,7 +1588,15 @@ function parseProperties(input) {
1555
1588
  const [rawKey, rawValue] = splitKeyValue(text);
1556
1589
  const key = unescapeProps(rawKey, line);
1557
1590
  if (key === "") continue;
1558
- pairs.push([key.split("."), unescapeProps(rawValue, line)]);
1591
+ const segments = key.split(".");
1592
+ if (tooDeep(segments.length)) {
1593
+ throw new ParseError(
1594
+ `key maps to a path ${segments.length} segments deep, over the limit of ${MAX_PATH_SEGMENTS}`,
1595
+ line,
1596
+ 1
1597
+ );
1598
+ }
1599
+ pairs.push([segments, unescapeProps(rawValue, line)]);
1559
1600
  }
1560
1601
  return nestPairs(pairs);
1561
1602
  }
@@ -3249,13 +3290,16 @@ function buildResolveContext(input, opts) {
3249
3290
  return { ast, resolveOpts };
3250
3291
  }
3251
3292
  function parse(input, opts = {}) {
3252
- const { ast, resolveOpts } = buildResolveContext(input, opts);
3253
- const value = resolve2(ast, resolveOpts);
3254
- if (value.kind !== "object") throw new Error("resolved value is not an object");
3255
- return new Config(value, { resolved: true, originDescription: opts.originDescription });
3293
+ return parseStringWithOptions(input, opts);
3256
3294
  }
3257
3295
  var parseString = parse;
3258
3296
  function parseStringWithOptions(input, opts = {}) {
3297
+ return guardStackDepth(
3298
+ () => parseStringInner(input, opts),
3299
+ (msg) => new ParseError(msg, 1, 1, opts.originDescription)
3300
+ );
3301
+ }
3302
+ function parseStringInner(input, opts) {
3259
3303
  const resolveSubstitutions = opts.resolveSubstitutions ?? true;
3260
3304
  const { ast, resolveOpts } = buildResolveContext(input, opts);
3261
3305
  if (resolveSubstitutions) {
@@ -3286,10 +3330,12 @@ function parseFileWithOptions(filePath, opts = {}) {
3286
3330
  });
3287
3331
  }
3288
3332
  async function parseAsync(input, opts = {}) {
3289
- const { ast, resolveOpts } = buildResolveContext(input, opts);
3290
- const value = await resolveAsync(ast, resolveOpts);
3291
- if (value.kind !== "object") throw new Error("resolved value is not an object");
3292
- return new Config(value);
3333
+ return guardStackDepthAsync(async () => {
3334
+ const { ast, resolveOpts } = buildResolveContext(input, opts);
3335
+ const value = await resolveAsync(ast, resolveOpts);
3336
+ if (value.kind !== "object") throw new Error("resolved value is not an object");
3337
+ return new Config(value);
3338
+ }, (msg) => new ParseError(msg, 1, 1, opts.originDescription));
3293
3339
  }
3294
3340
  function parseFile(filePath, opts = {}) {
3295
3341
  if (typeof process === "undefined" && !opts.readFileSync) {
@@ -3350,7 +3396,10 @@ function isNull(v) {
3350
3396
 
3351
3397
  // src/value-factory.ts
3352
3398
  function fromMap(values, originDescription) {
3353
- const fields = coerceObject(values, "");
3399
+ const fields = guardStackDepth(
3400
+ () => coerceObject(values, ""),
3401
+ (msg) => depthError(`fromMap: ${msg}`)
3402
+ );
3354
3403
  const root = { kind: "object", fields };
3355
3404
  return Config._fromResolvedValue(root, { originDescription });
3356
3405
  }
package/dist/index.js CHANGED
@@ -1494,6 +1494,39 @@ function stripBom(input) {
1494
1494
  return input.charCodeAt(0) === 65279 ? input.slice(1) : input;
1495
1495
  }
1496
1496
 
1497
+ // src/internal/depth.ts
1498
+ var MAX_PATH_SEGMENTS = 64;
1499
+ function tooDeep(segments) {
1500
+ return segments > MAX_PATH_SEGMENTS;
1501
+ }
1502
+ function guardStackDepth(fn, wrap) {
1503
+ try {
1504
+ return fn();
1505
+ } catch (e) {
1506
+ if (!isStackOverflow(e)) throw e;
1507
+ throw wrap(STACK_MESSAGE);
1508
+ }
1509
+ }
1510
+ var STACK_MESSAGE = "input is nested too deeply for this engine's stack, or contains a cycle (flatten the document, or break the cycle)";
1511
+ function isStackOverflow(e) {
1512
+ if (!(e instanceof RangeError)) return false;
1513
+ const m = e.message.toLowerCase();
1514
+ return m.includes("call stack") || // V8: "Maximum call stack size exceeded"
1515
+ m.includes("recursion") || // SpiderMonkey: "too much recursion"
1516
+ m.includes("stack overflow");
1517
+ }
1518
+ function depthError(message) {
1519
+ return new ConfigError(message, "");
1520
+ }
1521
+ async function guardStackDepthAsync(fn, wrap) {
1522
+ try {
1523
+ return await fn();
1524
+ } catch (e) {
1525
+ if (!isStackOverflow(e)) throw e;
1526
+ throw wrap(STACK_MESSAGE);
1527
+ }
1528
+ }
1529
+
1497
1530
  // src/internal/properties/properties.ts
1498
1531
  function parseProperties(input) {
1499
1532
  const pairs = [];
@@ -1501,7 +1534,15 @@ function parseProperties(input) {
1501
1534
  const [rawKey, rawValue] = splitKeyValue(text);
1502
1535
  const key = unescapeProps(rawKey, line);
1503
1536
  if (key === "") continue;
1504
- pairs.push([key.split("."), unescapeProps(rawValue, line)]);
1537
+ const segments = key.split(".");
1538
+ if (tooDeep(segments.length)) {
1539
+ throw new ParseError(
1540
+ `key maps to a path ${segments.length} segments deep, over the limit of ${MAX_PATH_SEGMENTS}`,
1541
+ line,
1542
+ 1
1543
+ );
1544
+ }
1545
+ pairs.push([segments, unescapeProps(rawValue, line)]);
1505
1546
  }
1506
1547
  return nestPairs(pairs);
1507
1548
  }
@@ -3194,13 +3235,16 @@ function buildResolveContext(input, opts) {
3194
3235
  return { ast, resolveOpts };
3195
3236
  }
3196
3237
  function parse(input, opts = {}) {
3197
- const { ast, resolveOpts } = buildResolveContext(input, opts);
3198
- const value = resolve2(ast, resolveOpts);
3199
- if (value.kind !== "object") throw new Error("resolved value is not an object");
3200
- return new Config(value, { resolved: true, originDescription: opts.originDescription });
3238
+ return parseStringWithOptions(input, opts);
3201
3239
  }
3202
3240
  var parseString = parse;
3203
3241
  function parseStringWithOptions(input, opts = {}) {
3242
+ return guardStackDepth(
3243
+ () => parseStringInner(input, opts),
3244
+ (msg) => new ParseError(msg, 1, 1, opts.originDescription)
3245
+ );
3246
+ }
3247
+ function parseStringInner(input, opts) {
3204
3248
  const resolveSubstitutions = opts.resolveSubstitutions ?? true;
3205
3249
  const { ast, resolveOpts } = buildResolveContext(input, opts);
3206
3250
  if (resolveSubstitutions) {
@@ -3231,10 +3275,12 @@ function parseFileWithOptions(filePath, opts = {}) {
3231
3275
  });
3232
3276
  }
3233
3277
  async function parseAsync(input, opts = {}) {
3234
- const { ast, resolveOpts } = buildResolveContext(input, opts);
3235
- const value = await resolveAsync(ast, resolveOpts);
3236
- if (value.kind !== "object") throw new Error("resolved value is not an object");
3237
- return new Config(value);
3278
+ return guardStackDepthAsync(async () => {
3279
+ const { ast, resolveOpts } = buildResolveContext(input, opts);
3280
+ const value = await resolveAsync(ast, resolveOpts);
3281
+ if (value.kind !== "object") throw new Error("resolved value is not an object");
3282
+ return new Config(value);
3283
+ }, (msg) => new ParseError(msg, 1, 1, opts.originDescription));
3238
3284
  }
3239
3285
  function parseFile(filePath, opts = {}) {
3240
3286
  if (typeof process === "undefined" && !opts.readFileSync) {
@@ -3295,7 +3341,10 @@ function isNull(v) {
3295
3341
 
3296
3342
  // src/value-factory.ts
3297
3343
  function fromMap(values, originDescription) {
3298
- const fields = coerceObject(values, "");
3344
+ const fields = guardStackDepth(
3345
+ () => coerceObject(values, ""),
3346
+ (msg) => depthError(`fromMap: ${msg}`)
3347
+ );
3299
3348
  const root = { kind: "object", fields };
3300
3349
  return Config._fromResolvedValue(root, { originDescription });
3301
3350
  }
package/dist/zod.cjs CHANGED
@@ -1530,6 +1530,28 @@ function stripBom(input) {
1530
1530
  return input.charCodeAt(0) === 65279 ? input.slice(1) : input;
1531
1531
  }
1532
1532
 
1533
+ // src/internal/depth.ts
1534
+ var MAX_PATH_SEGMENTS = 64;
1535
+ function tooDeep(segments) {
1536
+ return segments > MAX_PATH_SEGMENTS;
1537
+ }
1538
+ function guardStackDepth(fn, wrap) {
1539
+ try {
1540
+ return fn();
1541
+ } catch (e) {
1542
+ if (!isStackOverflow(e)) throw e;
1543
+ throw wrap(STACK_MESSAGE);
1544
+ }
1545
+ }
1546
+ var STACK_MESSAGE = "input is nested too deeply for this engine's stack, or contains a cycle (flatten the document, or break the cycle)";
1547
+ function isStackOverflow(e) {
1548
+ if (!(e instanceof RangeError)) return false;
1549
+ const m = e.message.toLowerCase();
1550
+ return m.includes("call stack") || // V8: "Maximum call stack size exceeded"
1551
+ m.includes("recursion") || // SpiderMonkey: "too much recursion"
1552
+ m.includes("stack overflow");
1553
+ }
1554
+
1533
1555
  // src/internal/properties/properties.ts
1534
1556
  function parseProperties(input) {
1535
1557
  const pairs = [];
@@ -1537,7 +1559,15 @@ function parseProperties(input) {
1537
1559
  const [rawKey, rawValue] = splitKeyValue(text);
1538
1560
  const key = unescapeProps(rawKey, line);
1539
1561
  if (key === "") continue;
1540
- pairs.push([key.split("."), unescapeProps(rawValue, line)]);
1562
+ const segments = key.split(".");
1563
+ if (tooDeep(segments.length)) {
1564
+ throw new ParseError(
1565
+ `key maps to a path ${segments.length} segments deep, over the limit of ${MAX_PATH_SEGMENTS}`,
1566
+ line,
1567
+ 1
1568
+ );
1569
+ }
1570
+ pairs.push([segments, unescapeProps(rawValue, line)]);
1541
1571
  }
1542
1572
  return nestPairs(pairs);
1543
1573
  }
@@ -3216,10 +3246,30 @@ function buildResolveContext(input, opts) {
3216
3246
  return { ast, resolveOpts };
3217
3247
  }
3218
3248
  function parse(input, opts = {}) {
3249
+ return parseStringWithOptions(input, opts);
3250
+ }
3251
+ function parseStringWithOptions(input, opts = {}) {
3252
+ return guardStackDepth(
3253
+ () => parseStringInner(input, opts),
3254
+ (msg) => new ParseError(msg, 1, 1, opts.originDescription)
3255
+ );
3256
+ }
3257
+ function parseStringInner(input, opts) {
3258
+ const resolveSubstitutions = opts.resolveSubstitutions ?? true;
3219
3259
  const { ast, resolveOpts } = buildResolveContext(input, opts);
3220
- const value = resolve2(ast, resolveOpts);
3221
- if (value.kind !== "object") throw new Error("resolved value is not an object");
3222
- return new Config(value, { resolved: true, originDescription: opts.originDescription });
3260
+ if (resolveSubstitutions) {
3261
+ const value = resolve2(ast, resolveOpts);
3262
+ if (value.kind !== "object") throw new Error("resolved value is not an object");
3263
+ return new Config(value, { resolved: true, originDescription: opts.originDescription });
3264
+ }
3265
+ const tree = buildTree(ast, resolveOpts);
3266
+ const hasPlaceholders = containsPlaceholders(tree);
3267
+ return Config._fromUnresolvedResObj(tree, {
3268
+ parseBaseDir: opts.baseDir,
3269
+ originDescription: opts.originDescription,
3270
+ resolved: !hasPlaceholders,
3271
+ resolveOpts
3272
+ });
3223
3273
  }
3224
3274
  function parseFile(filePath, opts = {}) {
3225
3275
  if (typeof process === "undefined" && !opts.readFileSync) {
package/dist/zod.js CHANGED
@@ -1498,6 +1498,28 @@ function stripBom(input) {
1498
1498
  return input.charCodeAt(0) === 65279 ? input.slice(1) : input;
1499
1499
  }
1500
1500
 
1501
+ // src/internal/depth.ts
1502
+ var MAX_PATH_SEGMENTS = 64;
1503
+ function tooDeep(segments) {
1504
+ return segments > MAX_PATH_SEGMENTS;
1505
+ }
1506
+ function guardStackDepth(fn, wrap) {
1507
+ try {
1508
+ return fn();
1509
+ } catch (e) {
1510
+ if (!isStackOverflow(e)) throw e;
1511
+ throw wrap(STACK_MESSAGE);
1512
+ }
1513
+ }
1514
+ var STACK_MESSAGE = "input is nested too deeply for this engine's stack, or contains a cycle (flatten the document, or break the cycle)";
1515
+ function isStackOverflow(e) {
1516
+ if (!(e instanceof RangeError)) return false;
1517
+ const m = e.message.toLowerCase();
1518
+ return m.includes("call stack") || // V8: "Maximum call stack size exceeded"
1519
+ m.includes("recursion") || // SpiderMonkey: "too much recursion"
1520
+ m.includes("stack overflow");
1521
+ }
1522
+
1501
1523
  // src/internal/properties/properties.ts
1502
1524
  function parseProperties(input) {
1503
1525
  const pairs = [];
@@ -1505,7 +1527,15 @@ function parseProperties(input) {
1505
1527
  const [rawKey, rawValue] = splitKeyValue(text);
1506
1528
  const key = unescapeProps(rawKey, line);
1507
1529
  if (key === "") continue;
1508
- pairs.push([key.split("."), unescapeProps(rawValue, line)]);
1530
+ const segments = key.split(".");
1531
+ if (tooDeep(segments.length)) {
1532
+ throw new ParseError(
1533
+ `key maps to a path ${segments.length} segments deep, over the limit of ${MAX_PATH_SEGMENTS}`,
1534
+ line,
1535
+ 1
1536
+ );
1537
+ }
1538
+ pairs.push([segments, unescapeProps(rawValue, line)]);
1509
1539
  }
1510
1540
  return nestPairs(pairs);
1511
1541
  }
@@ -3183,10 +3213,30 @@ function buildResolveContext(input, opts) {
3183
3213
  return { ast, resolveOpts };
3184
3214
  }
3185
3215
  function parse(input, opts = {}) {
3216
+ return parseStringWithOptions(input, opts);
3217
+ }
3218
+ function parseStringWithOptions(input, opts = {}) {
3219
+ return guardStackDepth(
3220
+ () => parseStringInner(input, opts),
3221
+ (msg) => new ParseError(msg, 1, 1, opts.originDescription)
3222
+ );
3223
+ }
3224
+ function parseStringInner(input, opts) {
3225
+ const resolveSubstitutions = opts.resolveSubstitutions ?? true;
3186
3226
  const { ast, resolveOpts } = buildResolveContext(input, opts);
3187
- const value = resolve2(ast, resolveOpts);
3188
- if (value.kind !== "object") throw new Error("resolved value is not an object");
3189
- return new Config(value, { resolved: true, originDescription: opts.originDescription });
3227
+ if (resolveSubstitutions) {
3228
+ const value = resolve2(ast, resolveOpts);
3229
+ if (value.kind !== "object") throw new Error("resolved value is not an object");
3230
+ return new Config(value, { resolved: true, originDescription: opts.originDescription });
3231
+ }
3232
+ const tree = buildTree(ast, resolveOpts);
3233
+ const hasPlaceholders = containsPlaceholders(tree);
3234
+ return Config._fromUnresolvedResObj(tree, {
3235
+ parseBaseDir: opts.baseDir,
3236
+ originDescription: opts.originDescription,
3237
+ resolved: !hasPlaceholders,
3238
+ resolveOpts
3239
+ });
3190
3240
  }
3191
3241
  function parseFile(filePath, opts = {}) {
3192
3242
  if (typeof process === "undefined" && !opts.readFileSync) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@o3co/ts.hocon",
3
- "version": "1.11.0",
3
+ "version": "1.12.0",
4
4
  "description": "Full Lightbend HOCON spec-compliant parser and config library for TypeScript",
5
5
  "keywords": [
6
6
  "hocon",