Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -33,6 +33,11 @@ public class CargoCliExtractor {
private final DetectableExecutableRunner executableRunner;
private final CargoDependencyGraphTransformer cargoDependencyTransformer;
private final CargoTomlParser cargoTomlParser;
public static final String CARGO_TOML_FILENAME = "Cargo.toml";
private static final String VIRTUAL_WORKSPACE_EXCLUSION_WARNING =
"Cannot exclude all workspace members for virtual manifest. " +
"Please check your workspace configuration (detect.cargo.ignore.all.workspaces or exclude properties). " +
"Zero components will be reported in SBOM.";

public CargoCliExtractor(DetectableExecutableRunner executableRunner, CargoDependencyGraphTransformer cargoDependencyTransformer, CargoTomlParser cargoTomlParser) {
this.executableRunner = executableRunner;
Expand Down Expand Up @@ -111,7 +116,7 @@ private Set<String> resolveWorkspaceMemberNames(Set<String> workspaceMemberPaths

for (String memberPath : workspaceMemberPaths) {
File memberDir = new File(workspaceRoot, memberPath);
File memberCargoToml = new File(memberDir, "Cargo.toml");
File memberCargoToml = new File(memberDir, CARGO_TOML_FILENAME);

if (memberCargoToml.exists()) {
String memberTomlContents = FileUtils.readFileToString(memberCargoToml, StandardCharsets.UTF_8);
Expand Down Expand Up @@ -144,12 +149,8 @@ private List<String> runCargoTreeCommand(
boolean noActiveWorkspaceMembers = hasExclusions && activeWorkspaceMembers.isEmpty();

// Case: User excluded all workspace members (via property or exclude config) for virtual workspace
if (isVirtualWorkspace && (shouldIgnoreAllWorkspaceMembers || noActiveWorkspaceMembers)) {
logger.warn(
"Cannot exclude all workspace members for virtual manifest. " +
"Please check your workspace configuration (detect.cargo.ignore.all.workspaces or exclude properties). " +
"Zero components will be reported in SBOM."
);
if (shouldSkipVirtualWorkspace(isVirtualWorkspace, shouldIgnoreAllWorkspaceMembers, noActiveWorkspaceMembers)) {
logger.warn(VIRTUAL_WORKSPACE_EXCLUSION_WARNING);
return new LinkedList<>();
}

Expand All @@ -176,6 +177,13 @@ private List<String> runCargoTreeCommand(

// Command 2: Get included packages dependencies
List<String> includedCommand = buildPackageCommand(includedWorkspaces, dependencyTypeFilter, cargoDetectableOptions);

// Add features flags if required
addFeatureFlags(includedCommand, cargoDetectableOptions);

if (!dependencyTypeFilter.shouldIncludeAll()) {
addEdgeExclusions(includedCommand, cargoDetectableOptions);
}
combinedOutput.addAll(runCargoTreeCommand(directory, cargoExe, includedCommand));

return combinedOutput;
Expand All @@ -191,6 +199,9 @@ private List<String> runCargoTreeCommand(
addWorkspaceFlags(command, cargoDetectableOptions);
}

// Add features flags if required
addFeatureFlags(command, cargoDetectableOptions);

if (!dependencyTypeFilter.shouldIncludeAll()) {
addEdgeExclusions(command, cargoDetectableOptions);
}
Expand All @@ -205,6 +216,10 @@ private List<String> runCargoTreeCommand(File directory, ExecutableTarget cargoE
return output.getStandardOutputAsList();
}

private boolean shouldSkipVirtualWorkspace(boolean isVirtualWorkspace, boolean shouldIgnoreAllWorkspaceMembers, boolean noActiveWorkspaceMembers) {
return isVirtualWorkspace && (shouldIgnoreAllWorkspaceMembers || noActiveWorkspaceMembers);
}

private List<String> buildPackageCommand(
List<String> packages,
EnumListFilter<CargoDependencyType> dependencyTypeFilter,
Expand Down Expand Up @@ -255,6 +270,42 @@ private void addWorkspaceFlags(List<String> command, CargoDetectableOptions opti
}
}

private void addFeatureFlags(List<String> command, CargoDetectableOptions options) {
List<String> features = options.getIncludedFeatures();
boolean isDefaultFeaturesDisabled = options.isDefaultFeaturesDisabled();

// Handle --no-default-features flag (independent of specific features)
if (isDefaultFeaturesDisabled) {
command.add("--no-default-features");
}

// Handle feature specifications
if (features != null && !features.isEmpty()) {
// Check for special keywords (case-insensitive)
boolean includeAllFeatures = features.stream()
.anyMatch(feature -> "ALL".equalsIgnoreCase(feature.trim()));
boolean includeNoFeatures = features.stream()
.anyMatch(feature -> "NONE".equalsIgnoreCase(feature.trim()));

if (includeNoFeatures) {
// NONE keyword: skip all feature processing
return;
} else if (includeAllFeatures) {
command.add("--all-features");
} else {
// Add each feature with its own --features flag
// This handles edge cases like features starting with digits (e.g., "2d", "3d")
for (String feature : features) {
String trimmedFeature = feature.trim();
if (!trimmedFeature.isEmpty()) {
command.add("--features");
command.add(trimmedFeature);
}
}
}
}
}

private void addEdgeExclusions(List<String> cargoTreeCommand, CargoDetectableOptions options) {
Map<CargoDependencyType, String> exclusionMap = new EnumMap<>(CargoDependencyType.class);
exclusionMap.put(CargoDependencyType.NORMAL, "no-normal");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,27 @@
public class CargoDetectableOptions {
private final EnumListFilter<CargoDependencyType> dependencyTypeFilter;
private final boolean cargoIgnoreAllWorkspacesMode;
private final boolean isDefaultFeaturesDisabled;
private final List<String> excludedWorkspaces;
private final List<String> includedWorkspaces;

public CargoDetectableOptions(
EnumListFilter<CargoDependencyType> dependencyTypeFilter,
boolean cargoIgnoreAllWorkspacesMode,
List<String> includedWorkspaces,
List<String> excludedWorkspaces
) {
private final List<String> includedFeatures;

public CargoDetectableOptions(EnumListFilter<CargoDependencyType> dependencyTypeFilter,
boolean cargoIgnoreAllWorkspacesMode,
boolean isDefaultFeaturesDisabled,
List<String> includedWorkspaces,
List<String> excludedWorkspaces,
List<String> includedFeatures) {
this.dependencyTypeFilter = dependencyTypeFilter;
this.cargoIgnoreAllWorkspacesMode = cargoIgnoreAllWorkspacesMode;
this.isDefaultFeaturesDisabled = isDefaultFeaturesDisabled;
this.includedWorkspaces = includedWorkspaces;
this.excludedWorkspaces = excludedWorkspaces;
this.includedFeatures = includedFeatures;
}

public CargoDetectableOptions(EnumListFilter<CargoDependencyType> dependencyTypeFilter) {
this(dependencyTypeFilter, false, new ArrayList<>(), new ArrayList<>());
this(dependencyTypeFilter, false, false, new ArrayList<>(), new ArrayList<>(), new ArrayList<>());
}

public EnumListFilter<CargoDependencyType> getDependencyTypeFilter() {
Expand All @@ -42,4 +46,12 @@ public List<String> getIncludedWorkspaces() {
public List<String> getExcludedWorkspaces() {
return excludedWorkspaces;
}

public List<String> getIncludedFeatures() {
return includedFeatures;
}

public boolean isDefaultFeaturesDisabled() {
return isDefaultFeaturesDisabled;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,20 @@ public class CargoLockfileExtractor {
private final CargoTomlParser cargoTomlParser;
private final CargoLockPackageDataTransformer cargoLockPackageDataTransformer;
private final CargoLockPackageTransformer cargoLockPackageTransformer;
private static final String FEATURE_FLAGS_NOT_SUPPORTED_WARNING =
"Feature inclusion or exclusion (detect.cargo.included.features, detect.cargo.disable.default.features) " +
"are not supported by the Cargo Lockfile Detector and will be ignored. " +
"Use Cargo CLI Detector for accurate feature-based dependency resolution. " +
"Cargo CLI Detector requires the 'cargo' executable to be available in PATH.";

private static final String PROC_MACRO_EXCLUSION_NOT_SUPPORTED_WARNING =
"PROC_MACRO exclusion is not supported by the Cargo Lockfile Detector and will be ignored. " +
"Supported exclusions for Cargo Lockfile Detector: [NORMAL, BUILD, DEV].";

private static final String VIRTUAL_WORKSPACE_ALL_MEMBERS_EXCLUDED_WARNING =
"Cannot exclude all workspace members for virtual manifest. " +
"Please check your workspace configuration (detect.cargo.ignore.all.workspaces or exclude properties). " +
"Zero components will be reported in SBOM.";

public CargoLockfileExtractor(
CargoTomlParser cargoTomlParser,
Expand All @@ -55,16 +69,14 @@ public CargoLockfileExtractor(
}

public Extraction extract(File cargoLockFile, @Nullable File cargoTomlFile, CargoDetectableOptions cargoDetectableOptions) throws IOException, DetectableException, MissingExternalIdException {
logFeatureFlagWarningIfApplicable(cargoDetectableOptions);
CargoLockData cargoLockData = new Toml().read(cargoLockFile).to(CargoLockData.class);
List<CargoLockPackageData> cargoLockPackageDataList = cargoLockData.getPackages().orElse(new ArrayList<>());
boolean exclusionEnabled = isDependencyExclusionEnabled(cargoDetectableOptions);
boolean isVirtualWorkspace = false;

if (exclusionEnabled && cargoDetectableOptions.getDependencyTypeFilter().shouldExclude(CargoDependencyType.PROC_MACRO)) {
logger.warn(
"PROC_MACRO exclusion is not supported by the Cargo Lockfile Detector and will be ignored. " +
"Supported exclusions for Cargo Lockfile Detector: [NORMAL, BUILD, DEV]. "
);
logger.warn(PROC_MACRO_EXCLUSION_NOT_SUPPORTED_WARNING);
}

if (cargoTomlFile == null && exclusionEnabled) {
Expand All @@ -88,11 +100,7 @@ public Extraction extract(File cargoLockFile, @Nullable File cargoTomlFile, Carg

// Early exit for virtual workspace with all members excluded
if (shouldReturnZeroComponents(cargoDetectableOptions, cargoTomlContents, workspaceRoot, isVirtualWorkspace)) {
logger.warn(
"Cannot exclude all workspace members for virtual manifest. " +
"Please check your workspace configuration (detect.cargo.ignore.all.workspaces or exclude properties). " +
"Zero components will be reported in SBOM."
);
logger.warn(VIRTUAL_WORKSPACE_ALL_MEMBERS_EXCLUDED_WARNING);
return new Extraction.Builder().success(new ArrayList<>()).nameVersionIfPresent(projectNameVersion).build();
}

Expand Down Expand Up @@ -155,6 +163,20 @@ public Extraction extract(File cargoLockFile, @Nullable File cargoTomlFile, Carg
.build();
}

private void logFeatureFlagWarningIfApplicable(CargoDetectableOptions cargoDetectableOptions) {
if (cargoDetectableOptions == null) {
return;
}

boolean hasIncludedFeatures = cargoDetectableOptions.getIncludedFeatures() != null &&
!cargoDetectableOptions.getIncludedFeatures().isEmpty();
boolean hasDisabledDefaultFeatures = cargoDetectableOptions.isDefaultFeaturesDisabled();

if (hasIncludedFeatures || hasDisabledDefaultFeatures) {
logger.warn(FEATURE_FLAGS_NOT_SUPPORTED_WARNING);
}
}

private boolean isDependencyExclusionEnabled(CargoDetectableOptions options) {
if (options == null) {
return false;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package com.blackduck.integration.detectable.detectables.cargo.functional;

import java.io.IOException;
import java.nio.file.Paths;
import java.util.Collections;

import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Assertions;

import com.blackduck.integration.bdio.model.Forge;
import com.blackduck.integration.detectable.Detectable;
import com.blackduck.integration.detectable.DetectableEnvironment;
import com.blackduck.integration.detectable.ExecutableTarget;
import com.blackduck.integration.detectable.detectable.exception.DetectableException;
import com.blackduck.integration.detectable.detectable.executable.resolver.CargoResolver;
import com.blackduck.integration.detectable.detectables.cargo.CargoDetectableOptions;
import com.blackduck.integration.detectable.extraction.Extraction;
import com.blackduck.integration.detectable.functional.DetectableFunctionalTest;
import com.blackduck.integration.detectable.util.graph.NameVersionGraphAssert;
import com.blackduck.integration.executable.ExecutableOutput;
import com.blackduck.integration.detectable.detectable.util.EnumListFilter;

/**
* Test case: detect.cargo.included.features=ALL
* Expected: All features and their dependencies in BOM.
*/
public class CargoCliAllFeaturesTest extends DetectableFunctionalTest {

public CargoCliAllFeaturesTest() throws IOException {
super("cargo-all-features");
}

@Override
protected void setup() throws IOException {
addFile(
Paths.get("Cargo.toml"),
"[package]",
"name = \"ripgrep\"",
"version = \"14.1.1\"",
"edition = \"2021\"",
"",
"[features]",
"default = [\"pcre2\", \"simd-accel\", \"json\"]",
"gzip = [\"dep:flate2\"]",
"bzip2 = [\"dep:bzip2\"]",
"",
"[dependencies]",
"regex = \"1.10.6\"",
"log = \"0.4.22\"",
"pcre2 = { version = \"0.2.7\", optional = true }",
"encoding_rs_io = { version = \"0.1.7\", optional = true }",
"serde = { version = \"1.0.210\", optional = true }",
"serde_json = { version = \"1.0.128\", optional = true }",
"flate2 = { version = \"1.0\", optional = true }",
"bzip2 = { version = \"0.4\", optional = true }"
);

ExecutableOutput cargoVersionOutput = createStandardOutput("cargo 1.85.0 (abc123 2020-06-17)");
addExecutableOutput(cargoVersionOutput, "cargo", "--version");

ExecutableOutput allFeaturesOutput = createStandardOutput(
"0ripgrep v14.1.1 (/path/to/ripgrep)",
"1regex v1.10.6",
"1log v0.4.22",
"1pcre2 v0.2.7",
"1encoding_rs_io v0.1.7",
"1serde v1.0.210",
"1serde_json v1.0.128",
"1flate2 v1.0",
"1bzip2 v0.4"
);
addExecutableOutput(allFeaturesOutput, "cargo", "tree", "--prefix", "depth", "--workspace", "--all-features");
}

@NotNull
@Override
public Detectable create(@NotNull DetectableEnvironment detectableEnvironment) {
class CargoResolverTest implements CargoResolver {
@Override
public ExecutableTarget resolveCargo(DetectableEnvironment environment) throws DetectableException {
return ExecutableTarget.forCommand("cargo");
}
}

return detectableFactory.createCargoCliDetectable(
detectableEnvironment,
new CargoResolverTest(),
new CargoDetectableOptions(
EnumListFilter.excludeNone(),
false,
false,
Collections.emptyList(),
Collections.emptyList(),
Collections.singletonList("ALL")
)
);
}

@Override
public void assertExtraction(@NotNull Extraction extraction) {
Assertions.assertEquals(1, extraction.getCodeLocations().size());

NameVersionGraphAssert graphAssert = new NameVersionGraphAssert(
Forge.CRATES,
extraction.getCodeLocations().get(0).getDependencyGraph()
);

graphAssert.hasRootSize(8);
graphAssert.hasRootDependency("regex", "1.10.6");
graphAssert.hasRootDependency("log", "0.4.22");
graphAssert.hasRootDependency("pcre2", "0.2.7");
graphAssert.hasRootDependency("encoding_rs_io", "0.1.7");
graphAssert.hasRootDependency("serde", "1.0.210");
graphAssert.hasRootDependency("serde_json", "1.0.128");
graphAssert.hasRootDependency("flate2", "1.0");
graphAssert.hasRootDependency("bzip2", "0.4");
}
}
Loading