use std::path::Path;
use std::process::Command;
use serde::Deserialize;
use crate::license::scanner::{LanguageLicenseScanner, PackageLicense};
pub struct RustLicenseScanner;
impl RustLicenseScanner {
pub fn new() -> Self {
Self
}
fn has_cargo_license(&self) -> bool {
Command::new("cargo")
.args(["license", "--version"])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn scan_with_cargo_license(&self, path: &Path) -> Result<Vec<PackageLicense>, String> {
let output = Command::new("cargo")
.args(["license", "--json"])
.current_dir(path)
.output()
.map_err(|e| format!("Failed to run cargo license: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("cargo license failed: {}", stderr));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let entries: Vec<CargoLicenseEntry> = serde_json::from_str(&stdout)
.map_err(|e| format!("Failed to parse cargo license output: {}", e))?;
Ok(entries
.into_iter()
.map(|e| {
PackageLicense::new(
&e.name,
&e.version,
&e.license.unwrap_or_default(),
"crates.io",
)
})
.collect())
}
fn scan_from_metadata(&self, path: &Path) -> Result<Vec<PackageLicense>, String> {
let output = Command::new("cargo")
.args(["metadata", "--format-version", "1", "--no-deps"])
.current_dir(path)
.output()
.map_err(|e| format!("Failed to run cargo metadata: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("cargo metadata failed: {}", stderr));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let metadata: CargoMetadata = serde_json::from_str(&stdout)
.map_err(|e| format!("Failed to parse cargo metadata: {}", e))?;
let mut packages = Vec::new();
let full_output = Command::new("cargo")
.args(["metadata", "--format-version", "1"])
.current_dir(path)
.output()
.map_err(|e| format!("Failed to run cargo metadata: {}", e))?;
if full_output.status.success() {
let full_stdout = String::from_utf8_lossy(&full_output.stdout);
if let Ok(full_metadata) = serde_json::from_str::<CargoMetadata>(&full_stdout) {
for pkg in full_metadata.packages {
let is_direct = metadata.packages.iter().any(|p| p.name == pkg.name);
let mut license = PackageLicense::new(
&pkg.name,
&pkg.version,
&pkg.license.unwrap_or_default(),
"crates.io",
);
license.is_direct = is_direct;
license.authors = pkg.authors;
license.repository = pkg.repository;
packages.push(license);
}
}
}
Ok(packages)
}
}
impl Default for RustLicenseScanner {
fn default() -> Self {
Self::new()
}
}
impl LanguageLicenseScanner for RustLicenseScanner {
fn name(&self) -> &str {
"cargo-license"
}
fn language(&self) -> &str {
"rust"
}
fn detect(&self, path: &Path) -> bool {
path.join("Cargo.toml").exists()
}
fn scan(&self, path: &Path) -> Result<Vec<PackageLicense>, String> {
if self.has_cargo_license() {
self.scan_with_cargo_license(path)
} else {
self.scan_from_metadata(path)
}
}
}
#[derive(Debug, Deserialize)]
struct CargoLicenseEntry {
name: String,
version: String,
license: Option<String>,
}
#[derive(Debug, Deserialize)]
struct CargoMetadata {
packages: Vec<CargoPackage>,
}
#[derive(Debug, Deserialize)]
struct CargoPackage {
name: String,
version: String,
license: Option<String>,
#[serde(default)]
authors: Vec<String>,
repository: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rust_scanner_detect() {
let scanner = RustLicenseScanner::new();
let temp_dir = tempfile::tempdir().unwrap();
assert!(!scanner.detect(temp_dir.path()));
std::fs::write(temp_dir.path().join("Cargo.toml"), "[package]").unwrap();
assert!(scanner.detect(temp_dir.path()));
}
}