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 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#[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#[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)>, }
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#[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 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}