1use super::incremental::IncrementalDiffEngine;
8use super::multi::{
9 ComparisonResult, ComplianceScoreEntry, ComplianceSnapshot, ComponentEvolution,
10 DependencySnapshot, DivergenceType, DivergentComponent, EvolutionSummary,
11 InconsistentComponent, MatrixResult, MultiDiffResult, MultiDiffSummary, SbomCluster,
12 SbomClustering, SbomInfo, SecurityImpact, TimelineResult, VariableComponent, VersionAtPoint,
13 VersionChangeType, VersionSpread, VulnerabilityMatrix, VulnerabilitySnapshot,
14};
15use super::{DiffEngine, DiffResult};
16use crate::matching::FuzzyMatchConfig;
17use crate::model::{NormalizedSbom, VulnerabilityCounts};
18use std::collections::{HashMap, HashSet};
19
20pub struct MultiDiffEngine {
26 fuzzy_config: Option<FuzzyMatchConfig>,
28 include_unchanged: bool,
30 graph_diff_config: Option<super::GraphDiffConfig>,
32 incremental: Option<IncrementalDiffEngine>,
34}
35
36impl MultiDiffEngine {
37 #[must_use]
38 pub const fn new() -> Self {
39 Self {
40 fuzzy_config: None,
41 include_unchanged: false,
42 graph_diff_config: None,
43 incremental: None,
44 }
45 }
46
47 #[must_use]
49 pub fn with_fuzzy_config(mut self, config: FuzzyMatchConfig) -> Self {
50 self.fuzzy_config = Some(config);
51 self.incremental = None;
52 self
53 }
54
55 #[must_use]
57 pub fn include_unchanged(mut self, include: bool) -> Self {
58 self.include_unchanged = include;
59 self.incremental = None;
60 self
61 }
62
63 #[must_use]
65 pub fn with_graph_diff(mut self, config: super::GraphDiffConfig) -> Self {
66 self.graph_diff_config = Some(config);
67 self.incremental = None;
68 self
69 }
70
71 fn ensure_engine(&mut self) {
73 if self.incremental.is_none() {
74 let mut engine = DiffEngine::new();
75 if let Some(config) = self.fuzzy_config.clone() {
76 engine = engine.with_fuzzy_config(config);
77 }
78 engine = engine.include_unchanged(self.include_unchanged);
79 if let Some(config) = self.graph_diff_config.clone() {
80 engine = engine.with_graph_diff(config);
81 }
82 self.incremental = Some(IncrementalDiffEngine::new(engine));
83 }
84 }
85
86 fn cached_diff(&mut self, old: &NormalizedSbom, new: &NormalizedSbom) -> DiffResult {
88 self.ensure_engine();
89 self.incremental
90 .as_ref()
91 .expect("engine initialized by ensure_engine")
92 .diff(old, new)
93 .into_result()
94 }
95
96 pub fn diff_multi(
98 &mut self,
99 baseline: &NormalizedSbom,
100 baseline_name: &str,
101 baseline_path: &str,
102 targets: &[(&NormalizedSbom, &str, &str)], ) -> MultiDiffResult {
104 let baseline_info = SbomInfo::from_sbom(
105 baseline,
106 baseline_name.to_string(),
107 baseline_path.to_string(),
108 );
109
110 let mut comparisons: Vec<ComparisonResult> = Vec::new();
112 let mut all_versions: HashMap<String, HashMap<String, String>> = HashMap::new(); for (id, comp) in &baseline.components {
116 let version = comp.version.clone().unwrap_or_default();
117 all_versions
118 .entry(id.value().to_string())
119 .or_default()
120 .insert(baseline_name.to_string(), version);
121 }
122
123 for (target_sbom, target_name, target_path) in targets {
124 let diff = self.cached_diff(baseline, target_sbom);
125 let target_info = SbomInfo::from_sbom(
126 target_sbom,
127 target_name.to_string(),
128 target_path.to_string(),
129 );
130
131 for (id, comp) in &target_sbom.components {
133 let version = comp.version.clone().unwrap_or_default();
134 all_versions
135 .entry(id.value().to_string())
136 .or_default()
137 .insert(target_name.to_string(), version);
138 }
139
140 comparisons.push(ComparisonResult {
141 target: target_info,
142 diff,
143 unique_components: vec![], divergent_components: vec![], });
146 }
147
148 let summary = self.compute_multi_diff_summary(
150 &baseline_info,
151 baseline,
152 &comparisons,
153 targets,
154 &all_versions,
155 );
156
157 for (i, comp) in comparisons.iter_mut().enumerate() {
159 let (target_sbom, target_name, _) = &targets[i];
160 comp.divergent_components =
161 self.find_divergent_components(baseline, target_sbom, target_name, &all_versions);
162 }
163
164 MultiDiffResult {
165 baseline: baseline_info,
166 comparisons,
167 summary,
168 }
169 }
170
171 fn compute_multi_diff_summary(
172 &self,
173 baseline_info: &SbomInfo,
174 baseline: &NormalizedSbom,
175 comparisons: &[ComparisonResult],
176 targets: &[(&NormalizedSbom, &str, &str)],
177 all_versions: &HashMap<String, HashMap<String, String>>,
178 ) -> MultiDiffSummary {
179 let baseline_components: HashSet<_> = baseline
180 .components
181 .keys()
182 .map(|k| k.value().to_string())
183 .collect();
184 let _target_names: Vec<_> = targets
185 .iter()
186 .map(|(_, name, _)| name.to_string())
187 .collect();
188
189 let mut universal: HashSet<String> = baseline_components.clone();
191 for (target_sbom, _, _) in targets {
192 let target_components: HashSet<_> = target_sbom
193 .components
194 .keys()
195 .map(|k| k.value().to_string())
196 .collect();
197 universal = universal
198 .intersection(&target_components)
199 .cloned()
200 .collect();
201 }
202
203 let mut variable_components: Vec<VariableComponent> = vec![];
205 for (comp_id, versions) in all_versions {
206 let unique_versions: HashSet<_> = versions.values().collect();
207 if unique_versions.len() > 1 {
208 let name = baseline
209 .components
210 .iter()
211 .find(|(id, _)| id.value() == comp_id)
212 .map(|(_, c)| c.name.clone())
213 .or_else(|| {
214 targets.iter().find_map(|(sbom, _, _)| {
215 sbom.components
216 .iter()
217 .find(|(id, _)| id.value() == comp_id)
218 .map(|(_, c)| c.name.clone())
219 })
220 })
221 .unwrap_or_else(|| comp_id.clone());
222
223 let baseline_version = versions.get(&baseline_info.name.clone()).cloned();
224 let all_versions_vec: Vec<_> = unique_versions.into_iter().cloned().collect();
225
226 let major_spread = calculate_major_version_spread(&all_versions_vec);
228
229 variable_components.push(VariableComponent {
230 id: comp_id.clone(),
231 name: name.clone(),
232 ecosystem: None,
233 version_spread: VersionSpread {
234 baseline: baseline_version,
235 min_version: all_versions_vec.iter().min().cloned(),
236 max_version: all_versions_vec.iter().max().cloned(),
237 unique_versions: all_versions_vec,
238 is_consistent: false,
239 major_version_spread: major_spread,
240 },
241 targets_with_component: versions.keys().cloned().collect(),
242 security_impact: classify_security_impact(&name),
243 });
244 }
245 }
246
247 let mut inconsistent_components: Vec<InconsistentComponent> = vec![];
249 let all_component_ids: HashSet<_> = all_versions.keys().cloned().collect();
250
251 for comp_id in &all_component_ids {
252 if universal.contains(comp_id) {
253 continue; }
255
256 let in_baseline = baseline_components.contains(comp_id);
257 let mut present_in: Vec<String> = vec![];
258 let mut missing_from: Vec<String> = vec![];
259
260 if in_baseline {
261 present_in.push(baseline_info.name.clone());
262 } else {
263 missing_from.push(baseline_info.name.clone());
264 }
265
266 for (target_sbom, target_name, _) in targets {
267 let has_component = target_sbom
268 .components
269 .iter()
270 .any(|(id, _)| id.value() == comp_id);
271 if has_component {
272 present_in.push(target_name.to_string());
273 } else {
274 missing_from.push(target_name.to_string());
275 }
276 }
277
278 if !missing_from.is_empty() {
279 let name = all_versions
280 .get(comp_id)
281 .and_then(|_| {
282 baseline
283 .components
284 .iter()
285 .find(|(id, _)| id.value() == comp_id)
286 .map(|(_, c)| c.name.clone())
287 })
288 .unwrap_or_else(|| comp_id.clone());
289
290 inconsistent_components.push(InconsistentComponent {
291 id: comp_id.clone(),
292 name,
293 in_baseline,
294 present_in,
295 missing_from,
296 });
297 }
298 }
299
300 let mut deviation_scores: HashMap<String, f64> = HashMap::new();
302 let mut max_deviation = 0.0f64;
303
304 for comp in comparisons {
305 let score = 100.0 - comp.diff.semantic_score;
306 deviation_scores.insert(comp.target.name.clone(), score);
307 max_deviation = max_deviation.max(score);
308 }
309
310 let vulnerability_matrix =
312 compute_vulnerability_matrix(baseline, &baseline_info.name, targets);
313
314 MultiDiffSummary {
315 baseline_component_count: baseline_info.component_count,
316 universal_components: universal.into_iter().collect(),
317 variable_components,
318 inconsistent_components,
319 deviation_scores,
320 max_deviation,
321 vulnerability_matrix,
322 }
323 }
324
325 fn find_divergent_components(
326 &self,
327 baseline: &NormalizedSbom,
328 target: &NormalizedSbom,
329 _target_name: &str,
330 all_versions: &HashMap<String, HashMap<String, String>>,
331 ) -> Vec<DivergentComponent> {
332 let mut divergent = vec![];
333
334 for (id, comp) in &target.components {
335 let comp_id = id.value().to_string();
336 let target_version = comp.version.clone().unwrap_or_default();
337
338 let baseline_version = baseline
340 .components
341 .iter()
342 .find(|(bid, _)| bid.value() == comp_id)
343 .and_then(|(_, bc)| bc.version.clone());
344
345 let divergence_type = if baseline_version.is_none() {
346 DivergenceType::Added
347 } else if baseline_version.as_ref() != Some(&target_version) {
348 DivergenceType::VersionMismatch
349 } else {
350 continue; };
352
353 divergent.push(DivergentComponent {
354 id: comp_id.clone(),
355 name: comp.name.clone(),
356 baseline_version,
357 target_version,
358 versions_across_targets: all_versions.get(&comp_id).cloned().unwrap_or_default(),
359 divergence_type,
360 });
361 }
362
363 for (id, comp) in &baseline.components {
365 let comp_id = id.value().to_string();
366 let in_target = target
367 .components
368 .iter()
369 .any(|(tid, _)| tid.value() == comp_id);
370
371 if !in_target {
372 divergent.push(DivergentComponent {
373 id: comp_id.clone(),
374 name: comp.name.clone(),
375 baseline_version: comp.version.clone(),
376 target_version: String::new(),
377 versions_across_targets: all_versions
378 .get(&comp_id)
379 .cloned()
380 .unwrap_or_default(),
381 divergence_type: DivergenceType::Removed,
382 });
383 }
384 }
385
386 divergent
387 }
388
389 pub fn timeline(
391 &mut self,
392 sboms: &[(&NormalizedSbom, &str, &str)], ) -> TimelineResult {
394 let sbom_infos: Vec<SbomInfo> = sboms
395 .iter()
396 .map(|(sbom, name, path)| SbomInfo::from_sbom(sbom, name.to_string(), path.to_string()))
397 .collect();
398
399 let mut incremental_diffs: Vec<DiffResult> = vec![];
401 for i in 0..sboms.len().saturating_sub(1) {
402 let diff = self.cached_diff(sboms[i].0, sboms[i + 1].0);
403 incremental_diffs.push(diff);
404 }
405
406 let mut cumulative_from_first: Vec<DiffResult> = vec![];
408 if !sboms.is_empty() {
409 for i in 1..sboms.len() {
410 let diff = self.cached_diff(sboms[0].0, sboms[i].0);
411 cumulative_from_first.push(diff);
412 }
413 }
414
415 let evolution_summary =
417 self.build_evolution_summary(sboms, &sbom_infos, &incremental_diffs);
418
419 TimelineResult {
420 sboms: sbom_infos,
421 incremental_diffs,
422 cumulative_from_first,
423 evolution_summary,
424 }
425 }
426
427 fn build_evolution_summary(
428 &self,
429 sboms: &[(&NormalizedSbom, &str, &str)],
430 sbom_infos: &[SbomInfo],
431 _incremental_diffs: &[DiffResult],
432 ) -> EvolutionSummary {
433 let mut version_history: HashMap<String, Vec<VersionAtPoint>> = HashMap::new();
435 let mut components_added: Vec<ComponentEvolution> = vec![];
436 let mut components_removed: Vec<ComponentEvolution> = vec![];
437 let mut all_components: HashSet<String> = HashSet::new();
438
439 for (sbom, _, _) in sboms {
441 for (id, _) in &sbom.components {
442 all_components.insert(id.value().to_string());
443 }
444 }
445
446 for comp_id in &all_components {
448 let mut history: Vec<VersionAtPoint> = vec![];
449 let mut first_seen: Option<(usize, String)> = None;
450 let mut last_seen: Option<usize> = None;
451 let mut prev_version: Option<String> = None;
452 let mut version_change_count: usize = 0;
453
454 for (i, (sbom, name, _)) in sboms.iter().enumerate() {
455 let comp = sbom.components.iter().find(|(id, _)| id.value() == comp_id);
456
457 let (version, change_type) = if let Some((_, c)) = comp {
458 let ver = c.version.clone();
459 let change = if first_seen.is_none() {
460 first_seen = Some((i, ver.clone().unwrap_or_default()));
461 VersionChangeType::Initial
462 } else {
463 let ct = classify_version_change(prev_version.as_ref(), ver.as_ref());
464 if !matches!(ct, VersionChangeType::Unchanged | VersionChangeType::Absent) {
466 version_change_count += 1;
467 }
468 ct
469 };
470 last_seen = Some(i);
471 prev_version.clone_from(&ver);
472 (ver, change)
473 } else if first_seen.is_some() {
474 (None, VersionChangeType::Removed)
475 } else {
476 (None, VersionChangeType::Absent)
477 };
478
479 history.push(VersionAtPoint {
480 sbom_index: i,
481 sbom_name: name.to_string(),
482 version,
483 change_type,
484 });
485 }
486
487 version_history.insert(comp_id.clone(), history);
488
489 if let Some((first_idx, first_ver)) = first_seen {
491 let still_present = last_seen == Some(sboms.len() - 1);
492 let current_version = if still_present {
493 sboms.last().and_then(|(sbom, _, _)| {
494 sbom.components
495 .iter()
496 .find(|(id, _)| id.value() == comp_id)
497 .and_then(|(_, c)| c.version.clone())
498 })
499 } else {
500 None
501 };
502
503 let name = sboms
504 .iter()
505 .find_map(|(sbom, _, _)| {
506 sbom.components
507 .iter()
508 .find(|(id, _)| id.value() == comp_id)
509 .map(|(_, c)| c.name.clone())
510 })
511 .unwrap_or_else(|| comp_id.clone());
512
513 let evolution = ComponentEvolution {
514 id: comp_id.clone(),
515 name,
516 first_seen_index: first_idx,
517 first_seen_version: first_ver,
518 last_seen_index: if still_present { None } else { last_seen },
519 current_version,
520 version_change_count,
521 };
522
523 if first_idx > 0 {
524 components_added.push(evolution.clone());
525 }
526 if !still_present {
527 components_removed.push(evolution);
528 }
529 }
530 }
531
532 let vulnerability_trend: Vec<VulnerabilitySnapshot> = sbom_infos
534 .iter()
535 .enumerate()
536 .map(|(i, info)| VulnerabilitySnapshot {
537 sbom_index: i,
538 sbom_name: info.name.clone(),
539 counts: info.vulnerability_counts.clone(),
540 new_vulnerabilities: vec![],
541 resolved_vulnerabilities: vec![],
542 })
543 .collect();
544
545 let dependency_trend: Vec<DependencySnapshot> = sboms
547 .iter()
548 .enumerate()
549 .map(|(i, (sbom, _, _))| {
550 let total_edges = sbom.edges.len();
551 let targets: HashSet<_> = sbom.edges.iter().map(|e| &e.to).collect();
553 let sources: HashSet<_> = sbom.edges.iter().map(|e| &e.from).collect();
554 let roots: HashSet<_> = sources.difference(&targets).collect();
555 let direct = sbom
556 .edges
557 .iter()
558 .filter(|e| roots.contains(&&e.from))
559 .count();
560 let transitive = total_edges.saturating_sub(direct);
561
562 DependencySnapshot {
563 sbom_index: i,
564 sbom_name: sbom_infos[i].name.clone(),
565 direct_dependencies: direct,
566 transitive_dependencies: transitive,
567 total_edges,
568 }
569 })
570 .collect();
571
572 let compliance_trend: Vec<ComplianceSnapshot> = sboms
574 .iter()
575 .enumerate()
576 .map(|(i, (sbom, name, _))| {
577 use crate::quality::{ComplianceChecker, ComplianceLevel};
578 let scores = ComplianceLevel::all()
579 .iter()
580 .map(|level| {
581 let result = ComplianceChecker::new(*level).check(sbom);
582 ComplianceScoreEntry {
583 standard: level.name().to_string(),
584 error_count: result.error_count,
585 warning_count: result.warning_count,
586 info_count: result.info_count,
587 is_compliant: result.is_compliant,
588 }
589 })
590 .collect();
591 ComplianceSnapshot {
592 sbom_index: i,
593 sbom_name: name.to_string(),
594 scores,
595 }
596 })
597 .collect();
598
599 EvolutionSummary {
600 components_added,
601 components_removed,
602 version_history,
603 vulnerability_trend,
604 license_changes: vec![],
605 dependency_trend,
606 compliance_trend,
607 }
608 }
609
610 pub fn matrix(
612 &mut self,
613 sboms: &[(&NormalizedSbom, &str, &str)], similarity_threshold: Option<f64>,
615 ) -> MatrixResult {
616 let sbom_infos: Vec<SbomInfo> = sboms
617 .iter()
618 .map(|(sbom, name, path)| SbomInfo::from_sbom(sbom, name.to_string(), path.to_string()))
619 .collect();
620
621 let n = sboms.len();
622 let num_pairs = n * (n - 1) / 2;
623
624 let mut diffs: Vec<Option<DiffResult>> = vec![None; num_pairs];
625 let mut similarity_scores: Vec<f64> = vec![0.0; num_pairs];
626
627 let mut idx = 0;
629 for i in 0..n {
630 for j in (i + 1)..n {
631 let diff = self.cached_diff(sboms[i].0, sboms[j].0);
632 let similarity = diff.semantic_score / 100.0;
633 similarity_scores[idx] = similarity;
634 diffs[idx] = Some(diff);
635 idx += 1;
636 }
637 }
638
639 let clustering = similarity_threshold
641 .map(|threshold| self.cluster_sboms(&sbom_infos, &similarity_scores, threshold));
642
643 MatrixResult {
644 sboms: sbom_infos,
645 diffs,
646 similarity_scores,
647 clustering,
648 }
649 }
650
651 fn cluster_sboms(
652 &self,
653 sboms: &[SbomInfo],
654 similarity_scores: &[f64],
655 threshold: f64,
656 ) -> SbomClustering {
657 let n = sboms.len();
658 let mut clusters: Vec<SbomCluster> = vec![];
659 let mut assigned: HashSet<usize> = HashSet::new();
660
661 for i in 0..n {
663 if assigned.contains(&i) {
664 continue;
665 }
666
667 let mut cluster_members = vec![i];
668 assigned.insert(i);
669
670 for j in (i + 1)..n {
671 if assigned.contains(&j) {
672 continue;
673 }
674
675 let idx = i * (2 * n - i - 1) / 2 + (j - i - 1);
677 let similarity = similarity_scores.get(idx).copied().unwrap_or(0.0);
678
679 if similarity >= threshold {
680 cluster_members.push(j);
681 assigned.insert(j);
682 }
683 }
684
685 if cluster_members.len() > 1 {
686 let mut total_sim = 0.0;
688 let mut count = 0;
689 for (mi, &a) in cluster_members.iter().enumerate() {
690 for &b in cluster_members.iter().skip(mi + 1) {
691 let (x, y) = if a < b { (a, b) } else { (b, a) };
692 let idx = x * (2 * n - x - 1) / 2 + (y - x - 1);
693 total_sim += similarity_scores.get(idx).copied().unwrap_or(0.0);
694 count += 1;
695 }
696 }
697
698 clusters.push(SbomCluster {
699 members: cluster_members.clone(),
700 centroid_index: cluster_members[0],
701 internal_similarity: if count > 0 {
702 total_sim / f64::from(count)
703 } else {
704 1.0
705 },
706 label: None,
707 });
708 }
709 }
710
711 let outliers: Vec<usize> = (0..n).filter(|i| !assigned.contains(i)).collect();
713
714 SbomClustering {
715 clusters,
716 outliers,
717 algorithm: "greedy".to_string(),
718 threshold,
719 }
720 }
721}
722
723impl Default for MultiDiffEngine {
724 fn default() -> Self {
725 Self::new()
726 }
727}
728
729fn classify_security_impact(name: &str) -> SecurityImpact {
731 let name_lower = name.to_lowercase();
732 let critical_components = [
733 "openssl",
734 "curl",
735 "libcurl",
736 "gnutls",
737 "mbedtls",
738 "wolfssl",
739 "boringssl",
740 ];
741 let high_components = [
742 "zlib", "libssh", "openssh", "gnupg", "gpg", "sqlite", "kernel", "glibc",
743 ];
744
745 if critical_components.iter().any(|c| name_lower.contains(c)) {
746 SecurityImpact::Critical
747 } else if high_components.iter().any(|c| name_lower.contains(c)) {
748 SecurityImpact::High
749 } else {
750 SecurityImpact::Low
751 }
752}
753
754fn calculate_major_version_spread(versions: &[String]) -> u32 {
756 let mut major_versions: HashSet<u64> = HashSet::new();
757
758 for version in versions {
759 if let Ok(v) = semver::Version::parse(version) {
761 major_versions.insert(v.major);
762 } else {
763 if let Some(major_str) = version.split(['.', '-', '_']).next()
765 && let Ok(major) = major_str.parse::<u64>()
766 {
767 major_versions.insert(major);
768 }
769 }
770 }
771
772 match (major_versions.iter().min(), major_versions.iter().max()) {
773 (Some(&min), Some(&max)) => (max - min) as u32,
774 _ => 0,
775 }
776}
777
778fn compute_vulnerability_matrix(
780 baseline: &NormalizedSbom,
781 baseline_name: &str,
782 targets: &[(&NormalizedSbom, &str, &str)],
783) -> VulnerabilityMatrix {
784 let mut vuln_sets: HashMap<String, HashSet<String>> = HashMap::new();
786 let mut per_sbom: HashMap<String, VulnerabilityCounts> = HashMap::new();
787
788 let baseline_vulns: HashSet<String> = baseline
790 .all_vulnerabilities()
791 .iter()
792 .map(|(_, v)| v.id.clone())
793 .collect();
794 vuln_sets.insert(baseline_name.to_string(), baseline_vulns);
795 per_sbom.insert(baseline_name.to_string(), baseline.vulnerability_counts());
796
797 for (sbom, name, _) in targets {
799 let target_vulns: HashSet<String> = sbom
800 .all_vulnerabilities()
801 .iter()
802 .map(|(_, v)| v.id.clone())
803 .collect();
804 vuln_sets.insert(name.to_string(), target_vulns);
805 per_sbom.insert(name.to_string(), sbom.vulnerability_counts());
806 }
807
808 let mut common_vulnerabilities: HashSet<String> =
810 vuln_sets.values().next().cloned().unwrap_or_default();
811
812 for vulns in vuln_sets.values() {
813 common_vulnerabilities = common_vulnerabilities
814 .intersection(vulns)
815 .cloned()
816 .collect();
817 }
818
819 let mut unique_vulnerabilities: HashMap<String, Vec<String>> = HashMap::new();
821
822 for (sbom_name, vulns) in &vuln_sets {
823 let mut unique: HashSet<String> = vulns.clone();
824
825 for (other_name, other_vulns) in &vuln_sets {
827 if other_name != sbom_name {
828 unique = unique.difference(other_vulns).cloned().collect();
829 }
830 }
831
832 if !unique.is_empty() {
833 unique_vulnerabilities.insert(sbom_name.clone(), unique.into_iter().collect());
834 }
835 }
836
837 VulnerabilityMatrix {
838 per_sbom,
839 unique_vulnerabilities,
840 common_vulnerabilities: common_vulnerabilities.into_iter().collect(),
841 }
842}
843
844fn classify_version_change(old: Option<&String>, new: Option<&String>) -> VersionChangeType {
846 match (old, new) {
847 (None, Some(_)) => VersionChangeType::Initial,
848 (Some(_), None) => VersionChangeType::Removed,
849 (Some(o), Some(n)) if o == n => VersionChangeType::Unchanged,
850 (Some(o), Some(n)) => {
851 if let (Ok(old_v), Ok(new_v)) = (semver::Version::parse(o), semver::Version::parse(n)) {
853 if new_v.major > old_v.major {
854 VersionChangeType::MajorUpgrade
855 } else if new_v.major < old_v.major {
856 VersionChangeType::Downgrade
857 } else if new_v.minor > old_v.minor {
858 VersionChangeType::MinorUpgrade
859 } else if new_v.minor < old_v.minor {
860 VersionChangeType::Downgrade
861 } else if new_v.patch > old_v.patch {
862 VersionChangeType::PatchUpgrade
863 } else {
864 VersionChangeType::Downgrade
865 }
866 } else {
867 if n > o {
869 VersionChangeType::PatchUpgrade
870 } else {
871 VersionChangeType::Downgrade
872 }
873 }
874 }
875 (None, None) => VersionChangeType::Absent,
876 }
877}