sbom_tools/diff/
traits.rs1use super::{
7 ComponentChange, DependencyChange, LicenseChange, VexStatusChange, VulnerabilityDetail,
8};
9use crate::model::{CanonicalId, NormalizedSbom};
10use std::collections::HashMap;
11
12pub type ComponentMatches = HashMap<CanonicalId, Option<CanonicalId>>;
14
15pub trait ChangeComputer: Send + Sync {
20 type ChangeSet;
22
23 fn compute(
25 &self,
26 old: &NormalizedSbom,
27 new: &NormalizedSbom,
28 matches: &ComponentMatches,
29 ) -> Self::ChangeSet;
30
31 fn name(&self) -> &str;
33}
34
35#[derive(Debug, Clone, Default)]
37pub struct ComponentChangeSet {
38 pub added: Vec<ComponentChange>,
39 pub removed: Vec<ComponentChange>,
40 pub modified: Vec<ComponentChange>,
41}
42
43impl ComponentChangeSet {
44 #[must_use]
45 pub fn new() -> Self {
46 Self::default()
47 }
48
49 #[must_use]
50 pub fn is_empty(&self) -> bool {
51 self.added.is_empty() && self.removed.is_empty() && self.modified.is_empty()
52 }
53
54 #[must_use]
55 pub fn total(&self) -> usize {
56 self.added.len() + self.removed.len() + self.modified.len()
57 }
58}
59
60#[derive(Debug, Clone, Default)]
62pub struct DependencyChangeSet {
63 pub added: Vec<DependencyChange>,
64 pub removed: Vec<DependencyChange>,
65}
66
67impl DependencyChangeSet {
68 #[must_use]
69 pub fn new() -> Self {
70 Self::default()
71 }
72
73 #[must_use]
74 pub fn is_empty(&self) -> bool {
75 self.added.is_empty() && self.removed.is_empty()
76 }
77
78 #[must_use]
79 pub fn total(&self) -> usize {
80 self.added.len() + self.removed.len()
81 }
82}
83
84#[derive(Debug, Clone, Default)]
86pub struct LicenseChangeSet {
87 pub new_licenses: Vec<LicenseChange>,
88 pub removed_licenses: Vec<LicenseChange>,
89 pub component_changes: Vec<(String, String, String)>, }
91
92impl LicenseChangeSet {
93 #[must_use]
94 pub fn new() -> Self {
95 Self::default()
96 }
97
98 #[must_use]
99 pub fn is_empty(&self) -> bool {
100 self.new_licenses.is_empty()
101 && self.removed_licenses.is_empty()
102 && self.component_changes.is_empty()
103 }
104}
105
106#[derive(Debug, Clone, Default)]
108pub struct VulnerabilityChangeSet {
109 pub introduced: Vec<VulnerabilityDetail>,
110 pub resolved: Vec<VulnerabilityDetail>,
111 pub persistent: Vec<VulnerabilityDetail>,
112 pub vex_changes: Vec<VexStatusChange>,
114}
115
116impl VulnerabilityChangeSet {
117 #[must_use]
118 pub fn new() -> Self {
119 Self::default()
120 }
121
122 #[must_use]
123 pub fn is_empty(&self) -> bool {
124 self.introduced.is_empty() && self.resolved.is_empty()
125 }
126
127 pub fn sort_by_severity(&mut self) {
129 let severity_order = |s: &str| match s {
130 "Critical" => 0,
131 "High" => 1,
132 "Medium" => 2,
133 "Low" => 3,
134 _ => 4,
135 };
136
137 self.introduced
138 .sort_by(|a, b| severity_order(&a.severity).cmp(&severity_order(&b.severity)));
139 self.resolved
140 .sort_by(|a, b| severity_order(&a.severity).cmp(&severity_order(&b.severity)));
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn test_component_change_set_empty() {
150 let set = ComponentChangeSet::new();
151 assert!(set.is_empty());
152 assert_eq!(set.total(), 0);
153 }
154
155 #[test]
156 fn test_dependency_change_set_empty() {
157 let set = DependencyChangeSet::new();
158 assert!(set.is_empty());
159 assert_eq!(set.total(), 0);
160 }
161
162 #[test]
163 fn test_license_change_set_empty() {
164 let set = LicenseChangeSet::new();
165 assert!(set.is_empty());
166 }
167
168 #[test]
169 fn test_vulnerability_change_set_empty() {
170 let set = VulnerabilityChangeSet::new();
171 assert!(set.is_empty());
172 }
173}