-
Notifications
You must be signed in to change notification settings - Fork 308
/
SpdxDocumentFile.kt
478 lines (416 loc) · 19.7 KB
/
SpdxDocumentFile.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
/*
* Copyright (C) 2020-2022 Bosch.IO GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/
@file:Suppress("TooManyFunctions")
package org.ossreviewtoolkit.analyzer.managers
import java.io.File
import java.util.SortedSet
import org.ossreviewtoolkit.analyzer.AbstractPackageManagerFactory
import org.ossreviewtoolkit.analyzer.PackageManager
import org.ossreviewtoolkit.analyzer.managers.utils.SpdxDocumentCache
import org.ossreviewtoolkit.analyzer.managers.utils.SpdxResolvedDocument
import org.ossreviewtoolkit.downloader.VersionControlSystem
import org.ossreviewtoolkit.model.Hash
import org.ossreviewtoolkit.model.Identifier
import org.ossreviewtoolkit.model.OrtIssue
import org.ossreviewtoolkit.model.Package
import org.ossreviewtoolkit.model.PackageLinkage
import org.ossreviewtoolkit.model.PackageReference
import org.ossreviewtoolkit.model.Project
import org.ossreviewtoolkit.model.ProjectAnalyzerResult
import org.ossreviewtoolkit.model.RemoteArtifact
import org.ossreviewtoolkit.model.Scope
import org.ossreviewtoolkit.model.VcsInfo
import org.ossreviewtoolkit.model.VcsType
import org.ossreviewtoolkit.model.config.AnalyzerConfiguration
import org.ossreviewtoolkit.model.config.RepositoryConfiguration
import org.ossreviewtoolkit.model.createAndLogIssue
import org.ossreviewtoolkit.model.orEmpty
import org.ossreviewtoolkit.model.utils.toPurl
import org.ossreviewtoolkit.utils.common.withoutPrefix
import org.ossreviewtoolkit.utils.ort.log
import org.ossreviewtoolkit.utils.spdx.SpdxConstants
import org.ossreviewtoolkit.utils.spdx.SpdxExpression
import org.ossreviewtoolkit.utils.spdx.model.SpdxDocument
import org.ossreviewtoolkit.utils.spdx.model.SpdxExternalDocumentReference
import org.ossreviewtoolkit.utils.spdx.model.SpdxExternalReference
import org.ossreviewtoolkit.utils.spdx.model.SpdxPackage
import org.ossreviewtoolkit.utils.spdx.model.SpdxRelationship
import org.ossreviewtoolkit.utils.spdx.toSpdx
private const val MANAGER_NAME = "SpdxDocumentFile"
private const val DEFAULT_SCOPE_NAME = "default"
private val SPDX_LINKAGE_RELATIONSHIPS = mapOf(
SpdxRelationship.Type.DYNAMIC_LINK to PackageLinkage.DYNAMIC,
SpdxRelationship.Type.STATIC_LINK to PackageLinkage.STATIC
)
private val SPDX_SCOPE_RELATIONSHIPS = listOf(
SpdxRelationship.Type.BUILD_DEPENDENCY_OF,
SpdxRelationship.Type.DEV_DEPENDENCY_OF,
SpdxRelationship.Type.OPTIONAL_DEPENDENCY_OF,
SpdxRelationship.Type.PROVIDED_DEPENDENCY_OF,
SpdxRelationship.Type.RUNTIME_DEPENDENCY_OF,
SpdxRelationship.Type.TEST_DEPENDENCY_OF
)
private val SPDX_VCS_PREFIXES = mapOf(
"git+" to VcsType.GIT,
"hg+" to VcsType.MERCURIAL,
"bzr+" to VcsType.UNKNOWN,
"svn+" to VcsType.SUBVERSION
)
/**
* Return true if the [SpdxDocument] describes a project. Otherwise, if it describes a package, return false.
*/
private fun SpdxDocument.isProject(): Boolean = projectPackage() != null
/**
* Return the [SpdxPackage] in the [SpdxDocument] that denotes a project, or null if no project but only packages are
* defined.
*/
internal fun SpdxDocument.projectPackage(): SpdxPackage? =
// An SpdxDocument that describes a project must have at least 2 packages, one for the project itself, and another
// one for at least one dependency package.
packages.takeIf { it.size > 1 || (it.size == 1 && externalDocumentRefs.isNotEmpty()) }
// The package that describes a project must have an "empty" package filename (as the "filename" is the project
// directory itself).
?.singleOrNull { it.packageFilename.isEmpty() || it.packageFilename == "." }
/**
* Return the concluded license to be used in ORT's data model, which expects a not present value to be null instead
* of NONE or NOASSERTION.
*/
private fun SpdxPackage.getConcludedLicense(): SpdxExpression? =
licenseConcluded.takeIf { SpdxConstants.isPresent(it) }?.toSpdx()
/**
* Return a [RemoteArtifact] for the artifact that the [downloadLocation][SpdxPackage.downloadLocation] points to. If
* the download location is a "not present" value, or if it points to a VCS location instead, return null.
*/
private fun SpdxPackage.getRemoteArtifact(): RemoteArtifact? =
when {
SpdxConstants.isNotPresent(downloadLocation) -> null
SPDX_VCS_PREFIXES.any { (prefix, _) -> downloadLocation.startsWith(prefix) } -> null
else -> {
if (downloadLocation.endsWith(".git")) {
log.warn {
"The download location $downloadLocation of SPDX package '$spdxId' looks like a Git repository " +
"URL but it lacks the 'git+' prefix and thus will be treated as an artifact URL."
}
}
RemoteArtifact(downloadLocation, Hash.NONE)
}
}
/**
* Return the [VcsInfo] contained in the [downloadLocation][SpdxPackage.downloadLocation], or null if the download
* location is a "not present" value / does not point to a VCS location.
*/
internal fun SpdxPackage.getVcsInfo(): VcsInfo? {
if (SpdxConstants.isNotPresent(downloadLocation)) return null
return SPDX_VCS_PREFIXES.mapNotNull { (prefix, vcsType) ->
downloadLocation.withoutPrefix(prefix)?.let { url ->
var vcsUrl = url
val vcsPath = vcsUrl.substringAfterLast('#', "")
vcsUrl = vcsUrl.removeSuffix("#$vcsPath")
val vcsRevision = vcsUrl.substringAfterLast('@', "")
vcsUrl = vcsUrl.removeSuffix("@$vcsRevision")
VcsInfo(vcsType, vcsUrl, vcsRevision, path = vcsPath)
}
}.firstOrNull()
}
/**
* Return the location of the first [external reference][SpdxExternalReference] of the given [type] in this
* [SpdxPackage], or null if there is no such reference.
*/
private fun SpdxPackage.locateExternalReference(type: SpdxExternalReference.Type): String? =
externalRefs.find { it.referenceType == type }?.referenceLocator
/**
* Return a CPE identifier for this package if present. Search for all CPE versions.
*/
private fun SpdxPackage.locateCpe(): String? =
locateExternalReference(SpdxExternalReference.Type.Cpe23Type)
?: locateExternalReference(SpdxExternalReference.Type.Cpe22Type)
/**
* Return whether the string has the format of an [SpdxExternalDocumentReference], with or without an additional
* package id.
*/
private fun String.isExternalDocumentReferenceId(): Boolean = startsWith(SpdxConstants.DOCUMENT_REF_PREFIX)
/**
* Map a "not preset" SPDX value, i.e. NONE or NOASSERTION, to an empty string.
*/
private fun String.mapNotPresentToEmpty(): String = takeUnless { SpdxConstants.isNotPresent(it) }.orEmpty()
/**
* Sanitize a string for use as an [Identifier] property where colons are not supported by replacing them with spaces,
* trimming, and finally collapsing multiple consecutive spaces.
*/
private fun String.sanitize(): String = replace(':', ' ').trim().replace(Regex("\\s{2,}"), " ")
/**
* Wrap any "present" SPDX value in a sorted set, or return an empty sorted set otherwise.
*/
private fun String?.wrapPresentInSortedSet(): SortedSet<String> {
if (SpdxConstants.isPresent(this)) {
withoutPrefix(SpdxConstants.PERSON)?.let { persons ->
// In case of a person, allow a comma-separated list of persons.
return persons.split(',').mapTo(sortedSetOf()) { it.trim() }
}
// Do not split an organization like "Acme, Inc." by comma.
withoutPrefix(SpdxConstants.ORGANIZATION)?.let {
return sortedSetOf(it)
}
}
return sortedSetOf()
}
/**
* Return the [PackageLinkage] between [dependency] and [dependant] as specified in [relationships]. If no
* relationship is found, return [PackageLinkage.DYNAMIC].
*/
private fun getLinkageForDependency(
dependency: SpdxPackage,
dependant: String,
relationships: List<SpdxRelationship>
): PackageLinkage =
relationships.mapNotNull { relation ->
SPDX_LINKAGE_RELATIONSHIPS[relation.relationshipType]?.takeIf {
val relationId = if (relation.relatedSpdxElement.isExternalDocumentReferenceId()) {
relation.relatedSpdxElement.substringAfter(":")
} else {
relation.relatedSpdxElement
}
relationId == dependency.spdxId && relation.spdxElementId == dependant
}
}.takeUnless { it.isEmpty() }?.single() ?: PackageLinkage.DYNAMIC
/**
* Return true if the [relation] as defined in [relationships] describes an [SPDX_LINKAGE_RELATIONSHIPS] in the
* [DEFAULT_SCOPE_NAME] so that the [source] depends on the [target].
*/
private fun hasDefaultScopeLinkage(
source: String, target: String, relation: SpdxRelationship.Type, relationships: List<SpdxRelationship>
): Boolean {
if (relation !in SPDX_LINKAGE_RELATIONSHIPS) return false
val hasScopeRelationship = relationships.any {
it.relationshipType in SPDX_SCOPE_RELATIONSHIPS
// Scope relationships are defined in "reverse" as a "dependency of".
&& it.relatedSpdxElement == source && it.spdxElementId == target
}
return !hasScopeRelationship
}
/**
* A "fake" package manager implementation that uses SPDX documents as definition files to declare projects and describe
* packages. See https://github.com/spdx/spdx-spec/issues/439 for details.
*/
class SpdxDocumentFile(
managerName: String,
analysisRoot: File,
analyzerConfig: AnalyzerConfiguration,
repoConfig: RepositoryConfiguration
) : PackageManager(managerName, analysisRoot, analyzerConfig, repoConfig) {
private val spdxDocumentCache = SpdxDocumentCache()
class Factory : AbstractPackageManagerFactory<SpdxDocumentFile>(MANAGER_NAME) {
override val globsForDefinitionFiles = listOf("*.spdx.yml", "*.spdx.yaml", "*.spdx.json")
override fun create(
analysisRoot: File,
analyzerConfig: AnalyzerConfiguration,
repoConfig: RepositoryConfiguration
) = SpdxDocumentFile(managerName, analysisRoot, analyzerConfig, repoConfig)
}
/**
* Create an [Identifier] out of this [SpdxPackage].
*/
private fun SpdxPackage.toIdentifier() =
Identifier(
type = managerName,
namespace = listOfNotNull(supplier, originator).firstOrNull()
?.withoutPrefix(SpdxConstants.ORGANIZATION).orEmpty().sanitize(),
name = name.sanitize(),
version = versionInfo.sanitize()
)
/**
* Create a [Package] out of this [SpdxPackage].
*/
private fun SpdxPackage.toPackage(definitionFile: File?, doc: SpdxResolvedDocument): Package {
val packageDescription = description.takeUnless { it.isEmpty() } ?: summary
// If the VCS information cannot be determined from the VCS working tree itself, fall back to try getting it
// from the download location.
val packageDir = definitionFile?.resolveSibling(packageFilename)
val vcs = packageDir?.let { VersionControlSystem.forDirectory(it)?.getInfo() } ?: getVcsInfo().orEmpty()
val generatedFromRelations = doc.relationships.filter {
it.relationshipType == SpdxRelationship.Type.GENERATED_FROM
}
val isBinaryArtifact = generatedFromRelations.any { it.spdxElementId == spdxId }
&& generatedFromRelations.none { it.relatedSpdxElement == spdxId }
val id = toIdentifier()
val artifact = getRemoteArtifact()
return Package(
id = id,
purl = locateExternalReference(SpdxExternalReference.Type.Purl) ?: id.toPurl(),
cpe = locateCpe(),
authors = originator.wrapPresentInSortedSet(),
declaredLicenses = sortedSetOf(licenseDeclared),
concludedLicense = getConcludedLicense(),
description = packageDescription,
homepageUrl = homepage.mapNotPresentToEmpty(),
binaryArtifact = artifact.takeIf { isBinaryArtifact }.orEmpty(),
sourceArtifact = artifact.takeUnless { isBinaryArtifact }.orEmpty(),
vcs = vcs
)
}
/**
* Return the dependencies of the package with the given [pkgId] defined in [doc] of the
* [SpdxRelationship.Type.DEPENDENCY_OF] type. Identified dependencies are mapped to ORT [Package]s and then
* added to [packages].
*/
private fun getDependencies(
pkgId: String,
doc: SpdxResolvedDocument,
packages: MutableSet<Package>
): SortedSet<PackageReference> =
getDependencies(pkgId, doc, packages, SpdxRelationship.Type.DEPENDENCY_OF) { target ->
val issues = mutableListOf<OrtIssue>()
doc.getSpdxPackageForId(target, issues)?.let { dependency ->
packages += dependency.toPackage(doc.getDefinitionFile(target), doc)
PackageReference(
id = dependency.toIdentifier(),
dependencies = getDependencies(target, doc, packages),
linkage = getLinkageForDependency(dependency, pkgId, doc.relationships),
issues = issues
)
}
}
/**
* Return the dependencies of the package with the given [pkgId] defined in [doc] of the given
* [dependencyOfRelation] type. Optionally, the [SpdxRelationship.Type.DEPENDS_ON] type is handled by
* [dependsOnCase]. Identified dependencies are mapped to ORT [Package]s and then added to [packages].
*/
private fun getDependencies(
pkgId: String,
doc: SpdxResolvedDocument,
packages: MutableSet<Package>,
dependencyOfRelation: SpdxRelationship.Type,
dependsOnCase: (String) -> PackageReference? = { null }
): SortedSet<PackageReference> =
doc.relationships.mapNotNullTo(sortedSetOf()) { (source, relation, target, _) ->
val issues = mutableListOf<OrtIssue>()
val isDependsOnRelation = relation == SpdxRelationship.Type.DEPENDS_ON || hasDefaultScopeLinkage(
source, target, relation, doc.relationships
)
when {
// Dependencies can either be defined on the target...
pkgId.equals(target, ignoreCase = true) && relation == dependencyOfRelation -> {
if (pkgId != target) {
issues += createAndLogIssue(
source = managerName,
message = "Source '$pkgId' has to match target '$target' case-sensitively."
)
}
doc.getSpdxPackageForId(source, issues)?.let { dependency ->
packages += dependency.toPackage(doc.getDefinitionFile(source), doc)
PackageReference(
id = dependency.toIdentifier(),
dependencies = getDependencies(
source,
doc,
packages,
SpdxRelationship.Type.DEPENDENCY_OF,
dependsOnCase
),
issues = issues,
linkage = getLinkageForDependency(dependency, target, doc.relationships)
)
}
}
// ...or on the source.
pkgId.equals(source, ignoreCase = true) && isDependsOnRelation -> {
if (pkgId != source) {
issues += createAndLogIssue(
source = managerName,
message = "Source '$source' has to match target '$pkgId' case-sensitively."
)
}
val pkgRef = dependsOnCase(target)
pkgRef?.copy(issues = issues + pkgRef.issues)
}
else -> null
}
}
/**
* Return a [Scope] created from the given type of [relation] for the [projectPackage][projectPackageId] in
* [spdxDocument], or `null` if there are no such relations. Identified dependencies are mapped to ORT [Package]s
* and then added to [packages].
*/
private fun createScope(
spdxDocument: SpdxResolvedDocument,
projectPackageId: String,
relation: SpdxRelationship.Type,
packages: MutableSet<Package>
): Scope? =
getDependencies(projectPackageId, spdxDocument, packages, relation).takeUnless {
it.isEmpty()
}?.let {
Scope(
name = relation.name.removeSuffix("_DEPENDENCY_OF").lowercase(),
dependencies = it
)
}
override fun mapDefinitionFiles(definitionFiles: List<File>): List<File> =
definitionFiles.associateWith {
spdxDocumentCache.load(it).getOrNull()
}.filter { (_, spdxDocument) ->
// Distinguish whether we have a project-style SPDX document that describes a project and its dependencies,
// or a package-style SPDX document that describes a single (dependency-)package.
spdxDocument?.isProject() == true
}.keys.also { remainingFiles ->
if (remainingFiles.isEmpty()) return definitionFiles
val discardedFiles = definitionFiles - remainingFiles
if (discardedFiles.isNotEmpty()) {
log.info {
"Discarded the following ${discardedFiles.size} non-project SPDX files: " +
discardedFiles.joinToString { "'$it'" }
}
}
}.toList()
override fun resolveDependencies(definitionFile: File, labels: Map<String, String>): List<ProjectAnalyzerResult> {
val transitiveDocument = SpdxResolvedDocument.load(spdxDocumentCache, definitionFile, managerName)
val spdxDocument = transitiveDocument.rootDocument.document
val packages = mutableSetOf<Package>()
val scopes = sortedSetOf<Scope>()
val projectPackage = if (!spdxDocument.isProject()) {
spdxDocument.packages[0]
} else {
requireNotNull(spdxDocument.projectPackage()) {
"The SPDX document file at '$definitionFile' does not describe a project."
}
}
log.info {
"File '$definitionFile' contains SPDX document '${spdxDocument.name}' which describes project " +
"'${projectPackage.name}'."
}
scopes += SPDX_SCOPE_RELATIONSHIPS.mapNotNullTo(sortedSetOf()) { type ->
createScope(transitiveDocument, projectPackage.spdxId, type, packages)
}
scopes += Scope(
name = DEFAULT_SCOPE_NAME,
dependencies = getDependencies(projectPackage.spdxId, transitiveDocument, packages)
)
val project = Project(
id = projectPackage.toIdentifier(),
cpe = projectPackage.locateCpe(),
definitionFilePath = VersionControlSystem.getPathInfo(definitionFile).path,
authors = projectPackage.originator.wrapPresentInSortedSet(),
declaredLicenses = sortedSetOf(projectPackage.licenseDeclared),
vcs = processProjectVcs(definitionFile.parentFile, VcsInfo.EMPTY),
homepageUrl = projectPackage.homepage.mapNotPresentToEmpty(),
scopeDependencies = scopes
)
return listOf(ProjectAnalyzerResult(project, packages.toSortedSet()))
}
}