@cdxgen/cdxgen 13.0.0 → 13.1.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 (170) hide show
  1. package/README.md +26 -13
  2. package/bin/audit.js +6 -0
  3. package/bin/cdxgen.js +140 -8
  4. package/bin/convert.js +7 -0
  5. package/bin/evinse.js +1 -1
  6. package/bin/hbom.js +13 -4
  7. package/bin/tracebom.js +9 -1
  8. package/data/component-tags.json +8 -1
  9. package/data/frameworks-list.json +86 -1
  10. package/data/remediations.json +1144 -0
  11. package/data/rules/asar-archives.yaml +1 -1
  12. package/data/rules/build-fidelity.yaml +782 -0
  13. package/data/rules/package-integrity.yaml +13 -13
  14. package/lib/audit/index.js +48 -4
  15. package/lib/audit/scoring.js +2 -8
  16. package/lib/cli/bomAssembly.js +83 -15
  17. package/lib/cli/cliOptions.js +124 -7
  18. package/lib/cli/index.js +76 -48
  19. package/lib/cli/jsBom.js +32 -3
  20. package/lib/cli/jvmBom.js +1494 -1315
  21. package/lib/cli/managedBom.js +97 -28
  22. package/lib/cli/nativeBom.js +189 -10
  23. package/lib/core/buildLedger.js +799 -0
  24. package/lib/core/deferredExit.js +141 -0
  25. package/lib/core/env.js +69 -1
  26. package/lib/core/fs.js +60 -0
  27. package/lib/core/logger.js +76 -25
  28. package/lib/core/severity.js +16 -0
  29. package/lib/core/syncFileWriter.js +106 -0
  30. package/lib/core/ui.js +46 -2
  31. package/lib/ecosystems/ecosystems.js +9 -0
  32. package/lib/ecosystems/gradleutils.js +355 -15
  33. package/lib/ecosystems/parsers-js.js +26 -0
  34. package/lib/ecosystems/parsers-jvm.js +9 -0
  35. package/lib/ecosystems/parsers-misc.js +16 -2
  36. package/lib/ecosystems/parsers-python.js +171 -35
  37. package/lib/ecosystems/parsers-rust.js +12 -2
  38. package/lib/ecosystems/pypiClassifiers.js +326 -0
  39. package/lib/ecosystems/pythonutils.js +49 -1
  40. package/lib/ecosystems/rubyutils.js +13 -0
  41. package/lib/ecosystems/utils.js +9 -1
  42. package/lib/evinser/evinser.js +30 -12
  43. package/lib/helpers/remote/dependency-track.js +81 -14
  44. package/lib/inventory/auditCategories.js +9 -0
  45. package/lib/inventory/depsUtils.js +72 -0
  46. package/lib/inventory/dosai.js +431 -22
  47. package/lib/inventory/envcontext.js +211 -19
  48. package/lib/inventory/fetchBatch.js +57 -0
  49. package/lib/inventory/jvmToolEnv.js +593 -0
  50. package/lib/inventory/osinfo.js +123 -16
  51. package/lib/inventory/table.js +9 -5
  52. package/lib/inventory/toolRequirements.js +903 -0
  53. package/lib/managers/binary.js +216 -40
  54. package/lib/managers/docker.js +26 -5
  55. package/lib/server/server.js +1 -1
  56. package/lib/stages/postgen/annotationBuilder.js +97 -0
  57. package/lib/stages/postgen/auditBom.js +76 -79
  58. package/lib/stages/postgen/introspection/annotate.js +320 -0
  59. package/lib/stages/postgen/introspection/commandFacts.js +293 -0
  60. package/lib/stages/postgen/introspection/emit.js +192 -0
  61. package/lib/stages/postgen/introspection/formulationEvidence.js +427 -0
  62. package/lib/stages/postgen/introspection/markers.js +227 -0
  63. package/lib/stages/postgen/introspection/reflect.js +1214 -0
  64. package/lib/stages/postgen/introspection/report.js +1208 -0
  65. package/lib/stages/postgen/introspection/score.js +1067 -0
  66. package/lib/stages/postgen/introspection/shapeCommand.js +304 -0
  67. package/lib/stages/postgen/parentEdge.js +156 -0
  68. package/lib/stages/postgen/postgen.js +115 -2
  69. package/lib/stages/postgen/ruleEngine.js +17 -0
  70. package/lib/stages/pregen/envAudit.js +42 -2
  71. package/lib/stages/pregen/pregen.js +1171 -6
  72. package/lib/validator/bomValidator.js +11 -2
  73. package/package.json +14 -14
  74. package/types/lib/audit/index.d.ts.map +1 -1
  75. package/types/lib/audit/scoring.d.ts +2 -8
  76. package/types/lib/audit/scoring.d.ts.map +1 -1
  77. package/types/lib/cli/bomAssembly.d.ts +14 -0
  78. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  79. package/types/lib/cli/cliOptions.d.ts +2 -1
  80. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  81. package/types/lib/cli/index.d.ts +8 -0
  82. package/types/lib/cli/index.d.ts.map +1 -1
  83. package/types/lib/cli/jsBom.d.ts.map +1 -1
  84. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  85. package/types/lib/cli/managedBom.d.ts.map +1 -1
  86. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  87. package/types/lib/core/buildLedger.d.ts +272 -0
  88. package/types/lib/core/buildLedger.d.ts.map +1 -0
  89. package/types/lib/core/deferredExit.d.ts +84 -0
  90. package/types/lib/core/deferredExit.d.ts.map +1 -0
  91. package/types/lib/core/env.d.ts +22 -1
  92. package/types/lib/core/env.d.ts.map +1 -1
  93. package/types/lib/core/fs.d.ts +21 -0
  94. package/types/lib/core/fs.d.ts.map +1 -1
  95. package/types/lib/core/logger.d.ts +14 -1
  96. package/types/lib/core/logger.d.ts.map +1 -1
  97. package/types/lib/core/severity.d.ts +11 -0
  98. package/types/lib/core/severity.d.ts.map +1 -0
  99. package/types/lib/core/syncFileWriter.d.ts +33 -0
  100. package/types/lib/core/syncFileWriter.d.ts.map +1 -0
  101. package/types/lib/core/ui.d.ts +1 -1
  102. package/types/lib/core/ui.d.ts.map +1 -1
  103. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  104. package/types/lib/ecosystems/gradleutils.d.ts +33 -0
  105. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  106. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  107. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  108. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  109. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  110. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  111. package/types/lib/ecosystems/pypiClassifiers.d.ts +80 -0
  112. package/types/lib/ecosystems/pypiClassifiers.d.ts.map +1 -0
  113. package/types/lib/ecosystems/pythonutils.d.ts +20 -0
  114. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  115. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  116. package/types/lib/ecosystems/utils.d.ts +4 -4
  117. package/types/lib/ecosystems/utils.d.ts.map +1 -1
  118. package/types/lib/evinser/evinser.d.ts +5 -1
  119. package/types/lib/evinser/evinser.d.ts.map +1 -1
  120. package/types/lib/helpers/remote/dependency-track.d.ts +45 -4
  121. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  122. package/types/lib/inventory/auditCategories.d.ts +8 -0
  123. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  124. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  125. package/types/lib/inventory/dosai.d.ts +20 -0
  126. package/types/lib/inventory/dosai.d.ts.map +1 -1
  127. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  128. package/types/lib/inventory/fetchBatch.d.ts +9 -0
  129. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  130. package/types/lib/inventory/jvmToolEnv.d.ts +189 -0
  131. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
  132. package/types/lib/inventory/osinfo.d.ts +83 -2
  133. package/types/lib/inventory/osinfo.d.ts.map +1 -1
  134. package/types/lib/inventory/table.d.ts +4 -1
  135. package/types/lib/inventory/table.d.ts.map +1 -1
  136. package/types/lib/inventory/toolRequirements.d.ts +216 -0
  137. package/types/lib/inventory/toolRequirements.d.ts.map +1 -0
  138. package/types/lib/managers/binary.d.ts +71 -0
  139. package/types/lib/managers/binary.d.ts.map +1 -1
  140. package/types/lib/managers/docker.d.ts.map +1 -1
  141. package/types/lib/stages/postgen/annotationBuilder.d.ts +53 -0
  142. package/types/lib/stages/postgen/annotationBuilder.d.ts.map +1 -0
  143. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  144. package/types/lib/stages/postgen/introspection/annotate.d.ts +81 -0
  145. package/types/lib/stages/postgen/introspection/annotate.d.ts.map +1 -0
  146. package/types/lib/stages/postgen/introspection/commandFacts.d.ts +32 -0
  147. package/types/lib/stages/postgen/introspection/commandFacts.d.ts.map +1 -0
  148. package/types/lib/stages/postgen/introspection/emit.d.ts +71 -0
  149. package/types/lib/stages/postgen/introspection/emit.d.ts.map +1 -0
  150. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts +154 -0
  151. package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts.map +1 -0
  152. package/types/lib/stages/postgen/introspection/markers.d.ts +57 -0
  153. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -0
  154. package/types/lib/stages/postgen/introspection/reflect.d.ts +77 -0
  155. package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -0
  156. package/types/lib/stages/postgen/introspection/report.d.ts +85 -0
  157. package/types/lib/stages/postgen/introspection/report.d.ts.map +1 -0
  158. package/types/lib/stages/postgen/introspection/score.d.ts +149 -0
  159. package/types/lib/stages/postgen/introspection/score.d.ts.map +1 -0
  160. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts +79 -0
  161. package/types/lib/stages/postgen/introspection/shapeCommand.d.ts.map +1 -0
  162. package/types/lib/stages/postgen/parentEdge.d.ts +35 -0
  163. package/types/lib/stages/postgen/parentEdge.d.ts.map +1 -0
  164. package/types/lib/stages/postgen/postgen.d.ts +2 -2
  165. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  166. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  167. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  168. package/types/lib/stages/pregen/pregen.d.ts +131 -0
  169. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  170. package/types/lib/validator/bomValidator.d.ts.map +1 -1
