Skip to main content

sbom_tools/diff/changes/
dependencies.rs

1//! Dependency change computer implementation.
2
3use crate::diff::DependencyChange;
4use crate::diff::traits::{ChangeComputer, ComponentMatches, DependencyChangeSet};
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        // Edge key includes (from, to, relationship, scope) for full identity comparison
37        type EdgeKey = (String, String, String, Option<String>);
38
39        // Map old edges to their canonical form for comparison
40        let mut normalized_old_edges: HashSet<EdgeKey> = HashSet::new();
41        for edge in &old.edges {
42            let from = matches
43                .get(&edge.from)
44                .and_then(|v| v.as_ref())
45                .map_or_else(|| edge.from.to_string(), std::string::ToString::to_string);
46            let to = matches
47                .get(&edge.to)
48                .and_then(|v| v.as_ref())
49                .map_or_else(|| edge.to.to_string(), std::string::ToString::to_string);
50            normalized_old_edges.insert((
51                from,
52                to,
53                edge.relationship.to_string(),
54                edge.scope.as_ref().map(std::string::ToString::to_string),
55            ));
56        }
57
58        // Find added dependencies
59        for edge in &new.edges {
60            let key: EdgeKey = (
61                edge.from.to_string(),
62                edge.to.to_string(),
63                edge.relationship.to_string(),
64                edge.scope.as_ref().map(std::string::ToString::to_string),
65            );
66            if !normalized_old_edges.contains(&key) {
67                result.added.push(DependencyChange::added(edge));
68            }
69        }
70
71        // Map new edges for comparison with old
72        let mut normalized_new_edges: HashSet<EdgeKey> = HashSet::new();
73        for edge in &new.edges {
74            normalized_new_edges.insert((
75                edge.from.to_string(),
76                edge.to.to_string(),
77                edge.relationship.to_string(),
78                edge.scope.as_ref().map(std::string::ToString::to_string),
79            ));
80        }
81
82        // Find removed dependencies
83        for edge in &old.edges {
84            let from = matches
85                .get(&edge.from)
86                .and_then(|v| v.as_ref())
87                .map_or_else(|| edge.from.to_string(), std::string::ToString::to_string);
88            let to = matches
89                .get(&edge.to)
90                .and_then(|v| v.as_ref())
91                .map_or_else(|| edge.to.to_string(), std::string::ToString::to_string);
92
93            let key: EdgeKey = (
94                from,
95                to,
96                edge.relationship.to_string(),
97                edge.scope.as_ref().map(std::string::ToString::to_string),
98            );
99            if !normalized_new_edges.contains(&key) {
100                result.removed.push(DependencyChange::removed(edge));
101            }
102        }
103
104        result
105    }
106
107    fn name(&self) -> &'static str {
108        "DependencyChangeComputer"
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn test_dependency_change_computer_default() {
118        let computer = DependencyChangeComputer;
119        assert_eq!(computer.name(), "DependencyChangeComputer");
120    }
121
122    #[test]
123    fn test_empty_sboms() {
124        let computer = DependencyChangeComputer;
125        let old = NormalizedSbom::default();
126        let new = NormalizedSbom::default();
127        let matches = ComponentMatches::new();
128
129        let result = computer.compute(&old, &new, &matches);
130        assert!(result.is_empty());
131    }
132}