@cdxgen/cdxgen 13.3.0 → 13.4.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 (255) hide show
  1. package/README.md +9 -6
  2. package/bin/cdxgen.js +90 -26
  3. package/bin/evinse.js +18 -1
  4. package/bin/repl.js +86 -0
  5. package/bin/sign.js +45 -15
  6. package/bin/tracebom.js +5 -1
  7. package/bin/validate.js +84 -18
  8. package/bin/verify.js +155 -86
  9. package/data/helpers/init.gradle +46 -21
  10. package/data/remediations.json +61 -0
  11. package/data/rules/dependency-sources.yaml +67 -0
  12. package/data/rules/scala-evidence.yaml +115 -0
  13. package/lib/audit/index.js +38 -6
  14. package/lib/cli/index.js +113 -54
  15. package/lib/cli/jsBom.js +50 -29
  16. package/lib/cli/jvmBom.js +665 -138
  17. package/lib/cli/managedBom.js +320 -200
  18. package/lib/cli/nativeBom.js +139 -30
  19. package/lib/core/activity.js +12 -0
  20. package/lib/core/env.js +17 -9
  21. package/lib/core/fs.js +3 -2
  22. package/lib/core/httpClient.js +159 -24
  23. package/lib/core/pluginRun.js +184 -0
  24. package/lib/core/processTree.js +100 -0
  25. package/lib/core/runState.js +39 -0
  26. package/lib/core/toml.js +72 -0
  27. package/lib/ecosystems/asarutils.js +4 -2
  28. package/lib/ecosystems/bunutils.js +5 -7
  29. package/lib/ecosystems/cargoCache.js +231 -8
  30. package/lib/ecosystems/cmakeResolver.js +10 -21
  31. package/lib/ecosystems/cppBuildContext.js +631 -0
  32. package/lib/ecosystems/cppEvidence.js +337 -22
  33. package/lib/ecosystems/cppIncludeResolver.js +294 -0
  34. package/lib/ecosystems/denoutils.js +93 -66
  35. package/lib/ecosystems/ecosystems.js +1967 -788
  36. package/lib/ecosystems/golem.js +28 -12
  37. package/lib/ecosystems/gradleutils.js +314 -30
  38. package/lib/ecosystems/jsEvidence.js +85 -24
  39. package/lib/ecosystems/kosi.js +21 -4
  40. package/lib/ecosystems/npmutils.js +64 -0
  41. package/lib/ecosystems/parsers-dotnet.js +567 -151
  42. package/lib/ecosystems/parsers-elm.js +46 -28
  43. package/lib/ecosystems/parsers-gleam.js +3 -4
  44. package/lib/ecosystems/parsers-go.js +324 -61
  45. package/lib/ecosystems/parsers-js.js +92 -26
  46. package/lib/ecosystems/parsers-julia.js +2 -3
  47. package/lib/ecosystems/parsers-jvm.js +204 -12
  48. package/lib/ecosystems/parsers-misc.js +182 -55
  49. package/lib/ecosystems/parsers-python.js +141 -10
  50. package/lib/ecosystems/parsers-rust.js +4 -4
  51. package/lib/ecosystems/parsers-terraform.js +1573 -19
  52. package/lib/ecosystems/pythonutils.js +102 -6
  53. package/lib/ecosystems/remote/huggingface.js +3 -0
  54. package/lib/ecosystems/rubyutils.js +67 -17
  55. package/lib/ecosystems/rusi.js +22 -9
  56. package/lib/ecosystems/sbtutils.js +243 -187
  57. package/lib/ecosystems/scalacliutils.js +342 -0
  58. package/lib/ecosystems/scalasem.js +2618 -0
  59. package/lib/ecosystems/terraformRegistry.js +288 -0
  60. package/lib/ecosystems/utils.js +2 -0
  61. package/lib/ecosystems/vendoredCode.js +156 -0
  62. package/lib/evinser/evinser.js +578 -69
  63. package/lib/helpers/agenticToolutils.js +2 -2
  64. package/lib/helpers/bomSigner.js +576 -128
  65. package/lib/inventory/aiCollector.js +4 -1
  66. package/lib/inventory/analyzer.js +110 -127
  67. package/lib/inventory/atomUtils.js +171 -48
  68. package/lib/inventory/auditCategories.js +7 -0
  69. package/lib/inventory/buildToolRateLimit.js +144 -0
  70. package/lib/inventory/cacheCommands.js +2 -1
  71. package/lib/inventory/cmakeBuildDirs.js +254 -0
  72. package/lib/inventory/deps.js +501 -124
  73. package/lib/inventory/dosai.js +248 -44
  74. package/lib/inventory/dosaiParsers.js +310 -15
  75. package/lib/inventory/dynamic.js +52 -6
  76. package/lib/inventory/evidenceUtils.js +154 -109
  77. package/lib/inventory/fetchBatch.js +574 -38
  78. package/lib/inventory/fetchDiskCache.js +597 -0
  79. package/lib/inventory/forgeEnricher.js +14 -5
  80. package/lib/inventory/jvmLocalRepos.js +917 -0
  81. package/lib/inventory/mcpConfigParser.js +2 -3
  82. package/lib/inventory/parentSelection.js +146 -0
  83. package/lib/inventory/rustFormulationParser.js +3 -4
  84. package/lib/inventory/scalaCoords.js +153 -0
  85. package/lib/inventory/source.js +8 -8
  86. package/lib/inventory/spdx.js +101 -0
  87. package/lib/inventory/traceRunner.js +88 -7
  88. package/lib/inventory/tracedCrypto.js +294 -0
  89. package/lib/managers/docker.js +33 -32
  90. package/lib/managers/oci.js +24 -2
  91. package/lib/parsers/cmakeCache.js +69 -0
  92. package/lib/parsers/cmakeLists.js +16 -2
  93. package/lib/parsers/cmakePresets.js +400 -0
  94. package/lib/parsers/compileCommands.js +557 -0
  95. package/lib/parsers/strictJson.js +322 -0
  96. package/lib/parsers/terraformConfig.js +1073 -0
  97. package/lib/parsers/terraformSource.js +652 -0
  98. package/lib/server/server.js +23 -0
  99. package/lib/stages/postgen/introspection/markers.js +1 -1
  100. package/lib/stages/postgen/postgen.js +15 -1
  101. package/lib/stages/postgen/specVersionCompat.js +32 -0
  102. package/lib/validator/index.js +25 -10
  103. package/lib/validator/reporters/json.js +1 -0
  104. package/package.json +21 -21
  105. package/types/lib/audit/index.d.ts.map +1 -1
  106. package/types/lib/cli/index.d.ts.map +1 -1
  107. package/types/lib/cli/jsBom.d.ts.map +1 -1
  108. package/types/lib/cli/jvmBom.d.ts +24 -1
  109. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  110. package/types/lib/cli/managedBom.d.ts +7 -4
  111. package/types/lib/cli/managedBom.d.ts.map +1 -1
  112. package/types/lib/cli/nativeBom.d.ts +3 -1
  113. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  114. package/types/lib/core/activity.d.ts.map +1 -1
  115. package/types/lib/core/env.d.ts +9 -0
  116. package/types/lib/core/env.d.ts.map +1 -1
  117. package/types/lib/core/fs.d.ts.map +1 -1
  118. package/types/lib/core/httpClient.d.ts +13 -8
  119. package/types/lib/core/httpClient.d.ts.map +1 -1
  120. package/types/lib/core/pluginRun.d.ts +97 -0
  121. package/types/lib/core/pluginRun.d.ts.map +1 -0
  122. package/types/lib/core/processTree.d.ts +36 -0
  123. package/types/lib/core/processTree.d.ts.map +1 -0
  124. package/types/lib/core/runState.d.ts +29 -0
  125. package/types/lib/core/runState.d.ts.map +1 -0
  126. package/types/lib/core/toml.d.ts +20 -0
  127. package/types/lib/core/toml.d.ts.map +1 -0
  128. package/types/lib/ecosystems/asarutils.d.ts.map +1 -1
  129. package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
  130. package/types/lib/ecosystems/cargoCache.d.ts +12 -0
  131. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -1
  132. package/types/lib/ecosystems/cmakeResolver.d.ts +5 -3
  133. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
  134. package/types/lib/ecosystems/cppBuildContext.d.ts +55 -0
  135. package/types/lib/ecosystems/cppBuildContext.d.ts.map +1 -0
  136. package/types/lib/ecosystems/cppEvidence.d.ts +18 -2
  137. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  138. package/types/lib/ecosystems/cppIncludeResolver.d.ts +92 -0
  139. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -0
  140. package/types/lib/ecosystems/denoutils.d.ts +9 -3
  141. package/types/lib/ecosystems/denoutils.d.ts.map +1 -1
  142. package/types/lib/ecosystems/ecosystems.d.ts +62 -13
  143. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  144. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  145. package/types/lib/ecosystems/gradleutils.d.ts +50 -0
  146. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  147. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  148. package/types/lib/ecosystems/kosi.d.ts.map +1 -1
  149. package/types/lib/ecosystems/npmutils.d.ts +15 -0
  150. package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
  151. package/types/lib/ecosystems/parsers-dotnet.d.ts +118 -4
  152. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  153. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -1
  154. package/types/lib/ecosystems/parsers-gleam.d.ts.map +1 -1
  155. package/types/lib/ecosystems/parsers-go.d.ts +44 -2
  156. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  157. package/types/lib/ecosystems/parsers-js.d.ts +6 -1
  158. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  159. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -1
  160. package/types/lib/ecosystems/parsers-jvm.d.ts +46 -1
  161. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  162. package/types/lib/ecosystems/parsers-misc.d.ts +3 -1
  163. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  164. package/types/lib/ecosystems/parsers-python.d.ts +53 -3
  165. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  166. package/types/lib/ecosystems/parsers-terraform.d.ts +38 -21
  167. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -1
  168. package/types/lib/ecosystems/pythonutils.d.ts +27 -2
  169. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  170. package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
  171. package/types/lib/ecosystems/rubyutils.d.ts +21 -1
  172. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  173. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  174. package/types/lib/ecosystems/sbtutils.d.ts +88 -2
  175. package/types/lib/ecosystems/sbtutils.d.ts.map +1 -1
  176. package/types/lib/ecosystems/scalacliutils.d.ts +56 -0
  177. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -0
  178. package/types/lib/ecosystems/scalasem.d.ts +311 -0
  179. package/types/lib/ecosystems/scalasem.d.ts.map +1 -0
  180. package/types/lib/ecosystems/terraformRegistry.d.ts +9 -0
  181. package/types/lib/ecosystems/terraformRegistry.d.ts.map +1 -0
  182. package/types/lib/ecosystems/utils.d.ts +2 -2
  183. package/types/lib/ecosystems/utils.d.ts.map +1 -1
  184. package/types/lib/ecosystems/vendoredCode.d.ts +32 -0
  185. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -0
  186. package/types/lib/evinser/evinser.d.ts +75 -8
  187. package/types/lib/evinser/evinser.d.ts.map +1 -1
  188. package/types/lib/helpers/bomSigner.d.ts +80 -4
  189. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  190. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  191. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  192. package/types/lib/inventory/atomUtils.d.ts +82 -10
  193. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  194. package/types/lib/inventory/auditCategories.d.ts +3 -0
  195. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  196. package/types/lib/inventory/buildToolRateLimit.d.ts +38 -0
  197. package/types/lib/inventory/buildToolRateLimit.d.ts.map +1 -0
  198. package/types/lib/inventory/cacheCommands.d.ts +2 -1
  199. package/types/lib/inventory/cacheCommands.d.ts.map +1 -1
  200. package/types/lib/inventory/cmakeBuildDirs.d.ts +58 -0
  201. package/types/lib/inventory/cmakeBuildDirs.d.ts.map +1 -0
  202. package/types/lib/inventory/deps.d.ts +59 -1
  203. package/types/lib/inventory/deps.d.ts.map +1 -1
  204. package/types/lib/inventory/dosai.d.ts +57 -8
  205. package/types/lib/inventory/dosai.d.ts.map +1 -1
  206. package/types/lib/inventory/dosaiParsers.d.ts +53 -7
  207. package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
  208. package/types/lib/inventory/dynamic.d.ts +10 -0
  209. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  210. package/types/lib/inventory/evidenceUtils.d.ts +19 -4
  211. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  212. package/types/lib/inventory/fetchBatch.d.ts +83 -14
  213. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  214. package/types/lib/inventory/fetchDiskCache.d.ts +169 -0
  215. package/types/lib/inventory/fetchDiskCache.d.ts.map +1 -0
  216. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  217. package/types/lib/inventory/jvmLocalRepos.d.ts +178 -0
  218. package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -0
  219. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  220. package/types/lib/inventory/parentSelection.d.ts +34 -0
  221. package/types/lib/inventory/parentSelection.d.ts.map +1 -0
  222. package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
  223. package/types/lib/inventory/scalaCoords.d.ts +80 -0
  224. package/types/lib/inventory/scalaCoords.d.ts.map +1 -0
  225. package/types/lib/inventory/source.d.ts.map +1 -1
  226. package/types/lib/inventory/spdx.d.ts +9 -0
  227. package/types/lib/inventory/spdx.d.ts.map +1 -1
  228. package/types/lib/inventory/traceRunner.d.ts +12 -0
  229. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  230. package/types/lib/inventory/tracedCrypto.d.ts +42 -0
  231. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -0
  232. package/types/lib/managers/docker.d.ts.map +1 -1
  233. package/types/lib/managers/oci.d.ts +3 -1
  234. package/types/lib/managers/oci.d.ts.map +1 -1
  235. package/types/lib/parsers/cmakeCache.d.ts +16 -0
  236. package/types/lib/parsers/cmakeCache.d.ts.map +1 -1
  237. package/types/lib/parsers/cmakeLists.d.ts +2 -1
  238. package/types/lib/parsers/cmakeLists.d.ts.map +1 -1
  239. package/types/lib/parsers/cmakePresets.d.ts +78 -0
  240. package/types/lib/parsers/cmakePresets.d.ts.map +1 -0
  241. package/types/lib/parsers/compileCommands.d.ts +115 -0
  242. package/types/lib/parsers/compileCommands.d.ts.map +1 -0
  243. package/types/lib/parsers/strictJson.d.ts +46 -0
  244. package/types/lib/parsers/strictJson.d.ts.map +1 -0
  245. package/types/lib/parsers/terraformConfig.d.ts +70 -0
  246. package/types/lib/parsers/terraformConfig.d.ts.map +1 -0
  247. package/types/lib/parsers/terraformSource.d.ts +66 -0
  248. package/types/lib/parsers/terraformSource.d.ts.map +1 -0
  249. package/types/lib/server/server.d.ts.map +1 -1
  250. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  251. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  252. package/types/lib/validator/index.d.ts +8 -0
  253. package/types/lib/validator/index.d.ts.map +1 -1
  254. package/types/lib/validator/reporters/json.d.ts.map +1 -1
  255. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +0 -462
