use super::EcosystemAdapter;
use crate::graph::GraphNode;
use anyhow::{Context, Result, bail};
use sha2::{Digest, Sha256};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
pub struct RustAdapter;
impl EcosystemAdapter for RustAdapter {
fn name(&self) -> &'static str {
"rust"
}
fn detect_tool(&self) -> Result<String> {
let output = Command::new("cargo").arg("--version").output().context(
"Failed to execute `cargo --version`. Ensure `cargo` is installed and in PATH.",
)?;
if !output.status.success() {
bail!(
"`cargo --version` failed with exit status: {}",
output.status
);
}
let version_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
Ok(version_str)
}
fn resolve(&self, workspace_path: &Path, frozen: bool) -> Result<PathBuf> {
let lockfile_path = workspace_path.join("Cargo.lock");
if frozen && lockfile_path.exists() {
return Ok(lockfile_path);
}
let mut cmd = Command::new("cargo");
cmd.arg("generate-lockfile").current_dir(workspace_path);
if frozen {
cmd.arg("--offline");
}
let output = cmd
.output()
.context("Failed to run `cargo generate-lockfile`")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!(
"`cargo generate-lockfile` failed in {}: {}",
workspace_path.display(),
stderr
);
}
if !lockfile_path.exists() {
bail!(
"`cargo generate-lockfile` completed but `Cargo.lock` missing at {}",
lockfile_path.display()
);
}
Ok(lockfile_path)
}
fn parse_lockfile(&self, lockfile_path: &Path) -> Result<Vec<GraphNode>> {
let workspace_dir = lockfile_path.parent().unwrap_or(Path::new("."));
let metadata_output = Command::new("cargo")
.args(["metadata", "--format-version", "1", "--no-deps"])
.current_dir(workspace_dir)
.output();
let metadata_features = if let Ok(ref output) = metadata_output {
if output.status.success() {
String::from_utf8_lossy(&output.stdout).to_string()
} else {
String::new()
}
} else {
String::new()
};
let content = fs::read_to_string(lockfile_path)
.with_context(|| format!("Failed to read Cargo.lock at {}", lockfile_path.display()))?;
let parsed: toml::Value = toml::from_str(&content).with_context(|| {
format!(
"Failed to parse Cargo.lock TOML at {}",
lockfile_path.display()
)
})?;
let mut nodes = Vec::new();
if let Some(packages) = parsed.get("package").and_then(|p| p.as_array()) {
for pkg in packages {
let name = pkg
.get("name")
.and_then(|n| n.as_str())
.unwrap_or("unknown")
.to_string();
let version = pkg
.get("version")
.and_then(|v| v.as_str())
.unwrap_or("0.0.0")
.to_string();
let hash_input = format!("rust:{}:{}:{}", name, version, metadata_features.len());
let content_hash = format!(
"sha256:{}",
hex::encode(Sha256::digest(hash_input.as_bytes()))
);
nodes.push(GraphNode {
ecosystem: "rust".to_string(),
package_name: name,
resolved_version: version,
content_hash,
platform_markers: None,
lock_ref: lockfile_path.to_string_lossy().to_string(),
});
}
}
Ok(nodes)
}
}