use crate::analyzer::Analyzer;
use crate::error::{Error, Result};
use crate::model::{Occurrence, ScanOpts, ScanResult, Scope, Totals, Unit, UnitKind};
use serde::Deserialize;
use std::collections::HashMap;
use std::process::{Command, Stdio};
pub struct CargoGeigerAnalyzer;
impl Analyzer for CargoGeigerAnalyzer {
fn id(&self) -> &str {
"cargo_geiger"
}
fn language(&self) -> &str {
"rust"
}
fn run(&self, opts: &ScanOpts) -> Result<ScanResult> {
let output = run_cargo_geiger(opts)?;
let report = parse_geiger_output(&output)?;
let (units, details) = convert_report(&report, opts);
let totals = Totals::from_units(&units);
Ok(ScanResult {
tool_version: env!("CARGO_PKG_VERSION").into(),
analyzer_id: self.id().into(),
language: self.language().into(),
scope: Scope::from(opts),
units,
totals,
details,
})
}
}
fn run_cargo_geiger(opts: &ScanOpts) -> Result<Vec<u8>> {
let mut cmd = Command::new("cargo");
cmd.arg("geiger")
.arg("--output-format")
.arg("json")
.stdout(Stdio::piped())
.stderr(Stdio::piped());
if opts.all_features {
cmd.arg("--all-features");
}
if opts.no_default_features {
cmd.arg("--no-default-features");
}
for feature in &opts.features {
cmd.arg("--features").arg(feature);
}
if opts.all_targets {
cmd.arg("--all-targets");
}
for target in &opts.targets {
cmd.arg("--target").arg(target);
}
if let Some(ref path) = opts.manifest_path {
cmd.arg("--manifest-path").arg(path);
}
let output = cmd.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(Error::Analyzer {
analyzer: "cargo_geiger".into(),
message: format!("cargo geiger failed: {}", stderr),
});
}
Ok(output.stdout)
}
#[derive(Debug, Deserialize)]
struct GeigerReport {
packages: Vec<GeigerPackage>,
}
#[derive(Debug, Deserialize)]
struct GeigerPackage {
package: PackageId,
unsafety: Unsafety,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct PackageId {
name: String,
version: String,
#[serde(default)]
source: Option<String>,
}
#[derive(Debug, Deserialize)]
struct Unsafety {
used: UnsafeCount,
unused: UnsafeCount,
}
#[derive(Debug, Deserialize)]
struct UnsafeCount {
functions: CountPair,
exprs: CountPair,
item_impls: CountPair,
item_traits: CountPair,
methods: CountPair,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct CountPair {
safe: u64,
#[serde(rename = "unsafe")]
unsafe_: u64,
}
impl UnsafeCount {
fn total_unsafe(&self) -> u64 {
self.functions.unsafe_
+ self.exprs.unsafe_
+ self.item_impls.unsafe_
+ self.item_traits.unsafe_
+ self.methods.unsafe_
}
}
fn parse_geiger_output(output: &[u8]) -> Result<GeigerReport> {
serde_json::from_slice(output).map_err(|e| Error::Analyzer {
analyzer: "cargo_geiger".into(),
message: format!("failed to parse cargo-geiger output: {}", e),
})
}
fn convert_report(report: &GeigerReport, opts: &ScanOpts) -> (Vec<Unit>, Vec<Occurrence>) {
let mut units: HashMap<String, (UnitKind, u64)> = HashMap::new();
for pkg in &report.packages {
let is_workspace = pkg.package.source.is_none();
let kind = if is_workspace {
UnitKind::Workspace
} else {
UnitKind::Dep
};
if opts.workspace_only && kind == UnitKind::Dep {
continue;
}
if !opts.include_deps && kind == UnitKind::Dep {
continue;
}
let unsafe_count = pkg.unsafety.used.total_unsafe() + pkg.unsafety.unused.total_unsafe();
let entry = units.entry(pkg.package.name.clone()).or_insert((kind, 0));
entry.1 += unsafe_count;
}
let mut unit_list: Vec<Unit> = units
.into_iter()
.map(|(name, (kind, count))| Unit {
name,
kind,
unsafe_count: count,
})
.collect();
unit_list.sort_by(|a, b| a.name.cmp(&b.name));
(unit_list, vec![])
}
#[cfg(test)]
mod tests {
use super::*;
fn make_count_pair(safe: u64, unsafe_: u64) -> CountPair {
CountPair { safe, unsafe_ }
}
fn make_unsafe_count(
funcs: u64,
exprs: u64,
impls: u64,
traits: u64,
methods: u64,
) -> UnsafeCount {
UnsafeCount {
functions: make_count_pair(0, funcs),
exprs: make_count_pair(0, exprs),
item_impls: make_count_pair(0, impls),
item_traits: make_count_pair(0, traits),
methods: make_count_pair(0, methods),
}
}
#[test]
fn test_unsafe_count_total() {
let count = make_unsafe_count(1, 2, 3, 4, 5);
assert_eq!(count.total_unsafe(), 15);
}
#[test]
fn test_unsafe_count_total_zeros() {
let count = make_unsafe_count(0, 0, 0, 0, 0);
assert_eq!(count.total_unsafe(), 0);
}
#[test]
fn test_parse_geiger_output_valid() {
let json = r#"{
"packages": [
{
"package": {"name": "my_crate", "version": "0.1.0"},
"unsafety": {
"used": {
"functions": {"safe": 10, "unsafe": 2},
"exprs": {"safe": 100, "unsafe": 5},
"item_impls": {"safe": 5, "unsafe": 0},
"item_traits": {"safe": 0, "unsafe": 0},
"methods": {"safe": 20, "unsafe": 1}
},
"unused": {
"functions": {"safe": 0, "unsafe": 0},
"exprs": {"safe": 0, "unsafe": 0},
"item_impls": {"safe": 0, "unsafe": 0},
"item_traits": {"safe": 0, "unsafe": 0},
"methods": {"safe": 0, "unsafe": 0}
}
}
}
]
}"#;
let report = parse_geiger_output(json.as_bytes()).unwrap();
assert_eq!(report.packages.len(), 1);
assert_eq!(report.packages[0].package.name, "my_crate");
assert_eq!(report.packages[0].unsafety.used.total_unsafe(), 8);
}
#[test]
fn test_parse_geiger_output_invalid() {
let json = r#"{"invalid": true}"#;
let result = parse_geiger_output(json.as_bytes());
assert!(result.is_err());
}
#[test]
fn test_convert_report_workspace_package() {
let report = GeigerReport {
packages: vec![GeigerPackage {
package: PackageId {
name: "my_crate".into(),
version: "0.1.0".into(),
source: None, },
unsafety: Unsafety {
used: make_unsafe_count(1, 2, 0, 0, 0),
unused: make_unsafe_count(0, 0, 0, 0, 0),
},
}],
};
let opts = ScanOpts::default();
let (units, details) = convert_report(&report, &opts);
assert_eq!(units.len(), 1);
assert_eq!(units[0].name, "my_crate");
assert_eq!(units[0].kind, UnitKind::Workspace);
assert_eq!(units[0].unsafe_count, 3);
assert!(details.is_empty()); }
#[test]
fn test_convert_report_dependency_package() {
let report = GeigerReport {
packages: vec![GeigerPackage {
package: PackageId {
name: "libc".into(),
version: "0.2.0".into(),
source: Some("registry+https://github.com/rust-lang/crates.io-index".into()),
},
unsafety: Unsafety {
used: make_unsafe_count(10, 20, 5, 0, 0),
unused: make_unsafe_count(0, 0, 0, 0, 0),
},
}],
};
let opts = ScanOpts {
include_deps: true,
..Default::default()
};
let (units, _) = convert_report(&report, &opts);
assert_eq!(units.len(), 1);
assert_eq!(units[0].name, "libc");
assert_eq!(units[0].kind, UnitKind::Dep);
assert_eq!(units[0].unsafe_count, 35);
}
#[test]
fn test_convert_report_filters_deps_when_workspace_only() {
let report = GeigerReport {
packages: vec![
GeigerPackage {
package: PackageId {
name: "my_crate".into(),
version: "0.1.0".into(),
source: None,
},
unsafety: Unsafety {
used: make_unsafe_count(1, 0, 0, 0, 0),
unused: make_unsafe_count(0, 0, 0, 0, 0),
},
},
GeigerPackage {
package: PackageId {
name: "libc".into(),
version: "0.2.0".into(),
source: Some("registry".into()),
},
unsafety: Unsafety {
used: make_unsafe_count(100, 0, 0, 0, 0),
unused: make_unsafe_count(0, 0, 0, 0, 0),
},
},
],
};
let opts = ScanOpts {
workspace_only: true,
..Default::default()
};
let (units, _) = convert_report(&report, &opts);
assert_eq!(units.len(), 1);
assert_eq!(units[0].name, "my_crate");
}
#[test]
fn test_convert_report_filters_deps_when_include_deps_false() {
let report = GeigerReport {
packages: vec![
GeigerPackage {
package: PackageId {
name: "my_crate".into(),
version: "0.1.0".into(),
source: None,
},
unsafety: Unsafety {
used: make_unsafe_count(1, 0, 0, 0, 0),
unused: make_unsafe_count(0, 0, 0, 0, 0),
},
},
GeigerPackage {
package: PackageId {
name: "libc".into(),
version: "0.2.0".into(),
source: Some("registry".into()),
},
unsafety: Unsafety {
used: make_unsafe_count(100, 0, 0, 0, 0),
unused: make_unsafe_count(0, 0, 0, 0, 0),
},
},
],
};
let opts = ScanOpts {
include_deps: false,
..Default::default()
};
let (units, _) = convert_report(&report, &opts);
assert_eq!(units.len(), 1);
assert_eq!(units[0].name, "my_crate");
}
#[test]
fn test_convert_report_sorted_output() {
let report = GeigerReport {
packages: vec![
GeigerPackage {
package: PackageId {
name: "zebra".into(),
version: "0.1.0".into(),
source: None,
},
unsafety: Unsafety {
used: make_unsafe_count(1, 0, 0, 0, 0),
unused: make_unsafe_count(0, 0, 0, 0, 0),
},
},
GeigerPackage {
package: PackageId {
name: "alpha".into(),
version: "0.1.0".into(),
source: None,
},
unsafety: Unsafety {
used: make_unsafe_count(1, 0, 0, 0, 0),
unused: make_unsafe_count(0, 0, 0, 0, 0),
},
},
],
};
let opts = ScanOpts::default();
let (units, _) = convert_report(&report, &opts);
assert_eq!(units[0].name, "alpha");
assert_eq!(units[1].name, "zebra");
}
}