@@ -0,0 +1,917 @@
1
+ import { readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import {
4
+ basename,
5
+ dirname,
6
+ isAbsolute,
7
+ join,
8
+ relative,
9
+ resolve,
10
+ } from "node:path";
11
+ import process from "node:process";
12
+
13
+ import { readEnvironmentVariable } from "../core/activity.js";
14
+ import { parseMavenArgs } from "../core/env.js";
15
+ import { safeExistsSync } from "../core/fs.js";
16
+ import { registerRunReset } from "../core/runState.js";
17
+ import { xml2js } from "../parsers/xml.js";
18
+
19
+ // Local JVM artifact caches: the Maven local repository, the Gradle module
20
+ // cache and the Coursier cache. Every Maven Central request cdxgen can avoid
21
+ // starts with knowing where these live and what they already hold, so the
22
+ // lookups here are shared by license enrichment, jar identification and the
23
+ // sbt parsers.
24
+
25
+ const SHA1_HEX = /^[a-f0-9]{40}$/;
26
+
27
+ // Index walks stop after this many files so that a pathological cache cannot
28
+ // stall a scan. Real caches hold tens of thousands of artifacts.
29
+ const MAX_SHA1_INDEX_ENTRIES = 500_000;
30
+
31
+ // A repository prefix in the Coursier layout always starts with
32
+ // <protocol>/<host> and in practice adds at most a couple of path segments
33
+ // ("maven2", "content/repositories/releases"). The prefix scan stops at this
34
+ // depth so it never descends into the group directories.
35
+ const MAX_COURSIER_PREFIX_DEPTH = 5;
36
+
37
+ const settingsCache = new Map();
38
+ const repositoryListCache = new Map();
39
+ const artifactCache = new Map();
40
+ const coursierPrefixCache = new Map();
41
+ const coursierLocationCache = new Map();
42
+ let sha1Index;
43
+
44
+ /**
45
+ * Forget every memoised lookup. A long-lived process (server mode) calls this
46
+ * per scan so that artifacts a build downloaded in the meantime are found, and
47
+ * tests call it after changing the cache environment variables.
48
+ *
49
+ * @returns {void}
50
+ */
51
+ export function resetJvmLocalRepoCaches() {
52
+ settingsCache.clear();
53
+ repositoryListCache.clear();
54
+ artifactCache.clear();
55
+ coursierPrefixCache.clear();
56
+ coursierLocationCache.clear();
57
+ sha1Index = undefined;
58
+ }
59
+
60
+ registerRunReset("jvm:local-repositories", resetJvmLocalRepoCaches);
61
+
62
+ /**
63
+ * The user's home directory, preferring HOME/USERPROFILE so that tests and
64
+ * sandboxed runs that redirect the home directory are honoured.
65
+ *
66
+ * @returns {string|undefined} Home directory.
67
+ */
68
+ function homeDir() {
69
+ return (
70
+ readEnvironmentVariable("HOME") ||
71
+ readEnvironmentVariable("USERPROFILE") ||
72
+ homedir()
73
+ );
74
+ }
75
+
76
+ /**
77
+ * Add a path to a list once, resolved to an absolute path.
78
+ *
79
+ * @param {string[]} list Accumulator.
80
+ * @param {string|undefined} candidate Path to add.
81
+ * @returns {void}
82
+ */
83
+ function pushUnique(list, candidate) {
84
+ if (!candidate || typeof candidate !== "string") {
85
+ return;
86
+ }
87
+ const absolute = resolve(candidate);
88
+ if (!list.includes(absolute)) {
89
+ list.push(absolute);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Expand the property references Maven allows in a settings.xml
95
+ * `localRepository` element: `${user.home}` and `${env.NAME}`. Anything else is
96
+ * left as is, which makes the path fail the existence check rather than point
97
+ * somewhere unintended.
98
+ *
99
+ * @param {string} value Raw element text.
100
+ * @returns {string} Expanded path.
101
+ */
102
+ function expandMavenProperties(value) {
103
+ return (
104
+ value
105
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: Maven property syntax, not a template.
106
+ .replaceAll("${user.home}", homeDir())
107
+ .replace(/\$\{env\.([A-Za-z0-9_]+)\}/g, (_match, name) => {
108
+ return readEnvironmentVariable(name) || "";
109
+ })
110
+ );
111
+ }
112
+
113
+ /**
114
+ * Read `<localRepository>` from a Maven settings file.
115
+ *
116
+ * @param {string} settingsFile Path to settings.xml.
117
+ * @returns {string|undefined} Absolute local repository path.
118
+ */
119
+ export function localRepositoryFromSettings(settingsFile) {
120
+ if (!settingsFile || !safeExistsSync(settingsFile)) {
121
+ return undefined;
122
+ }
123
+ let mtimeMs;
124
+ try {
125
+ mtimeMs = statSync(settingsFile).mtimeMs;
126
+ } catch (_err) {
127
+ return undefined;
128
+ }
129
+ const cacheKey = `${settingsFile}|${mtimeMs}`;
130
+ if (settingsCache.has(cacheKey)) {
131
+ return settingsCache.get(cacheKey);
132
+ }
133
+ let localRepository;
134
+ try {
135
+ const settings = xml2js(readFileSync(settingsFile, "utf-8"), {
136
+ compact: true,
137
+ textKey: "_",
138
+ }).settings;
139
+ const raw = settings?.localRepository?._?.trim();
140
+ if (raw) {
141
+ localRepository = resolve(
142
+ dirname(settingsFile),
143
+ expandMavenProperties(raw),
144
+ );
145
+ }
146
+ } catch (_err) {
147
+ localRepository = undefined;
148
+ }
149
+ settingsCache.set(cacheKey, localRepository);
150
+ return localRepository;
151
+ }
152
+
153
+ /**
154
+ * Pull the values cdxgen needs out of a Maven argument list: the
155
+ * `maven.repo.local` system property and the user settings file.
156
+ *
157
+ * @param {string[]} args Parsed arguments.
158
+ * @param {string} [baseDir] Directory relative paths resolve against.
159
+ * @returns {{repoLocal?: string, settingsFile?: string}}
160
+ */
161
+ export function mavenRepoArgs(args, baseDir = process.cwd()) {
162
+ const result = {};
163
+ for (let i = 0; i < args.length; i++) {
164
+ const arg = args[i];
165
+ let property;
166
+ if (arg === "-D" && i + 1 < args.length) {
167
+ property = args[++i];
168
+ } else if (arg.startsWith("-D")) {
169
+ property = arg.substring(2);
170
+ }
171
+ if (property?.startsWith("maven.repo.local=")) {
172
+ const value = property.substring("maven.repo.local=".length);
173
+ if (value) {
174
+ result.repoLocal = resolve(baseDir, value);
175
+ }
176
+ continue;
177
+ }
178
+ let settingsFile;
179
+ if ((arg === "-s" || arg === "--settings") && i + 1 < args.length) {
180
+ settingsFile = args[++i];
181
+ } else if (arg.startsWith("--settings=")) {
182
+ settingsFile = arg.substring("--settings=".length);
183
+ }
184
+ if (settingsFile) {
185
+ result.settingsFile = resolve(baseDir, settingsFile);
186
+ }
187
+ }
188
+ return result;
189
+ }
190
+
191
+ /**
192
+ * The arguments that hand a scanned project's own `settings.xml` to Maven.
193
+ *
194
+ * The file is passed as global settings (`-gs`), so the user's
195
+ * `~/.m2/settings.xml` still applies and wins on mirrors, servers and proxies.
196
+ * Nothing is added, and `skipped` says why, in secure mode, where a scanned
197
+ * repository must not choose Maven's repositories, proxies or credentials, or
198
+ * when MVN_ARGS or MAVEN_ARGS already names a settings file.
199
+ *
200
+ * @param {string} projectDir Directory holding the project's pom.xml.
201
+ * @param {{secureMode?: boolean}} [opts]
202
+ * @returns {{settingsFile?: string, args: string[], skipped?: "secure-mode"|"user-settings"}}
203
+ */
204
+ export function projectSettingsArgs(projectDir, { secureMode = false } = {}) {
205
+ const settingsFile = join(projectDir, "settings.xml");
206
+ if (!safeExistsSync(settingsFile)) {
207
+ return { args: [] };
208
+ }
209
+ if (secureMode) {
210
+ return { settingsFile, args: [], skipped: "secure-mode" };
211
+ }
212
+ const userArgs = ["MVN_ARGS", "MAVEN_ARGS"].flatMap((name) =>
213
+ parseMavenArgs(readEnvironmentVariable(name)),
214
+ );
215
+ const namesSettings = userArgs.some(
216
+ (arg) =>
217
+ ["-s", "--settings", "-gs", "--global-settings"].includes(arg) ||
218
+ arg.startsWith("--settings=") ||
219
+ arg.startsWith("--global-settings="),
220
+ );
221
+ if (namesSettings) {
222
+ return { settingsFile, args: [], skipped: "user-settings" };
223
+ }
224
+ return { settingsFile, args: ["-gs", settingsFile] };
225
+ }
226
+
227
+ /**
228
+ * Every Maven local repository this run may have populated, most specific
229
+ * first: MAVEN_CACHE_DIR, then an explicit `-Dmaven.repo.local` (from
230
+ * MVN_ARGS, MAVEN_ARGS, MAVEN_OPTS or the project's `.mvn/maven.config`), then
231
+ * `<localRepository>` from the settings Maven would read, then
232
+ * `~/.m2/repository`.
233
+ *
234
+ * @param {Object} [opts]
235
+ * @param {string} [opts.projectDir] Project directory, for `.mvn/maven.config`.
236
+ * @returns {string[]} Absolute paths. Not filtered for existence.
237
+ */
238
+ export function mavenLocalRepositories({ projectDir } = {}) {
239
+ const signature = [
240
+ "MVN_ARGS",
241
+ "MAVEN_ARGS",
242
+ "MAVEN_OPTS",
243
+ "MAVEN_CACHE_DIR",
244
+ "MAVEN_HOME",
245
+ "M2_HOME",
246
+ "HOME",
247
+ "USERPROFILE",
248
+ ]
249
+ .map((name) => readEnvironmentVariable(name) || "")
250
+ .concat(projectDir || "")
251
+ .join("\u0000");
252
+ if (repositoryListCache.has(signature)) {
253
+ return repositoryListCache.get(signature);
254
+ }
255
+ const repos = [];
256
+ pushUnique(repos, readEnvironmentVariable("MAVEN_CACHE_DIR"));
257
+ const settingsFiles = [];
258
+ const argSources = ["MVN_ARGS", "MAVEN_ARGS", "MAVEN_OPTS"].map((name) =>
259
+ parseMavenArgs(readEnvironmentVariable(name)),
260
+ );
261
+ if (projectDir) {
262
+ const mavenConfig = join(projectDir, ".mvn", "maven.config");
263
+ if (safeExistsSync(mavenConfig)) {
264
+ try {
265
+ argSources.push(parseMavenArgs(readFileSync(mavenConfig, "utf-8")));
266
+ } catch (_err) {
267
+ // An unreadable maven.config contributes nothing.
268
+ }
269
+ }
270
+ }
271
+ for (const args of argSources) {
272
+ const { repoLocal, settingsFile } = mavenRepoArgs(
273
+ args,
274
+ projectDir || process.cwd(),
275
+ );
276
+ pushUnique(repos, repoLocal);
277
+ pushUnique(settingsFiles, settingsFile);
278
+ }
279
+ pushUnique(settingsFiles, join(homeDir(), ".m2", "settings.xml"));
280
+ const mavenHome =
281
+ readEnvironmentVariable("MAVEN_HOME") || readEnvironmentVariable("M2_HOME");
282
+ if (mavenHome) {
283
+ pushUnique(settingsFiles, join(mavenHome, "conf", "settings.xml"));
284
+ }
285
+ for (const settingsFile of settingsFiles) {
286
+ pushUnique(repos, localRepositoryFromSettings(settingsFile));
287
+ }
288
+ pushUnique(repos, join(homeDir(), ".m2", "repository"));
289
+ repositoryListCache.set(signature, repos);
290
+ return repos;
291
+ }
292
+
293
+ /**
294
+ * Gradle module cache directories (`.../modules-2/files-2.1`): the one under
295
+ * GRADLE_USER_HOME, the cdxgen-specific GRADLE_CACHE_DIR, the default
296
+ * `~/.gradle` when neither is set, and the read-only shared cache named by
297
+ * GRADLE_RO_DEP_CACHE.
298
+ *
299
+ * @returns {string[]} Absolute paths. Not filtered for existence.
300
+ */
301
+ export function gradleCacheRoots() {
302
+ const roots = [];
303
+ const gradleUserHome = readEnvironmentVariable("GRADLE_USER_HOME");
304
+ const gradleCacheDir = readEnvironmentVariable("GRADLE_CACHE_DIR");
305
+ if (gradleUserHome) {
306
+ pushUnique(roots, join(gradleUserHome, "caches", "modules-2", "files-2.1"));
307
+ }
308
+ pushUnique(roots, gradleCacheDir);
309
+ if (!gradleUserHome && !gradleCacheDir) {
310
+ pushUnique(
311
+ roots,
312
+ join(homeDir(), ".gradle", "caches", "modules-2", "files-2.1"),
313
+ );
314
+ }
315
+ const readOnlyCache = readEnvironmentVariable("GRADLE_RO_DEP_CACHE");
316
+ if (readOnlyCache) {
317
+ pushUnique(roots, join(readOnlyCache, "modules-2", "files-2.1"));
318
+ }
319
+ return roots;
320
+ }
321
+
322
+ /**
323
+ * The Coursier cache directory used by sbt, Mill and scala-cli: COURSIER_CACHE
324
+ * when set, otherwise the platform default (XDG_CACHE_HOME is honoured on
325
+ * Linux).
326
+ *
327
+ * @returns {string|undefined} Absolute path. Not checked for existence.
328
+ */
329
+ export function coursierCacheDir() {
330
+ const configured = readEnvironmentVariable("COURSIER_CACHE");
331
+ if (configured) {
332
+ return resolve(configured);
333
+ }
334
+ const home = homeDir();
335
+ if (!home) {
336
+ return undefined;
337
+ }
338
+ if (process.platform === "darwin") {
339
+ return join(home, "Library", "Caches", "Coursier", "v1");
340
+ }
341
+ if (process.platform === "win32") {
342
+ const localAppData = readEnvironmentVariable("LOCALAPPDATA");
343
+ return localAppData
344
+ ? join(localAppData, "Coursier", "Cache", "v1")
345
+ : join(home, "AppData", "Local", "Coursier", "Cache", "v1");
346
+ }
347
+ const xdgCache = readEnvironmentVariable("XDG_CACHE_HOME");
348
+ return join(xdgCache || join(home, ".cache"), "coursier", "v1");
349
+ }
350
+
351
+ /**
352
+ * Repository prefixes present in a Coursier cache, found with a shallow,
353
+ * depth-bounded scan and memoised per cache root. See cdxgen issue 4291 for
354
+ * why this replaced a recursive glob per coordinate.
355
+ *
356
+ * @param {string} cacheRoot Coursier cache root.
357
+ * @returns {string[][]} Prefixes as path segments relative to the root.
358
+ */
359
+ function coursierRepoPrefixes(cacheRoot) {
360
+ if (coursierPrefixCache.has(cacheRoot)) {
361
+ return coursierPrefixCache.get(cacheRoot);
362
+ }
363
+ const prefixes = [];
364
+ // Breadth-first so that shallower prefixes, the common ones, come first.
365
+ let frontier = [[]];
366
+ for (let depth = 1; depth <= MAX_COURSIER_PREFIX_DEPTH; depth++) {
367
+ const next = [];
368
+ for (const parts of frontier) {
369
+ let entries;
370
+ try {
371
+ entries = readdirSync(join(cacheRoot, ...parts), {
372
+ withFileTypes: true,
373
+ });
374
+ } catch (_err) {
375
+ continue;
376
+ }
377
+ for (const entry of entries) {
378
+ if (!entry.isDirectory()) {
379
+ continue;
380
+ }
381
+ const childParts = [...parts, entry.name];
382
+ // <protocol>/<host> is the shortest thing that can be a repository.
383
+ if (depth >= 2) {
384
+ prefixes.push(childParts);
385
+ }
386
+ next.push(childParts);
387
+ }
388
+ }
389
+ frontier = next;
390
+ if (!frontier.length) {
391
+ break;
392
+ }
393
+ }
394
+ coursierPrefixCache.set(cacheRoot, prefixes);
395
+ return prefixes;
396
+ }
397
+
398
+ /**
399
+ * Convert a Coursier repository prefix into the repository URL it caches.
400
+ *
401
+ * @param {string[]} parts Prefix segments relative to the cache root.
402
+ * @returns {string|null} Repository URL.
403
+ */
404
+ export function coursierPrefixToUrl(parts) {
405
+ if (!parts || parts.length < 2) {
406
+ return null;
407
+ }
408
+ const protocol = parts[0];
409
+ if (protocol === "file") {
410
+ let localPath = parts.slice(1).join("/");
411
+ if (!localPath.startsWith("/")) {
412
+ localPath = `/${localPath}`;
413
+ }
414
+ return `file://${localPath}`;
415
+ }
416
+ const host = parts[1];
417
+ const repoPath = parts.slice(2).join("/");
418
+ let repoUrl = `${protocol}://${host}`;
419
+ if (repoPath) {
420
+ repoUrl += `/${repoPath}`;
421
+ }
422
+ return repoUrl.endsWith("/") ? repoUrl.slice(0, -1) : repoUrl;
423
+ }
424
+
425
+ /**
426
+ * Locate a Maven coordinate's directory in the Coursier cache.
427
+ *
428
+ * @param {string} group Maven groupId.
429
+ * @param {string} name Maven artifactId, including any Scala suffix.
430
+ * @param {string} version Version.
431
+ * @returns {{repoUrl: string, dir: string}|null} Repository URL and directory.
432
+ */
433
+ export function locateInCoursierCache(group, name, version) {
434
+ if (!group || !name || !version) {
435
+ return null;
436
+ }
437
+ const cacheRoot = coursierCacheDir();
438
+ if (!cacheRoot || !safeExistsSync(cacheRoot)) {
439
+ return null;
440
+ }
441
+ // The root is part of the key so a changed COURSIER_CACHE is never answered
442
+ // from a memo built against the previous one.
443
+ const cacheKey = `${cacheRoot}|${group}:${name}:${version}`;
444
+ if (coursierLocationCache.has(cacheKey)) {
445
+ return coursierLocationCache.get(cacheKey);
446
+ }
447
+ const groupParts = group.split(".");
448
+ const prefixes = coursierRepoPrefixes(cacheRoot);
449
+ let result = null;
450
+ for (let i = 0; i < prefixes.length; i++) {
451
+ const dir = join(cacheRoot, ...prefixes[i], ...groupParts, name, version);
452
+ if (safeExistsSync(dir)) {
453
+ const repoUrl = coursierPrefixToUrl(prefixes[i]);
454
+ if (repoUrl) {
455
+ result = { repoUrl, dir };
456
+ // Most coordinates in a build come from a handful of repositories, so
457
+ // the prefix that just matched moves to the front.
458
+ if (i > 0) {
459
+ prefixes.unshift(prefixes.splice(i, 1)[0]);
460
+ }
461
+ break;
462
+ }
463
+ }
464
+ }
465
+ coursierLocationCache.set(cacheKey, result);
466
+ return result;
467
+ }
468
+
469
+ /**
470
+ * The file name a Maven artifact is stored under.
471
+ *
472
+ * @param {string} name artifactId.
473
+ * @param {string} version Version.
474
+ * @param {string} [classifier] Classifier.
475
+ * @param {string} [extension] Extension without the dot.
476
+ * @returns {string} File name.
477
+ */
478
+ function artifactFileName(name, version, classifier, extension) {
479
+ return `${name}-${version}${classifier ? `-${classifier}` : ""}.${extension}`;
480
+ }
481
+
482
+ /**
483
+ * Find a file with an exact name in any immediate subdirectory of `dir`. The
484
+ * Gradle module cache keeps every file under a directory named after its
485
+ * SHA-1, so the name is known but the directory is not.
486
+ *
487
+ * @param {string} dir Version directory in the Gradle cache.
488
+ * @param {string} fileName Exact file name.
489
+ * @returns {{path: string, sha1: string}|undefined}
490
+ */
491
+ function findInHashedDirs(dir, fileName) {
492
+ let entries;
493
+ try {
494
+ entries = readdirSync(dir, { withFileTypes: true });
495
+ } catch (_err) {
496
+ return undefined;
497
+ }
498
+ for (const entry of entries) {
499
+ if (!entry.isDirectory()) {
500
+ continue;
501
+ }
502
+ const candidate = join(dir, entry.name, fileName);
503
+ if (safeExistsSync(candidate)) {
504
+ return { path: candidate, sha1: entry.name };
505
+ }
506
+ }
507
+ return undefined;
508
+ }
509
+
510
+ /**
511
+ * Locate a Maven artifact and its POM in the local caches.
512
+ *
513
+ * Only the exact file names Maven, Gradle and Coursier use are accepted, so a
514
+ * classifier jar (`-sources`, `-javadoc`, `-linux-x86_64`) is never mistaken
515
+ * for the main artifact. Results, including misses, are memoised until
516
+ * {@link resetJvmLocalRepoCaches}.
517
+ *
518
+ * @param {string} group Maven groupId.
519
+ * @param {string} name Maven artifactId.
520
+ * @param {string} version Version.
521
+ * @param {Object} [opts]
522
+ * @param {string} [opts.classifier] Classifier of the wanted jar.
523
+ * @param {string} [opts.extension] Artifact extension. Defaults to jar.
524
+ * @param {string} [opts.projectDir] Project directory, for `.mvn/maven.config`.
525
+ * @returns {{jarPath?: string, pomPath?: string, sha1?: string, repoUrl?: string}|null}
526
+ */
527
+ export function findLocalMavenArtifact(group, name, version, opts = {}) {
528
+ if (!group || !name || !version) {
529
+ return null;
530
+ }
531
+ const classifier = opts.classifier || "";
532
+ const extension = opts.extension || "jar";
533
+ const mavenRepos = mavenLocalRepositories({ projectDir: opts.projectDir });
534
+ const gradleRoots = gradleCacheRoots();
535
+ const coursierRoot = coursierCacheDir();
536
+ const cacheKey = [
537
+ group,
538
+ name,
539
+ version,
540
+ classifier,
541
+ extension,
542
+ mavenRepos.join(","),
543
+ gradleRoots.join(","),
544
+ coursierRoot,
545
+ ].join("|");
546
+ if (artifactCache.has(cacheKey)) {
547
+ return artifactCache.get(cacheKey);
548
+ }
549
+ const jarName = artifactFileName(name, version, classifier, extension);
550
+ const pomName = artifactFileName(name, version, "", "pom");
551
+ const result = {};
552
+ const groupPath = group.split(".");
553
+ for (const repo of mavenRepos) {
554
+ const dir = join(repo, ...groupPath, name, version);
555
+ if (!safeExistsSync(dir)) {
556
+ continue;
557
+ }
558
+ if (!result.jarPath && safeExistsSync(join(dir, jarName))) {
559
+ result.jarPath = join(dir, jarName);
560
+ }
561
+ if (!result.pomPath && safeExistsSync(join(dir, pomName))) {
562
+ result.pomPath = join(dir, pomName);
563
+ }
564
+ if (result.jarPath && result.pomPath) {
565
+ break;
566
+ }
567
+ }
568
+ if (!result.jarPath || !result.pomPath) {
569
+ for (const root of gradleRoots) {
570
+ const dir = join(root, group, name, version);
571
+ if (!safeExistsSync(dir)) {
572
+ continue;
573
+ }
574
+ if (!result.jarPath) {
575
+ const jar = findInHashedDirs(dir, jarName);
576
+ if (jar) {
577
+ result.jarPath = jar.path;
578
+ if (SHA1_HEX.test(jar.sha1)) {
579
+ result.sha1 = jar.sha1;
580
+ }
581
+ }
582
+ }
583
+ if (!result.pomPath) {
584
+ result.pomPath = findInHashedDirs(dir, pomName)?.path;
585
+ }
586
+ if (result.jarPath && result.pomPath) {
587
+ break;
588
+ }
589
+ }
590
+ }
591
+ if (!result.jarPath || !result.pomPath) {
592
+ const location = locateInCoursierCache(group, name, version);
593
+ if (location) {
594
+ if (!result.jarPath && safeExistsSync(join(location.dir, jarName))) {
595
+ result.jarPath = join(location.dir, jarName);
596
+ }
597
+ if (!result.pomPath && safeExistsSync(join(location.dir, pomName))) {
598
+ result.pomPath = join(location.dir, pomName);
599
+ }
600
+ if (result.jarPath || result.pomPath) {
601
+ result.repoUrl = location.repoUrl;
602
+ }
603
+ }
604
+ }
605
+ const found = result.jarPath || result.pomPath ? result : null;
606
+ artifactCache.set(cacheKey, found);
607
+ return found;
608
+ }
609
+
610
+ /**
611
+ * Split an artifact file name into its classifier and extension, given the
612
+ * artifactId and version it belongs to.
613
+ *
614
+ * @param {string} fileName File name, for example `netty-4.1.1-linux.jar`.
615
+ * @param {string} name artifactId.
616
+ * @param {string} version Version.
617
+ * @returns {{classifier: string, extension: string}|undefined} Undefined when
618
+ * the name does not belong to the coordinate.
619
+ */
620
+ export function splitArtifactFileName(fileName, name, version) {
621
+ const prefix = `${name}-${version}`;
622
+ if (!fileName.startsWith(prefix)) {
623
+ return undefined;
624
+ }
625
+ const rest = fileName.substring(prefix.length);
626
+ const dot = rest.lastIndexOf(".");
627
+ if (dot === -1) {
628
+ return undefined;
629
+ }
630
+ const extension = rest.substring(dot + 1);
631
+ const classifierPart = rest.substring(0, dot);
632
+ if (!classifierPart) {
633
+ return { classifier: "", extension };
634
+ }
635
+ if (!classifierPart.startsWith("-") || classifierPart.length < 2) {
636
+ return undefined;
637
+ }
638
+ return { classifier: classifierPart.substring(1), extension };
639
+ }
640
+
641
+ /**
642
+ * Coordinates from a path in the Maven repository layout:
643
+ * `<root>/<group path>/<artifactId>/<version>/<artifactId>-<version>[-<classifier>].<ext>`.
644
+ *
645
+ * @param {string[]} parts Path segments below the repository root.
646
+ * @returns {Object|undefined}
647
+ */
648
+ function coordinatesFromMavenLayout(parts) {
649
+ if (parts.length < 4) {
650
+ return undefined;
651
+ }
652
+ const fileName = parts[parts.length - 1];
653
+ const version = parts[parts.length - 2];
654
+ const name = parts[parts.length - 3];
655
+ const group = parts.slice(0, -3).join(".");
656
+ let split = splitArtifactFileName(fileName, name, version);
657
+ if (!split && version.endsWith("-SNAPSHOT")) {
658
+ // Timestamped snapshot files replace -SNAPSHOT with the build stamp.
659
+ const base = version.substring(0, version.length - "-SNAPSHOT".length);
660
+ const stamp = /^-\d{8}\.\d{6}-\d+/.exec(
661
+ fileName.substring(`${name}-${base}`.length),
662
+ );
663
+ if (fileName.startsWith(`${name}-${base}`) && stamp) {
664
+ split = splitArtifactFileName(fileName, name, `${base}${stamp[0]}`);
665
+ }
666
+ }
667
+ if (!group || !split) {
668
+ return undefined;
669
+ }
670
+ return { group, name, version, ...split };
671
+ }
672
+
673
+ /**
674
+ * Coordinates from a path in the Gradle module cache layout:
675
+ * `<root>/<group>/<artifactId>/<version>/<sha1>/<file>`.
676
+ *
677
+ * @param {string[]} parts Path segments below the cache root.
678
+ * @returns {Object|undefined}
679
+ */
680
+ function coordinatesFromGradleLayout(parts) {
681
+ if (parts.length !== 5) {
682
+ return undefined;
683
+ }
684
+ const [group, name, version, sha1, fileName] = parts;
685
+ const split = splitArtifactFileName(fileName, name, version);
686
+ if (!split) {
687
+ return undefined;
688
+ }
689
+ return {
690
+ group,
691
+ name,
692
+ version,
693
+ ...split,
694
+ ...(SHA1_HEX.test(sha1) ? { sha1 } : {}),
695
+ };
696
+ }
697
+
698
+ /**
699
+ * Segments of `filePath` below `root`, or undefined when it is not inside.
700
+ *
701
+ * @param {string} root Root directory.
702
+ * @param {string} filePath File path.
703
+ * @returns {string[]|undefined}
704
+ */
705
+ function segmentsBelow(root, filePath) {
706
+ const rel = relative(root, filePath);
707
+ if (!rel || rel.startsWith("..") || isAbsolute(rel)) {
708
+ return undefined;
709
+ }
710
+ return rel.split(/[\\/]/).filter(Boolean);
711
+ }
712
+
713
+ /**
714
+ * Infer Maven coordinates from where an artifact sits on disk.
715
+ *
716
+ * Paths inside a configured Maven repository or Gradle cache are read by
717
+ * their layout. Paths elsewhere are recognised by the conventional
718
+ * `.m2/repository` and `files-2.1` segments, so a copied cache still works. A
719
+ * Coursier path is resolved through its sibling POM, because the repository
720
+ * prefix and the group path cannot be told apart from the path alone.
721
+ *
722
+ * @param {string} filePath Absolute path of a jar or other artifact.
723
+ * @returns {{group: string, name: string, version: string, classifier: string, extension: string, sha1?: string}|undefined}
724
+ */
725
+ export function inferMavenCoordinatesFromPath(filePath) {
726
+ if (!filePath) {
727
+ return undefined;
728
+ }
729
+ const absolute = resolve(filePath);
730
+ for (const repo of mavenLocalRepositories()) {
731
+ const parts = segmentsBelow(repo, absolute);
732
+ if (parts) {
733
+ return coordinatesFromMavenLayout(parts);
734
+ }
735
+ }
736
+ for (const root of gradleCacheRoots()) {
737
+ const parts = segmentsBelow(root, absolute);
738
+ if (parts) {
739
+ return coordinatesFromGradleLayout(parts);
740
+ }
741
+ }
742
+ const segments = absolute.split(/[\\/]/);
743
+ for (let i = segments.length - 2; i > 0; i--) {
744
+ if (segments[i] === "repository" && segments[i - 1] === ".m2") {
745
+ return coordinatesFromMavenLayout(segments.slice(i + 1));
746
+ }
747
+ if (segments[i] === "files-2.1") {
748
+ return coordinatesFromGradleLayout(segments.slice(i + 1));
749
+ }
750
+ }
751
+ const coursierRoot = coursierCacheDir();
752
+ if (coursierRoot && segmentsBelow(coursierRoot, absolute)) {
753
+ const fileName = basename(absolute);
754
+ const version = basename(dirname(absolute));
755
+ const name = basename(dirname(dirname(absolute)));
756
+ const pomPath = join(dirname(absolute), `${name}-${version}.pom`);
757
+ const split = splitArtifactFileName(fileName, name, version);
758
+ if (split && safeExistsSync(pomPath)) {
759
+ try {
760
+ const project = xml2js(readFileSync(pomPath, "utf-8"), {
761
+ compact: true,
762
+ textKey: "_",
763
+ }).project;
764
+ const group = project?.groupId?._ || project?.parent?.groupId?._;
765
+ if (group && (project?.artifactId?._ || name) === name) {
766
+ return { group, name, version, ...split };
767
+ }
768
+ } catch (_err) {
769
+ return undefined;
770
+ }
771
+ }
772
+ }
773
+ return undefined;
774
+ }
775
+
776
+ /**
777
+ * Read the SHA-1 recorded in a Maven `*.sha1` checksum file.
778
+ *
779
+ * @param {string} file Checksum file.
780
+ * @returns {string|undefined} Lowercase hex digest.
781
+ */
782
+ function readSha1File(file) {
783
+ try {
784
+ const digest = readFileSync(file, "utf-8").trim().split(/\s+/)[0];
785
+ const lower = digest?.toLowerCase();
786
+ return SHA1_HEX.test(lower) ? lower : undefined;
787
+ } catch (_err) {
788
+ return undefined;
789
+ }
790
+ }
791
+
792
+ /**
793
+ * Walk a Maven repository and record `<jar>.sha1` digests.
794
+ *
795
+ * @param {string} dir Directory to walk.
796
+ * @param {Map<string, Object>} index Index being built.
797
+ * @param {string} repoRoot Repository root.
798
+ * @returns {void}
799
+ */
800
+ function indexMavenRepository(dir, index, repoRoot) {
801
+ const stack = [dir];
802
+ while (stack.length && index.size < MAX_SHA1_INDEX_ENTRIES) {
803
+ const current = stack.pop();
804
+ let entries;
805
+ try {
806
+ entries = readdirSync(current, { withFileTypes: true });
807
+ } catch (_err) {
808
+ continue;
809
+ }
810
+ for (const entry of entries) {
811
+ const entryPath = join(current, entry.name);
812
+ if (entry.isDirectory()) {
813
+ stack.push(entryPath);
814
+ } else if (entry.name.endsWith(".jar.sha1")) {
815
+ const digest = readSha1File(entryPath);
816
+ const jarPath = entryPath.substring(0, entryPath.length - 5);
817
+ const parts = segmentsBelow(repoRoot, jarPath);
818
+ const coordinates = parts && coordinatesFromMavenLayout(parts);
819
+ if (digest && coordinates && !index.has(digest)) {
820
+ index.set(digest, { ...coordinates, path: jarPath });
821
+ }
822
+ }
823
+ }
824
+ }
825
+ }
826
+
827
+ /**
828
+ * Walk a Gradle module cache. The SHA-1 is the directory name, so no file is
829
+ * read: `<root>/<group>/<artifactId>/<version>/<sha1>/<file>`.
830
+ *
831
+ * @param {string} root Cache root.
832
+ * @param {Map<string, Object>} index Index being built.
833
+ * @returns {void}
834
+ */
835
+ function indexGradleCache(root, index) {
836
+ const list = (dir) => {
837
+ try {
838
+ return readdirSync(dir, { withFileTypes: true }).filter((e) =>
839
+ e.isDirectory(),
840
+ );
841
+ } catch (_err) {
842
+ return [];
843
+ }
844
+ };
845
+ for (const groupEntry of list(root)) {
846
+ const groupDir = join(root, groupEntry.name);
847
+ for (const nameEntry of list(groupDir)) {
848
+ const nameDir = join(groupDir, nameEntry.name);
849
+ for (const versionEntry of list(nameDir)) {
850
+ const versionDir = join(nameDir, versionEntry.name);
851
+ for (const hashEntry of list(versionDir)) {
852
+ if (index.size >= MAX_SHA1_INDEX_ENTRIES) {
853
+ return;
854
+ }
855
+ const digest = hashEntry.name.toLowerCase();
856
+ if (!SHA1_HEX.test(digest) || index.has(digest)) {
857
+ continue;
858
+ }
859
+ let files;
860
+ try {
861
+ files = readdirSync(join(versionDir, hashEntry.name));
862
+ } catch (_err) {
863
+ continue;
864
+ }
865
+ for (const fileName of files) {
866
+ const coordinates = coordinatesFromGradleLayout([
867
+ groupEntry.name,
868
+ nameEntry.name,
869
+ versionEntry.name,
870
+ hashEntry.name,
871
+ fileName,
872
+ ]);
873
+ if (coordinates?.extension === "jar") {
874
+ index.set(digest, {
875
+ ...coordinates,
876
+ path: join(versionDir, hashEntry.name, fileName),
877
+ });
878
+ break;
879
+ }
880
+ }
881
+ }
882
+ }
883
+ }
884
+ }
885
+ }
886
+
887
+ /**
888
+ * Look up the Maven coordinates of a jar by its SHA-1 in the local caches.
889
+ *
890
+ * The index is built once, on first use, from the Gradle module cache (whose
891
+ * directory names are SHA-1s) and from the `*.jar.sha1` files Maven writes
892
+ * next to every downloaded jar. It answers the question cdxgen would otherwise
893
+ * send to the Maven Central search API.
894
+ *
895
+ * @param {string} sha1 Hex SHA-1 of the jar.
896
+ * @returns {{group: string, name: string, version: string, classifier: string, extension: string, path: string}|undefined}
897
+ */
898
+ export function findMavenCoordinatesBySha1(sha1) {
899
+ const digest = sha1?.toLowerCase();
900
+ if (!digest || !SHA1_HEX.test(digest)) {
901
+ return undefined;
902
+ }
903
+ if (!sha1Index) {
904
+ sha1Index = new Map();
905
+ for (const root of gradleCacheRoots()) {
906
+ if (safeExistsSync(root)) {
907
+ indexGradleCache(root, sha1Index);
908
+ }
909
+ }
910
+ for (const repo of mavenLocalRepositories()) {
911
+ if (safeExistsSync(repo)) {
912
+ indexMavenRepository(repo, sha1Index, repo);
913
+ }
914
+ }
915
+ }
916
+ return sha1Index.get(digest);
917
+ }