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    pub fn new() -> Self {
14        Self
15    }
16}
17
18impl Default for DependencyChangeComputer {
19    fn default() -> Self {
20        Self::new()
21    }
22}
23
24impl ChangeComputer for DependencyChangeComputer {
25    type ChangeSet = DependencyChangeSet;
26
27    fn compute(
28        &self,
29        old: &NormalizedSbom,
30        new: &NormalizedSbom,
31        matches: &ComponentMatches,
32    ) -> DependencyChangeSet {
33        let mut result = DependencyChangeSet::new();
34
35        // Map old edges to their canonical form for comparison
36        let mut normalized_old_edges: HashSet<(String, String)> = HashSet::new();
37        for edge in &old.edges {
38            let from = matches
39                .get(&edge.from)
40                .and_then(|v| v.as_ref())
41                .map(|id| id.to_string())
42                .unwrap_or_else(|| edge.from.to_string());
43            let to = matches
44                .get(&edge.to)
45                .and_then(|v| v.as_ref())
46                .map(|id| id.to_string())
47                .unwrap_or_else(|| edge.to.to_string());
48            normalized_old_edges.insert((from, to));
49        }
50
51        // Find added dependencies
52        for edge in &new.edges {
53            let from = edge.from.to_string();
54            let to = edge.to.to_string();
55            if !normalized_old_edges.contains(&(from, to)) {
56                result.added.push(DependencyChange::added(edge));
57            }
58        }
59
60        // Map new edges for comparison with old
61        let mut normalized_new_edges: HashSet<(String, String)> = HashSet::new();
62        for edge in &new.edges {
63            normalized_new_edges.insert((edge.from.to_string(), edge.to.to_string()));
64        }
65
66        // Find removed dependencies
67        for edge in &old.edges {
68            let from = matches
69                .get(&edge.from)
70                .and_then(|v| v.as_ref())
71                .map(|id| id.to_string())
72                .unwrap_or_else(|| edge.from.to_string());
73            let to = matches
74                .get(&edge.to)
75                .and_then(|v| v.as_ref())
76                .map(|id| id.to_string())
77                .unwrap_or_else(|| edge.to.to_string());
78
79            if !normalized_new_edges.contains(&(from, to)) {
80                result.removed.push(DependencyChange::removed(edge));
81            }
82        }
83
84        result
85    }
86
87    fn name(&self) -> &str {
88        "DependencyChangeComputer"
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn test_dependency_change_computer_default() {
98        let computer = DependencyChangeComputer::default();
99        assert_eq!(computer.name(), "DependencyChangeComputer");
100    }
101
102    #[test]
103    fn test_empty_sboms() {
104        let computer = DependencyChangeComputer::default();
105        let old = NormalizedSbom::default();
106        let new = NormalizedSbom::default();
107        let matches = ComponentMatches::new();
108
109        let result = computer.compute(&old, &new, &matches);
110        assert!(result.is_empty());
111    }
112}