1use crate::config::DiffConfig;
6use crate::pipeline::{
7 auto_detect_format, compute_diff, exit_codes, output_report, parse_sbom_with_context,
8 OutputTarget,
9};
10use crate::reports::ReportFormat;
11use crate::tui::{run_tui, App};
12use anyhow::Result;
13
14#[allow(clippy::needless_pass_by_value)]
23pub fn run_diff(config: DiffConfig) -> Result<i32> {
24 let quiet = config.behavior.quiet;
25
26 let mut old_parsed = parse_sbom_with_context(&config.paths.old, quiet)?;
28 let mut new_parsed = parse_sbom_with_context(&config.paths.new, quiet)?;
29
30 if !quiet {
31 tracing::info!(
32 "Parsed {} components from old SBOM, {} from new SBOM",
33 old_parsed.sbom().component_count(),
34 new_parsed.sbom().component_count()
35 );
36 }
37
38 #[cfg(feature = "enrichment")]
40 let enrichment_stats = {
41 if config.enrichment.enabled {
42 use crate::enrichment::OsvEnricherConfig;
43 use crate::pipeline::dirs;
44
45 let osv_config = OsvEnricherConfig {
46 cache_dir: config
47 .enrichment
48 .cache_dir
49 .clone()
50 .unwrap_or_else(dirs::osv_cache_dir),
51 cache_ttl: std::time::Duration::from_secs(config.enrichment.cache_ttl_hours * 3600),
52 bypass_cache: config.enrichment.bypass_cache,
53 timeout: std::time::Duration::from_secs(config.enrichment.timeout_secs),
54 ..Default::default()
55 };
56
57 let stats_old =
58 crate::pipeline::enrich_sbom(old_parsed.sbom_mut(), &osv_config, quiet);
59 let stats_new =
60 crate::pipeline::enrich_sbom(new_parsed.sbom_mut(), &osv_config, quiet);
61 Some((stats_old, stats_new))
62 } else {
63 None
64 }
65 };
66
67 #[cfg(not(feature = "enrichment"))]
68 {
69 if config.enrichment.enabled && !quiet {
70 tracing::warn!(
71 "Enrichment requested but the 'enrichment' feature is not enabled. \
72 Rebuild with --features enrichment to enable vulnerability enrichment."
73 );
74 }
75 }
76
77 let result = compute_diff(&config, &old_parsed.sbom, &new_parsed.sbom)?;
79
80 let exit_code = determine_exit_code(&config, &result);
82
83 let output_target = OutputTarget::from_option(config.output.file.clone());
85 let effective_output = auto_detect_format(config.output.format, &output_target);
86
87 if effective_output == ReportFormat::Tui {
88 let (old_sbom, old_raw) = old_parsed.into_parts();
89 let (new_sbom, new_raw) = new_parsed.into_parts();
90
91 #[cfg(feature = "enrichment")]
92 let mut app = {
93 let app = App::new_diff(result, old_sbom, new_sbom, &old_raw, &new_raw);
94 if let Some((stats_old, stats_new)) = enrichment_stats {
95 app.with_enrichment_stats(stats_old, stats_new)
96 } else {
97 app
98 }
99 };
100
101 #[cfg(not(feature = "enrichment"))]
102 let mut app = App::new_diff(result, old_sbom, new_sbom, &old_raw, &new_raw);
103
104 run_tui(&mut app)?;
105 } else {
106 old_parsed.drop_raw_content();
107 new_parsed.drop_raw_content();
108 output_report(&config, &result, &old_parsed.sbom, &new_parsed.sbom)?;
109 }
110
111 Ok(exit_code)
112}
113
114const fn determine_exit_code(config: &DiffConfig, result: &crate::diff::DiffResult) -> i32 {
116 if config.behavior.fail_on_vuln && result.summary.vulnerabilities_introduced > 0 {
117 return exit_codes::VULNS_INTRODUCED;
118 }
119 if config.behavior.fail_on_change && result.summary.total_changes > 0 {
120 return exit_codes::CHANGES_DETECTED;
121 }
122 exit_codes::SUCCESS
123}
124
125#[cfg(test)]
126mod tests {
127 use crate::pipeline::OutputTarget;
128 use std::path::PathBuf;
129
130 #[test]
131 fn test_output_target_conversion() {
132 let none_target = OutputTarget::from_option(None);
133 assert!(matches!(none_target, OutputTarget::Stdout));
134
135 let some_target = OutputTarget::from_option(Some(PathBuf::from("/tmp/test.json")));
136 assert!(matches!(some_target, OutputTarget::File(_)));
137 }
138}