-
Notifications
You must be signed in to change notification settings - Fork 78
Detect property to exclude Cargo dependencies #1421
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 28 commits
10883f5
0354a8e
2830904
1ac92d4
5344176
fba1188
f6e48d9
b66b9f7
37c4ed6
9e1e4a4
95d1bac
1293fb9
1a0e93a
0d8f6f2
d2a8a6c
5eb9b16
d2be399
83ebdf1
ae1ee8e
383b99e
204b05f
538bc03
537760c
f1be648
f3130f8
9955084
44a70b2
26ec154
8bdcb52
ff370af
2f9f6a8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package com.blackduck.integration.detectable.detectables.cargo; | ||
|
||
public enum CargoDependencyType { | ||
NORMAL, | ||
BUILD, | ||
DEV, | ||
PROC_MACRO | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package com.blackduck.integration.detectable.detectables.cargo; | ||
|
||
import com.blackduck.integration.detectable.detectable.util.EnumListFilter; | ||
|
||
public class CargoDetectableOptions { | ||
private final EnumListFilter<CargoDependencyType> dependencyTypeFilter; | ||
|
||
public CargoDetectableOptions(EnumListFilter<CargoDependencyType> dependencyTypeFilter) { | ||
this.dependencyTypeFilter = dependencyTypeFilter; | ||
} | ||
|
||
public EnumListFilter<CargoDependencyType> getDependencyTypeFilter() { | ||
return dependencyTypeFilter; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,11 +3,20 @@ | |
import java.io.File; | ||
import java.io.IOException; | ||
import java.nio.charset.StandardCharsets; | ||
import java.util.ArrayDeque; | ||
import java.util.ArrayList; | ||
import java.util.Collections; | ||
import java.util.Deque; | ||
import java.util.HashSet; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Objects; | ||
import java.util.Optional; | ||
import java.util.Set; | ||
import java.util.stream.Collectors; | ||
|
||
import com.blackduck.integration.detectable.detectable.util.EnumListFilter; | ||
import com.blackduck.integration.detectable.detectables.cargo.data.CargoLockPackageData; | ||
import org.apache.commons.io.FileUtils; | ||
import org.jetbrains.annotations.Nullable; | ||
|
||
|
@@ -39,26 +48,152 @@ public CargoExtractor( | |
this.cargoLockPackageTransformer = cargoLockPackageTransformer; | ||
} | ||
|
||
public Extraction extract(File cargoLockFile, @Nullable File cargoTomlFile) throws IOException, DetectableException, MissingExternalIdException { | ||
public Extraction extract(File cargoLockFile, @Nullable File cargoTomlFile, CargoDetectableOptions cargoDetectableOptions) throws IOException, DetectableException, MissingExternalIdException { | ||
CargoLockData cargoLockData = new Toml().read(cargoLockFile).to(CargoLockData.class); | ||
List<CargoLockPackage> packages = cargoLockData.getPackages() | ||
.orElse(new ArrayList<>()).stream() | ||
List<CargoLockPackageData> cargoLockPackageDataList = cargoLockData.getPackages().orElse(new ArrayList<>()); | ||
List<CargoLockPackageData> filteredPackages = cargoLockPackageDataList; | ||
boolean exclusionEnabled = isDependencyExclusionEnabled(cargoDetectableOptions); | ||
String cargoTomlContents = null; | ||
|
||
if(cargoTomlFile == null && exclusionEnabled) { | ||
return new Extraction.Builder() | ||
.failure("Cargo.toml file is required to exclude dependencies, but was not provided.") | ||
.build(); | ||
} | ||
|
||
if (cargoTomlFile != null) { | ||
cargoTomlContents = FileUtils.readFileToString(cargoTomlFile, StandardCharsets.UTF_8); | ||
} | ||
|
||
if (cargoTomlFile != null && exclusionEnabled) { | ||
Set<NameVersion> dependenciesToInclude = cargoTomlParser.parseDependenciesToInclude( | ||
cargoTomlContents, cargoDetectableOptions.getDependencyTypeFilter()); | ||
filteredPackages = includeDependencies(cargoLockPackageDataList, dependenciesToInclude); | ||
} | ||
|
||
List<CargoLockPackage> packages = filteredPackages.stream() | ||
.map(cargoLockPackageDataTransformer::transform) | ||
.collect(Collectors.toList()); | ||
|
||
DependencyGraph graph = cargoLockPackageTransformer.transformToGraph(packages); | ||
|
||
Optional<NameVersion> projectNameVersion = Optional.empty(); | ||
if (cargoTomlFile != null) { | ||
String cargoTomlContents = FileUtils.readFileToString(cargoTomlFile, StandardCharsets.UTF_8); | ||
andrian-sevastyanov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
projectNameVersion = cargoTomlParser.parseNameVersionFromCargoToml(cargoTomlContents); | ||
} | ||
|
||
CodeLocation codeLocation = new CodeLocation(graph); //TODO: Consider for producing a ProjectDependencyGraph | ||
|
||
return new Extraction.Builder() | ||
.success(codeLocation) | ||
.nameVersionIfPresent(projectNameVersion) | ||
.build(); | ||
} | ||
|
||
private boolean isDependencyExclusionEnabled(CargoDetectableOptions options) { | ||
if (options == null) { | ||
return false; | ||
} | ||
|
||
EnumListFilter<CargoDependencyType> filter = options.getDependencyTypeFilter(); | ||
return filter != null && !filter.shouldIncludeAll(); | ||
} | ||
|
||
|
||
private List<CargoLockPackageData> includeDependencies( | ||
List<CargoLockPackageData> packages, | ||
Set<NameVersion> dependenciesToInclude | ||
) { | ||
processTransitiveDependenciesForInclusion(packages, dependenciesToInclude); // Collect all transitive dependencies to include | ||
return filterPackagesByInclusion(packages, dependenciesToInclude); // Only keep direct and transitive dependencies | ||
} | ||
|
||
private void processTransitiveDependenciesForInclusion(List<CargoLockPackageData> packages, Set<NameVersion> dependenciesToInclude) { | ||
Set<NameVersion> resolvedToInclude = new HashSet<>(); | ||
for (NameVersion nv : dependenciesToInclude) { | ||
CargoLockPackageData pkg = findPackageByNameVersion(nv, packages); | ||
if (pkg != null) { | ||
String name = pkg.getName().orElse(null); | ||
String version = pkg.getVersion().orElse(null); | ||
resolvedToInclude.add(new NameVersion(name, version)); | ||
} else { | ||
resolvedToInclude.add(nv); | ||
} | ||
} | ||
dependenciesToInclude.clear(); | ||
dependenciesToInclude.addAll(resolvedToInclude); | ||
|
||
Deque<NameVersion> queue = new ArrayDeque<>(dependenciesToInclude); | ||
while (!queue.isEmpty()) { | ||
NameVersion current = queue.pop(); | ||
CargoLockPackageData currentPkg = findPackageByNameVersion(current, packages); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This find is done inside of a loop so it might be worth it to do some preprocessing for faster lookups. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This may still be a concern for big projects. You could try indexing packages by name for faster lookups before starting loops that call this method. There are a couple such loops. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, are you suggesting to construct a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, that's the idea. It can be a map of names to multiple name/versions. |
||
if (currentPkg != null) { | ||
currentPkg.getDependencies().ifPresent(dependencies -> { | ||
for (String depStr : dependencies) { | ||
NameVersion nameVersion = extractPackageNameVersion(depStr, packages); | ||
if (nameVersion != null && dependenciesToInclude.add(nameVersion)) { | ||
queue.add(nameVersion); | ||
} | ||
} | ||
}); | ||
} | ||
} | ||
} | ||
|
||
private List<CargoLockPackageData> filterPackagesByInclusion( | ||
List<CargoLockPackageData> packages, | ||
Set<NameVersion> dependenciesToInclude | ||
) { | ||
List<CargoLockPackageData> result = new ArrayList<>(); | ||
for (CargoLockPackageData pkg : packages) { | ||
String name = pkg.getName().orElse(null); | ||
String version = VersionUtils.stripBuildMetadata(pkg.getVersion().orElse(null)); | ||
for (NameVersion include : dependenciesToInclude) { | ||
String includeName = include.getName(); | ||
String includeVersion = VersionUtils.stripBuildMetadata(include.getVersion()); | ||
if (Objects.equals(name, includeName) && VersionUtils.versionMatches(includeVersion, version)) { | ||
result.add(pkg); | ||
break; | ||
} | ||
} | ||
} | ||
return result; | ||
} | ||
|
||
private NameVersion extractPackageNameVersion(String dependencyString, List<CargoLockPackageData> packages) { | ||
if (dependencyString == null || dependencyString.isEmpty()) { | ||
return null; | ||
} | ||
|
||
String[] parts = dependencyString.split(" "); | ||
String depName = parts[0].trim(); | ||
String depVersion = (parts.length > 1) ? parts[1].trim() : null; | ||
|
||
// If depVersion is null or empty, find by name only. | ||
// Otherwise, find by name and version. | ||
if (depVersion == null || depVersion.isEmpty()) { | ||
for (CargoLockPackageData pkg : packages) { | ||
String name = pkg.getName().orElse(null); | ||
String version = pkg.getVersion().orElse(null); | ||
if (depName.equals(name)) { | ||
return new NameVersion(name, version); | ||
} | ||
} | ||
} | ||
return new NameVersion(depName, depVersion); | ||
} | ||
|
||
private CargoLockPackageData findPackageByNameVersion(NameVersion nv, List<CargoLockPackageData> packages) { | ||
for (CargoLockPackageData pkg : packages) { | ||
String name = pkg.getName().orElse(null); | ||
String version = pkg.getVersion().orElse(null); | ||
|
||
// Matching name and use VersionUtils to check if the versions are compatible | ||
if (nv.getName().equals(name) | ||
&& version != null | ||
&& VersionUtils.versionMatches(VersionUtils.stripBuildMetadata(nv.getVersion()), VersionUtils.stripBuildMetadata(version))) { | ||
return pkg; | ||
} | ||
} | ||
return null; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you describe situations in which PROC_MACRO exclusion is useful? And I guess it's not applicable to lock file extractions?