@@ -0,0 +1,593 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ import { readEnvironmentVariable } from "../core/activity.js";
5
+ import { JVM_BUILD_TOOL_TYPE_PREFIXES } from "../core/env.js";
6
+ import { safeExistsSync } from "../core/fs.js";
7
+ import {
8
+ compareVersions,
9
+ extractJavaMajor,
10
+ isPrereleaseVersion,
11
+ } from "./toolRequirements.js";
12
+
13
+ /**
14
+ * The general-purpose version comparator, prerelease classifier, and JDK
15
+ * major extractor live in `toolRequirements.js` so every ecosystem shares one
16
+ * comparator; they are re-exported here under the names this module's
17
+ * consumers already use.
18
+ */
19
+ export {
20
+ compareVersions as compareSdkmanVersions,
21
+ extractJavaMajor,
22
+ isPrereleaseVersion,
23
+ };
24
+
25
+ /**
26
+ * Upper bound on the `.sdkmanrc` content that is scanned, so that a corrupt or
27
+ * hostile file cannot force cdxgen to walk unbounded content.
28
+ *
29
+ * @type {number}
30
+ */
31
+ const MAX_SDKMANRC_BYTES = 64 * 1024;
32
+
33
+ /**
34
+ * Maximum accepted length for a single sdkman version identifier.
35
+ *
36
+ * @type {number}
37
+ */
38
+ const MAX_VERSION_TOKEN_LENGTH = 64;
39
+
40
+ /**
41
+ * sdkman candidate names that cdxgen can provision for JVM builds. The keys
42
+ * are the canonical candidate identifiers used with `sdk install <candidate>`.
43
+ */
44
+ const JVM_BUILD_TOOL_CANDIDATES = ["maven", "gradle", "sbt", "scala"];
45
+
46
+ /**
47
+ * Anchored validator for sdkman version identifiers such as `3.9.9`,
48
+ * `4.0.0-rc-5`, `8.14`, or `21.0.7-tem`. Versions are interpolated into a
49
+ * shell command by the sdkman installer, so every identifier must be a single
50
+ * safe token: it must start with a digit and may only contain digits, ascii
51
+ * letters, dots, and hyphens.
52
+ *
53
+ * Every separator must be followed by at least one alphanumeric character, and
54
+ * a single repeated group covers both the dotted numeric part and the vendor
55
+ * or prerelease suffix. That leaves each character exactly one role, so the
56
+ * pattern matches in linear time on untrusted input.
57
+ *
58
+ * @param {string} version Candidate version identifier
59
+ * @returns {boolean} True when the identifier is safe to pass to sdkman
60
+ */
61
+ export function isValidSdkmanVersion(version) {
62
+ return (
63
+ typeof version === "string" &&
64
+ version.length > 0 &&
65
+ version.length <= MAX_VERSION_TOKEN_LENGTH &&
66
+ /^\d+(?:[-.][0-9A-Za-z]+)*$/.test(version)
67
+ );
68
+ }
69
+
70
+ /**
71
+ * Parse a versioned JVM build tool project type such as `maven3.9.9`,
72
+ * `mvn3.9.9`, `gradle8.14`, `sbt1.10`, or `scala3.6.4` into its sdkman
73
+ * candidate name and version token.
74
+ *
75
+ * The returned object distinguishes between a syntactically valid pin and a
76
+ * rejected one so callers can print a precise CLI error instead of silently
77
+ * ignoring the type.
78
+ *
79
+ * @param {string} projectType Project type argument from the CLI
80
+ * @returns {Object|undefined} `{ tool, version, valid }` when the type looks
81
+ * like a versioned JVM build tool, `undefined` otherwise. `valid` is false
82
+ * when the version token fails validation.
83
+ */
84
+ export function parseJvmToolProjectType(projectType) {
85
+ const pt = `${projectType || ""}`.toLowerCase();
86
+ for (const prefix of JVM_BUILD_TOOL_TYPE_PREFIXES) {
87
+ if (!pt.startsWith(prefix)) {
88
+ continue;
89
+ }
90
+ const version = pt.slice(prefix.length);
91
+ if (!/^\d/.test(version)) {
92
+ // Bare aliases such as "maven" or unrelated types are not versioned pins.
93
+ continue;
94
+ }
95
+ const tool = prefix === "mvn" ? "maven" : prefix;
96
+ return { tool, version, valid: isValidSdkmanVersion(version) };
97
+ }
98
+ return undefined;
99
+ }
100
+
101
+ /**
102
+ * Parse a `.sdkmanrc` environment file into the sdkman candidates it pins.
103
+ * Only the JVM-related candidates cdxgen knows how to provision are kept;
104
+ * unknown keys are ignored so newer sdkman files remain readable. Malformed
105
+ * lines are skipped instead of aborting the whole file.
106
+ *
107
+ * @param {string} filePath Project directory that may contain a `.sdkmanrc`
108
+ * @returns {Object|undefined} Map of candidate name to exact version
109
+ * identifier, or `undefined` when the file is missing or unreadable.
110
+ */
111
+ export function parseSdkmanrc(filePath) {
112
+ const sdkmanrcFile = join(filePath, ".sdkmanrc");
113
+ if (!safeExistsSync(sdkmanrcFile)) {
114
+ return undefined;
115
+ }
116
+ let content;
117
+ try {
118
+ content = readFileSync(sdkmanrcFile, {
119
+ encoding: "utf-8",
120
+ flag: "r",
121
+ }).slice(0, MAX_SDKMANRC_BYTES);
122
+ } catch (_e) {
123
+ return undefined;
124
+ }
125
+ const pins = {};
126
+ const acceptedCandidates = new Set(["java", ...JVM_BUILD_TOOL_CANDIDATES]);
127
+ for (const rawLine of content.split(/\r?\n/)) {
128
+ const line = rawLine.trim();
129
+ if (!line || line.startsWith("#") || line.startsWith("!")) {
130
+ continue;
131
+ }
132
+ const separatorIndex = line.indexOf("=");
133
+ if (separatorIndex <= 0) {
134
+ continue;
135
+ }
136
+ const candidate = line.slice(0, separatorIndex).trim().toLowerCase();
137
+ const version = line.slice(separatorIndex + 1).trim();
138
+ if (!acceptedCandidates.has(candidate)) {
139
+ continue;
140
+ }
141
+ if (!isValidSdkmanVersion(version)) {
142
+ continue;
143
+ }
144
+ if (candidate === "java" && !version.includes("-")) {
145
+ // sdkman java identifiers carry a vendor suffix, e.g. 21.0.7-tem. A
146
+ // bare number such as "21" is not installable, so skip it.
147
+ continue;
148
+ }
149
+ pins[candidate] = version;
150
+ }
151
+ return Object.keys(pins).length ? pins : undefined;
152
+ }
153
+
154
+ /**
155
+ * Read the version pinned by a Gradle wrapper's `gradle-wrapper.properties`.
156
+ * The `distributionUrl` value may escape colons as `\:` per the Java
157
+ * properties format; the escapes are removed before parsing.
158
+ *
159
+ * @param {string} rootPath Directory holding the `gradle/wrapper` folder
160
+ * @returns {Object|undefined} `{ version, distributionUrl, distributionSha256Sum? }`
161
+ * or `undefined` when the wrapper is missing or unparsable.
162
+ */
163
+ export function readGradleWrapperVersion(rootPath) {
164
+ const wrapperPropsFile = join(
165
+ rootPath,
166
+ "gradle",
167
+ "wrapper",
168
+ "gradle-wrapper.properties",
169
+ );
170
+ if (!safeExistsSync(wrapperPropsFile)) {
171
+ return undefined;
172
+ }
173
+ let propContent;
174
+ try {
175
+ propContent = readFileSync(wrapperPropsFile, {
176
+ encoding: "utf-8",
177
+ flag: "r",
178
+ });
179
+ } catch (_e) {
180
+ return undefined;
181
+ }
182
+ const properties = parsePropertiesContent(propContent);
183
+ const distributionUrl = properties.distributionUrl?.replaceAll("\\:", ":");
184
+ if (!distributionUrl) {
185
+ return undefined;
186
+ }
187
+ const version = extractVersionFromZipBasename(distributionUrl);
188
+ if (!version) {
189
+ return undefined;
190
+ }
191
+ const wrapperInfo = { version, distributionUrl };
192
+ const distributionSha256Sum = properties.distributionSha256Sum;
193
+ // The checksum is copied into the BOM as a CycloneDX hash, whose schema
194
+ // accepts hex digests only. Anything else in the properties file is dropped
195
+ // rather than emitted as a hash that would fail validation downstream.
196
+ if (/^[a-fA-F0-9]{64}$/.test(`${distributionSha256Sum}`)) {
197
+ wrapperInfo.distributionSha256Sum = distributionSha256Sum;
198
+ }
199
+ return wrapperInfo;
200
+ }
201
+
202
+ /**
203
+ * Read the version pinned by a Maven wrapper's `maven-wrapper.properties`.
204
+ * Legacy wrappers (pre wrapper-distribution downloads) lack a
205
+ * `distributionUrl` and are treated as unpinned.
206
+ *
207
+ * @param {string} rootPath Directory holding the `.mvn/wrapper` folder
208
+ * @returns {Object|undefined} `{ version, distributionUrl }` or `undefined`
209
+ * when the wrapper is missing or unpinned.
210
+ */
211
+ export function readMavenWrapperVersion(rootPath) {
212
+ const wrapperPropsFile = join(
213
+ rootPath,
214
+ ".mvn",
215
+ "wrapper",
216
+ "maven-wrapper.properties",
217
+ );
218
+ if (!safeExistsSync(wrapperPropsFile)) {
219
+ return undefined;
220
+ }
221
+ let propContent;
222
+ try {
223
+ propContent = readFileSync(wrapperPropsFile, {
224
+ encoding: "utf-8",
225
+ flag: "r",
226
+ });
227
+ } catch (_e) {
228
+ return undefined;
229
+ }
230
+ const properties = parsePropertiesContent(propContent);
231
+ const distributionUrl = properties.distributionUrl?.replaceAll("\\:", ":");
232
+ if (!distributionUrl) {
233
+ return undefined;
234
+ }
235
+ const version = extractVersionFromZipBasename(distributionUrl);
236
+ if (!version) {
237
+ return undefined;
238
+ }
239
+ return { version, distributionUrl };
240
+ }
241
+
242
+ /**
243
+ * Read the sbt version declared in `project/build.properties`. Kept here as a
244
+ * small properties-file read so environment preparation does not depend on
245
+ * higher layers.
246
+ *
247
+ * @param {string} projectPath SBT project directory
248
+ * @returns {string|undefined} Declared sbt version, when present and valid.
249
+ */
250
+ export function readSbtBuildPropertiesVersion(projectPath) {
251
+ const buildPropsFile = join(projectPath, "project", "build.properties");
252
+ if (!safeExistsSync(buildPropsFile)) {
253
+ return undefined;
254
+ }
255
+ let propContent;
256
+ try {
257
+ propContent = readFileSync(buildPropsFile, {
258
+ encoding: "utf-8",
259
+ flag: "r",
260
+ });
261
+ } catch (_e) {
262
+ return undefined;
263
+ }
264
+ const sbtVersion = parsePropertiesContent(propContent)["sbt.version"];
265
+ return isValidSdkmanVersion(sbtVersion) ? sbtVersion : undefined;
266
+ }
267
+
268
+ /**
269
+ * Parse the text content of a Java `.properties` file into a flat map. Only
270
+ * the keys cdxgen consumes are of interest, so a line-based parse with
271
+ * `key=value` splitting is sufficient and avoids regex-heavy parsing of
272
+ * untrusted files.
273
+ *
274
+ * @param {string} content Properties file content
275
+ * @returns {Object} Map of property name to raw value
276
+ */
277
+ function parsePropertiesContent(content) {
278
+ const properties = {};
279
+ for (const rawLine of content.split(/\r?\n/)) {
280
+ const line = rawLine.trim();
281
+ if (!line || line.startsWith("#") || line.startsWith("!")) {
282
+ continue;
283
+ }
284
+ const separatorIndex = line.indexOf("=");
285
+ if (separatorIndex <= 0) {
286
+ continue;
287
+ }
288
+ const key = line.slice(0, separatorIndex).trim();
289
+ const value = line.slice(separatorIndex + 1).trim();
290
+ if (key && !(key in properties)) {
291
+ properties[key] = value;
292
+ }
293
+ }
294
+ return properties;
295
+ }
296
+
297
+ /**
298
+ * Extract a version identifier from the basename of a distribution zip such
299
+ * as `gradle-8.14.3-bin.zip` or `apache-maven-3.9.9-bin.zip` using token
300
+ * splits instead of a capture-everything regex.
301
+ *
302
+ * @param {string} url Distribution URL
303
+ * @returns {string|undefined} Validated version identifier.
304
+ */
305
+ function extractVersionFromZipBasename(url) {
306
+ const pathSeparatorIndex = Math.max(
307
+ url.lastIndexOf("/"),
308
+ url.lastIndexOf("\\"),
309
+ );
310
+ const basename =
311
+ pathSeparatorIndex >= 0 ? url.slice(pathSeparatorIndex + 1) : url;
312
+ for (const token of basename.split("-")) {
313
+ if (/^\d/.test(token) && isValidSdkmanVersion(token)) {
314
+ return token;
315
+ }
316
+ }
317
+ return undefined;
318
+ }
319
+
320
+ /**
321
+ * Minimum JDK major version required to run each build tool, keyed by the
322
+ * tool's major version line. Versions absent from the table fall back to the
323
+ * tool's `default` entry.
324
+ *
325
+ * Sources: Maven release history (maven.apache.org/docs/history.html), the
326
+ * Gradle compatibility table (docs.gradle.org), and the sbt setup notes.
327
+ *
328
+ * @type {Object<string, Object<number, number>|number>}
329
+ */
330
+ export const TOOL_JDK_REQUIREMENTS = {
331
+ maven: { default: 8, 3: 8, 4: 17 },
332
+ gradle: { default: 8, 8: 8, 9: 17 },
333
+ sbt: { default: 8, 1: 8, 2: 17 },
334
+ scala: { default: 8, 2: 8, 3: 8 },
335
+ };
336
+
337
+ /**
338
+ * Minimum Gradle version that supports a given JDK major, for the
339
+ * combinations where running an older Gradle on a newer JDK is known to fail.
340
+ * Used to warn instead of guess-installing when the environment contradicts
341
+ * the project's Gradle pin.
342
+ *
343
+ * @type {Object<number, string>}
344
+ */
345
+ export const GRADLE_JAVA_CAPS = {
346
+ 17: "7.3",
347
+ 21: "8.5",
348
+ 23: "8.10",
349
+ 25: "9.1.0",
350
+ 26: "9.4.0",
351
+ };
352
+
353
+ /** JDK major that satisfies every current build tool minimum. */
354
+ export const DEFAULT_JAVA_MAJOR = 21;
355
+
356
+ /**
357
+ * Look up the minimum JDK major required to run a tool version.
358
+ *
359
+ * @param {string} tool Tool name (maven, gradle, sbt, scala)
360
+ * @param {string} version Tool version identifier
361
+ * @returns {number|undefined} Minimum JDK major, when known.
362
+ */
363
+ export function minimumJdkForToolVersion(tool, version) {
364
+ const toolRequirements = TOOL_JDK_REQUIREMENTS[`${tool}`.toLowerCase()];
365
+ if (!toolRequirements || !isValidSdkmanVersion(`${version}`)) {
366
+ return undefined;
367
+ }
368
+ const major = Number.parseInt(`${version}`, 10);
369
+ if (Number.isNaN(major)) {
370
+ return undefined;
371
+ }
372
+ return toolRequirements[major] ?? toolRequirements.default;
373
+ }
374
+
375
+ /**
376
+ * Compute the highest JDK major required across a set of tool pins.
377
+ *
378
+ * @param {Array<{tool: string, version: string}>} toolPins Pinned tools
379
+ * @returns {number|undefined} Highest required JDK major, when any pin maps.
380
+ */
381
+ export function determineRequiredJavaVersion(toolPins) {
382
+ let required;
383
+ for (const pin of toolPins || []) {
384
+ const minimum = minimumJdkForToolVersion(pin?.tool, pin?.version);
385
+ if (
386
+ minimum !== undefined &&
387
+ (required === undefined || minimum > required)
388
+ ) {
389
+ required = minimum;
390
+ }
391
+ }
392
+ return required;
393
+ }
394
+
395
+ /**
396
+ * Minimum Gradle version that can run on the given JDK major, when such a cap
397
+ * is known.
398
+ *
399
+ * @param {number} javaMajor JDK major version
400
+ * @returns {string|undefined} Minimum Gradle version, when capped.
401
+ */
402
+ export function minimumGradleVersionForJava(javaMajor) {
403
+ return GRADLE_JAVA_CAPS[javaMajor];
404
+ }
405
+
406
+ /**
407
+ * Collect the version tokens printed by `sdk list <candidate>`. The output is
408
+ * a fixed-width table whose cells may carry `+`, `*`, or `>` status markers;
409
+ * every token that passes the sdkman version validator is kept, which
410
+ * naturally drops banners, legends, and shell noise.
411
+ *
412
+ * @param {string} stdout Raw `sdk list` output
413
+ * @returns {string[]} Version identifiers found in the output.
414
+ */
415
+ export function extractSdkListVersions(stdout) {
416
+ if (typeof stdout !== "string") {
417
+ return [];
418
+ }
419
+ const versions = [];
420
+ for (const rawLine of stdout.split(/\r?\n/)) {
421
+ for (const token of rawLine.trim().split(/\s+/)) {
422
+ const cleaned = token.replace(/^[+*>]+/, "");
423
+ if (cleaned && isValidSdkmanVersion(cleaned)) {
424
+ versions.push(cleaned);
425
+ }
426
+ }
427
+ }
428
+ return versions;
429
+ }
430
+
431
+ /**
432
+ * Resolve a partial version prefix such as `3.9` or `8.14` to the newest
433
+ * matching version in `sdk list` output. Stable versions are preferred unless
434
+ * the requested prefix itself is a prerelease.
435
+ *
436
+ * @param {string} stdout Raw `sdk list <candidate>` output
437
+ * @param {string} prefix Partial or exact version prefix; empty matches all
438
+ * @returns {string|undefined} Best matching version identifier.
439
+ */
440
+ export function resolvePartialVersion(stdout, prefix) {
441
+ const versions = extractSdkListVersions(stdout);
442
+ const wantedPrefix = `${prefix || ""}`;
443
+ const allowPrerelease = isPrereleaseVersion(wantedPrefix);
444
+ let best;
445
+ for (const version of versions) {
446
+ const matches =
447
+ !wantedPrefix ||
448
+ version === wantedPrefix ||
449
+ version.startsWith(`${wantedPrefix}.`) ||
450
+ version.startsWith(`${wantedPrefix}-`);
451
+ if (!matches) {
452
+ continue;
453
+ }
454
+ if (!allowPrerelease && isPrereleaseVersion(version)) {
455
+ continue;
456
+ }
457
+ if (best === undefined || compareVersions(version, best) > 0) {
458
+ best = version;
459
+ }
460
+ }
461
+ return best;
462
+ }
463
+
464
+ /**
465
+ * Environment variable hints that carry a tool version, used by container
466
+ * images and CI setups to describe the preinstalled JVM toolchain.
467
+ *
468
+ * @type {Object<string, string>}
469
+ */
470
+ export const JVM_TOOL_VERSION_ENV_HINTS = {
471
+ maven: "MAVEN_VERSION",
472
+ gradle: "GRADLE_VERSION",
473
+ sbt: "SBT_VERSION",
474
+ scala: "SCALA_VERSION",
475
+ };
476
+
477
+ /**
478
+ * Environment variable overrides that declare the sdkman version to use for
479
+ * a tool, mirroring the existing `JAVA<NN>_TOOL` pattern.
480
+ *
481
+ * @type {Object<string, string>}
482
+ */
483
+ export const JVM_TOOL_VERSION_OVERRIDES = {
484
+ maven: "MAVEN_TOOL",
485
+ gradle: "GRADLE_TOOL",
486
+ sbt: "SBT_TOOL",
487
+ scala: "SCALA_TOOL",
488
+ };
489
+
490
+ /**
491
+ * Read the `MAVEN_TOOL`/`GRADLE_TOOL`/`SBT_TOOL`/`SCALA_TOOL` override for a
492
+ * tool, when set to a valid sdkman version identifier.
493
+ *
494
+ * @param {string} tool Tool name
495
+ * @returns {string|undefined} Overridden version identifier.
496
+ */
497
+ export function readToolVersionOverride(tool) {
498
+ const envName = JVM_TOOL_VERSION_OVERRIDES[tool];
499
+ if (!envName) {
500
+ return undefined;
501
+ }
502
+ const value = readEnvironmentVariable(envName);
503
+ return isValidSdkmanVersion(value) ? value : undefined;
504
+ }
505
+
506
+ /**
507
+ * Read the container/CI version hint (`MAVEN_VERSION` etc.) for a tool, when
508
+ * set to a valid sdkman version identifier.
509
+ *
510
+ * @param {string} tool Tool name
511
+ * @returns {string|undefined} Hinted version identifier.
512
+ */
513
+ export function readToolVersionEnvHint(tool) {
514
+ const envName = JVM_TOOL_VERSION_ENV_HINTS[tool];
515
+ if (!envName) {
516
+ return undefined;
517
+ }
518
+ const value = readEnvironmentVariable(envName);
519
+ return isValidSdkmanVersion(value) ? value : undefined;
520
+ }
521
+
522
+ /**
523
+ * Well-known build files scanned for the JDK major a project targets.
524
+ *
525
+ * @type {string[]}
526
+ */
527
+ const JAVA_BUILD_FILES = ["build.gradle", "build.gradle.kts", "pom.xml"];
528
+
529
+ /**
530
+ * Detect the JDK major a project asks for from its build files: Gradle
531
+ * toolchain declarations (`JavaLanguageVersion.of(N)`) and Maven compiler or
532
+ * enforcer properties (`maven.compiler.release`, `maven.compiler.source`,
533
+ * `requireJavaVersion`). The scan is intentionally line-based and
534
+ * conservative: it only recognises whole-number values.
535
+ *
536
+ * @param {string} projectPath Project directory
537
+ * @returns {number|undefined} JDK major requested by the project.
538
+ */
539
+ export function detectProjectJavaMajor(projectPath) {
540
+ if (!projectPath) {
541
+ return undefined;
542
+ }
543
+ for (const buildFileName of JAVA_BUILD_FILES) {
544
+ const buildFile = join(projectPath, buildFileName);
545
+ if (!safeExistsSync(buildFile)) {
546
+ continue;
547
+ }
548
+ let content;
549
+ try {
550
+ content = readFileSync(buildFile, { encoding: "utf-8", flag: "r" });
551
+ } catch (_e) {
552
+ continue;
553
+ }
554
+ const detected = scanBuildFileForJavaMajor(content);
555
+ if (detected !== undefined) {
556
+ return detected;
557
+ }
558
+ }
559
+ return undefined;
560
+ }
561
+
562
+ /**
563
+ * Scan Gradle or Maven build file content for the JDK major it targets.
564
+ *
565
+ * @param {string} content Build file content
566
+ * @returns {number|undefined} JDK major, when recognisable.
567
+ */
568
+ function scanBuildFileForJavaMajor(content) {
569
+ for (const rawLine of content.split(/\r?\n/)) {
570
+ const line = rawLine.trim();
571
+ let match = line.match(/JavaLanguageVersion\.of\((\d{1,2})\)/);
572
+ if (match) {
573
+ return Number.parseInt(match[1], 10);
574
+ }
575
+ match = line.match(
576
+ /<(?:maven\.compiler\.release|maven\.compiler\.source|release|source)>(?:1\.)?(\d{1,2})</,
577
+ );
578
+ if (match) {
579
+ return Number.parseInt(match[1], 10);
580
+ }
581
+ // Enforcer requireJavaVersion ranges such as <version>[17,)</version>.
582
+ // Dependency and plugin version ranges share this element name, so only
583
+ // values that are plausible JDK majors are accepted.
584
+ match = line.match(/<version>\[(?:1\.)?(\d{1,2})[,)\]]/);
585
+ if (match) {
586
+ const major = Number.parseInt(match[1], 10);
587
+ if (major >= 8) {
588
+ return major;
589
+ }
590
+ }
591
+ }
592
+ return undefined;
593
+ }