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    #[must_use]
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    #[must_use]
48    pub fn is_empty(&self) -> bool {
49        self.added.is_empty() && self.removed.is_empty() && self.modified.is_empty()
50    }
51
52    #[must_use]
53    pub fn total(&self) -> usize {
54        self.added.len() + self.removed.len() + self.modified.len()
55    }
56}
57
58/// Container for dependency changes (added, removed).
59#[derive(Debug, Clone, Default)]
60pub struct DependencyChangeSet {
61    pub added: Vec<DependencyChange>,
62    pub removed: Vec<DependencyChange>,
63}
64
65impl DependencyChangeSet {
66    #[must_use]
67    pub fn new() -> Self {
68        Self::default()
69    }
70
71    #[must_use]
72    pub fn is_empty(&self) -> bool {
73        self.added.is_empty() && self.removed.is_empty()
74    }
75
76    #[must_use]
77    pub fn total(&self) -> usize {
78        self.added.len() + self.removed.len()
79    }
80}
81
82/// Container for license changes.
83#[derive(Debug, Clone, Default)]
84pub struct LicenseChangeSet {
85    pub new_licenses: Vec<LicenseChange>,
86    pub removed_licenses: Vec<LicenseChange>,
87    pub component_changes: Vec<(String, String, String)>, // (component, old_license, new_license)
88}
89
90impl LicenseChangeSet {
91    #[must_use]
92    pub fn new() -> Self {
93        Self::default()
94    }
95
96    #[must_use]
97    pub fn is_empty(&self) -> bool {
98        self.new_licenses.is_empty()
99            && self.removed_licenses.is_empty()
100            && self.component_changes.is_empty()
101    }
102}
103
104/// Container for vulnerability changes.
105#[derive(Debug, Clone, Default)]
106pub struct VulnerabilityChangeSet {
107    pub introduced: Vec<VulnerabilityDetail>,
108    pub resolved: Vec<VulnerabilityDetail>,
109    pub persistent: Vec<VulnerabilityDetail>,
110}
111
112impl VulnerabilityChangeSet {
113    #[must_use]
114    pub fn new() -> Self {
115        Self::default()
116    }
117
118    #[must_use]
119    pub fn is_empty(&self) -> bool {
120        self.introduced.is_empty() && self.resolved.is_empty()
121    }
122
123    /// Sort vulnerabilities by severity (critical first).
124    pub fn sort_by_severity(&mut self) {
125        let severity_order = |s: &str| match s {
126            "Critical" => 0,
127            "High" => 1,
128            "Medium" => 2,
129            "Low" => 3,
130            _ => 4,
131        };
132
133        self.introduced
134            .sort_by(|a, b| severity_order(&a.severity).cmp(&severity_order(&b.severity)));
135        self.resolved
136            .sort_by(|a, b| severity_order(&a.severity).cmp(&severity_order(&b.severity)));
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn test_component_change_set_empty() {
146        let set = ComponentChangeSet::new();
147        assert!(set.is_empty());
148        assert_eq!(set.total(), 0);
149    }
150
151    #[test]
152    fn test_dependency_change_set_empty() {
153        let set = DependencyChangeSet::new();
154        assert!(set.is_empty());
155        assert_eq!(set.total(), 0);
156    }
157
158    #[test]
159    fn test_license_change_set_empty() {
160        let set = LicenseChangeSet::new();
161        assert!(set.is_empty());
162    }
163
164    #[test]
165    fn test_vulnerability_change_set_empty() {
166        let set = VulnerabilityChangeSet::new();
167        assert!(set.is_empty());
168    }
169}