1use std::{
46 collections::HashMap,
47 ops::Add,
48 process::{Command, Stdio},
49};
50
51use cargo_metadata::CargoOpt;
52use serde::Deserialize;
53
54use crate::{errors::GeigerError, ManifestPath, NameVersion};
55
56#[derive(Debug)]
59pub struct GeigerClient {
60 #[cfg(test)]
61 output: GeigerOutput,
62 unsafety: HashMap<NameVersion, GeigerUnsafety>,
63}
64
65impl GeigerClient {
66 pub fn new(
91 manifest_path: &ManifestPath,
92 features: Vec<CargoOpt>,
93 ) -> Result<Self, Box<GeigerError>> {
94 let mut cmd = Command::new("cargo-geiger");
95 cmd.args(["--output-format", "Json"])
96 .arg("--quiet") .arg("--manifest-path")
98 .arg(manifest_path.as_path());
99
100 for f in features {
101 match f {
103 CargoOpt::AllFeatures => {
104 cmd.arg("--all-features");
105 }
106 CargoOpt::NoDefaultFeatures => {
107 cmd.arg("--no-default-features");
108 }
109 CargoOpt::SomeFeatures(s) => {
110 if !s.is_empty() {
111 cmd.arg("--features");
112 cmd.args(s);
113 }
114 }
115 }
116 }
117
118 let output = cmd
119 .stdin(Stdio::null())
120 .output()
121 .unwrap_or_else(|e| {
122 panic!(
123 "geiger command failed to start with error: {e}, are you sure `cargo-geiger` is installed?"
124 )
125 });
126
127 if !output.status.success() {
128 println!("cargo-geiger exited with non-zero exit code, but it was ignored");
131 eprintln!("stderr: {}", String::from_utf8_lossy(&output.stderr));
132 }
137
138 let stdout = String::from_utf8_lossy(&output.stdout);
139 let res = Self::from_json(&stdout);
140 match res {
141 Ok(s) => Ok(s),
142 Err(e) => Err(Box::new(GeigerError::UnexpectedOutput(
143 e.to_string(),
144 stdout.to_string(),
145 ))),
146 }
147 }
148
149 pub fn from_json(geiger_output: &str) -> Result<Self, serde_json::Error> {
157 let output = serde_json::from_str::<GeigerOutput>(geiger_output)?;
158 Ok(Self::from(output))
159 }
160
161 #[must_use]
162 pub fn unsafety(&self, gid: &NameVersion) -> Option<GeigerUnsafety> {
163 self.unsafety.get(gid).copied()
164 }
165}
166
167impl From<GeigerOutput> for GeigerClient {
168 fn from(value: GeigerOutput) -> Self {
169 let mut unsafety = HashMap::with_capacity(value.packages.len());
170 for p in &value.packages {
171 unsafety.insert(p.package.id.clone(), p.unsafety);
172 }
173 Self {
174 #[cfg(test)]
175 output: value,
176 unsafety,
177 }
178 }
179}
180
181#[must_use]
187pub(crate) fn two_digit_percentage(part: u32, total: u32) -> f64 {
188 let res = f64::from(part) / f64::from(total);
189 if res.is_finite() {
190 (res * 10000.0).round() / 100.0
192 } else {
193 0.0
194 }
195}
196
197#[derive(Debug, Clone, Deserialize, Default)]
199pub struct GeigerOutput {
200 pub packages: Vec<GeigerPackageOutput>,
201}
202
203#[derive(Debug, Clone, Deserialize)]
204pub struct GeigerPackageOutput {
205 pub package: GeigerPackage,
206 pub unsafety: GeigerUnsafety,
207}
208
209#[derive(Debug, Clone, Deserialize)]
211pub struct GeigerPackage {
212 pub id: NameVersion,
213 }
215
216#[derive(Debug, Clone, Copy, Deserialize)]
223pub struct GeigerUnsafety {
224 pub used: GeigerCategories,
225 pub unused: GeigerCategories,
226 pub forbids_unsafe: bool,
227}
228
229impl GeigerUnsafety {
230 #[must_use]
233 pub fn total(&self) -> GeigerCategories {
234 GeigerCategories {
235 functions: self.used.functions + self.unused.functions,
236 exprs: self.used.exprs + self.unused.exprs,
237 item_impls: self.used.item_impls + self.unused.item_impls,
238 item_traits: self.used.item_traits + self.unused.item_traits,
239 methods: self.used.methods + self.unused.methods,
240 }
241 }
242
243 #[must_use]
244 pub fn used_safe(&self) -> u32 {
245 self.used.total_safe()
246 }
247
248 #[must_use]
249 pub fn used_unsafe(&self) -> u32 {
250 self.used.total_unsafe()
251 }
252
253 #[must_use]
254 pub fn unused_safe(&self) -> u32 {
255 self.unused.total_safe()
256 }
257
258 #[must_use]
259 pub fn unused_unsafe(&self) -> u32 {
260 self.unused.total_unsafe()
261 }
262
263 #[must_use]
264 pub fn total_safe(&self) -> u32 {
265 self.used_safe() + self.unused_safe()
266 }
267
268 #[must_use]
269 pub fn total_unsafe(&self) -> u32 {
270 self.used_unsafe() + self.unused_unsafe()
271 }
272
273 #[must_use]
278 pub fn percentage_unsafe(&self) -> f64 {
279 two_digit_percentage(
280 self.total_unsafe(),
281 self.total_safe() + self.total_unsafe(),
282 )
283 }
284}
285
286#[derive(Debug, Clone, Copy, Deserialize)]
288pub struct GeigerCategories {
289 pub functions: GeigerCount,
290 pub exprs: GeigerCount,
291 pub item_impls: GeigerCount,
292 pub item_traits: GeigerCount,
293 pub methods: GeigerCount,
294}
295
296impl GeigerCategories {
297 #[must_use]
300 pub fn total(&self) -> GeigerCount {
301 self.functions
302 + self.exprs
303 + self.item_impls
304 + self.item_traits
305 + self.methods
306 }
307
308 #[must_use]
309 pub fn total_safe(&self) -> u32 {
310 self.functions.safe
311 + self.exprs.safe
312 + self.item_impls.safe
313 + self.item_traits.safe
314 + self.methods.safe
315 }
316
317 #[must_use]
318 pub fn total_unsafe(&self) -> u32 {
319 self.functions.unsafe_
320 + self.exprs.unsafe_
321 + self.item_impls.unsafe_
322 + self.item_traits.unsafe_
323 + self.methods.unsafe_
324 }
325}
326
327impl Add<GeigerCategories> for GeigerCategories {
328 type Output = GeigerCategories;
329
330 fn add(self, rhs: GeigerCategories) -> Self::Output {
331 GeigerCategories {
332 functions: self.functions + rhs.functions,
333 exprs: self.exprs + rhs.exprs,
334 item_impls: self.item_impls + rhs.item_impls,
335 item_traits: self.item_traits + rhs.item_traits,
336 methods: self.methods + rhs.methods,
337 }
338 }
339}
340
341#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
344pub struct GeigerCount {
345 pub safe: u32,
346 pub unsafe_: u32,
347}
348
349impl GeigerCount {
350 #[must_use]
352 pub fn total(&self) -> u32 {
353 self.safe + self.unsafe_
354 }
355
356 #[must_use]
359 pub fn percentage_unsafe(&self) -> f64 {
360 two_digit_percentage(self.unsafe_, self.total())
361 }
362}
363
364impl Add<GeigerCount> for GeigerCount {
365 type Output = GeigerCount;
366
367 fn add(self, rhs: GeigerCount) -> Self::Output {
368 GeigerCount {
369 safe: self.safe + rhs.safe,
370 unsafe_: self.unsafe_ + rhs.unsafe_,
371 }
372 }
373}
374
375#[cfg(test)]
376mod test {
377 use std::{fs, path::Path};
378
379 use test_case::test_case;
380
381 use crate::{geiger::GeigerCount, ManifestPath};
382
383 use super::{GeigerClient, GeigerOutput};
384
385 #[test_case(0, 0 => 0.0)]
386 #[test_case(3, 1 => 25.0)]
387 #[test_case(9, 1 => 10.0)]
388 #[test_case(2, 1 => 33.33)]
389 fn two_digit_percentage(safe_count: u32, unsafe_count: u32) -> f64 {
390 super::two_digit_percentage(unsafe_count, safe_count + unsafe_count)
391 }
392
393 #[test_case("simple_deps")]
394 #[test_case("known_advisory_deps")]
395 #[test_case("feature_deps")]
396 #[test_case("forbids_unsafe")]
397 fn geiger_from_path(crate_name: &'static str) {
398 let path_string =
399 format!("test_data/fake_crates/{crate_name}/Cargo.toml");
400 let path = ManifestPath::from(path_string);
401 GeigerClient::new(&path, vec![]).unwrap();
402 }
403
404 #[test_case("simple_deps")]
405 fn deserialize_geiger_output_smoke_test(crate_name: &'static str) {
406 let path_string = format!("test_data/geiger-output/{crate_name}.json");
407 let path = Path::new(&path_string);
408 let json_string = fs::read_to_string(path).unwrap();
409 serde_json::from_str::<GeigerOutput>(&json_string).unwrap();
410 }
411
412 #[test_case(0, 0, 0, 0)]
413 #[test_case(1, 1, 0, 0)]
414 #[test_case(1, 2, 3, 4)]
415 fn add_geiger_counts(safe0: u32, unsafe0: u32, safe1: u32, unsafe1: u32) {
416 let gc0 = GeigerCount {
417 safe: safe0,
418 unsafe_: unsafe0,
419 };
420 let gc1 = GeigerCount {
421 safe: safe1,
422 unsafe_: unsafe1,
423 };
424 let gc_res = GeigerCount {
425 safe: safe0 + safe1,
426 unsafe_: unsafe0 + unsafe1,
427 };
428 assert_eq!(gc0 + gc1, gc_res);
429 }
430}