Skip to content

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

Draft
wants to merge 19 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
10883f5
property to exclude dev dependencies in cargo cli detector add
zahidblackduck Apr 23, 2025
0354a8e
cargo cli extractor dev dependencies exclusion add
zahidblackduck Apr 24, 2025
2830904
cargo lockfile detector dependency exclusion add
zahidblackduck Apr 28, 2025
1ac92d4
detect rule factory edit
zahidblackduck Apr 30, 2025
5344176
cargo version match util update
zahidblackduck Apr 30, 2025
fba1188
cargo version match util add
zahidblackduck Apr 30, 2025
f6e48d9
cargo lockfile dev,build dependency exclusion add
zahidblackduck May 6, 2025
b66b9f7
Merge conflict resolve with master
zahidblackduck May 6, 2025
37c4ed6
cargo lock file dependency transformer refactor
zahidblackduck May 6, 2025
9e1e4a4
wildcard imports cleared
zahidblackduck May 7, 2025
95d1bac
method name refactor to express actual intent
zahidblackduck May 7, 2025
1293fb9
doc description updated for new detect properties
zahidblackduck May 8, 2025
1a0e93a
handle dependency exclusion if present in multiple sections of Cargo.…
zahidblackduck May 15, 2025
0d8f6f2
handle dependency exclusion if present in multiple sections of Cargo.…
zahidblackduck May 15, 2025
d2a8a6c
properties doc updated with an example usage
zahidblackduck May 15, 2025
5eb9b16
Merge remote-tracking branch 'origin/master' into dev/zahidblackduck/…
zahidblackduck May 15, 2025
d2be399
refactor dependency exclusion to use NameVersion without precedence f…
zahidblackduck May 16, 2025
83ebdf1
remove code comment and empty line
zahidblackduck May 16, 2025
ae1ee8e
refactor condition to check actual dependency exclusion filter
zahidblackduck May 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,17 @@ public class CargoCliDetectable extends Detectable {
private final CargoResolver cargoResolver;
private final CargoCliExtractor cargoCliExtractor;
private final DetectableExecutableRunner executableRunner;
private final CargoDetectableOptions cargoDetectableOptions;
private ExecutableTarget cargoExe;
private File cargoToml;

public CargoCliDetectable(DetectableEnvironment environment, FileFinder fileFinder, CargoResolver cargoResolver, CargoCliExtractor cargoCliExtractor, DetectableExecutableRunner executableRunner) {
public CargoCliDetectable(DetectableEnvironment environment, FileFinder fileFinder, CargoResolver cargoResolver, CargoCliExtractor cargoCliExtractor, DetectableExecutableRunner executableRunner, CargoDetectableOptions cargoDetectableOptions) {
super(environment);
this.fileFinder = fileFinder;
this.cargoResolver = cargoResolver;
this.cargoCliExtractor = cargoCliExtractor;
this.executableRunner = executableRunner;
this.cargoDetectableOptions = cargoDetectableOptions;
}

@Override
Expand Down Expand Up @@ -72,7 +74,7 @@ public DetectableResult extractable() throws DetectableException {
@Override
public Extraction extract(ExtractionEnvironment extractionEnvironment) throws IOException, DetectableException, MissingExternalIdException, ExecutableRunnerException {
try {
return cargoCliExtractor.extract(environment.getDirectory(), cargoExe, cargoToml);
return cargoCliExtractor.extract(environment.getDirectory(), cargoExe, cargoToml, cargoDetectableOptions);
} catch (Exception e) {
logger.error("Failed to extract Cargo dependencies.", e);
return new Extraction.Builder().failure("Cargo extraction failed due to an exception: " + e.getMessage()).build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.ArrayList;
import java.util.Optional;
import java.util.Map;
import java.util.EnumMap;

public class CargoCliExtractor {
private static final List<String> CARGO_TREE_COMMAND = Arrays.asList("tree", "--no-dedupe", "--prefix", "depth");
Expand All @@ -32,8 +35,12 @@ public CargoCliExtractor(DetectableExecutableRunner executableRunner, CargoDepen
this.cargoTomlParser = cargoTomlParser;
}

public Extraction extract(File directory, ExecutableTarget cargoExe, File cargoTomlFile) throws ExecutableFailedException, IOException {
ExecutableOutput cargoOutput = executableRunner.executeSuccessfully(ExecutableUtils.createFromTarget(directory, cargoExe, CARGO_TREE_COMMAND));
public Extraction extract(File directory, ExecutableTarget cargoExe, File cargoTomlFile, CargoDetectableOptions cargoDetectableOptions) throws ExecutableFailedException, IOException {
List<String> cargoTreeCommand = new ArrayList<>(CARGO_TREE_COMMAND);

addEdgeExclusions(cargoTreeCommand, cargoDetectableOptions);

ExecutableOutput cargoOutput = executableRunner.executeSuccessfully(ExecutableUtils.createFromTarget(directory, cargoExe, cargoTreeCommand));
List<String> cargoTreeOutput = cargoOutput.getStandardOutputAsList();

DependencyGraph graph = cargoDependencyTransformer.transform(cargoTreeOutput);
Expand All @@ -51,4 +58,24 @@ public Extraction extract(File directory, ExecutableTarget cargoExe, File cargoT
.nameVersionIfPresent(projectNameVersion)
.build();
}

private void addEdgeExclusions(List<String> cargoTreeCommand, CargoDetectableOptions options) {
Map<CargoDependencyType, String> exclusionMap = new EnumMap<>(CargoDependencyType.class);
exclusionMap.put(CargoDependencyType.NORMAL, "no-normal");
exclusionMap.put(CargoDependencyType.BUILD, "no-build");
exclusionMap.put(CargoDependencyType.DEV, "no-dev");
exclusionMap.put(CargoDependencyType.PROC_MACRO, "no-proc-macro");
Copy link
Contributor

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?


List<String> exclusions = new ArrayList<>();
for (Map.Entry<CargoDependencyType, String> entry : exclusionMap.entrySet()) {
if (options.getDependencyTypeFilter().shouldExclude(entry.getKey())) {
exclusions.add(entry.getValue());
}
}

if (!exclusions.isEmpty()) {
cargoTreeCommand.add("--edges");
cargoTreeCommand.add(String.join(",", exclusions));
}
}
}
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
Expand Up @@ -5,9 +5,13 @@
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import com.blackduck.integration.detectable.detectables.cargo.data.CargoLockPackageData;
import org.apache.commons.io.FileUtils;
import org.jetbrains.annotations.Nullable;

Expand Down Expand Up @@ -39,26 +43,72 @@ 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;
String cargoTomlContents = FileUtils.readFileToString(cargoTomlFile, StandardCharsets.UTF_8);

if (cargoDetectableOptions != null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of checking on cargoDetectableOptions object, can we check on dependencyTypeFilter instead? There could be more properties added to this detector in future not related to dependency exclusion.

Copy link
Collaborator Author

@zahidblackduck zahidblackduck May 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, you're right. The actual filtering is handled by dependencyTypeFilter. This null check is just to verify whether any options were passed at all. The filtering logic using dependencyTypeFilter is applied later in the CargoTomlParser#parseDependenciesToExclude(..) method.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that, my only concern is that if in future lets say we add some new property for excluding workspaces as an example then those options will also be passed in CargoDetectableOptions, so we will make this unnecessary call to cargoTomlParser which is not required if no dependency exclusion is involved. I hope I made sense.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current code setup doesn't call the methods of CargoTomlParser if no dependency exclusion is involved. Nonetheless, I aim to address the issue that you pointed out in any future enhancement of cargo related issue.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know, currently your code is not getting executed if cargoDetectableOptions is null. It is not guaranteed that you are going to work on this in any future enhancement related to Cargo and it could be easily missed. I believe we should address this now, its a simple change and not a complicated ask.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, sure. I'll update accordingly.

Map<String, String> excludableDependencyMap = cargoTomlParser.parseDependenciesToExclude(cargoTomlContents, cargoDetectableOptions);
filteredPackages = excludeDependencies(cargoLockPackageDataList, excludableDependencyMap);
}

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);
Copy link
Contributor

@andrian-sevastyanov andrian-sevastyanov May 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously we were checking whether the file is not null before reading it. Now, we don't. FileUtils.readFileToString throws an exception.
We should continue to check whether the file is there before reading it.

Also, I think something like this should happen:

if cargoTomlFile == null and isDependencyExclusionEnabled():
    failExtraction() // because we can't reliably determine dependency types; also, we might want to do this in the `extractable()` method of Detectable

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 List<CargoLockPackageData> excludeDependencies(
List<CargoLockPackageData> packages,
Map<String, String> excludableDependencyMap
) {
Set<String> excludedNames = new HashSet<>();

List<CargoLockPackageData> filtered = packages.stream()
.filter(pkg -> {
String name = pkg.getName().orElse(null);
String version = pkg.getVersion().orElse(null);
if (name == null || version == null) return true;

if (excludableDependencyMap.containsKey(name)) {
String constraint = excludableDependencyMap.get(name);
boolean matches = constraint == null || VersionUtils.versionMatches(constraint, version);
if (matches) {
excludedNames.add(name);
return false;
}
}

return true;
})
.collect(Collectors.toList());

return filtered.stream()
.map(pkg -> new CargoLockPackageData(
pkg.getName().orElse(null),
pkg.getVersion().orElse(null),
pkg.getSource().orElse(null),
pkg.getChecksum().orElse(null),
pkg.getDependencies()
.orElse(new ArrayList<>())
.stream()
.filter(dep -> !excludedNames.contains(dep))
.collect(Collectors.toList())
))
.collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,15 @@ public class CargoLockDetectable extends Detectable {

private final FileFinder fileFinder;
private final CargoExtractor cargoExtractor;

private final CargoDetectableOptions cargoDetectableOptions;
private File cargoLock;
private File cargoToml;

public CargoLockDetectable(DetectableEnvironment environment, FileFinder fileFinder, CargoExtractor cargoExtractor) {
public CargoLockDetectable(DetectableEnvironment environment, FileFinder fileFinder, CargoExtractor cargoExtractor, CargoDetectableOptions cargoDetectableOptions) {
super(environment);
this.fileFinder = fileFinder;
this.cargoExtractor = cargoExtractor;
this.cargoDetectableOptions = cargoDetectableOptions;
}

@Override
Expand All @@ -52,6 +53,6 @@ public DetectableResult extractable() {

@Override
public Extraction extract(ExtractionEnvironment extractionEnvironment) throws IOException, DetectableException, MissingExternalIdException {
return cargoExtractor.extract(cargoLock, cargoToml);
return cargoExtractor.extract(cargoLock, cargoToml, cargoDetectableOptions);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,50 @@ public static int compareVersions(String version1, String version2) {
}
return 0;
}

public static boolean versionMatches(String constraint, String actualVersion) {
if (constraint == null || actualVersion == null) {
return false;
}

String normalizedActual = normalizeVersion(actualVersion);
String normalizedConstraintVersion;

if (constraint.startsWith(">=")) {
normalizedConstraintVersion = normalizeVersion(constraint.substring(2));
return compareVersions(normalizedActual, normalizedConstraintVersion) >= 0;
} else if (constraint.startsWith(">")) {
normalizedConstraintVersion = normalizeVersion(constraint.substring(1));
return compareVersions(normalizedActual, normalizedConstraintVersion) > 0;
} else if (constraint.startsWith("<=")) {
normalizedConstraintVersion = normalizeVersion(constraint.substring(2));
return compareVersions(normalizedActual, normalizedConstraintVersion) <= 0;
} else if (constraint.startsWith("<")) {
normalizedConstraintVersion = normalizeVersion(constraint.substring(1));
return compareVersions(normalizedActual, normalizedConstraintVersion) < 0;
} else if (constraint.startsWith("=")) {
normalizedConstraintVersion = normalizeVersion(constraint.substring(1));
return compareVersions(normalizedActual, normalizedConstraintVersion) == 0;
} else {
// Default to exact match
normalizedConstraintVersion = normalizeVersion(constraint);
return compareVersions(normalizedActual, normalizedConstraintVersion) == 0;
}
}

private static String normalizeVersion(String version) {
String[] parts = version.split("\\.");
StringBuilder normalized = new StringBuilder();
for (int i = 0; i < 3; i++) {
if (i < parts.length) {
normalized.append(parts[i]);
} else {
normalized.append("0");
}
if (i < 2) {
normalized.append(".");
}
}
return normalized.toString();
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
package com.blackduck.integration.detectable.detectables.cargo.parse;

import java.util.Optional;
import java.util.*;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another wildcard import statement.


import com.blackduck.integration.detectable.detectables.cargo.CargoDependencyType;
import com.blackduck.integration.detectable.detectables.cargo.CargoDetectableOptions;
import org.tomlj.Toml;
import org.tomlj.TomlParseResult;

import com.blackduck.integration.util.NameVersion;
import org.tomlj.TomlTable;

public class CargoTomlParser {
private static final String NAME_KEY = "name";
private static final String VERSION_KEY = "version";
private static final String PACKAGE_KEY = "package";
private static final String NORMAL_DEPENDENCIES_KEY = "dependencies";
private static final String BUILD_DEPENDENCIES_KEY = "build-dependencies";
private static final String DEV_DEPENDENCIES_KEY = "dev-dependencies";

public Optional<NameVersion> parseNameVersionFromCargoToml(String tomlFileContents) {
TomlParseResult cargoTomlObject = Toml.parse(tomlFileContents);
Expand All @@ -22,4 +28,41 @@ public Optional<NameVersion> parseNameVersionFromCargoToml(String tomlFileConten
return Optional.empty();
}

public Map<String, String> parseDependenciesToExclude(String tomlFileContents, CargoDetectableOptions cargoDetectableOptions) {
TomlParseResult toml = Toml.parse(tomlFileContents);
Map<String, String> allDeps = new HashMap<>();

if (cargoDetectableOptions.getDependencyTypeFilter().shouldExclude(CargoDependencyType.NORMAL)) {
allDeps.putAll(parseDependenciesToExcludeFromTomlSection(toml, NORMAL_DEPENDENCIES_KEY));
}
if (cargoDetectableOptions.getDependencyTypeFilter().shouldExclude(CargoDependencyType.BUILD)) {
allDeps.putAll(parseDependenciesToExcludeFromTomlSection(toml, BUILD_DEPENDENCIES_KEY));
}
if (cargoDetectableOptions.getDependencyTypeFilter().shouldExclude(CargoDependencyType.DEV)) {
allDeps.putAll(parseDependenciesToExcludeFromTomlSection(toml, DEV_DEPENDENCIES_KEY));
}

return allDeps;
}

private Map<String, String> parseDependenciesToExcludeFromTomlSection(TomlParseResult toml, String sectionKey) {
Map<String, String> deps = new HashMap<>();
TomlTable table = toml.getTable(sectionKey);
if (table == null) {
return deps;
}

for (String key : table.keySet()) {
Object value = table.get(key);
if (value instanceof String) {
deps.put(key, (String) value);
} else if (value instanceof TomlTable) {
TomlTable dependencyTable = (TomlTable) value;
String version = dependencyTable.getString(VERSION_KEY); // May be null
deps.put(key, version);
}
}

return deps;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.blackduck.integration.detectable.detectables.cargo.transform;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.blackduck.integration.bdio.graph.DependencyGraph;
import com.blackduck.integration.bdio.graph.builder.LazyExternalIdDependencyGraphBuilder;
Expand All @@ -9,6 +11,7 @@
import com.blackduck.integration.bdio.model.Forge;
import com.blackduck.integration.bdio.model.dependency.Dependency;
import com.blackduck.integration.bdio.model.dependency.DependencyFactory;
import com.blackduck.integration.bdio.model.externalid.ExternalId;
import com.blackduck.integration.bdio.model.externalid.ExternalIdFactory;
import com.blackduck.integration.detectable.detectable.exception.DetectableException;
import com.blackduck.integration.detectable.detectables.cargo.model.CargoLockPackage;
Expand All @@ -26,15 +29,21 @@ public DependencyGraph transformToGraph(List<CargoLockPackage> lockPackages) thr
String parentName = lockPackage.getPackageNameVersion().getName();
String parentVersion = lockPackage.getPackageNameVersion().getVersion();
LazyId parentId = LazyId.fromNameAndVersion(parentName, parentVersion);
Dependency parentDependency = dependencyFactory.createNameVersionDependency(Forge.CRATES, parentName, parentVersion);
ExternalId parentExternalId = externalIdFactory.createNameVersionExternalId(Forge.CRATES, parentName, parentVersion);

graph.addChildToRoot(parentId);
graph.setDependencyInfo(parentId, parentDependency.getName(), parentDependency.getVersion(), parentDependency.getExternalId());
graph.setDependencyInfo(parentId, parentName, parentVersion, parentExternalId);
graph.setDependencyAsAlias(parentId, LazyId.fromName(parentName));
graph.addChildToRoot(parentId);

lockPackage.getDependencies().forEach(childPackage -> {
if (childPackage.getVersion().isPresent()) {
LazyId childId = LazyId.fromNameAndVersion(childPackage.getName(), childPackage.getVersion().get());
String childName = childPackage.getName();
String childVersion = childPackage.getVersion().get();
LazyId childId = LazyId.fromNameAndVersion(childName, childVersion);
ExternalId childExternalId = externalIdFactory.createNameVersionExternalId(Forge.CRATES, childName, childVersion);

graph.setDependencyInfo(childId, childName, childVersion, childExternalId);
graph.setDependencyAsAlias(childId, LazyId.fromName(childName));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are aliases used for? I've seen this before but not sure what the end effect is.

graph.addChildWithParent(childId, parentId);
} else {
LazyId childId = LazyId.fromName(childPackage.getName());
Expand Down
Loading