@cdxgen/cdxgen 13.1.0 → 13.3.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 +32 -15
- package/bin/cdxgen.js +243 -122
- package/bin/evinse.js +7 -0
- package/bin/sign.js +88 -14
- package/bin/tracebom.js +64 -4
- package/bin/validate.js +29 -7
- package/bin/verify.js +79 -39
- package/data/crypto-oid.json +88 -12
- package/data/remediations.json +47 -0
- package/data/rules/ai-bom.yaml +78 -0
- package/data/rules/cargo-rust.yaml +132 -0
- package/data/rules/mcp-servers.yaml +122 -0
- package/lib/audit/targets.js +6 -1
- package/lib/cli/bomAssembly.js +117 -15
- package/lib/cli/cliOptions.js +93 -1
- package/lib/cli/index.js +235 -5
- package/lib/cli/jsBom.js +108 -26
- package/lib/cli/jvmBom.js +68 -12
- package/lib/cli/managedBom.js +264 -41
- package/lib/cli/nativeBom.js +923 -15
- package/lib/core/activity.js +120 -2
- package/lib/core/env.js +44 -7
- package/lib/core/fs.js +174 -6
- package/lib/core/metadataProperties.js +97 -0
- package/lib/core/projectConfig.js +31 -4
- package/lib/core/userAgent.js +151 -0
- package/lib/ecosystems/bunutils.js +269 -101
- package/lib/ecosystems/cargoCache.js +364 -0
- package/lib/ecosystems/caxa.js +447 -7
- package/lib/ecosystems/cmakeResolver.js +66 -0
- package/lib/ecosystems/cppEvidence.js +104 -33
- package/lib/ecosystems/ecosystems.js +260 -99
- package/lib/ecosystems/golem.js +11 -1
- package/lib/ecosystems/gradleutils.js +185 -9
- package/lib/ecosystems/jsEvidence.js +19 -20
- package/lib/ecosystems/kosi.js +1263 -0
- package/lib/ecosystems/npmutils.js +110 -9
- package/lib/ecosystems/parsers-crystal.js +179 -0
- package/lib/ecosystems/parsers-dotnet.js +120 -49
- package/lib/ecosystems/parsers-elm.js +816 -0
- package/lib/ecosystems/parsers-gleam.js +1 -1
- package/lib/ecosystems/parsers-go.js +47 -9
- package/lib/ecosystems/parsers-js.js +171 -50
- package/lib/ecosystems/parsers-julia.js +268 -0
- package/lib/ecosystems/parsers-lua.js +218 -0
- package/lib/ecosystems/parsers-misc.js +919 -161
- package/lib/ecosystems/parsers-nim.js +221 -0
- package/lib/ecosystems/parsers-opam.js +180 -0
- package/lib/ecosystems/parsers-perl.js +277 -0
- package/lib/ecosystems/parsers-python.js +27 -15
- package/lib/ecosystems/parsers-r.js +148 -0
- package/lib/ecosystems/parsers-rebar.js +411 -0
- package/lib/ecosystems/parsers-rust.js +954 -33
- package/lib/ecosystems/parsers-spack.js +204 -0
- package/lib/ecosystems/parsers-stack.js +264 -0
- package/lib/ecosystems/parsers-terraform.js +194 -0
- package/lib/ecosystems/rubyutils.js +1426 -166
- package/lib/ecosystems/rusi.js +158 -7
- package/lib/evinser/evinser.js +547 -129
- package/lib/evinser/swiftsem.js +1914 -302
- package/lib/helpers/agenticToolutils.js +925 -0
- package/lib/helpers/bomSigner.js +652 -221
- package/lib/inventory/agentFormulationParser.js +57 -4
- package/lib/inventory/aiCollector.js +504 -10
- package/lib/inventory/aiInventory.js +12 -0
- package/lib/inventory/analyzer.js +488 -30
- package/lib/inventory/astWorker.js +6 -0
- package/lib/inventory/atomUtils.js +568 -31
- package/lib/inventory/auditCategories.js +8 -0
- package/lib/inventory/cbomutils.js +30 -0
- package/lib/inventory/cdxrs.js +110 -14
- package/lib/inventory/communityAiConfigParser.js +215 -2
- package/lib/inventory/depsUtils.js +183 -11
- package/lib/inventory/display.js +4 -5
- package/lib/inventory/dosai.js +627 -13
- package/lib/inventory/dynamic.js +39 -1
- package/lib/inventory/envcontext.js +9 -6
- package/lib/inventory/evidenceUtils.js +26 -5
- package/lib/inventory/fetchBatch.js +190 -19
- package/lib/inventory/fetchRate.js +74 -11
- package/lib/inventory/forgeEnricher.js +3 -2
- package/lib/inventory/jvmToolEnv.js +6 -5
- package/lib/inventory/mcpConfigParser.js +166 -2
- package/lib/inventory/mcpDiscovery.js +11 -0
- package/lib/inventory/mcpServerManifest.js +389 -0
- package/lib/inventory/mcpbBundle.js +419 -0
- package/lib/inventory/plugins.js +3 -0
- package/lib/inventory/protobom.js +62 -89
- package/lib/inventory/protobomLoader.js +4 -0
- package/lib/inventory/purl.js +152 -3
- package/lib/inventory/rustFormulationParser.js +1 -1
- package/lib/inventory/sbomDocument.js +2 -70
- package/lib/inventory/source.js +47 -23
- package/lib/inventory/stylePackageRefs.js +41 -0
- package/lib/inventory/svelteUtils.js +524 -0
- package/lib/inventory/traceRunner.js +210 -21
- package/lib/managers/binary.js +115 -24
- package/lib/managers/docker.js +440 -20
- package/lib/managers/oci.js +39 -1
- package/lib/parsers/cmakeLists.js +452 -0
- package/lib/parsers/huggingfaceUtils.js +15 -7
- package/lib/parsers/largeJson.js +454 -0
- package/lib/parsers/npmrc.js +19 -1
- package/lib/parsers/onnx.js +294 -0
- package/lib/parsers/safetensors.js +114 -0
- package/lib/stages/postgen/introspection/markers.js +29 -6
- package/lib/stages/postgen/postgen.js +237 -16
- package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
- package/lib/validator/bomValidator.js +6 -16
- package/lib/validator/index.js +48 -10
- package/lib/validator/reporters/console.js +3 -0
- package/lib/validator/reporters/json.js +1 -0
- package/lib/validator/reporters/sarif.js +7 -0
- package/package.json +27 -27
- package/types/lib/audit/targets.d.ts.map +1 -1
- package/types/lib/cli/bomAssembly.d.ts +2 -1
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/cliOptions.d.ts +33 -1
- package/types/lib/cli/cliOptions.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts +2 -2
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jsBom.d.ts +23 -0
- package/types/lib/cli/jsBom.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts +33 -0
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts +88 -8
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/activity.d.ts.map +1 -1
- package/types/lib/core/env.d.ts +21 -0
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/fs.d.ts +55 -0
- package/types/lib/core/fs.d.ts.map +1 -1
- package/types/lib/core/metadataProperties.d.ts +39 -0
- package/types/lib/core/metadataProperties.d.ts.map +1 -0
- package/types/lib/core/projectConfig.d.ts +6 -2
- package/types/lib/core/projectConfig.d.ts.map +1 -1
- package/types/lib/core/userAgent.d.ts +56 -0
- package/types/lib/core/userAgent.d.ts.map +1 -0
- package/types/lib/ecosystems/bunutils.d.ts +12 -5
- package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
- package/types/lib/ecosystems/cargoCache.d.ts +75 -0
- package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
- package/types/lib/ecosystems/caxa.d.ts +14 -0
- package/types/lib/ecosystems/caxa.d.ts.map +1 -1
- package/types/lib/ecosystems/cmakeResolver.d.ts +23 -0
- package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
- package/types/lib/ecosystems/cppEvidence.d.ts +2 -2
- package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/ecosystems.d.ts +15 -1
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/golem.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +40 -4
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/jsEvidence.d.ts +5 -2
- package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/kosi.d.ts +86 -0
- package/types/lib/ecosystems/kosi.d.ts.map +1 -0
- package/types/lib/ecosystems/npmutils.d.ts +42 -0
- package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
- package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
- package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
- package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-go.d.ts +10 -0
- package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-js.d.ts +10 -4
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
- package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
- package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-misc.d.ts +90 -3
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
- package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
- package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
- package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-r.d.ts +35 -0
- package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
- package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
- package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
- package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
- package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
- package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
- package/types/lib/ecosystems/pythonutils.d.ts +1 -1
- package/types/lib/ecosystems/rubyutils.d.ts +143 -2
- package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rusi.d.ts +13 -0
- package/types/lib/ecosystems/rusi.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +81 -2
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/evinser/swiftsem.d.ts +280 -15
- package/types/lib/evinser/swiftsem.d.ts.map +1 -1
- package/types/lib/helpers/agenticToolutils.d.ts +87 -0
- package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
- package/types/lib/helpers/bomSigner.d.ts +106 -13
- package/types/lib/helpers/bomSigner.d.ts.map +1 -1
- package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
- package/types/lib/inventory/aiCollector.d.ts.map +1 -1
- package/types/lib/inventory/aiInventory.d.ts.map +1 -1
- package/types/lib/inventory/analyzer.d.ts.map +1 -1
- package/types/lib/inventory/atomUtils.d.ts +145 -3
- package/types/lib/inventory/atomUtils.d.ts.map +1 -1
- package/types/lib/inventory/auditCategories.d.ts +4 -0
- package/types/lib/inventory/auditCategories.d.ts.map +1 -1
- package/types/lib/inventory/cbomutils.d.ts.map +1 -1
- package/types/lib/inventory/cdxrs.d.ts +8 -3
- package/types/lib/inventory/cdxrs.d.ts.map +1 -1
- package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/depsUtils.d.ts +15 -0
- package/types/lib/inventory/depsUtils.d.ts.map +1 -1
- package/types/lib/inventory/display.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +87 -1
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/dynamic.d.ts.map +1 -1
- package/types/lib/inventory/envcontext.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/fetchBatch.d.ts +34 -0
- package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
- package/types/lib/inventory/fetchRate.d.ts +9 -6
- package/types/lib/inventory/fetchRate.d.ts.map +1 -1
- package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
- package/types/lib/inventory/jvmToolEnv.d.ts +5 -4
- package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
- package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
- package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
- package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
- package/types/lib/inventory/mcpbBundle.d.ts +19 -0
- package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
- package/types/lib/inventory/plugins.d.ts.map +1 -1
- package/types/lib/inventory/protobom.d.ts +18 -13
- package/types/lib/inventory/protobom.d.ts.map +1 -1
- package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
- package/types/lib/inventory/purl.d.ts +71 -2
- package/types/lib/inventory/purl.d.ts.map +1 -1
- package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
- package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
- package/types/lib/inventory/svelteUtils.d.ts +124 -0
- package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
- package/types/lib/inventory/traceRunner.d.ts +47 -1
- package/types/lib/inventory/traceRunner.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts +30 -1
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts +29 -0
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/oci.d.ts +14 -0
- package/types/lib/managers/oci.d.ts.map +1 -1
- package/types/lib/parsers/cmakeLists.d.ts +76 -0
- package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
- package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
- package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
- package/types/lib/parsers/largeJson.d.ts +52 -0
- package/types/lib/parsers/largeJson.d.ts.map +1 -0
- package/types/lib/parsers/npmrc.d.ts +5 -0
- package/types/lib/parsers/npmrc.d.ts.map +1 -1
- package/types/lib/parsers/onnx.d.ts +28 -0
- package/types/lib/parsers/onnx.d.ts.map +1 -0
- package/types/lib/parsers/safetensors.d.ts +22 -0
- package/types/lib/parsers/safetensors.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/markers.d.ts +3 -2
- package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts +33 -0
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/index.d.ts +10 -1
- package/types/lib/validator/index.d.ts.map +1 -1
- package/types/lib/validator/reporters/console.d.ts.map +1 -1
- package/types/lib/validator/reporters/json.d.ts.map +1 -1
- package/types/lib/validator/reporters/sarif.d.ts.map +1 -1
|
@@ -4,7 +4,7 @@ import process from "node:process";
|
|
|
4
4
|
import { URL } from "node:url";
|
|
5
5
|
|
|
6
6
|
import { build } from "@cdxgen/cdx-purl";
|
|
7
|
-
import toml from "
|
|
7
|
+
import * as toml from "smol-toml";
|
|
8
8
|
import { parse as _load } from "yaml";
|
|
9
9
|
|
|
10
10
|
import { DEBUG_MODE } from "../core/activity.js";
|
|
@@ -105,7 +105,9 @@ export async function parsePiplockData(lockData) {
|
|
|
105
105
|
Object.keys(depBlock).forEach((p) => {
|
|
106
106
|
const pkg = depBlock[p];
|
|
107
107
|
if (Object.hasOwn(pkg, "version")) {
|
|
108
|
-
|
|
108
|
+
// `===` (arbitrary equality) pins as tightly as `==`, so both are
|
|
109
|
+
// stripped whole rather than leaving a stray `=` behind.
|
|
110
|
+
const versionStr = pkg.version.replace(/^\s*={2,3}\s*/, "");
|
|
109
111
|
pkgList.push({ name: p, version: versionStr });
|
|
110
112
|
}
|
|
111
113
|
});
|
|
@@ -1427,6 +1429,16 @@ export async function parsePyLockData(lockData, lockFile, pyProjectFile) {
|
|
|
1427
1429
|
};
|
|
1428
1430
|
}
|
|
1429
1431
|
|
|
1432
|
+
/**
|
|
1433
|
+
* First version clause of a PEP 508 requirement: an operator and the version
|
|
1434
|
+
* that follows it.
|
|
1435
|
+
*
|
|
1436
|
+
* `===` (arbitrary equality) is tried before `==`, whitespace may separate the
|
|
1437
|
+
* operator from the version, and a version may carry a PEP 440 epoch (`1!2.0`)
|
|
1438
|
+
* or a local version label (`2.1.0+cpu`).
|
|
1439
|
+
*/
|
|
1440
|
+
const PEP440_VERSION_CLAUSE = /(===|==|!=|<=|>=|~=|<|>)\s*([0-9A-Za-z.*_+!-]*)/;
|
|
1441
|
+
|
|
1430
1442
|
/**
|
|
1431
1443
|
* Method to parse requirements.txt file. This must be replaced with atom parsedeps.
|
|
1432
1444
|
*
|
|
@@ -1513,10 +1525,13 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
|
|
|
1513
1525
|
continue;
|
|
1514
1526
|
}
|
|
1515
1527
|
let comment = null;
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1528
|
+
// Equivalent to /\s+#(.*)$/ but linear: an unanchored `\s+` backtracks
|
|
1529
|
+
// quadratically on long whitespace runs without a '#', which a crafted
|
|
1530
|
+
// requirements.txt used to burn seconds of CPU per line.
|
|
1531
|
+
const hashIndex = l.indexOf("#");
|
|
1532
|
+
if (hashIndex > 0 && /\s/.test(l[hashIndex - 1])) {
|
|
1533
|
+
comment = l.substring(hashIndex + 1).trim();
|
|
1534
|
+
l = l.substring(0, hashIndex).trim();
|
|
1520
1535
|
}
|
|
1521
1536
|
const properties = reqFile
|
|
1522
1537
|
? [
|
|
@@ -1607,9 +1622,7 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
|
|
|
1607
1622
|
if (PYTHON_STD_MODULES.includes(name)) {
|
|
1608
1623
|
continue;
|
|
1609
1624
|
}
|
|
1610
|
-
const versionMatch = versionSpecifiers.match(
|
|
1611
|
-
/(==|!=|<=|>=|<|>|~=)([0-9.a-zA-Z_*\-+]*)/,
|
|
1612
|
-
);
|
|
1625
|
+
const versionMatch = versionSpecifiers.match(PEP440_VERSION_CLAUSE);
|
|
1613
1626
|
let version = null;
|
|
1614
1627
|
if (versionMatch) {
|
|
1615
1628
|
version = versionMatch[2].replaceAll("*", "0") || null;
|
|
@@ -1636,7 +1649,7 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
|
|
|
1636
1649
|
value: extras.join(","),
|
|
1637
1650
|
});
|
|
1638
1651
|
}
|
|
1639
|
-
if (versionSpecifiers && !versionSpecifiers.startsWith("==")) {
|
|
1652
|
+
if (versionSpecifiers && !versionSpecifiers.trim().startsWith("==")) {
|
|
1640
1653
|
properties.push({
|
|
1641
1654
|
name: "cdx:pypi:versionSpecifiers",
|
|
1642
1655
|
value: versionSpecifiers.trim(),
|
|
@@ -1673,9 +1686,7 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
|
|
|
1673
1686
|
if (PYTHON_STD_MODULES.includes(name)) {
|
|
1674
1687
|
continue;
|
|
1675
1688
|
}
|
|
1676
|
-
const versionMatch = versionSpecifiers.match(
|
|
1677
|
-
/(==|!=|<=|>=|<|>|~=)([0-9.a-zA-Z_*\-+]*)/,
|
|
1678
|
-
);
|
|
1689
|
+
const versionMatch = versionSpecifiers.match(PEP440_VERSION_CLAUSE);
|
|
1679
1690
|
let version = null;
|
|
1680
1691
|
if (versionMatch) {
|
|
1681
1692
|
version = versionMatch[2].replaceAll("*", "0") || null;
|
|
@@ -1691,7 +1702,7 @@ async function parseReqData(reqFile, reqData = null, fetchDepsInfo = false) {
|
|
|
1691
1702
|
if (licenses) {
|
|
1692
1703
|
apkg.licenses = licenses;
|
|
1693
1704
|
}
|
|
1694
|
-
if (versionSpecifiers && !versionSpecifiers.startsWith("==")) {
|
|
1705
|
+
if (versionSpecifiers && !versionSpecifiers.trim().startsWith("==")) {
|
|
1695
1706
|
properties.push({
|
|
1696
1707
|
name: "cdx:pypi:versionSpecifiers",
|
|
1697
1708
|
value: versionSpecifiers.trim(),
|
|
@@ -2201,6 +2212,7 @@ function normaliseMojoVersion(spec) {
|
|
|
2201
2212
|
const trimmed = spec.trim();
|
|
2202
2213
|
if (!trimmed) return undefined;
|
|
2203
2214
|
// Strip common comparison operators and take the first concrete version.
|
|
2204
|
-
|
|
2215
|
+
// A PEP 440 epoch (`1!2.0`) is part of the version, not a separator.
|
|
2216
|
+
const match = trimmed.match(/((?:\d+!)?\d+(?:\.\d+)*(?:[+-][A-Za-z0-9.]+)?)/);
|
|
2205
2217
|
return match ? match[1] : undefined;
|
|
2206
2218
|
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import { tryBuildPurl } from "../inventory/purl.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* R project parser for renv lock files.
|
|
7
|
+
*
|
|
8
|
+
* `renv.lock` records the R version, the repositories packages were resolved
|
|
9
|
+
* from, and every package in the project library with its exact version, an
|
|
10
|
+
* integrity hash, and the names of the packages it requires. The lock is a
|
|
11
|
+
* JSON document, which makes it a reliable offline source of both the
|
|
12
|
+
* resolved dependency set and the graph between packages.
|
|
13
|
+
*
|
|
14
|
+
* Packages are identified with the registered `cran` purl type
|
|
15
|
+
* (`pkg:cran/<Package>@<Version>`). Requirement strings may carry version
|
|
16
|
+
* constraints (`"R (>= 3.6)"`); only the leading package name is used, and
|
|
17
|
+
* the R runtime itself is recorded as the parent's version rather than an
|
|
18
|
+
* edge.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse an `renv.lock` file.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} renvLockFile Path to `renv.lock`
|
|
25
|
+
* @returns {{
|
|
26
|
+
* pkgList: object[],
|
|
27
|
+
* dependencies: object[],
|
|
28
|
+
* rootInputs: string[],
|
|
29
|
+
* parentComponent: object,
|
|
30
|
+
* rVersion: string|undefined
|
|
31
|
+
* }}
|
|
32
|
+
*/
|
|
33
|
+
export function parseRenvLock(renvLockFile) {
|
|
34
|
+
let lock;
|
|
35
|
+
try {
|
|
36
|
+
lock = JSON.parse(readFileSync(renvLockFile, "utf-8"));
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.warn(`Failed to parse ${renvLockFile}: ${error.message}`);
|
|
39
|
+
return emptyResult();
|
|
40
|
+
}
|
|
41
|
+
if (!lock || typeof lock !== "object" || !lock.Packages) {
|
|
42
|
+
return emptyResult();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const rVersion =
|
|
46
|
+
typeof lock.R?.Version === "string" && lock.R.Version
|
|
47
|
+
? lock.R.Version
|
|
48
|
+
: undefined;
|
|
49
|
+
|
|
50
|
+
const pkgList = [];
|
|
51
|
+
const refByName = new Map();
|
|
52
|
+
for (const record of Object.values(lock.Packages)) {
|
|
53
|
+
if (!record?.Package || !record?.Version) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const purl = tryBuildPurl({
|
|
57
|
+
type: "cran",
|
|
58
|
+
name: record.Package,
|
|
59
|
+
version: record.Version,
|
|
60
|
+
});
|
|
61
|
+
const properties = [{ name: "internal:SrcFile", value: renvLockFile }];
|
|
62
|
+
if (typeof record.Source === "string" && record.Source) {
|
|
63
|
+
properties.push({
|
|
64
|
+
name: "cdx:renv:source",
|
|
65
|
+
// Normalised to a category so repository flavours stay comparable.
|
|
66
|
+
value: record.Source.toLowerCase(),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (typeof record.Repository === "string" && record.Repository) {
|
|
70
|
+
properties.push({
|
|
71
|
+
name: "cdx:renv:repository",
|
|
72
|
+
value: record.Repository,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (typeof record.Hash === "string" && record.Hash) {
|
|
76
|
+
// The renv hash covers the package DESCRIPTION, not the artifact bytes,
|
|
77
|
+
// so it belongs in a property rather than the CycloneDX hashes array.
|
|
78
|
+
properties.push({ name: "cdx:renv:hash", value: record.Hash });
|
|
79
|
+
}
|
|
80
|
+
const pkg = {
|
|
81
|
+
name: record.Package,
|
|
82
|
+
version: record.Version,
|
|
83
|
+
type: "library",
|
|
84
|
+
scope: "required",
|
|
85
|
+
properties,
|
|
86
|
+
};
|
|
87
|
+
if (purl) {
|
|
88
|
+
pkg.purl = purl;
|
|
89
|
+
pkg["bom-ref"] = decodeURIComponent(purl);
|
|
90
|
+
} else {
|
|
91
|
+
pkg["bom-ref"] = `library:${record.Package}:${record.Version}`;
|
|
92
|
+
}
|
|
93
|
+
pkgList.push(pkg);
|
|
94
|
+
refByName.set(record.Package, pkg["bom-ref"]);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Requirements give the graph between packages; the R runtime ("R" with a
|
|
98
|
+
// constraint) is skipped because it is not a lock entry.
|
|
99
|
+
const dependencies = [];
|
|
100
|
+
const requiredNames = new Set();
|
|
101
|
+
for (const record of Object.values(lock.Packages)) {
|
|
102
|
+
const ref = refByName.get(record?.Package);
|
|
103
|
+
if (!ref) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const dependsOn = new Set();
|
|
107
|
+
for (const requirement of Array.isArray(record.Requirements)
|
|
108
|
+
? record.Requirements
|
|
109
|
+
: []) {
|
|
110
|
+
if (typeof requirement !== "string" || !requirement) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const required = requirement.split(/\s+/u)[0];
|
|
114
|
+
const targetRef = required ? refByName.get(required) : undefined;
|
|
115
|
+
if (targetRef && targetRef !== ref) {
|
|
116
|
+
dependsOn.add(targetRef);
|
|
117
|
+
requiredNames.add(required);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
dependencies.push({ ref, dependsOn: [...dependsOn].sort() });
|
|
121
|
+
}
|
|
122
|
+
const rootInputs = pkgList
|
|
123
|
+
.filter((pkg) => !requiredNames.has(pkg.name))
|
|
124
|
+
.map((pkg) => pkg["bom-ref"]);
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
pkgList,
|
|
128
|
+
dependencies,
|
|
129
|
+
rootInputs,
|
|
130
|
+
parentComponent: {},
|
|
131
|
+
rVersion,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Shared empty result for unreadable or malformed locks.
|
|
137
|
+
*
|
|
138
|
+
* @returns {{pkgList: object[], dependencies: object[], rootInputs: string[], parentComponent: object, rVersion: undefined}}
|
|
139
|
+
*/
|
|
140
|
+
function emptyResult() {
|
|
141
|
+
return {
|
|
142
|
+
pkgList: [],
|
|
143
|
+
dependencies: [],
|
|
144
|
+
rootInputs: [],
|
|
145
|
+
parentComponent: {},
|
|
146
|
+
rVersion: undefined,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import { tryBuildPurl } from "../inventory/purl.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Erlang rebar3 parser for `rebar.lock`.
|
|
7
|
+
*
|
|
8
|
+
* A rebar lock holds one or two Erlang terms, each ended by a period:
|
|
9
|
+
*
|
|
10
|
+
* {"1.2.0", [{<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.10.0">>},0}]}.
|
|
11
|
+
* [{pkg_hash, [{<<"cowboy">>, <<"3AFD...">>}]},
|
|
12
|
+
* {pkg_hash_ext, [...]}].
|
|
13
|
+
*
|
|
14
|
+
* The first term pairs the lock format version with the locked dependency
|
|
15
|
+
* list; the hashes live in a separate second term, which is absent from locks
|
|
16
|
+
* that hold no hex packages. Lock format 1.0.0 omitted the version and wrote
|
|
17
|
+
* the dependency list as the only term.
|
|
18
|
+
*
|
|
19
|
+
* Rather than a large regular expression over the surface syntax, a small
|
|
20
|
+
* recursive reader parses the terms into plain JavaScript values: tuples and
|
|
21
|
+
* lists become arrays, binaries and strings become strings, atoms become
|
|
22
|
+
* strings. Only the shapes rebar actually writes are understood; anything
|
|
23
|
+
* else is skipped.
|
|
24
|
+
*
|
|
25
|
+
* Hex dependencies are identified with the registered `hex` purl type, since
|
|
26
|
+
* rebar resolves Erlang packages through hex.pm. The `pkg_hash` values hex
|
|
27
|
+
* publishes are SHA-256 digests of the package tarball, so they are emitted
|
|
28
|
+
* into the CycloneDX `hashes` array where consumers expect content hashes.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parse a `rebar.lock` file.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} rebarLockFile Path to `rebar.lock`
|
|
35
|
+
* @returns {{ pkgList: object[] }} Locked packages
|
|
36
|
+
*/
|
|
37
|
+
export function parseRebarLock(rebarLockFile) {
|
|
38
|
+
let text;
|
|
39
|
+
try {
|
|
40
|
+
text = readFileSync(rebarLockFile, "utf-8");
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.warn(`Failed to read ${rebarLockFile}: ${error.message}`);
|
|
43
|
+
return { pkgList: [] };
|
|
44
|
+
}
|
|
45
|
+
const terms = parseErlangTerms(text);
|
|
46
|
+
const deps = lockedDependencies(terms);
|
|
47
|
+
const pkgHashes = hexHashesFromTerms(terms);
|
|
48
|
+
|
|
49
|
+
const pkgList = [];
|
|
50
|
+
const seen = new Set();
|
|
51
|
+
for (const entry of deps) {
|
|
52
|
+
if (!Array.isArray(entry) || typeof entry[0] !== "string" || !entry[0]) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const name = entry[0];
|
|
56
|
+
const spec = entry[1];
|
|
57
|
+
let pkgName = name;
|
|
58
|
+
let version;
|
|
59
|
+
if (Array.isArray(spec) && spec[0] === "pkg") {
|
|
60
|
+
if (typeof spec[1] === "string" && spec[1]) {
|
|
61
|
+
pkgName = spec[1];
|
|
62
|
+
}
|
|
63
|
+
if (typeof spec[2] === "string" && spec[2]) {
|
|
64
|
+
version = spec[2];
|
|
65
|
+
}
|
|
66
|
+
} else if (Array.isArray(spec) && spec[0] === "git") {
|
|
67
|
+
// Git-sourced entries carry {git, Url, {ref, Sha}}; the revision is the
|
|
68
|
+
// most precise version available without running git.
|
|
69
|
+
const ref =
|
|
70
|
+
Array.isArray(spec[2]) && spec[2][0] === "ref" ? spec[2][1] : undefined;
|
|
71
|
+
if (typeof ref === "string" && ref) {
|
|
72
|
+
version = ref;
|
|
73
|
+
}
|
|
74
|
+
} else if (typeof spec === "string" && spec) {
|
|
75
|
+
version = spec;
|
|
76
|
+
}
|
|
77
|
+
if (seen.has(pkgName)) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
seen.add(pkgName);
|
|
81
|
+
pkgList.push(
|
|
82
|
+
rebarPackage(pkgName, version, {
|
|
83
|
+
// rebar records the dependency depth; level 0 entries are the
|
|
84
|
+
// project's own dependencies, deeper entries are transitive.
|
|
85
|
+
direct: entry[2] === 0,
|
|
86
|
+
sha256: pkgHashes.get(pkgName) || pkgHashes.get(name),
|
|
87
|
+
srcFile: rebarLockFile,
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return { pkgList };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Build a component-like package record for a rebar dependency.
|
|
96
|
+
*
|
|
97
|
+
* @param {string} name Hex package name
|
|
98
|
+
* @param {string|undefined} version Resolved version or git revision
|
|
99
|
+
* @param {object} opts Extra context (`direct`, `sha256`, `srcFile`)
|
|
100
|
+
* @returns {object} Package record
|
|
101
|
+
*/
|
|
102
|
+
function rebarPackage(name, version, opts) {
|
|
103
|
+
const purl = tryBuildPurl({
|
|
104
|
+
type: "hex",
|
|
105
|
+
name,
|
|
106
|
+
version: version || undefined,
|
|
107
|
+
});
|
|
108
|
+
const properties = [
|
|
109
|
+
{ name: "internal:SrcFile", value: opts.srcFile },
|
|
110
|
+
{
|
|
111
|
+
name: "cdx:rebar:dependency",
|
|
112
|
+
value: opts.direct ? "direct" : "transitive",
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
const pkg = {
|
|
116
|
+
name,
|
|
117
|
+
...(version ? { version } : {}),
|
|
118
|
+
type: "library",
|
|
119
|
+
scope: "required",
|
|
120
|
+
properties,
|
|
121
|
+
};
|
|
122
|
+
if (opts.sha256 && /^[0-9a-f]{64}$/iu.test(opts.sha256)) {
|
|
123
|
+
pkg.hashes = [{ alg: "SHA-256", content: opts.sha256.toLowerCase() }];
|
|
124
|
+
}
|
|
125
|
+
if (purl) {
|
|
126
|
+
pkg.purl = purl;
|
|
127
|
+
pkg["bom-ref"] = decodeURIComponent(purl);
|
|
128
|
+
} else {
|
|
129
|
+
pkg["bom-ref"] = `library:${name}:${version || ""}`;
|
|
130
|
+
}
|
|
131
|
+
return pkg;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Pick the locked dependency list out of the parsed terms.
|
|
136
|
+
*
|
|
137
|
+
* The current format wraps it in `{Version, Deps}`; format 1.0.0 wrote the
|
|
138
|
+
* list on its own. A dependency entry is always a tuple whose first element
|
|
139
|
+
* is the application name, which tells the two shapes apart.
|
|
140
|
+
*
|
|
141
|
+
* @param {Array} terms Parsed top-level terms
|
|
142
|
+
* @returns {Array} Dependency entries
|
|
143
|
+
*/
|
|
144
|
+
function lockedDependencies(terms) {
|
|
145
|
+
for (const term of terms) {
|
|
146
|
+
if (!Array.isArray(term)) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (typeof term[0] === "string" && Array.isArray(term[1])) {
|
|
150
|
+
// {Version, Deps}
|
|
151
|
+
return term[1];
|
|
152
|
+
}
|
|
153
|
+
if (term.every(isDependencyEntry)) {
|
|
154
|
+
// A bare dependency list, including the empty lock `[].`
|
|
155
|
+
return term;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Recognise a locked dependency entry: `{Name, Source, Level}`.
|
|
163
|
+
*
|
|
164
|
+
* @param {*} entry Candidate entry
|
|
165
|
+
* @returns {boolean} true when the entry looks like a dependency tuple
|
|
166
|
+
*/
|
|
167
|
+
function isDependencyEntry(entry) {
|
|
168
|
+
return (
|
|
169
|
+
Array.isArray(entry) &&
|
|
170
|
+
typeof entry[0] === "string" &&
|
|
171
|
+
entry[0].length > 0 &&
|
|
172
|
+
entry.length >= 2
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Collect the `pkg_hash` digests from the parsed terms.
|
|
178
|
+
*
|
|
179
|
+
* The digests live in their own top-level term, a list of
|
|
180
|
+
* `{pkg_hash, [{Name, Digest}]}` tuples. `pkg_hash_ext` covers a different
|
|
181
|
+
* packaging of the same release and is ignored so one digest is emitted per
|
|
182
|
+
* package.
|
|
183
|
+
*
|
|
184
|
+
* @param {Array} terms Parsed top-level terms
|
|
185
|
+
* @returns {Map<string, string>} Package name to SHA-256 hex digest
|
|
186
|
+
*/
|
|
187
|
+
function hexHashesFromTerms(terms) {
|
|
188
|
+
const hashes = new Map();
|
|
189
|
+
for (const term of terms) {
|
|
190
|
+
if (!Array.isArray(term)) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
for (const item of term) {
|
|
194
|
+
if (
|
|
195
|
+
!Array.isArray(item) ||
|
|
196
|
+
item[0] !== "pkg_hash" ||
|
|
197
|
+
!Array.isArray(item[1])
|
|
198
|
+
) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
for (const pair of item[1]) {
|
|
202
|
+
if (Array.isArray(pair) && typeof pair[0] === "string") {
|
|
203
|
+
hashes.set(pair[0], pair[1]);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return hashes;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Parse every period-terminated Erlang term in a source text.
|
|
213
|
+
*
|
|
214
|
+
* @param {string} text Source text
|
|
215
|
+
* @returns {Array} Parsed terms, in file order
|
|
216
|
+
*/
|
|
217
|
+
export function parseErlangTerms(text) {
|
|
218
|
+
const state = { text, pos: 0 };
|
|
219
|
+
const terms = [];
|
|
220
|
+
// A lock holds at most two terms; the bound stops a malformed file from
|
|
221
|
+
// driving the reader in circles.
|
|
222
|
+
for (let i = 0; i < 8; i += 1) {
|
|
223
|
+
skipTrivia(state);
|
|
224
|
+
if (state.pos >= text.length) {
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
const before = state.pos;
|
|
228
|
+
let term;
|
|
229
|
+
try {
|
|
230
|
+
term = readValue(state);
|
|
231
|
+
} catch {
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
if (state.pos === before) {
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
terms.push(term);
|
|
238
|
+
skipTrivia(state);
|
|
239
|
+
if (text[state.pos] === ".") {
|
|
240
|
+
state.pos += 1;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return terms;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Advance past whitespace and `%` line comments.
|
|
248
|
+
*
|
|
249
|
+
* @param {{text: string, pos: number}} state Reader state
|
|
250
|
+
*/
|
|
251
|
+
function skipTrivia(state) {
|
|
252
|
+
const { text } = state;
|
|
253
|
+
let { pos } = state;
|
|
254
|
+
while (pos < text.length) {
|
|
255
|
+
const char = text[pos];
|
|
256
|
+
if (char === " " || char === "\t" || char === "\r" || char === "\n") {
|
|
257
|
+
pos += 1;
|
|
258
|
+
} else if (char === "%") {
|
|
259
|
+
while (pos < text.length && text[pos] !== "\n") {
|
|
260
|
+
pos += 1;
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
state.pos = pos;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Read one Erlang value at the cursor.
|
|
271
|
+
*
|
|
272
|
+
* @param {{text: string, pos: number}} state Reader state
|
|
273
|
+
* @returns {*} Parsed value
|
|
274
|
+
*/
|
|
275
|
+
function readValue(state) {
|
|
276
|
+
skipTrivia(state);
|
|
277
|
+
const char = state.text[state.pos];
|
|
278
|
+
if (char === "{") {
|
|
279
|
+
return readTupleOrList(state, "}");
|
|
280
|
+
}
|
|
281
|
+
if (char === "[") {
|
|
282
|
+
return readTupleOrList(state, "]");
|
|
283
|
+
}
|
|
284
|
+
if (char === '"') {
|
|
285
|
+
return readString(state, '"');
|
|
286
|
+
}
|
|
287
|
+
if (char === "<" && state.text[state.pos + 1] === "<") {
|
|
288
|
+
return readBinary(state);
|
|
289
|
+
}
|
|
290
|
+
if (char === "'") {
|
|
291
|
+
return readString(state, "'");
|
|
292
|
+
}
|
|
293
|
+
if (char === "-" || (char >= "0" && char <= "9")) {
|
|
294
|
+
return readNumber(state);
|
|
295
|
+
}
|
|
296
|
+
return readAtom(state);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Read a `{...}` tuple or `[...]` list into an array.
|
|
301
|
+
*
|
|
302
|
+
* @param {{text: string, pos: number}} state Reader state
|
|
303
|
+
* @param {string} closer `}` or `]`
|
|
304
|
+
* @returns {Array} Elements
|
|
305
|
+
*/
|
|
306
|
+
function readTupleOrList(state, closer) {
|
|
307
|
+
state.pos += 1;
|
|
308
|
+
const items = [];
|
|
309
|
+
skipTrivia(state);
|
|
310
|
+
if (state.text[state.pos] === closer) {
|
|
311
|
+
state.pos += 1;
|
|
312
|
+
return items;
|
|
313
|
+
}
|
|
314
|
+
for (;;) {
|
|
315
|
+
items.push(readValue(state));
|
|
316
|
+
skipTrivia(state);
|
|
317
|
+
const next = state.text[state.pos];
|
|
318
|
+
if (next === ",") {
|
|
319
|
+
state.pos += 1;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (next === closer) {
|
|
323
|
+
state.pos += 1;
|
|
324
|
+
return items;
|
|
325
|
+
}
|
|
326
|
+
// Malformed content: bail out rather than scanning the rest of the file.
|
|
327
|
+
throw new Error(`unexpected character at offset ${state.pos}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Read a quoted string, honouring Erlang escapes.
|
|
333
|
+
*
|
|
334
|
+
* @param {{text: string, pos: number}} state Reader state
|
|
335
|
+
* @param {string} quote Quote character
|
|
336
|
+
* @returns {string} Decoded string
|
|
337
|
+
*/
|
|
338
|
+
function readString(state, quote) {
|
|
339
|
+
state.pos += 1;
|
|
340
|
+
let out = "";
|
|
341
|
+
while (state.pos < state.text.length) {
|
|
342
|
+
const char = state.text[state.pos];
|
|
343
|
+
if (char === "\\") {
|
|
344
|
+
out += state.text[state.pos + 1] ?? "";
|
|
345
|
+
state.pos += 2;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (char === quote) {
|
|
349
|
+
state.pos += 1;
|
|
350
|
+
return out;
|
|
351
|
+
}
|
|
352
|
+
out += char;
|
|
353
|
+
state.pos += 1;
|
|
354
|
+
}
|
|
355
|
+
throw new Error("unterminated string");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Read a `<<"...">>` binary as a string.
|
|
360
|
+
*
|
|
361
|
+
* @param {{text: string, pos: number}} state Reader state
|
|
362
|
+
* @returns {string} Decoded binary content
|
|
363
|
+
*/
|
|
364
|
+
function readBinary(state) {
|
|
365
|
+
state.pos += 2;
|
|
366
|
+
skipTrivia(state);
|
|
367
|
+
if (state.text[state.pos] !== '"') {
|
|
368
|
+
throw new Error("only string binaries are supported");
|
|
369
|
+
}
|
|
370
|
+
const value = readString(state, '"');
|
|
371
|
+
skipTrivia(state);
|
|
372
|
+
if (state.text[state.pos] === ">" && state.text[state.pos + 1] === ">") {
|
|
373
|
+
state.pos += 2;
|
|
374
|
+
return value;
|
|
375
|
+
}
|
|
376
|
+
throw new Error("unterminated binary");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Read an integer or float.
|
|
381
|
+
*
|
|
382
|
+
* @param {{text: string, pos: number}} state Reader state
|
|
383
|
+
* @returns {number} Parsed number
|
|
384
|
+
*/
|
|
385
|
+
function readNumber(state) {
|
|
386
|
+
const start = state.pos;
|
|
387
|
+
if (state.text[state.pos] === "-") {
|
|
388
|
+
state.pos += 1;
|
|
389
|
+
}
|
|
390
|
+
while (/[0-9.]/u.test(state.text[state.pos] || "")) {
|
|
391
|
+
state.pos += 1;
|
|
392
|
+
}
|
|
393
|
+
return Number(state.text.slice(start, state.pos));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Read a bare atom such as `pkg` or `git`.
|
|
398
|
+
*
|
|
399
|
+
* @param {{text: string, pos: number}} state Reader state
|
|
400
|
+
* @returns {string} Atom name
|
|
401
|
+
*/
|
|
402
|
+
function readAtom(state) {
|
|
403
|
+
const start = state.pos;
|
|
404
|
+
while (/[A-Za-z0-9_@]/u.test(state.text[state.pos] || "")) {
|
|
405
|
+
state.pos += 1;
|
|
406
|
+
}
|
|
407
|
+
if (state.pos === start) {
|
|
408
|
+
throw new Error(`cannot parse token at offset ${start}`);
|
|
409
|
+
}
|
|
410
|
+
return state.text.slice(start, state.pos);
|
|
411
|
+
}
|