@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
@@ -1,17 +1,12 @@
1
- import { lstatSync, readFileSync } from "node:fs";
1
+ import { lstatSync, readFileSync, statSync } from "node:fs";
2
2
  import { homedir, tmpdir } from "node:os";
3
- import {
4
- delimiter as _delimiter,
5
- sep as _sep,
6
- basename,
7
- dirname,
8
- join,
9
- } from "node:path";
3
+ import { delimiter as _delimiter, basename, dirname, join } from "node:path";
10
4
 
11
5
  import { build, Purl } from "@cdxgen/cdx-purl";
12
6
  import StreamZip from "node-stream-zip";
13
7
 
14
8
  import { DEBUG_MODE, readEnvironmentVariable } from "../core/activity.js";
9
+ import { commandOutputText } from "../core/buildLedger.js";
15
10
  import { parseMavenArgs } from "../core/env.js";
16
11
  import {
17
12
  getAllFiles,
@@ -23,9 +18,82 @@ import {
23
18
  } from "../core/fs.js";
24
19
  import { isWin } from "../core/paths.js";
25
20
  import { xml2js } from "../parsers/xml.js";
21
+ import { noteBuildToolRateLimit } from "./buildToolRateLimit.js";
22
+ import {
23
+ inferMavenCoordinatesFromPath,
24
+ mavenLocalRepositories,
25
+ splitArtifactFileName,
26
+ } from "./jvmLocalRepos.js";
26
27
  import { pypiBomRef } from "./purl.js";
28
+ import { findLicenseId, spdxLicenses } from "./spdx.js";
29
+
30
+ const jarNSMapping_cache = new Map();
31
+
32
+ /**
33
+ * Whether a jar holds sources or javadoc rather than classes.
34
+ *
35
+ * @param {string} jarName Jar file name.
36
+ * @returns {boolean}
37
+ */
38
+ export function isDocumentationJar(jarName) {
39
+ return jarName.endsWith("-sources.jar") || jarName.endsWith("-javadoc.jar");
40
+ }
27
41
 
28
- const jarNSMapping_cache = {};
42
+ /**
43
+ * Size and modification time of a file, used to tell whether a memoised
44
+ * reading of it is still current.
45
+ *
46
+ * @param {string} file File path.
47
+ * @returns {string} Stamp, or an empty string when the file cannot be read.
48
+ */
49
+ function jarFileStamp(file) {
50
+ try {
51
+ const stats = statSync(file);
52
+ return `${stats.size}:${stats.mtimeMs}`;
53
+ } catch (_err) {
54
+ return "";
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Build the purl of a jar. Eclipse p2 artifacts mirrored into a Maven
60
+ * repository keep their dedicated purl type qualifiers.
61
+ *
62
+ * @param {string} group groupId.
63
+ * @param {string} name artifactId.
64
+ * @param {string} version Version.
65
+ * @param {string} [classifier] Classifier.
66
+ * @returns {string} Purl string.
67
+ */
68
+ function jarPurl(group, name, version, classifier = undefined) {
69
+ let namespace = group || null;
70
+ let type = "jar";
71
+ // See https://github.com/CycloneDX/cyclonedx-maven-plugin/issues/137
72
+ // and https://github.com/cdxgen/cdxgen/pull/510#issuecomment-1702551615
73
+ for (const [prefix, p2Type] of [
74
+ ["p2.osgi.bundle", "osgi-bundle"],
75
+ ["p2.eclipse.plugin", "eclipse-plugin"],
76
+ ["p2.binary", "eclipse-executable"],
77
+ ["p2.org.eclipse.update.feature", "eclipse-feature"],
78
+ ]) {
79
+ if (namespace?.startsWith(prefix)) {
80
+ namespace = prefix;
81
+ type = p2Type;
82
+ break;
83
+ }
84
+ }
85
+ const qualifiers = { type };
86
+ if (classifier) {
87
+ qualifiers.classifier = classifier;
88
+ }
89
+ return new Purl({
90
+ type: "maven",
91
+ namespace,
92
+ name,
93
+ version: version || null,
94
+ qualifiers,
95
+ }).toString();
96
+ }
29
97
 
30
98
  /**
31
99
  * Collect maven dependencies
@@ -42,14 +110,20 @@ export async function collectMvnDependencies(
42
110
  includeCacheDir = false,
43
111
  ) {
44
112
  let jarNSMapping = {};
113
+ // MAVEN_CACHE_DIR when set, otherwise the local repository Maven itself
114
+ // uses for this project.
45
115
  const MAVEN_CACHE_DIR =
46
116
  readEnvironmentVariable("MAVEN_CACHE_DIR") ||
117
+ mavenLocalRepositories({ projectDir: basePath || undefined }).find((repo) =>
118
+ safeExistsSync(repo),
119
+ ) ||
47
120
  join(homedir(), ".m2", "repository");
48
121
  const tempDir = safeMkdtempSync(join(tmpdir(), "mvn-deps-"));
122
+ // No -U: forcing update checks re-requests every maven-metadata.xml and
123
+ // every artifact Maven already knows is missing, once per module.
49
124
  let copyArgs = [
50
125
  "dependency:copy-dependencies",
51
126
  `-DoutputDirectory=${tempDir}`,
52
- "-U",
53
127
  "-Dmdep.copyPom=true",
54
128
  "-Dmdep.useRepositoryLayout=true",
55
129
  "-Dmdep.includeScope=compile",
@@ -66,27 +140,41 @@ export async function collectMvnDependencies(
66
140
  cwd: basePath,
67
141
  shell: isWin,
68
142
  });
143
+ const rateLimited = noteBuildToolRateLimit(
144
+ "maven",
145
+ commandOutputText(result),
146
+ {
147
+ command: `${basename(mavenCmd)} ${copyArgs.join(" ")}`,
148
+ exitCode: typeof result.status === "number" ? result.status : undefined,
149
+ },
150
+ );
69
151
  if (result.status !== 0 || result.error) {
70
- console.error(result.stderr, result.error);
71
- console.log(
72
- "You can try the following remediation tips to resolve this error:\n",
73
- );
74
- console.log(
75
- "1. Check if the correct version of maven is installed and available in the PATH. Check if the environment variable MVN_ARGS needs to be set.",
76
- );
77
- console.log(
78
- "2. Perform 'mvn compile package' before invoking this command. Fix any errors found during this invocation.",
79
- );
80
- console.log(
81
- "3. Ensure the temporary directory is available and has sufficient disk space to copy all the artifacts.",
82
- );
152
+ if (!rateLimited) {
153
+ console.error(result.stderr, result.error);
154
+ console.log(
155
+ "You can try the following remediation tips to resolve this error:\n",
156
+ );
157
+ console.log(
158
+ "1. Check if the correct version of maven is installed and available in the PATH. Check if the environment variable MVN_ARGS needs to be set.",
159
+ );
160
+ console.log(
161
+ "2. Perform 'mvn compile package' before invoking this command. Fix any errors found during this invocation.",
162
+ );
163
+ console.log(
164
+ "3. Ensure the temporary directory is available and has sufficient disk space to copy all the artifacts.",
165
+ );
166
+ }
83
167
  } else {
84
168
  jarNSMapping = await collectJarNS(tempDir);
85
169
  }
86
170
  }
87
171
  if (includeCacheDir || basePath === MAVEN_CACHE_DIR) {
88
- // slow operation
89
- jarNSMapping = await collectJarNS(MAVEN_CACHE_DIR);
172
+ // slow operation. The project's own dependencies, copied above, take
173
+ // precedence over the rest of the cache.
174
+ jarNSMapping = {
175
+ ...(await collectJarNS(MAVEN_CACHE_DIR)),
176
+ ...jarNSMapping,
177
+ };
90
178
  }
91
179
 
92
180
  // Clean up
@@ -122,11 +210,18 @@ export async function collectJarNS(jarPath, pomPathMap = {}) {
122
210
  : getAllFiles(jarPath, "**/*.jar");
123
211
  if (jarFiles?.length) {
124
212
  for (const jf of jarFiles) {
213
+ const jarName = basename(jf);
214
+ // Sources and javadoc jars hold no runtime classes. Reading them would
215
+ // attach their hashes to the main artifact's purl.
216
+ if (isDocumentationJar(jarName)) {
217
+ continue;
218
+ }
125
219
  let pomname =
126
- pomPathMap[basename(jf).replace(".jar", ".pom")] ||
220
+ pomPathMap[jarName.replace(".jar", ".pom")] ||
127
221
  jf.replace(".jar", ".pom");
128
222
  let pomData;
129
223
  let purl;
224
+ let manifestLicenses;
130
225
  // In some cases, the pom name might be slightly different to the jar name
131
226
  if (!safeExistsSync(pomname)) {
132
227
  let searchDir = dirname(jf);
@@ -144,92 +239,54 @@ export async function collectJarNS(jarPath, pomPathMap = {}) {
144
239
  // TODO: Replace with parsePom which contains pomPurl
145
240
  pomData = parsePomXml(readFileSync(pomname, { encoding: "utf-8" }));
146
241
  if (pomData) {
147
- const purlObj = new Purl({
148
- type: "maven",
149
- namespace: pomData.groupId || "" || null,
150
- name: pomData.artifactId,
151
- version: pomData.version || null,
152
- qualifiers: { type: "jar" } || null,
153
- });
154
- purl = purlObj.toString();
242
+ purl = jarPurl(
243
+ pomData.groupId,
244
+ pomData.artifactId,
245
+ pomData.version,
246
+ pomData.version
247
+ ? splitArtifactFileName(
248
+ jarName,
249
+ pomData.artifactId,
250
+ pomData.version,
251
+ )?.classifier
252
+ : undefined,
253
+ );
254
+ }
255
+ } else {
256
+ const coordinates = inferMavenCoordinatesFromPath(jf);
257
+ if (coordinates?.extension === "jar") {
258
+ purl = jarPurl(
259
+ coordinates.group,
260
+ coordinates.name,
261
+ coordinates.version,
262
+ coordinates.classifier,
263
+ );
155
264
  }
156
- } else if (jf.includes(join(".m2", "repository"))) {
157
- // Let's try our best to construct a purl for .m2 cache entries of the form
158
- // .m2/repository/org/apache/logging/log4j/log4j-web/3.0.0-SNAPSHOT/log4j-web-3.0.0-SNAPSHOT.jar
159
- const tmpA = jf.split(join(".m2", "repository", ""));
160
- if (tmpA?.length) {
161
- const tmpJarPath = tmpA[tmpA.length - 1];
162
- // This would yield log4j-web-3.0.0-SNAPSHOT.jar
163
- const jarFileName = basename(tmpJarPath).replace(".jar", "");
164
- const tmpDirParts = dirname(tmpJarPath).split(_sep);
165
- // Retrieve the version
166
- let jarVersion = tmpDirParts.pop();
167
- if (jarVersion === "plugins") {
168
- jarVersion = tmpDirParts.pop();
169
- if (jarVersion === "eclipse") {
170
- jarVersion = tmpDirParts.pop();
265
+ // No POM sits beside this jar, but the jar carries its own Maven
266
+ // descriptor, so its metadata and manifest licence are read here,
267
+ // before any remote lookup. Only a jar whose path names its
268
+ // coordinates is read: they choose its descriptor, and an entry
269
+ // without a purl never becomes a component.
270
+ if (purl) {
271
+ const embedded = await readEmbeddedJarMetadata(jf, coordinates);
272
+ if (embedded?.pomXml) {
273
+ try {
274
+ pomData = parsePomXml(embedded.pomXml);
275
+ } catch (_err) {
276
+ pomData = undefined;
171
277
  }
172
278
  }
173
- // The result would form the group name
174
- let jarGroupName = tmpDirParts.join(".").replace(/^\./, "");
175
- let qualifierType = "jar";
176
- // Support for p2 bundles and plugins
177
- // See https://github.com/CycloneDX/cyclonedx-maven-plugin/issues/137
178
- // See https://github.com/cdxgen/cdxgen/pull/510#issuecomment-1702551615
179
- if (jarGroupName.startsWith("p2.osgi.bundle")) {
180
- jarGroupName = "p2.osgi.bundle";
181
- qualifierType = "osgi-bundle";
182
- } else if (jarGroupName.startsWith("p2.eclipse.plugin")) {
183
- jarGroupName = "p2.eclipse.plugin";
184
- qualifierType = "eclipse-plugin";
185
- } else if (jarGroupName.startsWith("p2.binary")) {
186
- jarGroupName = "p2.binary";
187
- qualifierType = "eclipse-executable";
188
- } else if (jarGroupName.startsWith("p2.org.eclipse.update.feature")) {
189
- jarGroupName = "p2.org.eclipse.update.feature";
190
- qualifierType = "eclipse-feature";
191
- }
192
- const purlObj = new Purl({
193
- type: "maven",
194
- namespace: jarGroupName || null,
195
- name: jarFileName.replace(`-${jarVersion}`, ""),
196
- version: jarVersion || null,
197
- qualifiers: { type: qualifierType } || null,
198
- });
199
- purl = purlObj.toString();
200
- }
201
- } else if (jf.includes(join(".gradle", "caches"))) {
202
- // Let's try our best to construct a purl for gradle cache entries of the form
203
- // .gradle/caches/modules-2/files-2.1/org.xmlresolver/xmlresolver/4.2.0/f4dbdaa83d636dcac91c9003ffa7fb173173fe8d/xmlresolver-4.2.0-data.jar
204
- const tmpA = jf.split(join("files-2.1", ""));
205
- if (tmpA?.length) {
206
- const tmpJarPath = tmpA[tmpA.length - 1];
207
- // This would yield xmlresolver-4.2.0-data.jar
208
- const jarFileName = basename(tmpJarPath).replace(".jar", "");
209
- const tmpDirParts = dirname(tmpJarPath).split(_sep);
210
- // This would remove the hash from the end of the directory name
211
- tmpDirParts.pop();
212
- // Retrieve the version
213
- const jarVersion = tmpDirParts.pop();
214
- const pkgName = jarFileName.replace(`-${jarVersion}`, "");
215
- // The result would form the group name
216
- let jarGroupName = tmpDirParts.join(".").replace(/^\./, "");
217
- if (jarGroupName.includes(pkgName)) {
218
- jarGroupName = jarGroupName.replace(`.${pkgName}`, "");
279
+ if (embedded?.manifestLicenses?.length) {
280
+ manifestLicenses = embedded.manifestLicenses;
219
281
  }
220
- const purlObj = new Purl({
221
- type: "maven",
222
- namespace: jarGroupName || null,
223
- name: pkgName,
224
- version: jarVersion || null,
225
- qualifiers: { type: "jar" } || null,
226
- });
227
- purl = purlObj.toString();
228
282
  }
229
283
  }
230
- // If we have a hit from the cache, use it.
231
- if (purl && jarNSMapping_cache[purl]) {
232
- jarNSMapping[purl] = jarNSMapping_cache[purl];
284
+ // If we have a hit from the cache, use it. The entry is reused only
285
+ // for the same file, unchanged, so a rebuilt SNAPSHOT is read again.
286
+ const fileStamp = jarFileStamp(jf);
287
+ const cached = purl ? jarNSMapping_cache.get(purl) : undefined;
288
+ if (cached && cached.jarFile === jf && cached.stamp === fileStamp) {
289
+ jarNSMapping[purl] = cached.entry;
233
290
  } else {
234
291
  if (DEBUG_MODE) {
235
292
  console.log(`Parsing ${jf}`);
@@ -254,10 +311,15 @@ export async function collectJarNS(jarPath, pomPathMap = {}) {
254
311
  pom: pomData,
255
312
  namespaces: nsList,
256
313
  hashes,
314
+ ...(manifestLicenses?.length ? { manifestLicenses } : {}),
257
315
  };
258
316
  // Retain in the global cache to speed up future lookups
259
317
  if (purl) {
260
- jarNSMapping_cache[purl] = jarNSMapping[purl];
318
+ jarNSMapping_cache.set(purl, {
319
+ jarFile: jf,
320
+ stamp: fileStamp,
321
+ entry: jarNSMapping[purl],
322
+ });
261
323
  }
262
324
  }
263
325
  }
@@ -398,15 +460,24 @@ export function parseJarManifest(jarMetadata) {
398
460
  if (!jarMetadata) {
399
461
  return metadata;
400
462
  }
401
- jarMetadata.split("\n").forEach((l) => {
402
- l = l.replaceAll("\r", "");
403
- if (l.includes(": ")) {
404
- const tmpA = l.split(": ");
405
- if (tmpA && tmpA.length === 2) {
406
- metadata[tmpA[0]] = tmpA[1].replaceAll("\r", "");
463
+ // Manifests wrap at 72 bytes: a line that starts with a single space
464
+ // continues the header above it.
465
+ let name;
466
+ for (const rawLine of jarMetadata.split("\n")) {
467
+ const l = rawLine.replaceAll("\r", "");
468
+ if (l.startsWith(" ")) {
469
+ if (name !== undefined) {
470
+ metadata[name] += l.slice(1);
407
471
  }
472
+ continue;
408
473
  }
409
- });
474
+ name = undefined;
475
+ const at = l.indexOf(": ");
476
+ if (at > 0) {
477
+ name = l.slice(0, at);
478
+ metadata[name] = l.slice(at + 2);
479
+ }
480
+ }
410
481
  return metadata;
411
482
  }
412
483
 
@@ -505,22 +576,328 @@ export function parsePomProperties(pomProperties) {
505
576
  /**
506
577
  * Method to get pom properties from maven directory
507
578
  *
579
+ * A shaded or fat jar carries a `pom.properties` for every artifact folded
580
+ * into it. When the jar's file name is known, the descriptor whose
581
+ * `artifactId-version` (or `artifactId`) starts the file name is the jar's
582
+ * own; otherwise the first descriptor is used, as before.
583
+ *
508
584
  * @param {string} mavenDir Path to maven directory
585
+ * @param {string} [jarName] File name of the jar the directory came from
509
586
  *
510
587
  * @return array with pom properties
511
588
  */
512
- export function getPomPropertiesFromMavenDir(mavenDir) {
513
- let pomProperties = {};
514
- if (safeExistsSync(mavenDir) && lstatSync(mavenDir).isDirectory()) {
515
- const pomPropertiesFiles = getAllFiles(mavenDir, "**/pom.properties");
516
- if (pomPropertiesFiles?.length) {
517
- const pomPropertiesString = readFileSync(pomPropertiesFiles[0], {
518
- encoding: "utf-8",
519
- });
520
- pomProperties = parsePomProperties(pomPropertiesString);
589
+ export function getPomPropertiesFromMavenDir(mavenDir, jarName = undefined) {
590
+ return selectEmbeddedDescriptor(mavenDir, jarName)?.properties ?? {};
591
+ }
592
+
593
+ /**
594
+ * Choose the embedded Maven descriptor of one jar from every descriptor an
595
+ * extracted `META-INF/maven` directory holds.
596
+ *
597
+ * @param {string} mavenDir Path to the extracted META-INF/maven directory
598
+ * @param {string} [jarName] File name of the jar the directory came from
599
+ * @returns {{file: string, properties: Object}|undefined} The chosen
600
+ * descriptor, or undefined when the directory holds none.
601
+ */
602
+ function selectEmbeddedDescriptor(mavenDir, jarName = undefined) {
603
+ if (!safeExistsSync(mavenDir) || !lstatSync(mavenDir).isDirectory()) {
604
+ return undefined;
605
+ }
606
+ const pomPropertiesFiles = getAllFiles(mavenDir, "**/pom.properties").sort();
607
+ if (!pomPropertiesFiles?.length) {
608
+ return undefined;
609
+ }
610
+ const candidates = pomPropertiesFiles.map((f) => ({
611
+ file: f,
612
+ properties: parsePomProperties(readFileSync(f, { encoding: "utf-8" })),
613
+ }));
614
+ const parsed = candidates.map((c) => c.properties);
615
+ if (parsed.length > 1 && jarName) {
616
+ const stem = jarName.replace(/\.[jwe]ar$/, "");
617
+ const exact = candidates.find(
618
+ (c) =>
619
+ c.properties.artifactId &&
620
+ c.properties.version &&
621
+ (stem === `${c.properties.artifactId}-${c.properties.version}` ||
622
+ stem.startsWith(
623
+ `${c.properties.artifactId}-${c.properties.version}-`,
624
+ )),
625
+ );
626
+ if (exact) {
627
+ return exact;
628
+ }
629
+ // The longest matching artifactId wins: netty-common-4.1.jar belongs to
630
+ // netty-common, not to an embedded netty descriptor.
631
+ const byName = candidates
632
+ .filter(
633
+ (c) =>
634
+ c.properties.artifactId &&
635
+ (stem === c.properties.artifactId ||
636
+ stem.startsWith(`${c.properties.artifactId}-`)),
637
+ )
638
+ .sort(
639
+ (a, b) =>
640
+ b.properties.artifactId.length - a.properties.artifactId.length,
641
+ );
642
+ if (
643
+ byName.length === 1 ||
644
+ (byName.length > 1 &&
645
+ byName[0].properties.artifactId.length >
646
+ byName[1].properties.artifactId.length)
647
+ ) {
648
+ return byName[0];
649
+ }
650
+ }
651
+ return candidates[0];
652
+ }
653
+
654
+ /** Per-entry size bounds for the Maven descriptor files read out of a jar.
655
+ * The jar is untrusted input, and its declared entry sizes are the only cheap
656
+ * defence against an archive that expands without bound. */
657
+ const EMBEDDED_POM_PROPERTIES_LIMIT = 64 * 1024;
658
+ const EMBEDDED_POM_XML_LIMIT = 1024 * 1024;
659
+ const EMBEDDED_MANIFEST_LIMIT = 256 * 1024;
660
+ /** A shaded jar can fold in thousands of descriptors; reading a bounded
661
+ * number keeps a hostile archive from turning one zip listing into
662
+ * unbounded work. */
663
+ const EMBEDDED_DESCRIPTOR_LIMIT = 500;
664
+
665
+ /**
666
+ * Read a file, refusing anything larger than the bound, so a jar whose
667
+ * descriptor declares a huge size is never fully read.
668
+ *
669
+ * @param {string} file File path.
670
+ * @param {number} limit Size bound in bytes.
671
+ * @returns {string|undefined} File contents, or undefined when missing or too large.
672
+ */
673
+ function readBoundedFile(file, limit) {
674
+ try {
675
+ if (!safeExistsSync(file) || statSync(file).size > limit) {
676
+ return undefined;
677
+ }
678
+ return readFileSync(file, { encoding: "utf-8" });
679
+ } catch (_err) {
680
+ return undefined;
681
+ }
682
+ }
683
+
684
+ /**
685
+ * Split a manifest header value on a separator that is not inside double
686
+ * quotes. Quoted strings hold commas and semicolons of their own, as in
687
+ * `description="Apache License, Version 2.0"`.
688
+ *
689
+ * @param {string} value Header value.
690
+ * @param {string} separator One character.
691
+ * @returns {string[]} The parts, quotes kept.
692
+ */
693
+ function splitOutsideQuotes(value, separator) {
694
+ const parts = [];
695
+ let current = "";
696
+ let quoted = false;
697
+ for (const ch of value) {
698
+ if (ch === '"') {
699
+ quoted = !quoted;
700
+ } else if (ch === separator && !quoted) {
701
+ parts.push(current);
702
+ current = "";
703
+ continue;
704
+ }
705
+ current += ch;
706
+ }
707
+ parts.push(current);
708
+ return parts;
709
+ }
710
+
711
+ /**
712
+ * Trim a manifest value and remove one pair of surrounding double quotes.
713
+ *
714
+ * @param {string} value Value.
715
+ * @returns {string} The bare value.
716
+ */
717
+ function unquoteManifestValue(value) {
718
+ const trimmed = value.trim();
719
+ return trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')
720
+ ? trimmed.slice(1, -1).trim()
721
+ : trimmed;
722
+ }
723
+
724
+ /**
725
+ * The licences a MANIFEST.MF `Bundle-License` value names. The value is a
726
+ * comma-separated list; each item is an SPDX id, a licence name or a URL,
727
+ * optionally followed by `;link=` and `;description=` attributes, and any
728
+ * part may be quoted. Each item is mapped through findLicenseId; an item it
729
+ * cannot map is replaced by its `link` URL when one is given, which the
730
+ * licence URL lookup resolves later. `<<EXTERNAL>>` names no licence.
731
+ *
732
+ * @param {string} value Raw Bundle-License value.
733
+ * @returns {string[]} Licence ids, names or URLs, without duplicates.
734
+ */
735
+ export function parseManifestLicenseList(value) {
736
+ if (!value || typeof value !== "string") {
737
+ return [];
738
+ }
739
+ const licenses = [];
740
+ for (const clause of splitOutsideQuotes(value, ",")) {
741
+ const [rawName, ...attributes] = splitOutsideQuotes(clause, ";");
742
+ const name = unquoteManifestValue(rawName);
743
+ if (!name || name === "<<EXTERNAL>>") {
744
+ continue;
745
+ }
746
+ let link;
747
+ for (const attribute of attributes) {
748
+ const at = attribute.indexOf("=");
749
+ if (at > 0 && attribute.slice(0, at).trim() === "link") {
750
+ link = unquoteManifestValue(attribute.slice(at + 1));
751
+ }
752
+ }
753
+ const id = findLicenseId(name);
754
+ const mapped = id !== name || spdxLicenses.includes(name);
755
+ const license = mapped ? id : link?.startsWith("http") ? link : name;
756
+ if (license && !licenses.includes(license)) {
757
+ licenses.push(license);
758
+ }
759
+ }
760
+ return licenses;
761
+ }
762
+
763
+ /**
764
+ * The Maven descriptor a jar carries for itself, read from an extracted
765
+ * `META-INF/maven` directory: the pom.properties that names the jar's own
766
+ * coordinates and the pom.xml stored beside it.
767
+ *
768
+ * @param {string} mavenDir Path to the extracted META-INF/maven directory
769
+ * @param {string} [jarName] File name of the jar the directory came from
770
+ * @returns {{properties: Object, pomXml?: string}|undefined} The chosen
771
+ * descriptor, with its pom.xml text when one is stored beside it.
772
+ */
773
+ export function readEmbeddedMavenDescriptor(mavenDir, jarName = undefined) {
774
+ const selected = selectEmbeddedDescriptor(mavenDir, jarName);
775
+ if (!selected) {
776
+ return undefined;
777
+ }
778
+ const pomXml = readBoundedFile(
779
+ join(dirname(selected.file), "pom.xml"),
780
+ EMBEDDED_POM_XML_LIMIT,
781
+ );
782
+ return pomXml
783
+ ? { properties: selected.properties, pomXml }
784
+ : { properties: selected.properties };
785
+ }
786
+
787
+ /**
788
+ * Choose the descriptor of the jar itself from the descriptors held inside
789
+ * its zip: the one whose pom.properties names the jar's own group and
790
+ * artifact, the matching version first. A shaded jar holds many descriptors
791
+ * whose coordinates belong to the folded-in artifacts, so when none matches,
792
+ * nothing is chosen rather than a dependency's descriptor.
793
+ *
794
+ * @param {Array<{name: string, properties: Object}>} descriptors Descriptors found in the zip.
795
+ * @param {{group: string, name: string, version: string}} coordinates The jar's own coordinates.
796
+ * @returns {{name: string, properties: Object}|undefined}
797
+ */
798
+ function chooseZipDescriptor(descriptors, coordinates) {
799
+ if (!coordinates?.group || !coordinates.name) {
800
+ return undefined;
801
+ }
802
+ const byCoordinates = descriptors.filter(
803
+ (d) =>
804
+ d.properties.groupId === coordinates.group &&
805
+ d.properties.artifactId === coordinates.name,
806
+ );
807
+ const byVersion = byCoordinates.find(
808
+ (d) => coordinates.version && d.properties.version === coordinates.version,
809
+ );
810
+ return byVersion || byCoordinates[0];
811
+ }
812
+
813
+ /**
814
+ * The Maven descriptor and manifest licence a jar carries for itself, read
815
+ * from its zip without extracting it. Entry names are only matched and
816
+ * paired, never used as paths, and each entry is read only under a size
817
+ * bound, because the archive is untrusted input.
818
+ *
819
+ * @param {string} jarFile Path to the jar.
820
+ * @param {{group: string, name: string, version: string}} coordinates The jar's own coordinates, which choose its descriptor.
821
+ * @returns {Promise<{properties?: Object, pomXml?: string, manifestLicenses?: string[]}|undefined>}
822
+ */
823
+ export async function readEmbeddedJarMetadata(jarFile, coordinates) {
824
+ let zip;
825
+ try {
826
+ zip = new StreamZip.async({ file: jarFile });
827
+ const entries = await zip.entries();
828
+ const decoder = new TextDecoder("utf-8");
829
+ const descriptors = [];
830
+ const pomEntries = new Map();
831
+ let manifestEntry;
832
+ for (const entry of Object.values(entries)) {
833
+ if (entry.isDirectory) {
834
+ continue;
835
+ }
836
+ if (
837
+ !manifestEntry &&
838
+ entry.name === "META-INF/MANIFEST.MF" &&
839
+ entry.size <= EMBEDDED_MANIFEST_LIMIT
840
+ ) {
841
+ manifestEntry = entry;
842
+ continue;
843
+ }
844
+ if (!entry.name.startsWith("META-INF/maven/")) {
845
+ continue;
846
+ }
847
+ if (entry.name.endsWith("/pom.properties")) {
848
+ if (
849
+ descriptors.length >= EMBEDDED_DESCRIPTOR_LIMIT ||
850
+ entry.size > EMBEDDED_POM_PROPERTIES_LIMIT
851
+ ) {
852
+ continue;
853
+ }
854
+ const text = decoder.decode(
855
+ Buffer.from(await zip.entryData(entry.name)),
856
+ );
857
+ descriptors.push({
858
+ name: entry.name,
859
+ properties: parsePomProperties(text),
860
+ });
861
+ } else if (entry.name.endsWith("/pom.xml")) {
862
+ const dir = entry.name.slice(0, -"pom.xml".length);
863
+ if (!pomEntries.has(dir)) {
864
+ pomEntries.set(dir, entry);
865
+ }
866
+ }
867
+ }
868
+ const chosen = chooseZipDescriptor(descriptors, coordinates);
869
+ const result = {};
870
+ if (chosen) {
871
+ result.properties = chosen.properties;
872
+ const dir = chosen.name.slice(0, -"pom.properties".length);
873
+ const pomEntry = pomEntries.get(dir);
874
+ if (pomEntry && pomEntry.size <= EMBEDDED_POM_XML_LIMIT) {
875
+ result.pomXml = decoder.decode(
876
+ Buffer.from(await zip.entryData(pomEntry.name)),
877
+ );
878
+ }
879
+ }
880
+ if (manifestEntry) {
881
+ const manifest = parseJarManifest(
882
+ decoder.decode(
883
+ Buffer.from(await zip.entryData("META-INF/MANIFEST.MF")),
884
+ ),
885
+ );
886
+ const licenses = parseManifestLicenseList(manifest["Bundle-License"]);
887
+ if (licenses.length) {
888
+ result.manifestLicenses = licenses;
889
+ }
890
+ }
891
+ return chosen || manifestEntry ? result : undefined;
892
+ } catch (_err) {
893
+ return undefined;
894
+ } finally {
895
+ try {
896
+ await zip.close();
897
+ } catch (_err) {
898
+ // An already-failed zip reports its close as well.
521
899
  }
522
900
  }
523
- return pomProperties;
524
901
  }
525
902
 
526
903
  /**