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::{DependencyEdge, NormalizedSbom};
6use std::collections::HashSet;
7
8type EdgeKey = (String, String, String, Option<String>);
9
10/// Build a normalized edge key, optionally mapping IDs through component matches.
11fn edge_key(edge: &DependencyEdge, matches: Option<&ComponentMatches>) -> EdgeKey {
12    let from = if let Some(m) = matches {
13        m.get(&edge.from)
14            .and_then(|v| v.as_ref())
15            .map_or_else(|| edge.from.to_string(), ToString::to_string)
16    } else {
17        edge.from.to_string()
18    };
19    let to = if let Some(m) = matches {
20        m.get(&edge.to)
21            .and_then(|v| v.as_ref())
22            .map_or_else(|| edge.to.to_string(), ToString::to_string)
23    } else {
24        edge.to.to_string()
25    };
26    (
27        from,
28        to,
29        edge.relationship.to_string(),
30        edge.scope.as_ref().map(ToString::to_string),
31    )
32}
33
34/// Computes dependency-level changes between SBOMs.
35pub struct DependencyChangeComputer;
36
37impl DependencyChangeComputer {
38    /// Create a new dependency change computer.
39    #[must_use]
40    pub const fn new() -> Self {
41        Self
42    }
43}
44
45impl Default for DependencyChangeComputer {
46    fn default() -> Self {
47        Self::new()
48    }
49}
50
51impl ChangeComputer for DependencyChangeComputer {
52    type ChangeSet = DependencyChangeSet;
53
54    fn compute(
55        &self,
56        old: &NormalizedSbom,
57        new: &NormalizedSbom,
58        matches: &ComponentMatches,
59    ) -> DependencyChangeSet {
60        let mut result = DependencyChangeSet::new();
61
62        // Normalize old edges (map through component matches)
63        let normalized_old_edges: HashSet<EdgeKey> = old
64            .edges
65            .iter()
66            .map(|e| edge_key(e, Some(matches)))
67            .collect();
68
69        // Normalize new edges (no match mapping needed)
70        let normalized_new_edges: HashSet<EdgeKey> =
71            new.edges.iter().map(|e| edge_key(e, None)).collect();
72
73        // Find added dependencies (in new but not in old)
74        for edge in &new.edges {
75            let key = edge_key(edge, None);
76            if !normalized_old_edges.contains(&key) {
77                result.added.push(DependencyChange::added(edge));
78            }
79        }
80
81        // Find removed dependencies (in old but not in new)
82        for edge in &old.edges {
83            let key = edge_key(edge, Some(matches));
84            if !normalized_new_edges.contains(&key) {
85                result.removed.push(DependencyChange::removed(edge));
86            }
87        }
88
89        result
90    }
91
92    fn name(&self) -> &'static str {
93        "DependencyChangeComputer"
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn test_dependency_change_computer_default() {
103        let computer = DependencyChangeComputer;
104        assert_eq!(computer.name(), "DependencyChangeComputer");
105    }
106
107    #[test]
108    fn test_empty_sboms() {
109        let computer = DependencyChangeComputer;
110        let old = NormalizedSbom::default();
111        let new = NormalizedSbom::default();
112        let matches = ComponentMatches::new();
113
114        let result = computer.compute(&old, &new, &matches);
115        assert!(result.is_empty());
116    }
117}