Skip to main content

sbom_tools/diff/
traits.rs

1//! Trait definitions for diff computation strategies.
2//!
3//! This module provides abstractions for computing different types of changes,
4//! enabling modular and testable diff operations.
5
6use super::{ComponentChange, DependencyChange, LicenseChange, VulnerabilityDetail};
7use crate::model::{CanonicalId, NormalizedSbom};
8use std::collections::HashMap;
9
10/// Result of matching components between two SBOMs.
11pub type ComponentMatches = HashMap<CanonicalId, Option<CanonicalId>>;
12
13/// Trait for computing a specific type of change between SBOMs.
14///
15/// Implementors provide logic for detecting a particular category of changes
16/// (components, dependencies, licenses, vulnerabilities).
17pub trait ChangeComputer: Send + Sync {
18    /// The type of changes this computer produces.
19    type ChangeSet;
20
21    /// Compute changes between old and new SBOMs given component matches.
22    fn compute(
23        &self,
24        old: &NormalizedSbom,
25        new: &NormalizedSbom,
26        matches: &ComponentMatches,
27    ) -> Self::ChangeSet;
28
29    /// Get the name of this change computer for logging/debugging.
30    fn name(&self) -> &str;
31}
32
33/// Container for component changes (added, removed, modified).
34#[derive(Debug, Clone, Default)]
35pub struct ComponentChangeSet {
36    pub added: Vec<ComponentChange>,
37    pub removed: Vec<ComponentChange>,
38    pub modified: Vec<ComponentChange>,
39}
40
41impl ComponentChangeSet {
42    pub fn new() -> Self {
43        Self::default()
44    }
45
46    pub fn is_empty(&self) -> bool {
47        self.added.is_empty() && self.removed.is_empty() && self.modified.is_empty()
48    }
49
50    pub fn total(&self) -> usize {
51        self.added.len() + self.removed.len() + self.modified.len()
52    }
53}
54
55/// Container for dependency changes (added, removed).
56#[derive(Debug, Clone, Default)]
57pub struct DependencyChangeSet {
58    pub added: Vec<DependencyChange>,
59    pub removed: Vec<DependencyChange>,
60}
61
62impl DependencyChangeSet {
63    pub fn new() -> Self {
64        Self::default()
65    }
66
67    pub fn is_empty(&self) -> bool {
68        self.added.is_empty() && self.removed.is_empty()
69    }
70
71    pub fn total(&self) -> usize {
72        self.added.len() + self.removed.len()
73    }
74}
75
76/// Container for license changes.
77#[derive(Debug, Clone, Default)]
78pub struct LicenseChangeSet {
79    pub new_licenses: Vec<LicenseChange>,
80    pub removed_licenses: Vec<LicenseChange>,
81    pub component_changes: Vec<(String, String, String)>, // (component, old_license, new_license)
82}
83
84impl LicenseChangeSet {
85    pub fn new() -> Self {
86        Self::default()
87    }
88
89    pub fn is_empty(&self) -> bool {
90        self.new_licenses.is_empty()
91            && self.removed_licenses.is_empty()
92            && self.component_changes.is_empty()
93    }
94}
95
96/// Container for vulnerability changes.
97#[derive(Debug, Clone, Default)]
98pub struct VulnerabilityChangeSet {
99    pub introduced: Vec<VulnerabilityDetail>,
100    pub resolved: Vec<VulnerabilityDetail>,
101    pub persistent: Vec<VulnerabilityDetail>,
102}
103
104impl VulnerabilityChangeSet {
105    pub fn new() -> Self {
106        Self::default()
107    }
108
109    pub fn is_empty(&self) -> bool {
110        self.introduced.is_empty() && self.resolved.is_empty()
111    }
112
113    /// Sort vulnerabilities by severity (critical first).
114    pub fn sort_by_severity(&mut self) {
115        let severity_order = |s: &str| match s {
116            "Critical" => 0,
117            "High" => 1,
118            "Medium" => 2,
119            "Low" => 3,
120            _ => 4,
121        };
122
123        self.introduced
124            .sort_by(|a, b| severity_order(&a.severity).cmp(&severity_order(&b.severity)));
125        self.resolved
126            .sort_by(|a, b| severity_order(&a.severity).cmp(&severity_order(&b.severity)));
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn test_component_change_set_empty() {
136        let set = ComponentChangeSet::new();
137        assert!(set.is_empty());
138        assert_eq!(set.total(), 0);
139    }
140
141    #[test]
142    fn test_dependency_change_set_empty() {
143        let set = DependencyChangeSet::new();
144        assert!(set.is_empty());
145        assert_eq!(set.total(), 0);
146    }
147
148    #[test]
149    fn test_license_change_set_empty() {
150        let set = LicenseChangeSet::new();
151        assert!(set.is_empty());
152    }
153
154    #[test]
155    fn test_vulnerability_change_set_empty() {
156        let set = VulnerabilityChangeSet::new();
157        assert!(set.is_empty());
158    }
159}