@cdxgen/cdxgen 13.1.0 → 13.2.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.
Files changed (186) hide show
  1. package/README.md +8 -7
  2. package/bin/cdxgen.js +16 -3
  3. package/bin/evinse.js +2 -0
  4. package/data/crypto-oid.json +88 -12
  5. package/data/remediations.json +23 -0
  6. package/data/rules/ai-bom.yaml +78 -0
  7. package/data/rules/cargo-rust.yaml +132 -0
  8. package/data/rules/mcp-servers.yaml +122 -0
  9. package/lib/audit/targets.js +6 -1
  10. package/lib/cli/bomAssembly.js +12 -0
  11. package/lib/cli/index.js +192 -1
  12. package/lib/cli/jsBom.js +59 -2
  13. package/lib/cli/jvmBom.js +22 -0
  14. package/lib/cli/managedBom.js +152 -0
  15. package/lib/cli/nativeBom.js +798 -1
  16. package/lib/core/activity.js +120 -2
  17. package/lib/core/env.js +39 -1
  18. package/lib/core/userAgent.js +151 -0
  19. package/lib/ecosystems/cargoCache.js +364 -0
  20. package/lib/ecosystems/caxa.js +13 -5
  21. package/lib/ecosystems/cppEvidence.js +40 -2
  22. package/lib/ecosystems/ecosystems.js +241 -91
  23. package/lib/ecosystems/golem.js +11 -1
  24. package/lib/ecosystems/gradleutils.js +143 -2
  25. package/lib/ecosystems/kosi.js +1256 -0
  26. package/lib/ecosystems/parsers-crystal.js +179 -0
  27. package/lib/ecosystems/parsers-dotnet.js +69 -39
  28. package/lib/ecosystems/parsers-elm.js +816 -0
  29. package/lib/ecosystems/parsers-gleam.js +1 -1
  30. package/lib/ecosystems/parsers-julia.js +268 -0
  31. package/lib/ecosystems/parsers-lua.js +218 -0
  32. package/lib/ecosystems/parsers-misc.js +172 -0
  33. package/lib/ecosystems/parsers-nim.js +221 -0
  34. package/lib/ecosystems/parsers-opam.js +180 -0
  35. package/lib/ecosystems/parsers-perl.js +277 -0
  36. package/lib/ecosystems/parsers-python.js +1 -1
  37. package/lib/ecosystems/parsers-r.js +148 -0
  38. package/lib/ecosystems/parsers-rebar.js +411 -0
  39. package/lib/ecosystems/parsers-rust.js +932 -17
  40. package/lib/ecosystems/parsers-spack.js +204 -0
  41. package/lib/ecosystems/parsers-stack.js +264 -0
  42. package/lib/ecosystems/parsers-terraform.js +194 -0
  43. package/lib/ecosystems/rusi.js +126 -6
  44. package/lib/evinser/evinser.js +111 -1
  45. package/lib/helpers/agenticToolutils.js +925 -0
  46. package/lib/inventory/agentFormulationParser.js +57 -4
  47. package/lib/inventory/aiCollector.js +504 -10
  48. package/lib/inventory/aiInventory.js +12 -0
  49. package/lib/inventory/analyzer.js +488 -30
  50. package/lib/inventory/astWorker.js +6 -0
  51. package/lib/inventory/auditCategories.js +8 -0
  52. package/lib/inventory/cbomutils.js +30 -0
  53. package/lib/inventory/cdxrs.js +7 -2
  54. package/lib/inventory/communityAiConfigParser.js +215 -2
  55. package/lib/inventory/dosai.js +77 -0
  56. package/lib/inventory/envcontext.js +9 -6
  57. package/lib/inventory/evidenceUtils.js +13 -0
  58. package/lib/inventory/fetchBatch.js +74 -2
  59. package/lib/inventory/fetchRate.js +74 -11
  60. package/lib/inventory/forgeEnricher.js +3 -2
  61. package/lib/inventory/jvmToolEnv.js +6 -5
  62. package/lib/inventory/mcpConfigParser.js +166 -2
  63. package/lib/inventory/mcpDiscovery.js +11 -0
  64. package/lib/inventory/mcpServerManifest.js +389 -0
  65. package/lib/inventory/mcpbBundle.js +419 -0
  66. package/lib/inventory/plugins.js +3 -0
  67. package/lib/inventory/protobom.js +54 -88
  68. package/lib/inventory/protobomLoader.js +4 -0
  69. package/lib/inventory/rustFormulationParser.js +1 -1
  70. package/lib/inventory/source.js +10 -4
  71. package/lib/inventory/stylePackageRefs.js +41 -0
  72. package/lib/inventory/svelteUtils.js +524 -0
  73. package/lib/parsers/huggingfaceUtils.js +15 -7
  74. package/lib/parsers/onnx.js +294 -0
  75. package/lib/parsers/safetensors.js +114 -0
  76. package/lib/stages/postgen/introspection/markers.js +29 -6
  77. package/lib/stages/postgen/postgen.js +169 -1
  78. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
  79. package/package.json +25 -25
  80. package/types/lib/audit/targets.d.ts.map +1 -1
  81. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  82. package/types/lib/cli/index.d.ts +2 -2
  83. package/types/lib/cli/index.d.ts.map +1 -1
  84. package/types/lib/cli/jsBom.d.ts +23 -0
  85. package/types/lib/cli/jsBom.d.ts.map +1 -1
  86. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  87. package/types/lib/cli/managedBom.d.ts +33 -0
  88. package/types/lib/cli/managedBom.d.ts.map +1 -1
  89. package/types/lib/cli/nativeBom.d.ts +88 -8
  90. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  91. package/types/lib/core/activity.d.ts.map +1 -1
  92. package/types/lib/core/env.d.ts +21 -0
  93. package/types/lib/core/env.d.ts.map +1 -1
  94. package/types/lib/core/userAgent.d.ts +56 -0
  95. package/types/lib/core/userAgent.d.ts.map +1 -0
  96. package/types/lib/ecosystems/cargoCache.d.ts +75 -0
  97. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
  98. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  99. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  100. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  101. package/types/lib/ecosystems/ecosystems.d.ts +15 -1
  102. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  103. package/types/lib/ecosystems/gradleutils.d.ts +20 -0
  104. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  105. package/types/lib/ecosystems/kosi.d.ts +73 -0
  106. package/types/lib/ecosystems/kosi.d.ts.map +1 -0
  107. package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
  108. package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
  109. package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
  110. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  111. package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
  112. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
  113. package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
  114. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
  115. package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
  116. package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
  117. package/types/lib/ecosystems/parsers-misc.d.ts +23 -0
  118. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  119. package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
  120. package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
  121. package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
  122. package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
  123. package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
  124. package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
  125. package/types/lib/ecosystems/parsers-r.d.ts +35 -0
  126. package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
  127. package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
  128. package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
  129. package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
  130. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  131. package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
  132. package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
  133. package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
  134. package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
  135. package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
  136. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
  137. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  138. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  139. package/types/lib/evinser/evinser.d.ts +29 -0
  140. package/types/lib/evinser/evinser.d.ts.map +1 -1
  141. package/types/lib/helpers/agenticToolutils.d.ts +87 -0
  142. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
  143. package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
  144. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  145. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  146. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  147. package/types/lib/inventory/auditCategories.d.ts +4 -0
  148. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  149. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  150. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  151. package/types/lib/inventory/dosai.d.ts +23 -0
  152. package/types/lib/inventory/dosai.d.ts.map +1 -1
  153. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  154. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  155. package/types/lib/inventory/fetchBatch.d.ts +34 -0
  156. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  157. package/types/lib/inventory/fetchRate.d.ts +9 -6
  158. package/types/lib/inventory/fetchRate.d.ts.map +1 -1
  159. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  160. package/types/lib/inventory/jvmToolEnv.d.ts +5 -4
  161. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
  162. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  163. package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
  164. package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
  165. package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
  166. package/types/lib/inventory/mcpbBundle.d.ts +19 -0
  167. package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
  168. package/types/lib/inventory/plugins.d.ts.map +1 -1
  169. package/types/lib/inventory/protobom.d.ts +18 -13
  170. package/types/lib/inventory/protobom.d.ts.map +1 -1
  171. package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
  172. package/types/lib/inventory/source.d.ts.map +1 -1
  173. package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
  174. package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
  175. package/types/lib/inventory/svelteUtils.d.ts +124 -0
  176. package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
  177. package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
  178. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  179. package/types/lib/parsers/onnx.d.ts +28 -0
  180. package/types/lib/parsers/onnx.d.ts.map +1 -0
  181. package/types/lib/parsers/safetensors.d.ts +22 -0
  182. package/types/lib/parsers/safetensors.d.ts.map +1 -0
  183. package/types/lib/stages/postgen/introspection/markers.d.ts +3 -2
  184. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -1
  185. package/types/lib/stages/postgen/postgen.d.ts +33 -0
  186. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
@@ -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
+ }