Skip to main content

sbom_tools/diff/changes/
dependencies.rs

1//! Dependency change computer implementation.
2
3use crate::diff::traits::{ChangeComputer, ComponentMatches, DependencyChangeSet};
4use crate::diff::DependencyChange;
5use crate::model::NormalizedSbom;
6use std::collections::HashSet;
7
8/// Computes dependency-level changes between SBOMs.
9pub struct DependencyChangeComputer;
10
11impl DependencyChangeComputer {
12    /// Create a new dependency change computer.
13    #[must_use] 
14    pub const fn new() -> Self {
15        Self
16    }
17}
18
19impl Default for DependencyChangeComputer {
20    fn default() -> Self {
21        Self::new()
22    }
23}
24
25impl ChangeComputer for DependencyChangeComputer {
26    type ChangeSet = DependencyChangeSet;
27
28    fn compute(
29        &self,
30        old: &NormalizedSbom,
31        new: &NormalizedSbom,
32        matches: &ComponentMatches,
33    ) -> DependencyChangeSet {
34        let mut result = DependencyChangeSet::new();
35
36        // Map old edges to their canonical form for comparison
37        let mut normalized_old_edges: HashSet<(String, String)> = HashSet::new();
38        for edge in &old.edges {
39            let from = matches
40                .get(&edge.from)
41                .and_then(|v| v.as_ref()).map_or_else(|| edge.from.to_string(), std::string::ToString::to_string);
42            let to = matches
43                .get(&edge.to)
44                .and_then(|v| v.as_ref()).map_or_else(|| edge.to.to_string(), std::string::ToString::to_string);
45            normalized_old_edges.insert((from, to));
46        }
47
48        // Find added dependencies
49        for edge in &new.edges {
50            let from = edge.from.to_string();
51            let to = edge.to.to_string();
52            if !normalized_old_edges.contains(&(from, to)) {
53                result.added.push(DependencyChange::added(edge));
54            }
55        }
56
57        // Map new edges for comparison with old
58        let mut normalized_new_edges: HashSet<(String, String)> = HashSet::new();
59        for edge in &new.edges {
60            normalized_new_edges.insert((edge.from.to_string(), edge.to.to_string()));
61        }
62
63        // Find removed dependencies
64        for edge in &old.edges {
65            let from = matches
66                .get(&edge.from)
67                .and_then(|v| v.as_ref()).map_or_else(|| edge.from.to_string(), std::string::ToString::to_string);
68            let to = matches
69                .get(&edge.to)
70                .and_then(|v| v.as_ref()).map_or_else(|| edge.to.to_string(), std::string::ToString::to_string);
71
72            if !normalized_new_edges.contains(&(from, to)) {
73                result.removed.push(DependencyChange::removed(edge));
74            }
75        }
76
77        result
78    }
79
80    fn name(&self) -> &'static str {
81        "DependencyChangeComputer"
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn test_dependency_change_computer_default() {
91        let computer = DependencyChangeComputer::default();
92        assert_eq!(computer.name(), "DependencyChangeComputer");
93    }
94
95    #[test]
96    fn test_empty_sboms() {
97        let computer = DependencyChangeComputer::default();
98        let old = NormalizedSbom::default();
99        let new = NormalizedSbom::default();
100        let matches = ComponentMatches::new();
101
102        let result = computer.compute(&old, &new, &matches);
103        assert!(result.is_empty());
104    }
105}