1use super::changes::{
4 ComponentChangeComputer, DependencyChangeComputer, LicenseChangeComputer,
5 VulnerabilityChangeComputer,
6};
7pub use super::engine_config::LargeSbomConfig;
8use super::engine_matching::{match_components, ComponentMatchResult};
9use super::engine_rules::{apply_rules, remap_match_result};
10use super::traits::ChangeComputer;
11use super::{diff_dependency_graph, CostModel, DiffResult, GraphDiffConfig, MatchInfo};
12use crate::error::SbomDiffError;
13use crate::matching::{
14 ComponentMatcher, FuzzyMatchConfig, FuzzyMatcher, MatchingRulesConfig, RuleEngine,
15};
16use crate::model::NormalizedSbom;
17use std::borrow::Cow;
18
19pub struct DiffEngine {
21 cost_model: CostModel,
22 fuzzy_config: FuzzyMatchConfig,
23 include_unchanged: bool,
24 graph_diff_config: Option<GraphDiffConfig>,
25 rule_engine: Option<RuleEngine>,
26 custom_matcher: Option<Box<dyn ComponentMatcher>>,
27 large_sbom_config: LargeSbomConfig,
28}
29
30impl DiffEngine {
31 pub fn new() -> Self {
33 Self {
34 cost_model: CostModel::default(),
35 fuzzy_config: FuzzyMatchConfig::balanced(),
36 include_unchanged: false,
37 graph_diff_config: None,
38 rule_engine: None,
39 custom_matcher: None,
40 large_sbom_config: LargeSbomConfig::default(),
41 }
42 }
43
44 pub fn with_cost_model(mut self, cost_model: CostModel) -> Self {
46 self.cost_model = cost_model;
47 self
48 }
49
50 pub fn with_fuzzy_config(mut self, config: FuzzyMatchConfig) -> Self {
52 self.fuzzy_config = config;
53 self
54 }
55
56 pub fn include_unchanged(mut self, include: bool) -> Self {
58 self.include_unchanged = include;
59 self
60 }
61
62 pub fn with_graph_diff(mut self, config: GraphDiffConfig) -> Self {
64 self.graph_diff_config = Some(config);
65 self
66 }
67
68 pub fn with_matching_rules(mut self, config: MatchingRulesConfig) -> Result<Self, String> {
70 self.rule_engine = Some(RuleEngine::new(config)?);
71 Ok(self)
72 }
73
74 pub fn with_rule_engine(mut self, engine: RuleEngine) -> Self {
76 self.rule_engine = Some(engine);
77 self
78 }
79
80 pub fn with_matcher(mut self, matcher: Box<dyn ComponentMatcher>) -> Self {
82 self.custom_matcher = Some(matcher);
83 self
84 }
85
86 pub fn with_large_sbom_config(mut self, config: LargeSbomConfig) -> Self {
88 self.large_sbom_config = config;
89 self
90 }
91
92 pub fn large_sbom_config(&self) -> &LargeSbomConfig {
94 &self.large_sbom_config
95 }
96
97 pub fn has_custom_matcher(&self) -> bool {
99 self.custom_matcher.is_some()
100 }
101
102 pub fn graph_diff_enabled(&self) -> bool {
104 self.graph_diff_config.is_some()
105 }
106
107 pub fn has_matching_rules(&self) -> bool {
109 self.rule_engine.is_some()
110 }
111
112 pub fn diff(&self, old: &NormalizedSbom, new: &NormalizedSbom) -> Result<DiffResult, SbomDiffError> {
114 let mut result = DiffResult::new();
115
116 if old.content_hash == new.content_hash && old.content_hash != 0 {
118 return Ok(result);
119 }
120
121 let (old_filtered, new_filtered, canonical_maps) =
124 if let Some(rule_result) = apply_rules(self.rule_engine.as_ref(), old, new) {
125 result.rules_applied = rule_result.rules_count;
126 (
127 Cow::Owned(rule_result.old_filtered),
128 Cow::Owned(rule_result.new_filtered),
129 Some((rule_result.old_canonical, rule_result.new_canonical)),
130 )
131 } else {
132 (Cow::Borrowed(old), Cow::Borrowed(new), None)
133 };
134
135 let default_matcher;
137 let matcher: &dyn ComponentMatcher = match &self.custom_matcher {
138 Some(m) => m.as_ref(),
139 None => {
140 default_matcher = FuzzyMatcher::new(self.fuzzy_config.clone());
141 &default_matcher
142 }
143 };
144
145 let mut component_matches = match_components(
146 &old_filtered,
147 &new_filtered,
148 matcher,
149 &self.fuzzy_config,
150 &self.large_sbom_config,
151 );
152
153 if let Some((old_canonical, new_canonical)) = &canonical_maps {
155 component_matches = remap_match_result(&component_matches, old_canonical, new_canonical);
156 }
157
158 self.compute_all_changes(&old_filtered, &new_filtered, &component_matches, matcher, &mut result);
160
161 if let Some(ref graph_config) = self.graph_diff_config {
163 let (graph_changes, graph_summary) =
164 diff_dependency_graph(&old_filtered, &new_filtered, &component_matches.matches, graph_config);
165 result.graph_changes = graph_changes;
166 result.graph_summary = Some(graph_summary);
167 }
168
169 result.semantic_score = self.cost_model.calculate_semantic_score(
171 result.components.added.len(),
172 result.components.removed.len(),
173 result.components.modified.len(),
174 result.licenses.component_changes.len(),
175 result.vulnerabilities.introduced.len(),
176 result.vulnerabilities.resolved.len(),
177 result.dependencies.added.len(),
178 result.dependencies.removed.len(),
179 );
180
181 result.calculate_summary();
182 Ok(result)
183 }
184
185 fn compute_all_changes(
187 &self,
188 old: &NormalizedSbom,
189 new: &NormalizedSbom,
190 match_result: &ComponentMatchResult,
191 matcher: &dyn ComponentMatcher,
192 result: &mut DiffResult,
193 ) {
194 let comp_computer = ComponentChangeComputer::new(self.cost_model.clone());
196 let comp_changes = comp_computer.compute(old, new, &match_result.matches);
197 result.components.added = comp_changes.added;
198 result.components.removed = comp_changes.removed;
199 result.components.modified = comp_changes
200 .modified
201 .into_iter()
202 .map(|mut change| {
203 if let (Some(ref old_id), Some(ref new_id)) =
206 (&change.old_canonical_id, &change.canonical_id)
207 {
208 if let (Some(old_comp), Some(new_comp)) =
209 (old.components.get(old_id), new.components.get(new_id))
210 {
211 let explanation = matcher.explain_match(old_comp, new_comp);
212 let mut match_info = MatchInfo::from_explanation(&explanation);
213
214 if let Some(&score) =
216 match_result.pairs.get(&(old_id.clone(), new_id.clone()))
217 {
218 match_info.score = score;
219 }
220
221 change = change.with_match_info(match_info);
222 }
223 }
224 change
225 })
226 .collect();
227
228 let dep_computer = DependencyChangeComputer::new();
230 let dep_changes = dep_computer.compute(old, new, &match_result.matches);
231 result.dependencies.added = dep_changes.added;
232 result.dependencies.removed = dep_changes.removed;
233
234 let lic_computer = LicenseChangeComputer::new();
236 let lic_changes = lic_computer.compute(old, new, &match_result.matches);
237 result.licenses.new_licenses = lic_changes.new_licenses;
238 result.licenses.removed_licenses = lic_changes.removed_licenses;
239
240 let vuln_computer = VulnerabilityChangeComputer::new();
242 let vuln_changes = vuln_computer.compute(old, new, &match_result.matches);
243 result.vulnerabilities.introduced = vuln_changes.introduced;
244 result.vulnerabilities.resolved = vuln_changes.resolved;
245 result.vulnerabilities.persistent = vuln_changes.persistent;
246 }
247}
248
249impl Default for DiffEngine {
250 fn default() -> Self {
251 Self::new()
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258
259 #[test]
260 fn test_empty_diff() {
261 let engine = DiffEngine::new();
262 let sbom = NormalizedSbom::default();
263 let result = engine.diff(&sbom, &sbom).expect("diff should succeed");
264 assert!(!result.has_changes());
265 }
266}