@intentius/chant 0.46.0 → 0.50.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/dist/audit/catalog.d.ts +13 -3
- package/dist/audit/catalog.d.ts.map +1 -1
- package/dist/audit/core.d.ts +30 -3
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +9 -2
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/fetch.d.ts.map +1 -1
- package/dist/audit/report-html.d.ts.map +1 -1
- package/dist/audit/report-model.d.ts +6 -0
- package/dist/audit/report-model.d.ts.map +1 -1
- package/dist/audit/report.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/audit/secrets.d.ts +95 -0
- package/dist/audit/secrets.d.ts.map +1 -0
- package/dist/audit/wrangler.d.ts +33 -0
- package/dist/audit/wrangler.d.ts.map +1 -0
- package/dist/build.d.ts +3 -3
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +7 -0
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +23 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts +31 -0
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +12 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +32 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -0
- package/dist/cli/handlers/scenario.d.ts +39 -0
- package/dist/cli/handlers/scenario.d.ts.map +1 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
- package/dist/cli/mcp/server.d.ts +35 -2
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/cli/mcp/tools/explain.d.ts +6 -0
- package/dist/cli/mcp/tools/explain.d.ts.map +1 -1
- package/dist/cli/mcp/types.d.ts +29 -1
- package/dist/cli/mcp/types.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +1 -1
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +14 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/reporters/stylish.d.ts +15 -1
- package/dist/cli/reporters/stylish.d.ts.map +1 -1
- package/dist/codegen/docs-rule-scanning.d.ts.map +1 -1
- package/dist/components/auto-release.d.ts +4 -0
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/capability.d.ts +17 -2
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +7 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/component.d.ts +15 -0
- package/dist/components/component.d.ts.map +1 -1
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/starter-plugin.d.ts +2 -0
- package/dist/components/starter-plugin.d.ts.map +1 -1
- package/dist/components/verbs/ensure-secret.d.ts +50 -0
- package/dist/components/verbs/ensure-secret.d.ts.map +1 -0
- package/dist/components/verbs/index.d.ts +13 -0
- package/dist/components/verbs/index.d.ts.map +1 -1
- package/dist/components/verbs/r2-sync.d.ts +76 -0
- package/dist/components/verbs/r2-sync.d.ts.map +1 -0
- package/dist/components/verbs/run-agent.d.ts +499 -0
- package/dist/components/verbs/run-agent.d.ts.map +1 -0
- package/dist/components/verbs/sign.d.ts +30 -0
- package/dist/components/verbs/sign.d.ts.map +1 -1
- package/dist/components/verbs/wrangler.d.ts +108 -0
- package/dist/components/verbs/wrangler.d.ts.map +1 -0
- package/dist/composite.d.ts +6 -1
- package/dist/composite.d.ts.map +1 -1
- package/dist/config.d.ts +26 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/deep-observation.d.ts +14 -0
- package/dist/deep-observation.d.ts.map +1 -1
- package/dist/discovery/collect.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +15 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/fold-rank.d.ts +66 -0
- package/dist/discovery/fold-rank.d.ts.map +1 -0
- package/dist/discovery/index.d.ts +15 -0
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/param-deps.d.ts +17 -0
- package/dist/discovery/param-deps.d.ts.map +1 -0
- package/dist/effect-receipt.d.ts +177 -0
- package/dist/effect-receipt.d.ts.map +1 -0
- package/dist/fold/fold.d.ts +55 -2
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +20 -0
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lexicon-schema.d.ts +2 -0
- package/dist/lexicon-schema.d.ts.map +1 -1
- package/dist/lexicon.d.ts +137 -3
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/change-set.d.ts +33 -5
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/converge-ledger.d.ts +90 -0
- package/dist/lifecycle/converge-ledger.d.ts.map +1 -0
- package/dist/lifecycle/deep-diff.d.ts +18 -0
- package/dist/lifecycle/deep-diff.d.ts.map +1 -1
- package/dist/lifecycle/deep-observe.d.ts +9 -1
- package/dist/lifecycle/deep-observe.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +33 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -0
- package/dist/lifecycle/git.d.ts +145 -21
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/index.d.ts +6 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/lease.d.ts +113 -0
- package/dist/lifecycle/lease.d.ts.map +1 -0
- package/dist/lifecycle/observation-baseline.d.ts +21 -3
- package/dist/lifecycle/observation-baseline.d.ts.map +1 -1
- package/dist/lifecycle/receipt-plan.d.ts +62 -0
- package/dist/lifecycle/receipt-plan.d.ts.map +1 -0
- package/dist/lifecycle/release-ledger.d.ts +20 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/scenario-eval.d.ts +42 -0
- package/dist/lifecycle/scenario-eval.d.ts.map +1 -0
- package/dist/lifecycle/scenario.d.ts +163 -0
- package/dist/lifecycle/scenario.d.ts.map +1 -0
- package/dist/lifecycle/symptoms.d.ts +63 -0
- package/dist/lifecycle/symptoms.d.ts.map +1 -0
- package/dist/lifecycle/teardown.d.ts +6 -4
- package/dist/lifecycle/teardown.d.ts.map +1 -1
- package/dist/lifecycle/unobserved-gate.d.ts +67 -0
- package/dist/lifecycle/unobserved-gate.d.ts.map +1 -0
- package/dist/lint/knowledge-checks.d.ts +48 -0
- package/dist/lint/knowledge-checks.d.ts.map +1 -0
- package/dist/lint/output-checks.d.ts +5 -0
- package/dist/lint/output-checks.d.ts.map +1 -0
- package/dist/lint/output-docs.d.ts +94 -0
- package/dist/lint/output-docs.d.ts.map +1 -0
- package/dist/lint/pipeline-change-gate.d.ts +101 -0
- package/dist/lint/pipeline-change-gate.d.ts.map +1 -0
- package/dist/lint/post-synth.d.ts +41 -0
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/lint/receipt-checks.d.ts +9 -0
- package/dist/lint/receipt-checks.d.ts.map +1 -0
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts +11 -0
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts.map +1 -0
- package/dist/lint/rules/cor022-receipt-leaf.d.ts +13 -0
- package/dist/lint/rules/cor022-receipt-leaf.d.ts.map +1 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts +3 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts.map +1 -0
- package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
- package/dist/lint/rules/index.d.ts +3 -1
- package/dist/lint/rules/index.d.ts.map +1 -1
- package/dist/lsp/lexicon-providers.d.ts +7 -0
- package/dist/lsp/lexicon-providers.d.ts.map +1 -1
- package/dist/okf-read.d.ts +78 -0
- package/dist/okf-read.d.ts.map +1 -0
- package/dist/op/activity-contract.d.ts +139 -0
- package/dist/op/activity-contract.d.ts.map +1 -0
- package/dist/op/builders.d.ts +140 -3
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/converge-rule.d.ts +161 -0
- package/dist/op/converge-rule.d.ts.map +1 -0
- package/dist/op/generate-pipeline.d.ts +39 -0
- package/dist/op/generate-pipeline.d.ts.map +1 -0
- package/dist/op/index.d.ts +18 -2
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +2 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/op-verb-class.d.ts +42 -0
- package/dist/op/op-verb-class.d.ts.map +1 -0
- package/dist/op/operator.d.ts +128 -0
- package/dist/op/operator.d.ts.map +1 -0
- package/dist/op/receipt-store.d.ts +138 -0
- package/dist/op/receipt-store.d.ts.map +1 -0
- package/dist/op/step-output-ref.d.ts +187 -0
- package/dist/op/step-output-ref.d.ts.map +1 -0
- package/dist/op/types.d.ts +49 -2
- package/dist/op/types.d.ts.map +1 -1
- package/dist/provenance.d.ts +73 -3
- package/dist/provenance.d.ts.map +1 -1
- package/dist/runtime-adapter.d.ts +7 -1
- package/dist/runtime-adapter.d.ts.map +1 -1
- package/dist/secret-materialization.d.ts +138 -0
- package/dist/secret-materialization.d.ts.map +1 -0
- package/dist/secret-provenance.d.ts +218 -0
- package/dist/secret-provenance.d.ts.map +1 -0
- package/dist/serializer.d.ts +29 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/toml.d.ts +40 -5
- package/dist/toml.d.ts.map +1 -1
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/audit/catalog.test.ts +1 -1
- package/src/audit/catalog.ts +75 -3
- package/src/audit/core.test.ts +57 -0
- package/src/audit/core.ts +0 -0
- package/src/audit/detect-bundle.test.ts +1 -1
- package/src/audit/discover.test.ts +24 -0
- package/src/audit/discover.ts +40 -4
- package/src/audit/fetch.test.ts +216 -3
- package/src/audit/fetch.ts +270 -59
- package/src/audit/report-html.ts +5 -2
- package/src/audit/report-model.ts +9 -0
- package/src/audit/report.test.ts +22 -0
- package/src/audit/report.ts +3 -2
- package/src/audit/rules-doc.ts +13 -1
- package/src/audit/secrets.test.ts +303 -0
- package/src/audit/secrets.ts +406 -0
- package/src/audit/wrangler.test.ts +230 -0
- package/src/audit/wrangler.ts +290 -0
- package/src/build.test.ts +41 -0
- package/src/build.ts +39 -6
- package/src/cli/command-group.ts +1 -1
- package/src/cli/commands/__fixtures__/audit-fountain/agents/fleet.yaml +27 -0
- package/src/cli/commands/__fixtures__/audit-fountain/k8s/deploy.yaml +16 -0
- package/src/cli/commands/__fixtures__/audit-fountain-clean/fleet.yaml +20 -0
- package/src/cli/commands/__fixtures__/schemas/sarif-2.1.0.schema.json +2882 -0
- package/src/cli/commands/audit.test.ts +268 -1
- package/src/cli/commands/audit.ts +87 -18
- package/src/cli/commands/build.test.ts +245 -0
- package/src/cli/commands/build.ts +210 -21
- package/src/cli/commands/lint.ts +15 -3
- package/src/cli/handlers/build.ts +2 -0
- package/src/cli/handlers/components.test.ts +199 -1
- package/src/cli/handlers/components.ts +160 -3
- package/src/cli/handlers/explain.test.ts +70 -1
- package/src/cli/handlers/graph.test.ts +20 -0
- package/src/cli/handlers/graph.ts +12 -3
- package/src/cli/handlers/lifecycle.test.ts +115 -1
- package/src/cli/handlers/lifecycle.ts +96 -15
- package/src/cli/handlers/operator.test.ts +255 -0
- package/src/cli/handlers/operator.ts +240 -0
- package/src/cli/handlers/scenario.test.ts +456 -0
- package/src/cli/handlers/scenario.ts +330 -0
- package/src/cli/main.test.ts +23 -0
- package/src/cli/main.ts +72 -1
- package/src/cli/mcp/resource-handlers.ts +38 -1
- package/src/cli/mcp/server.test.ts +323 -3
- package/src/cli/mcp/server.ts +84 -7
- package/src/cli/mcp/tools/explain.ts +51 -2
- package/src/cli/mcp/types.ts +27 -1
- package/src/cli/plugins.ts +4 -2
- package/src/cli/registry.ts +14 -2
- package/src/cli/reporters/stylish.test.ts +154 -0
- package/src/cli/reporters/stylish.ts +154 -33
- package/src/codegen/docs-rule-scanning.test.ts +42 -0
- package/src/codegen/docs-rule-scanning.ts +25 -2
- package/src/components/README.md +7 -0
- package/src/components/auto-release.ts +6 -0
- package/src/components/capability.ts +17 -2
- package/src/components/cli-support.test.ts +17 -0
- package/src/components/cli-support.ts +13 -1
- package/src/components/component-schema.test.ts +32 -0
- package/src/components/component.schema.json +6 -0
- package/src/components/component.test.ts +21 -0
- package/src/components/component.ts +15 -0
- package/src/components/driver.ts +12 -4
- package/src/components/registry.test.ts +7 -2
- package/src/components/starter-plugin.ts +17 -0
- package/src/components/verbs/ensure-secret.test.ts +130 -0
- package/src/components/verbs/ensure-secret.ts +79 -0
- package/src/components/verbs/index.ts +13 -0
- package/src/components/verbs/r2-sync.test.ts +107 -0
- package/src/components/verbs/r2-sync.ts +124 -0
- package/src/components/verbs/run-agent.test.ts +683 -0
- package/src/components/verbs/run-agent.ts +786 -0
- package/src/components/verbs/sign.test.ts +19 -0
- package/src/components/verbs/sign.ts +34 -2
- package/src/components/verbs/wrangler.test.ts +170 -0
- package/src/components/verbs/wrangler.ts +241 -0
- package/src/composite.ts +31 -2
- package/src/config.test.ts +15 -0
- package/src/config.ts +30 -0
- package/src/deep-observation.test.ts +19 -0
- package/src/deep-observation.ts +17 -0
- package/src/discovery/collect.ts +11 -2
- package/src/discovery/fold-import.test.ts +54 -0
- package/src/discovery/fold-import.ts +178 -38
- package/src/discovery/fold-rank.test.ts +197 -0
- package/src/discovery/fold-rank.ts +346 -0
- package/src/discovery/index.ts +16 -1
- package/src/discovery/param-deps.test.ts +118 -0
- package/src/discovery/param-deps.ts +170 -0
- package/src/effect-receipt-exclusion.test.ts +190 -0
- package/src/effect-receipt.test.ts +419 -0
- package/src/effect-receipt.ts +412 -0
- package/src/fold/fold.test.ts +6 -2
- package/src/fold/fold.ts +184 -3
- package/src/fold/subset.test.ts +95 -6
- package/src/fold/subset.ts +66 -2
- package/src/index.ts +4 -0
- package/src/lexicon-schema.ts +3 -0
- package/src/lexicon.ts +151 -5
- package/src/lifecycle/change-set.ts +46 -7
- package/src/lifecycle/converge-ledger.test.ts +199 -0
- package/src/lifecycle/converge-ledger.ts +179 -0
- package/src/lifecycle/deep-diff.test.ts +79 -1
- package/src/lifecycle/deep-diff.ts +23 -0
- package/src/lifecycle/deep-observe.ts +13 -2
- package/src/lifecycle/gate-ledger.test.ts +103 -0
- package/src/lifecycle/gate-ledger.ts +140 -0
- package/src/lifecycle/git.test.ts +430 -0
- package/src/lifecycle/git.ts +446 -84
- package/src/lifecycle/index.ts +6 -0
- package/src/lifecycle/lease.test.ts +343 -0
- package/src/lifecycle/lease.ts +270 -0
- package/src/lifecycle/observation-baseline.test.ts +46 -0
- package/src/lifecycle/observation-baseline.ts +33 -1
- package/src/lifecycle/receipt-plan.test.ts +250 -0
- package/src/lifecycle/receipt-plan.ts +249 -0
- package/src/lifecycle/release-ledger.ts +20 -0
- package/src/lifecycle/scenario-eval.test.ts +199 -0
- package/src/lifecycle/scenario-eval.ts +158 -0
- package/src/lifecycle/scenario.test.ts +195 -0
- package/src/lifecycle/scenario.ts +321 -0
- package/src/lifecycle/symptoms.test.ts +116 -0
- package/src/lifecycle/symptoms.ts +126 -0
- package/src/lifecycle/teardown.test.ts +31 -0
- package/src/lifecycle/teardown.ts +6 -4
- package/src/lifecycle/unobserved-gate.test.ts +109 -0
- package/src/lifecycle/unobserved-gate.ts +102 -0
- package/src/lint/knowledge-checks.test.ts +80 -0
- package/src/lint/knowledge-checks.ts +74 -0
- package/src/lint/output-checks.test.ts +85 -0
- package/src/lint/output-checks.ts +99 -0
- package/src/lint/output-docs.test.ts +220 -0
- package/src/lint/output-docs.ts +204 -0
- package/src/lint/pipeline-change-gate.test.ts +144 -0
- package/src/lint/pipeline-change-gate.ts +153 -0
- package/src/lint/post-synth.test.ts +97 -0
- package/src/lint/post-synth.ts +60 -0
- package/src/lint/receipt-checks.test.ts +101 -0
- package/src/lint/receipt-checks.ts +93 -0
- package/src/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.ts +26 -0
- package/src/lint/rules/comp/comp.test.ts +49 -1
- package/src/lint/rules/cor022-receipt-leaf.test.ts +116 -0
- package/src/lint/rules/cor022-receipt-leaf.ts +130 -0
- package/src/lint/rules/cor024-receipt-secret-pointer.test.ts +121 -0
- package/src/lint/rules/cor024-receipt-secret-pointer.ts +218 -0
- package/src/lint/rules/evl001-non-literal-expression.test.ts +35 -3
- package/src/lint/rules/evl001-non-literal-expression.ts +7 -0
- package/src/lint/rules/index.ts +7 -1
- package/src/lsp/lexicon-providers.test.ts +44 -0
- package/src/lsp/lexicon-providers.ts +11 -1
- package/src/okf-read.test.ts +149 -0
- package/src/okf-read.ts +197 -0
- package/src/op/activity-contract.test.ts +180 -0
- package/src/op/activity-contract.ts +278 -0
- package/src/op/builders-exports.test.ts +17 -1
- package/src/op/builders.ts +198 -6
- package/src/op/converge-rule.test.ts +179 -0
- package/src/op/converge-rule.ts +311 -0
- package/src/op/effect-step.test.ts +311 -0
- package/src/op/generate-pipeline.test.ts +53 -0
- package/src/op/generate-pipeline.ts +99 -0
- package/src/op/index.ts +40 -3
- package/src/op/local-executor.test.ts +92 -0
- package/src/op/local-executor.ts +212 -29
- package/src/op/op-verb-class.test.ts +126 -0
- package/src/op/op-verb-class.ts +115 -0
- package/src/op/op.test.ts +25 -2
- package/src/op/operator.test.ts +346 -0
- package/src/op/operator.ts +213 -0
- package/src/op/receipt-store.ts +211 -0
- package/src/op/step-output-ref.test.ts +334 -0
- package/src/op/step-output-ref.ts +453 -0
- package/src/op/types.ts +51 -2
- package/src/provenance.test.ts +151 -4
- package/src/provenance.ts +118 -4
- package/src/runtime-adapter.ts +31 -10
- package/src/secret-materialization.test.ts +199 -0
- package/src/secret-materialization.ts +235 -0
- package/src/secret-provenance.test.ts +388 -0
- package/src/secret-provenance.ts +475 -0
- package/src/serializer.ts +30 -0
- package/src/toml.test.ts +157 -384
- package/src/toml.ts +371 -5
- package/src/yaml.test.ts +88 -0
- package/src/yaml.ts +76 -6
package/src/toml.ts
CHANGED
|
@@ -1,9 +1,375 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* TOML
|
|
2
|
+
* Lightweight TOML parser (#446).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Written for the audit engine's spike: is there a pure, dependency-free way
|
|
5
|
+
* to parse a config format the engine can't read today? TOML fits — it is a
|
|
6
|
+
* fully-specified, static (no macros/interpolation/imports) grammar, so a
|
|
7
|
+
* hand-rolled tokenizer + recursive-descent parser covers it without any
|
|
8
|
+
* runtime surface a Workers sandbox would reject (no `eval`, no native
|
|
9
|
+
* modules, no filesystem/network).
|
|
10
|
+
*
|
|
11
|
+
* Covers the subset of TOML v1.0 that real-world deploy configs
|
|
12
|
+
* (`wrangler.toml`, `Cargo.toml`, `pyproject.toml`, `netlify.toml`) actually
|
|
13
|
+
* use: basic/literal strings (single-line), integers, floats, booleans,
|
|
14
|
+
* arrays (including multi-line), inline tables, dotted keys, standard tables
|
|
15
|
+
* (`[a.b]`), and arrays of tables (`[[a.b]]`).
|
|
16
|
+
*
|
|
17
|
+
* Deliberately NOT a full spec implementation:
|
|
18
|
+
* - Multi-line strings (`"""..."""`, `'''...'''`) are read as a single
|
|
19
|
+
* opaque string body (no line-ending backslash trimming); good enough to
|
|
20
|
+
* not crash, not spec-perfect for continuation whitespace.
|
|
21
|
+
* - Dates/times are returned as their raw source text (a string), not a
|
|
22
|
+
* `Date` — none of the checks built on this parser need calendar math.
|
|
23
|
+
* - A dotted-key path element must be a bare word or a plain quoted string;
|
|
24
|
+
* mixing exotic key shapes mid-path is not supported.
|
|
25
|
+
* These gaps only matter for exotic TOML; they don't affect the shapes the
|
|
26
|
+
* audit checks read (`[vars]`, `[[kv_namespaces]]`, `[env.NAME]`, …).
|
|
6
27
|
*/
|
|
7
28
|
|
|
8
|
-
export
|
|
9
|
-
|
|
29
|
+
export class TomlParseError extends Error {}
|
|
30
|
+
|
|
31
|
+
type Token =
|
|
32
|
+
| { t: "["; }
|
|
33
|
+
| { t: "]"; }
|
|
34
|
+
| { t: "[["; }
|
|
35
|
+
| { t: "]]"; }
|
|
36
|
+
| { t: "{"; }
|
|
37
|
+
| { t: "}"; }
|
|
38
|
+
| { t: "="; }
|
|
39
|
+
| { t: ","; }
|
|
40
|
+
| { t: "."; }
|
|
41
|
+
| { t: "nl"; }
|
|
42
|
+
| { t: "str"; v: string; }
|
|
43
|
+
| { t: "bare"; v: string; }
|
|
44
|
+
| { t: "eof"; };
|
|
45
|
+
|
|
46
|
+
const STOP_CHARS = new Set([" ", "\t", "\r", "\n", "[", "]", "{", "}", "=", ",", ".", "#", '"', "'"]);
|
|
47
|
+
/** Same stop set, minus `.` — used for numeric-looking tokens so `3.14` lexes whole. */
|
|
48
|
+
const NUM_STOP_CHARS = new Set([...STOP_CHARS].filter((ch) => ch !== "."));
|
|
49
|
+
|
|
50
|
+
/** Read a quoted string (basic `"..."`/`'...'` or multi-line `"""..."""`/`'''...'''`) starting at `i` (the opening quote). */
|
|
51
|
+
function readString(s: string, i: number): { value: string; next: number } {
|
|
52
|
+
const q = s[i];
|
|
53
|
+
const literal = q === "'";
|
|
54
|
+
const triple = s[i + 1] === q && s[i + 2] === q;
|
|
55
|
+
const quoteLen = triple ? 3 : 1;
|
|
56
|
+
let j = i + quoteLen;
|
|
57
|
+
// A newline immediately after an opening multi-line delimiter is trimmed (TOML spec).
|
|
58
|
+
if (triple && s[j] === "\r") j++;
|
|
59
|
+
if (triple && s[j] === "\n") j++;
|
|
60
|
+
let out = "";
|
|
61
|
+
while (true) {
|
|
62
|
+
if (j >= s.length) throw new TomlParseError(`Unterminated string starting at offset ${i}`);
|
|
63
|
+
if (s.startsWith(q.repeat(quoteLen), j)) {
|
|
64
|
+
return { value: out, next: j + quoteLen };
|
|
65
|
+
}
|
|
66
|
+
const c = s[j];
|
|
67
|
+
if (!triple && (c === "\n" || c === "\r")) {
|
|
68
|
+
throw new TomlParseError(`Unterminated single-line string starting at offset ${i}`);
|
|
69
|
+
}
|
|
70
|
+
if (literal) {
|
|
71
|
+
out += c;
|
|
72
|
+
j++;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (c === "\\") {
|
|
76
|
+
const esc = s[j + 1];
|
|
77
|
+
if (triple && (esc === "\n" || esc === "\r" || esc === " " || esc === "\t")) {
|
|
78
|
+
// Line-ending backslash: consume the backslash, trailing whitespace, the
|
|
79
|
+
// newline, and all leading whitespace on the next line (folds away).
|
|
80
|
+
let k = j + 1;
|
|
81
|
+
while (s[k] === " " || s[k] === "\t") k++;
|
|
82
|
+
if (s[k] === "\r") k++;
|
|
83
|
+
if (s[k] === "\n") k++;
|
|
84
|
+
while (s[k] === " " || s[k] === "\t" || s[k] === "\n" || s[k] === "\r") k++;
|
|
85
|
+
j = k;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const simple: Record<string, string> = { n: "\n", t: "\t", r: "\r", b: "\b", f: "\f", '"': '"', "\\": "\\" };
|
|
89
|
+
if (esc !== undefined && esc in simple) {
|
|
90
|
+
out += simple[esc];
|
|
91
|
+
j += 2;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (esc === "u" || esc === "U") {
|
|
95
|
+
const width = esc === "u" ? 4 : 8;
|
|
96
|
+
const hex = s.slice(j + 2, j + 2 + width);
|
|
97
|
+
if (hex.length < width || !/^[0-9a-fA-F]+$/.test(hex)) {
|
|
98
|
+
throw new TomlParseError(`Invalid unicode escape at offset ${j}`);
|
|
99
|
+
}
|
|
100
|
+
out += String.fromCodePoint(parseInt(hex, 16));
|
|
101
|
+
j += 2 + width;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
throw new TomlParseError(`Invalid escape \\${esc} at offset ${j}`);
|
|
105
|
+
}
|
|
106
|
+
out += c;
|
|
107
|
+
j++;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function lex(input: string): Token[] {
|
|
112
|
+
const s = input.replace(/\r\n/g, "\n");
|
|
113
|
+
const tokens: Token[] = [];
|
|
114
|
+
let i = 0;
|
|
115
|
+
let atLineStart = true;
|
|
116
|
+
const n = s.length;
|
|
117
|
+
while (i < n) {
|
|
118
|
+
const c = s[i];
|
|
119
|
+
if (c === "\n") {
|
|
120
|
+
tokens.push({ t: "nl" });
|
|
121
|
+
i++;
|
|
122
|
+
atLineStart = true;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (c === " " || c === "\t") {
|
|
126
|
+
i++;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (c === "#") {
|
|
130
|
+
while (i < n && s[i] !== "\n") i++;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (c === "[") {
|
|
134
|
+
if (atLineStart && s[i + 1] === "[") {
|
|
135
|
+
tokens.push({ t: "[[" });
|
|
136
|
+
i += 2;
|
|
137
|
+
} else {
|
|
138
|
+
tokens.push({ t: "[" });
|
|
139
|
+
i += 1;
|
|
140
|
+
}
|
|
141
|
+
atLineStart = false;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (c === "]") {
|
|
145
|
+
if (s[i + 1] === "]" && tokens.length > 0 && hasUnmatchedArrayTableOpen(tokens)) {
|
|
146
|
+
tokens.push({ t: "]]" });
|
|
147
|
+
i += 2;
|
|
148
|
+
} else {
|
|
149
|
+
tokens.push({ t: "]" });
|
|
150
|
+
i += 1;
|
|
151
|
+
}
|
|
152
|
+
atLineStart = false;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (c === "{") { tokens.push({ t: "{" }); i++; atLineStart = false; continue; }
|
|
156
|
+
if (c === "}") { tokens.push({ t: "}" }); i++; atLineStart = false; continue; }
|
|
157
|
+
if (c === "=") { tokens.push({ t: "=" }); i++; atLineStart = false; continue; }
|
|
158
|
+
if (c === ",") { tokens.push({ t: "," }); i++; atLineStart = false; continue; }
|
|
159
|
+
if (c === ".") { tokens.push({ t: "." }); i++; atLineStart = false; continue; }
|
|
160
|
+
if (c === '"' || c === "'") {
|
|
161
|
+
const { value, next } = readString(s, i);
|
|
162
|
+
tokens.push({ t: "str", v: value });
|
|
163
|
+
i = next;
|
|
164
|
+
atLineStart = false;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
// A dotted key (`a.b`) uses `.` as a segment separator, so a plain
|
|
168
|
+
// bareword stops there. A value that looks numeric (starts with a digit,
|
|
169
|
+
// or +/- followed by one) needs `.` to stay IN the token so floats
|
|
170
|
+
// (`3.14`) and dates (`2024-01-01`, already dash-based) lex as one piece.
|
|
171
|
+
const numericStart = /[0-9]/.test(c) || ((c === "+" || c === "-") && /[0-9]/.test(s[i + 1] ?? ""));
|
|
172
|
+
const stopSet = numericStart ? NUM_STOP_CHARS : STOP_CHARS;
|
|
173
|
+
let j = i;
|
|
174
|
+
while (j < n && !stopSet.has(s[j])) j++;
|
|
175
|
+
if (j === i) throw new TomlParseError(`Unexpected character ${JSON.stringify(c)} at offset ${i}`);
|
|
176
|
+
tokens.push({ t: "bare", v: s.slice(i, j) });
|
|
177
|
+
i = j;
|
|
178
|
+
atLineStart = false;
|
|
179
|
+
}
|
|
180
|
+
tokens.push({ t: "eof" });
|
|
181
|
+
return tokens;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Is the most recent unmatched `[[` in `tokens` still open (no `]]` seen since)? Used only to decide whether a trailing `]]` closes an array-of-tables header vs. two array closes. */
|
|
185
|
+
function hasUnmatchedArrayTableOpen(tokens: Token[]): boolean {
|
|
186
|
+
let depth = 0;
|
|
187
|
+
for (const tok of tokens) {
|
|
188
|
+
if (tok.t === "[[") depth++;
|
|
189
|
+
else if (tok.t === "]]") depth--;
|
|
190
|
+
}
|
|
191
|
+
return depth > 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
type TomlValue = string | number | boolean | TomlValue[] | { [k: string]: TomlValue };
|
|
195
|
+
type TomlTable = { [k: string]: TomlValue };
|
|
196
|
+
|
|
197
|
+
class Cursor {
|
|
198
|
+
constructor(private tokens: Token[], private pos = 0) {}
|
|
199
|
+
peek(): Token { return this.tokens[this.pos]; }
|
|
200
|
+
next(): Token { return this.tokens[this.pos++]; }
|
|
201
|
+
skipNl(): void { while (this.peek().t === "nl") this.pos++; }
|
|
202
|
+
expect<T extends Token["t"]>(t: T): Extract<Token, { t: T }> {
|
|
203
|
+
const tok = this.next();
|
|
204
|
+
if (tok.t !== t) throw new TomlParseError(`Expected ${t}, got ${tok.t}`);
|
|
205
|
+
return tok as Extract<Token, { t: T }>;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Read a dotted key path (`a.b.c`) — bare words or quoted strings joined by `.`. */
|
|
210
|
+
function readPath(c: Cursor): string[] {
|
|
211
|
+
const parts: string[] = [];
|
|
212
|
+
while (true) {
|
|
213
|
+
const tok = c.next();
|
|
214
|
+
if (tok.t === "bare") parts.push(tok.v);
|
|
215
|
+
else if (tok.t === "str") parts.push(tok.v);
|
|
216
|
+
else throw new TomlParseError(`Expected a key segment, got ${tok.t}`);
|
|
217
|
+
if (c.peek().t === ".") { c.next(); continue; }
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
return parts;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const INT_RE = /^[+-]?(0|[1-9](_?\d)*)$/;
|
|
224
|
+
const HEX_RE = /^0x[0-9A-Fa-f](_?[0-9A-Fa-f])*$/;
|
|
225
|
+
const OCT_RE = /^0o[0-7](_?[0-7])*$/;
|
|
226
|
+
const BIN_RE = /^0b[01](_?[01])*$/;
|
|
227
|
+
const FLOAT_RE = /^[+-]?(\d(_?\d)*)(\.\d(_?\d)*)?([eE][+-]?\d(_?\d)*)?$/;
|
|
228
|
+
|
|
229
|
+
/** Coerce a bare value token to boolean / number / raw string (dates fall through as strings). */
|
|
230
|
+
function coerceBare(raw: string): TomlValue {
|
|
231
|
+
if (raw === "true") return true;
|
|
232
|
+
if (raw === "false") return false;
|
|
233
|
+
if (raw === "nan" || raw === "+nan" || raw === "-nan") return NaN;
|
|
234
|
+
if (raw === "inf" || raw === "+inf") return Infinity;
|
|
235
|
+
if (raw === "-inf") return -Infinity;
|
|
236
|
+
if (HEX_RE.test(raw)) return parseInt(raw.replace(/_/g, ""), 16);
|
|
237
|
+
if (OCT_RE.test(raw)) return parseInt(raw.replace(/_/g, "").slice(2), 8);
|
|
238
|
+
if (BIN_RE.test(raw)) return parseInt(raw.replace(/_/g, "").slice(2), 2);
|
|
239
|
+
if (INT_RE.test(raw)) return parseInt(raw.replace(/_/g, ""), 10);
|
|
240
|
+
if (FLOAT_RE.test(raw) && /[.eE]/.test(raw)) return parseFloat(raw.replace(/_/g, ""));
|
|
241
|
+
// Not a recognized scalar shape — likely a date/time or something exotic.
|
|
242
|
+
// Kept as its raw source text rather than failing the whole document.
|
|
243
|
+
return raw;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function parseValue(c: Cursor): TomlValue {
|
|
247
|
+
const tok = c.peek();
|
|
248
|
+
if (tok.t === "str") { c.next(); return tok.v; }
|
|
249
|
+
if (tok.t === "bare") { c.next(); return coerceBare(tok.v); }
|
|
250
|
+
if (tok.t === "[") return parseArray(c);
|
|
251
|
+
if (tok.t === "{") return parseInlineTable(c);
|
|
252
|
+
throw new TomlParseError(`Unexpected token ${tok.t} where a value was expected`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function parseArray(c: Cursor): TomlValue[] {
|
|
256
|
+
c.expect("[");
|
|
257
|
+
const out: TomlValue[] = [];
|
|
258
|
+
c.skipNl();
|
|
259
|
+
while (c.peek().t !== "]") {
|
|
260
|
+
out.push(parseValue(c));
|
|
261
|
+
c.skipNl();
|
|
262
|
+
if (c.peek().t === ",") { c.next(); c.skipNl(); continue; }
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
c.skipNl();
|
|
266
|
+
c.expect("]");
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function assignDotted(target: TomlTable, path: string[], value: TomlValue): void {
|
|
271
|
+
let cur: TomlTable = target;
|
|
272
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
273
|
+
const seg = path[i];
|
|
274
|
+
const existing = cur[seg];
|
|
275
|
+
if (existing && typeof existing === "object" && !Array.isArray(existing)) {
|
|
276
|
+
cur = existing as TomlTable;
|
|
277
|
+
} else {
|
|
278
|
+
const created: TomlTable = {};
|
|
279
|
+
cur[seg] = created;
|
|
280
|
+
cur = created;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
cur[path[path.length - 1]] = value;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function parseInlineTable(c: Cursor): TomlTable {
|
|
287
|
+
c.expect("{");
|
|
288
|
+
const out: TomlTable = {};
|
|
289
|
+
// Inline tables are single-line per spec, but stray newlines are tolerated
|
|
290
|
+
// here rather than rejected — the audit must never crash on odd input.
|
|
291
|
+
c.skipNl();
|
|
292
|
+
while (c.peek().t !== "}") {
|
|
293
|
+
const path = readPath(c);
|
|
294
|
+
c.expect("=");
|
|
295
|
+
const value = parseValue(c);
|
|
296
|
+
assignDotted(out, path, value);
|
|
297
|
+
c.skipNl();
|
|
298
|
+
if (c.peek().t === ",") { c.next(); c.skipNl(); continue; }
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
c.skipNl();
|
|
302
|
+
c.expect("}");
|
|
303
|
+
return out;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Navigate/create a nested table (or the last element of a nested array-of-tables) at `path`, per TOML header semantics. */
|
|
307
|
+
function navigateTable(root: TomlTable, path: string[]): TomlTable {
|
|
308
|
+
let cur: TomlTable = root;
|
|
309
|
+
for (const seg of path) {
|
|
310
|
+
const existing = cur[seg];
|
|
311
|
+
if (existing === undefined) {
|
|
312
|
+
const created: TomlTable = {};
|
|
313
|
+
cur[seg] = created;
|
|
314
|
+
cur = created;
|
|
315
|
+
} else if (Array.isArray(existing)) {
|
|
316
|
+
const last = existing[existing.length - 1];
|
|
317
|
+
if (!last || typeof last !== "object") throw new TomlParseError(`Cannot navigate into array segment "${seg}"`);
|
|
318
|
+
cur = last as TomlTable;
|
|
319
|
+
} else if (typeof existing === "object") {
|
|
320
|
+
cur = existing as TomlTable;
|
|
321
|
+
} else {
|
|
322
|
+
throw new TomlParseError(`Key "${seg}" is already a scalar value`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return cur;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function navigateArrayTable(root: TomlTable, path: string[]): TomlTable {
|
|
329
|
+
const parent = navigateTable(root, path.slice(0, -1));
|
|
330
|
+
const last = path[path.length - 1];
|
|
331
|
+
const existing = parent[last];
|
|
332
|
+
const arr: TomlValue[] = Array.isArray(existing) ? existing : [];
|
|
333
|
+
if (!Array.isArray(existing)) parent[last] = arr;
|
|
334
|
+
const entry: TomlTable = {};
|
|
335
|
+
arr.push(entry);
|
|
336
|
+
return entry;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Parse a TOML document into a plain nested object. Throws {@link TomlParseError}
|
|
341
|
+
* on malformed input — callers that scan arbitrary repo files should wrap the
|
|
342
|
+
* call in a try/catch (matching the rest of the audit engine's tolerance for
|
|
343
|
+
* unparseable content).
|
|
344
|
+
*/
|
|
345
|
+
export function parseTOML(input: string): TomlTable {
|
|
346
|
+
const tokens = lex(input);
|
|
347
|
+
const c = new Cursor(tokens);
|
|
348
|
+
const root: TomlTable = {};
|
|
349
|
+
let current: TomlTable = root;
|
|
350
|
+
c.skipNl();
|
|
351
|
+
while (c.peek().t !== "eof") {
|
|
352
|
+
const tok = c.peek();
|
|
353
|
+
if (tok.t === "[") {
|
|
354
|
+
c.next();
|
|
355
|
+
const path = readPath(c);
|
|
356
|
+
c.expect("]");
|
|
357
|
+
current = navigateTable(root, path);
|
|
358
|
+
} else if (tok.t === "[[") {
|
|
359
|
+
c.next();
|
|
360
|
+
const path = readPath(c);
|
|
361
|
+
c.expect("]]");
|
|
362
|
+
current = navigateArrayTable(root, path);
|
|
363
|
+
} else {
|
|
364
|
+
const path = readPath(c);
|
|
365
|
+
c.expect("=");
|
|
366
|
+
const value = parseValue(c);
|
|
367
|
+
assignDotted(current, path, value);
|
|
368
|
+
}
|
|
369
|
+
const end = c.next();
|
|
370
|
+
if (end.t === "eof") break;
|
|
371
|
+
if (end.t !== "nl") throw new TomlParseError(`Expected end of line, got ${end.t}`);
|
|
372
|
+
c.skipNl();
|
|
373
|
+
}
|
|
374
|
+
return root;
|
|
375
|
+
}
|
package/src/yaml.test.ts
CHANGED
|
@@ -226,6 +226,94 @@ describe("parseScalar", () => {
|
|
|
226
226
|
expect(parseScalar("hello")).toBe("hello");
|
|
227
227
|
});
|
|
228
228
|
});
|
|
229
|
+
// Quoted scalars (#1860) — stripping the delimiters is only half the job; the
|
|
230
|
+
// escapes inside them have to be decoded too. A double-quoted `\n` that stayed
|
|
231
|
+
// two literal characters is what sent a seven-line setup script to a shell as
|
|
232
|
+
// one line.
|
|
233
|
+
describe("parseScalar quoted scalars (#1860)", () => {
|
|
234
|
+
test("double-quoted escapes decode", () => {
|
|
235
|
+
expect(parseScalar(String.raw`"a\nb"`)).toBe("a\nb");
|
|
236
|
+
expect(parseScalar(String.raw`"a\tb"`)).toBe("a\tb");
|
|
237
|
+
expect(parseScalar(String.raw`"say \"hi\""`)).toBe('say "hi"');
|
|
238
|
+
expect(parseScalar(String.raw`"back\\slash"`)).toBe("back\\slash");
|
|
239
|
+
expect(parseScalar(String.raw`"a\/b"`)).toBe("a/b");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("hex, unicode and 32-bit escapes", () => {
|
|
243
|
+
expect(parseScalar(String.raw`"\x41"`)).toBe("A");
|
|
244
|
+
expect(parseScalar(String.raw`"A"`)).toBe("A");
|
|
245
|
+
expect(parseScalar(String.raw`"\U0001F600"`)).toBe("\u{1F600}");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("an escaped line break folds away with its indentation", () => {
|
|
249
|
+
expect(parseScalar('"a\\\n b"')).toBe("ab");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("single-quoted '' is one quote", () => {
|
|
253
|
+
expect(parseScalar("'it''s'")).toBe("it's");
|
|
254
|
+
expect(parseScalar("'''quoted'''")).toBe("'quoted'");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("a lone quote character is not a quoted scalar", () => {
|
|
258
|
+
expect(parseScalar('"')).toBe('"');
|
|
259
|
+
expect(parseScalar("'")).toBe("'");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("a body with a bare quote keeps the old strip-only behaviour", () => {
|
|
263
|
+
expect(parseScalar('"a" + "b"')).toBe('a" + "b');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("an unknown or truncated escape keeps the old strip-only behaviour", () => {
|
|
267
|
+
expect(parseScalar(String.raw`"a\qb"`)).toBe(String.raw`a\qb`);
|
|
268
|
+
expect(parseScalar(String.raw`"\x4"`)).toBe(String.raw`\x4`);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Round-trip parity for the scalars `emitYAML` quotes: what it writes,
|
|
273
|
+
// `parseYAML` must read back unchanged. The single-quote half of #1860 was
|
|
274
|
+
// invisible until this existed. Multiline strings are excluded on purpose —
|
|
275
|
+
// the emitter sends those through a block scalar, which #910 already covers
|
|
276
|
+
// and which does not preserve trailing newlines.
|
|
277
|
+
describe("emitYAML/parseYAML round trip (#1860)", () => {
|
|
278
|
+
const values = [
|
|
279
|
+
"'quoted'",
|
|
280
|
+
"#hash's",
|
|
281
|
+
"it's a test",
|
|
282
|
+
"plain",
|
|
283
|
+
"yes",
|
|
284
|
+
"a: b",
|
|
285
|
+
"$VAR",
|
|
286
|
+
"",
|
|
287
|
+
];
|
|
288
|
+
|
|
289
|
+
for (const v of values) {
|
|
290
|
+
test(`round-trips ${JSON.stringify(v)}`, () => {
|
|
291
|
+
expect(parseYAML(`k: ${emitYAML(v, 0)}\n`).k).toBe(v);
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// The fountain lexicon emits multiline strings as `JSON.stringify(s)` — a
|
|
297
|
+
// double-quoted scalar, not a block scalar — so this is the shape that
|
|
298
|
+
// actually reached the applier (#1860).
|
|
299
|
+
describe("double-quoted multiline manifest values (#1860)", () => {
|
|
300
|
+
test("a joined shell script keeps its newlines", () => {
|
|
301
|
+
const script = [
|
|
302
|
+
"set -e",
|
|
303
|
+
"sudo service postgresql start || true",
|
|
304
|
+
`sudo -u postgres psql -tc "ALTER USER postgres PASSWORD 'postgres'" || true`,
|
|
305
|
+
"cd /workspace/fountain",
|
|
306
|
+
].join("\n");
|
|
307
|
+
|
|
308
|
+
const yaml = `spec:\n setup_script: ${JSON.stringify(script)}\n`;
|
|
309
|
+
const parsed = parseYAML(yaml) as { spec: { setup_script: string } };
|
|
310
|
+
|
|
311
|
+
expect(parsed.spec.setup_script).toBe(script);
|
|
312
|
+
expect(parsed.spec.setup_script.split("\n")).toHaveLength(4);
|
|
313
|
+
expect(parsed.spec.setup_script).not.toContain(String.raw`\n`);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
229
317
|
|
|
230
318
|
// Block scalars (#910) — round-trip parity with js-yaml for literal/folded/chomping,
|
|
231
319
|
// including block scalars nested inside array items (the case that mis-parsed).
|
package/src/yaml.ts
CHANGED
|
@@ -559,15 +559,85 @@ export function parseScalar(value: string): unknown {
|
|
|
559
559
|
if (value === "" || value === "~" || value === "null") return null;
|
|
560
560
|
if (value === "true" || value === "yes") return true;
|
|
561
561
|
if (value === "false" || value === "no") return false;
|
|
562
|
-
//
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
)
|
|
567
|
-
|
|
562
|
+
// Quoted scalars carry escapes, and stripping the quotes is only half the
|
|
563
|
+
// job: a double-quoted `\n` is a newline, a single-quoted `''` is one
|
|
564
|
+
// quote. Dropping that step leaves the escape sequence in the value as
|
|
565
|
+
// literal characters, which is how a multiline `setup_script` reached a
|
|
566
|
+
// shell as one line of `set -e\nsudo ...` (#1860).
|
|
567
|
+
if (value.length >= 2) {
|
|
568
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
569
|
+
const unescaped = unescapeDoubleQuoted(value.slice(1, -1));
|
|
570
|
+
// `null` means the body held a bare `"`, so this was never a single
|
|
571
|
+
// well-formed scalar (`"a" + "b"`). Keep the old behaviour there.
|
|
572
|
+
return unescaped ?? value.slice(1, -1);
|
|
573
|
+
}
|
|
574
|
+
if (value.startsWith("'") && value.endsWith("'")) {
|
|
575
|
+
return value.slice(1, -1).replace(/''/g, "'");
|
|
576
|
+
}
|
|
568
577
|
}
|
|
569
578
|
// Number
|
|
570
579
|
const num = Number(value);
|
|
571
580
|
if (!isNaN(num) && value !== "") return num;
|
|
572
581
|
return value;
|
|
573
582
|
}
|
|
583
|
+
|
|
584
|
+
/** YAML double-quoted escapes, per the spec's escape table. */
|
|
585
|
+
const DQ_ESCAPES: Record<string, string> = {
|
|
586
|
+
"0": "\0",
|
|
587
|
+
a: "\x07",
|
|
588
|
+
b: "\b",
|
|
589
|
+
t: "\t",
|
|
590
|
+
"\t": "\t",
|
|
591
|
+
n: "\n",
|
|
592
|
+
v: "\v",
|
|
593
|
+
f: "\f",
|
|
594
|
+
r: "\r",
|
|
595
|
+
e: "\x1b",
|
|
596
|
+
" ": " ",
|
|
597
|
+
'"': '"',
|
|
598
|
+
"/": "/",
|
|
599
|
+
"\\": "\\",
|
|
600
|
+
N: "\x85",
|
|
601
|
+
_: "\xa0",
|
|
602
|
+
L: "\u2028",
|
|
603
|
+
P: "\u2029",
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Decode the body of a double-quoted YAML scalar.
|
|
608
|
+
*
|
|
609
|
+
* Returns `null` when the body holds an unescaped `"`, which means the caller
|
|
610
|
+
* was not looking at one quoted scalar after all.
|
|
611
|
+
*/
|
|
612
|
+
function unescapeDoubleQuoted(body: string): string | null {
|
|
613
|
+
let out = "";
|
|
614
|
+
for (let i = 0; i < body.length; i++) {
|
|
615
|
+
const ch = body[i];
|
|
616
|
+
if (ch === '"') return null;
|
|
617
|
+
if (ch !== "\\") {
|
|
618
|
+
out += ch;
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
const esc = body[++i];
|
|
622
|
+
if (esc === undefined) return null;
|
|
623
|
+
// \xXX, \uXXXX, \UXXXXXXXX
|
|
624
|
+
const width = esc === "x" ? 2 : esc === "u" ? 4 : esc === "U" ? 8 : 0;
|
|
625
|
+
if (width > 0) {
|
|
626
|
+
const hex = body.slice(i + 1, i + 1 + width);
|
|
627
|
+
if (hex.length < width || !/^[0-9a-fA-F]+$/.test(hex)) return null;
|
|
628
|
+
out += String.fromCodePoint(parseInt(hex, 16));
|
|
629
|
+
i += width;
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
// An escaped line break is a line continuation: it and the following
|
|
633
|
+
// indentation fold away to nothing.
|
|
634
|
+
if (esc === "\n") {
|
|
635
|
+
while (body[i + 1] === " " || body[i + 1] === "\t") i++;
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
const mapped = DQ_ESCAPES[esc];
|
|
639
|
+
if (mapped === undefined) return null;
|
|
640
|
+
out += mapped;
|
|
641
|
+
}
|
|
642
|
+
return out;
|
|
643
|
+
}
|