sbom_tools/diff/
traits.rs1use super::{ComponentChange, DependencyChange, LicenseChange, VulnerabilityDetail};
7use crate::model::{CanonicalId, NormalizedSbom};
8use std::collections::HashMap;
9
10pub type ComponentMatches = HashMap<CanonicalId, Option<CanonicalId>>;
12
13pub trait ChangeComputer: Send + Sync {
18 type ChangeSet;
20
21 fn compute(
23 &self,
24 old: &NormalizedSbom,
25 new: &NormalizedSbom,
26 matches: &ComponentMatches,
27 ) -> Self::ChangeSet;
28
29 fn name(&self) -> &str;
31}
32
33#[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#[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#[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)>, }
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#[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 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}