use crate::error::{AuditError, Result};
use crate::types::DependencySource;
use cargo_metadata::{CargoOpt, Metadata, MetadataCommand, Package, PackageId};
use std::collections::HashSet;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct ParsedDependency {
pub name: String,
pub version: String,
pub is_direct: bool,
pub source: DependencySource,
pub package_id: PackageId,
}
pub fn parse_project(project_path: &Path) -> Result<Vec<ParsedDependency>> {
let metadata = get_cargo_metadata(project_path)?;
extract_dependencies(&metadata)
}
fn get_cargo_metadata(project_path: &Path) -> Result<Metadata> {
let manifest_path = project_path.join("Cargo.toml");
if !manifest_path.exists() {
return Err(AuditError::parse(format!(
"Cargo.toml not found at {}",
manifest_path.display()
)));
}
let metadata = MetadataCommand::new()
.manifest_path(&manifest_path)
.features(CargoOpt::AllFeatures)
.exec()?;
Ok(metadata)
}
fn extract_dependencies(metadata: &Metadata) -> Result<Vec<ParsedDependency>> {
let mut dependencies = Vec::new();
let root_packages: Vec<&Package> = if let Some(resolve) = &metadata.resolve {
resolve
.root
.as_ref()
.and_then(|root_id| metadata.packages.iter().find(|p| &p.id == root_id))
.map(|p| vec![p])
.unwrap_or_else(|| {
metadata
.workspace_members
.iter()
.filter_map(|id| metadata.packages.iter().find(|p| &p.id == id))
.collect()
})
} else {
return Err(AuditError::parse("No dependency resolution found"));
};
let mut direct_deps: HashSet<PackageId> = HashSet::new();
for root_pkg in &root_packages {
for dep in &root_pkg.dependencies {
if let Some(pkg) = metadata.packages.iter().find(|p| p.name == dep.name) {
direct_deps.insert(pkg.id.clone());
}
}
}
if let Some(resolve) = &metadata.resolve {
let root_ids: HashSet<_> = root_packages.iter().map(|p| &p.id).collect();
for node in &resolve.nodes {
if root_ids.contains(&node.id) {
continue;
}
if let Some(pkg) = metadata.packages.iter().find(|p| p.id == node.id) {
let is_direct = direct_deps.contains(&pkg.id);
let source = determine_source(pkg);
dependencies.push(ParsedDependency {
name: pkg.name.clone(),
version: pkg.version.to_string(),
is_direct,
source,
package_id: pkg.id.clone(),
});
}
}
}
Ok(dependencies)
}
fn determine_source(package: &Package) -> DependencySource {
if let Some(source) = &package.source {
let source_str = source.repr.as_str();
if source_str.starts_with("registry+") {
DependencySource::CratesIo
} else if source_str.starts_with("git+") {
let url = source_str
.strip_prefix("git+")
.and_then(|s| s.split('?').next())
.unwrap_or(source_str)
.to_string();
DependencySource::Git { url }
} else if source_str.starts_with("path+") {
let path = source_str
.strip_prefix("path+file://")
.or_else(|| source_str.strip_prefix("path+"))
.unwrap_or(source_str)
.to_string();
DependencySource::Path { path }
} else {
DependencySource::Unknown
}
} else {
DependencySource::Path {
path: package.manifest_path.parent()
.map(|p| p.as_std_path().display().to_string())
.unwrap_or_else(|| "unknown".to_string()),
}
}
}
pub fn get_project_name(project_path: &Path) -> Result<String> {
let metadata = get_cargo_metadata(project_path)?;
if let Some(resolve) = &metadata.resolve {
if let Some(root_id) = &resolve.root {
if let Some(root_pkg) = metadata.packages.iter().find(|p| &p.id == root_id) {
return Ok(root_pkg.name.clone());
}
}
}
if let Some(first_pkg) = metadata.packages.first() {
Ok(first_pkg.name.clone())
} else {
Err(AuditError::parse("Could not determine project name"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_determine_source() {
}
}