2121
2222package org.ossreviewtoolkit.plugins.packagemanagers.cargo
2323
24- import com.fasterxml.jackson.databind.JsonNode
24+ import com.fasterxml.jackson.module.kotlin.readValue
2525
2626import com.moandjiezana.toml.Toml
2727
@@ -49,7 +49,6 @@ import org.ossreviewtoolkit.model.jsonMapper
4949import org.ossreviewtoolkit.model.orEmpty
5050import org.ossreviewtoolkit.utils.common.CommandLineTool
5151import org.ossreviewtoolkit.utils.common.splitOnWhitespace
52- import org.ossreviewtoolkit.utils.common.textValueOrEmpty
5352import org.ossreviewtoolkit.utils.common.unquote
5453import org.ossreviewtoolkit.utils.ort.DeclaredLicenseProcessor
5554import org.ossreviewtoolkit.utils.ort.ProcessedDeclaredLicense
@@ -86,9 +85,8 @@ class Cargo(
8685 * Cargo.lock is located next to Cargo.toml or in one of the parent directories. The latter is the case when the
8786 * project is part of a workspace. Cargo.lock is then located next to the Cargo.toml file defining the workspace.
8887 */
89- private fun resolveLockfile (metadata : JsonNode ): File {
90- val workspaceRoot = metadata[" workspace_root" ].textValueOrEmpty()
91- val workingDir = File (workspaceRoot)
88+ private fun resolveLockfile (metadata : CargoMetadata ): File {
89+ val workingDir = File (metadata.workspaceRoot)
9290 val lockfile = workingDir.resolve(" Cargo.lock" )
9391
9492 requireLockfile(workingDir) { lockfile.isFile }
@@ -136,29 +134,23 @@ class Cargo(
136134 name : String ,
137135 version : String ,
138136 packages : Map <String , Package >,
139- metadata : JsonNode
137+ metadata : CargoMetadata
140138 ): PackageReference {
141- val node = metadata[" packages" ].single {
142- it[" name" ].textValue() == name && it[" version" ].textValue() == version
143- }
139+ val node = metadata.packages.single { it.name == name && it.version == version }
144140
145- val dependencies = node[ " dependencies" ] .filter {
141+ val dependencies = node. dependencies.filter {
146142 // Filter dev and build dependencies, because they are not transitive.
147- val kind = it[" kind" ].textValueOrEmpty()
148- kind != " dev" && kind != " build"
143+ it.kind != " dev" && it.kind != " build"
149144 }.mapNotNullTo(mutableSetOf ()) {
150145 // TODO: Handle renamed dependencies here, see:
151146 // https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#renaming-dependencies-in-cargotoml
152- val dependencyName = it[" name" ].textValue()
153-
154- getResolvedVersion(name, version, dependencyName, metadata)?.let { dependencyVersion ->
155- buildDependencyTree(dependencyName, dependencyVersion, packages, metadata)
147+ getResolvedVersion(name, version, it.name, metadata)?.let { dependencyVersion ->
148+ buildDependencyTree(it.name, dependencyVersion, packages, metadata)
156149 }
157150 }
158151
159- val id = parseCargoId(node)
160- val pkg = packages.getValue(id)
161- val linkage = if (isProjectDependency(id)) PackageLinkage .PROJECT_STATIC else PackageLinkage .STATIC
152+ val pkg = packages.getValue(node.id)
153+ val linkage = if (isProjectDependency(node.id)) PackageLinkage .PROJECT_STATIC else PackageLinkage .STATIC
162154
163155 return pkg.toReference(linkage, dependencies)
164156 }
@@ -172,29 +164,26 @@ class Cargo(
172164
173165 val workingDir = definitionFile.parentFile
174166 val metadataProcess = run (workingDir, " metadata" , " --format-version=1" )
175- val metadata = jsonMapper.readTree (metadataProcess.stdout)
167+ val metadata = jsonMapper.readValue< CargoMetadata > (metadataProcess.stdout)
176168 val hashes = readHashes(resolveLockfile(metadata))
177169
178- val packages = metadata[ " packages" ] .associateBy(
179- { parseCargoId(it) },
170+ val packages = metadata. packages.associateBy(
171+ { it.id },
180172 { parsePackage(it, hashes) }
181173 )
182174
183- val projectId = metadata[" workspace_members" ]
184- .map { it.textValueOrEmpty() }
185- .single { it.startsWith(" $projectName $projectVersion " ) }
175+ val projectId = metadata.workspaceMembers.single { it.startsWith(" $projectName $projectVersion " ) }
186176
187- val projectNode = metadata[ " packages" ] .single { it[ " id " ].textValueOrEmpty() == projectId }
188- val groupedDependencies = projectNode[ " dependencies" ] .groupBy { it[ " kind" ].textValueOrEmpty () }
177+ val projectNode = metadata. packages.single { it.id == projectId }
178+ val groupedDependencies = projectNode. dependencies.groupBy { it. kind.orEmpty () }
189179
190- fun getTransitiveDependencies (directDependencies : List <JsonNode >? , scope : String ): Scope ? {
180+ fun getTransitiveDependencies (directDependencies : List <CargoMetadata . Dependency >? , scope : String ): Scope ? {
191181 if (directDependencies == null ) return null
192182
193183 val transitiveDependencies = directDependencies
194184 .mapNotNull { dependency ->
195- val dependencyName = dependency[" name" ].textValue()
196- val version = getResolvedVersion(projectName, projectVersion, dependencyName, metadata)
197- version?.let { Pair (dependencyName, it) }
185+ val version = getResolvedVersion(projectName, projectVersion, dependency.name, metadata)
186+ version?.let { Pair (dependency.name, it) }
198187 }
199188 .mapTo(mutableSetOf ()) {
200189 buildDependencyTree(name = it.first, version = it.second, packages = packages, metadata = metadata)
@@ -239,18 +228,16 @@ class Cargo(
239228
240229private val PATH_DEPENDENCY_REGEX = Regex (""" ^.*\(path\+file://(.*)\)$""" )
241230
242- private fun parseCargoId (node : JsonNode ) = node[" id" ].textValueOrEmpty()
243-
244- private fun parseDeclaredLicenses (node : JsonNode ): Set <String > {
245- val declaredLicenses = node[" license" ].textValueOrEmpty().split(' /' )
231+ private fun parseDeclaredLicenses (pkg : CargoMetadata .Package ): Set <String > {
232+ val declaredLicenses = pkg.license.orEmpty().split(' /' )
246233 .map { it.trim() }
247234 .filterTo(mutableSetOf ()) { it.isNotEmpty() }
248235
249236 // Cargo allows declaring non-SPDX licenses only by referencing a license file. If a license file is specified, add
250237 // an unknown declared license to indicate that there is a declared license, but we cannot know which it is at this
251238 // point.
252239 // See: https://doc.rust-lang.org/cargo/reference/manifest.html#the-license-and-license-file-fields
253- if (node[ " license_file " ].textValueOrEmpty ().isNotBlank()) {
240+ if (pkg.licenseFile.orEmpty ().isNotBlank()) {
254241 declaredLicenses + = SpdxConstants .NOASSERTION
255242 }
256243
@@ -264,51 +251,43 @@ private fun processDeclaredLicenses(licenses: Set<String>): ProcessedDeclaredLic
264251 // https://github.com/rust-lang/cargo/pull/4920
265252 DeclaredLicenseProcessor .process(licenses, operator = SpdxOperator .OR )
266253
267- private fun parsePackage (node : JsonNode , hashes : Map <String , String >): Package {
268- val declaredLicenses = parseDeclaredLicenses(node )
254+ private fun parsePackage (pkg : CargoMetadata . Package , hashes : Map <String , String >): Package {
255+ val declaredLicenses = parseDeclaredLicenses(pkg )
269256 val declaredLicensesProcessed = processDeclaredLicenses(declaredLicenses)
270257
271258 return Package (
272- id = parsePackageId(node),
273- authors = parseAuthors(node[" authors" ]),
259+ id = Identifier (
260+ type = " Crate" ,
261+ // Note that Rust / Cargo do not support package namespaces, see:
262+ // https://samsieber.tech/posts/2020/09/registry-structure-influence/
263+ namespace = " " ,
264+ name = pkg.name,
265+ version = pkg.version
266+ ),
267+ authors = pkg.authors.mapNotNullTo(mutableSetOf ()) { parseAuthorString(it) },
274268 declaredLicenses = declaredLicenses,
275269 declaredLicensesProcessed = declaredLicensesProcessed,
276- description = node[ " description" ].textValueOrEmpty (),
270+ description = pkg. description.orEmpty (),
277271 binaryArtifact = RemoteArtifact .EMPTY ,
278- sourceArtifact = parseSourceArtifact(node , hashes).orEmpty(),
272+ sourceArtifact = parseSourceArtifact(pkg , hashes).orEmpty(),
279273 homepageUrl = " " ,
280- vcs = parseVcsInfo(node )
274+ vcs = VcsHost .parseUrl(pkg.repository.orEmpty() )
281275 )
282276}
283277
284- private fun parsePackageId (node : JsonNode ) =
285- Identifier (
286- type = " Crate" ,
287- // Note that Rust / Cargo do not support package namespaces, see:
288- // https://samsieber.tech/posts/2020/09/registry-structure-influence/
289- namespace = " " ,
290- name = node[" name" ].textValueOrEmpty(),
291- version = node[" version" ].textValueOrEmpty()
292- )
293-
294- private fun parseRepositoryUrl (node : JsonNode ) = node[" repository" ].textValueOrEmpty()
295-
296278// Match source dependencies that directly reference git repositories. The specified tag or branch
297279// name is ignored (i.e. not captured) in favor of the actual commit hash that they currently refer
298280// to.
299281// See https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#specifying-dependencies-from-git-repositories
300282// for the specification for this kind of dependency.
301283private val GIT_DEPENDENCY_REGEX = Regex (" git\\ +(https://.*)\\ ?(?:rev|tag|branch)=.+#([0-9a-zA-Z]+)" )
302284
303- private fun parseSourceArtifact (node : JsonNode , hashes : Map <String , String >): RemoteArtifact ? {
304- val source = node[ " source " ]?.textValue() ? : return null
285+ private fun parseSourceArtifact (pkg : CargoMetadata . Package , hashes : Map <String , String >): RemoteArtifact ? {
286+ val source = pkg.source ? : return null
305287
306288 if (source == " registry+https://github.com/rust-lang/crates.io-index" ) {
307- val name = node[" name" ]?.textValue() ? : return null
308- val version = node[" version" ]?.textValue() ? : return null
309- val url = " https://crates.io/api/v1/crates/$name /$version /download"
310- val id = parseCargoId(node)
311- val hash = Hash .create(hashes[id].orEmpty())
289+ val url = " https://crates.io/api/v1/crates/${pkg.name} /${pkg.version} /download"
290+ val hash = Hash .create(hashes[pkg.id].orEmpty())
312291 return RemoteArtifact (url, hash)
313292 }
314293
@@ -317,34 +296,24 @@ private fun parseSourceArtifact(node: JsonNode, hashes: Map<String, String>): Re
317296 return RemoteArtifact (url, Hash .create(hash))
318297}
319298
320- private fun parseVcsInfo (node : JsonNode ) = VcsHost .parseUrl(parseRepositoryUrl(node))
321-
322299private fun getResolvedVersion (
323300 parentName : String ,
324301 parentVersion : String ,
325302 dependencyName : String ,
326- metadata : JsonNode
303+ metadata : CargoMetadata
327304): String? {
328- val node = metadata[" resolve" ][" nodes" ].single {
329- it[" id" ].textValue().startsWith(" $parentName $parentVersion " )
330- }
305+ val node = metadata.resolve.nodes.single { it.id.startsWith(" $parentName $parentVersion " ) }
331306
332- // This is null if the dependency is optional and the feature was not enabled. In this case the version was not
307+ // This is empty if the dependency is optional and the feature was not enabled. In that case the version was not
333308 // resolved and the dependency should not appear in the dependency tree. An example for a dependency string is
334309 // "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", for more details see
335310 // https://doc.rust-lang.org/cargo/commands/cargo-metadata.html.
336- node[ " dependencies" ] .forEach {
337- val substrings = it.textValue(). splitOnWhitespace()
338- require(substrings.size > 1 ) { " Unexpected format while parsing dependency JSON node ." }
311+ node. dependencies.forEach {
312+ val substrings = it.splitOnWhitespace()
313+ require(substrings.size > 1 ) { " Unexpected format while parsing dependency ' $it ' ." }
339314
340315 if (substrings[0 ] == dependencyName) return substrings[1 ]
341316 }
342317
343318 return null
344319}
345-
346- /* *
347- * Parse information about authors from the given [node] with package metadata.
348- */
349- private fun parseAuthors (node : JsonNode ? ): Set <String > =
350- node?.mapNotNullTo(mutableSetOf ()) { parseAuthorString(it.textValue()) } ? : emptySet()
0 commit comments