diff --git a/README.md b/README.md
index 4ad526dc..546c72b0 100644
--- a/README.md
+++ b/README.md
@@ -172,6 +172,7 @@ public class TrustifyExample {
Golang - Go Modules
Python - pip Installer
Gradle - Gradle Installation
+Rust - Cargo
@@ -315,6 +316,19 @@ test {
```
+
+Rust Cargo users can add a comment with #trustify-da-ignore next to the package to be ignored in Cargo.toml:
+
+```toml
+[dependencies]
+serde = "1.0.136" # trustify-da-ignore
+tokio = { version = "1.0", features = ["full"] }
+
+[workspace.dependencies]
+regex = "1.5.4" # trustify-da-ignore
+```
+
+
#### Ignore Strategies - experimental
@@ -337,6 +351,7 @@ System.setProperty("TRUSTIFY_DA_PNPM_PATH", "/path/to/custom/pnpm");
System.setProperty("TRUSTIFY_DA_YARN_PATH", "/path/to/custom/yarn");
System.setProperty("TRUSTIFY_DA_GO_PATH", "/path/to/custom/go");
System.setProperty("TRUSTIFY_DA_GRADLE_PATH", "/path/to/custom/gradle");
+System.setProperty("TRUSTIFY_DA_CARGO_PATH", "/path/to/custom/cargo");
//python - python3, pip3 take precedence if python version > 3 installed
System.setProperty("TRUSTIFY_DA_PYTHON3_PATH", "/path/to/python3");
System.setProperty("TRUSTIFY_DA_PIP3_PATH", "/path/to/pip3");
@@ -452,6 +467,11 @@ following keys for setting custom paths for the said executables.
pip |
TRUSTIFY_DA_PIP_PATH |
+
+| Cargo Package Manager |
+cargo |
+TRUSTIFY_DA_CARGO_PATH |
+
@@ -653,6 +673,9 @@ java -jar trustify-da-java-client-cli.jar component /path/to/requirements.txt
# Component analysis with summary
java -jar trustify-da-java-client-cli.jar component /path/to/go.mod --summary
+# Rust Cargo analysis
+java -jar trustify-da-java-client-cli.jar stack /path/to/Cargo.toml --summary
+
# Container image analysis with JSON output (default)
java -jar trustify-da-java-client-cli.jar image nginx:latest
diff --git a/src/main/java/io/github/guacsec/trustifyda/providers/CargoProvider.java b/src/main/java/io/github/guacsec/trustifyda/providers/CargoProvider.java
index e15269ae..52ca76d4 100644
--- a/src/main/java/io/github/guacsec/trustifyda/providers/CargoProvider.java
+++ b/src/main/java/io/github/guacsec/trustifyda/providers/CargoProvider.java
@@ -45,7 +45,12 @@
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
import java.util.logging.Logger;
import org.tomlj.Toml;
import org.tomlj.TomlParseResult;
@@ -341,23 +346,64 @@ private CargoMetadata executeCargoMetadata() throws IOException, InterruptedExce
.directory(workingDir.toFile())
.start();
+ // Use bounded executor to read streams concurrently to avoid two potential deadlocks:
+ // 1. buffer deadlock (process blocks on write when output buffers fill up while Java waits for
+ // process completion)
+ // 2. stalled process deadlock (readAllBytes() hangs forever when cargo process stalls
+ // completely with no timeout protection)
+ // Bounded executor allows proper cancellation and cleanup vs CompletableFuture common pool
+ ExecutorService streamExecutor = Executors.newFixedThreadPool(2);
String output;
- try (InputStream is = process.getInputStream()) {
- output = new String(is.readAllBytes(), StandardCharsets.UTF_8);
- }
+ String errorOutput;
+
+ try {
+ Future outputFuture =
+ streamExecutor.submit(
+ () -> {
+ try (InputStream is = process.getInputStream()) {
+ return new String(is.readAllBytes(), StandardCharsets.UTF_8);
+ } catch (IOException e) {
+ log.warning("Failed to read stdout from cargo metadata: " + e.getMessage());
+ return "";
+ }
+ });
+
+ Future errorFuture =
+ streamExecutor.submit(
+ () -> {
+ try (InputStream is = process.getErrorStream()) {
+ return new String(is.readAllBytes(), StandardCharsets.UTF_8);
+ } catch (IOException e) {
+ log.warning("Failed to read stderr from cargo metadata: " + e.getMessage());
+ return "";
+ }
+ });
+
+ boolean finished = process.waitFor(TIMEOUT, TimeUnit.SECONDS);
+
+ if (!finished) {
+ process.destroyForcibly();
+ outputFuture.cancel(true);
+ errorFuture.cancel(true);
+ throw new InterruptedException("cargo metadata timed out after " + TIMEOUT + " seconds");
+ }
- boolean finished = process.waitFor(TIMEOUT, TimeUnit.SECONDS);
+ try {
+ // Short timeout since process already finished
+ output = outputFuture.get(1, TimeUnit.SECONDS);
+ errorOutput = errorFuture.get(1, TimeUnit.SECONDS);
+ } catch (ExecutionException | TimeoutException e) {
+ log.warning("Failed to read process output: " + e.getMessage());
+ return null;
+ }
+ } finally {
+ streamExecutor.shutdownNow();
+ }
+ // Safe to call exitValue() - we confirmed the process finished
int exitCode = process.exitValue();
if (exitCode != 0) {
- String errorOutput = "";
- try (InputStream errorStream = process.getErrorStream()) {
- errorOutput = new String(errorStream.readAllBytes(), StandardCharsets.UTF_8);
- } catch (IOException e) {
- log.warning("Failed to read error stream: " + e.getMessage());
- }
-
String errorMessage = "cargo metadata failed with exit code: " + exitCode;
if (!errorOutput.isEmpty()) {
errorMessage += ". Error: " + errorOutput.trim();
@@ -373,11 +419,6 @@ private CargoMetadata executeCargoMetadata() throws IOException, InterruptedExce
return null;
}
- if (!finished) {
- process.destroyForcibly();
- throw new InterruptedException("cargo metadata timed out after " + TIMEOUT + " seconds");
- }
-
try {
CargoMetadata metadata = MAPPER.readValue(output, CargoMetadata.class);
if (debugLoggingIsNeeded()) {
@@ -552,12 +593,14 @@ private DependencyInfo getPackageInfo(String packageId, Map