Skip to main content

sbom_tools/diff/changes/
licenses.rs

1//! License change computer implementation.
2
3use crate::diff::LicenseChange;
4use crate::diff::traits::{ChangeComputer, ComponentMatches, LicenseChangeSet};
5use crate::model::NormalizedSbom;
6use std::collections::HashMap;
7
8/// Computes license-level changes between SBOMs.
9pub struct LicenseChangeComputer;
10
11impl LicenseChangeComputer {
12    /// Create a new license change computer.
13    #[must_use]
14    pub const fn new() -> Self {
15        Self
16    }
17}
18
19impl Default for LicenseChangeComputer {
20    fn default() -> Self {
21        Self::new()
22    }
23}
24
25impl ChangeComputer for LicenseChangeComputer {
26    type ChangeSet = LicenseChangeSet;
27
28    fn compute(
29        &self,
30        old: &NormalizedSbom,
31        new: &NormalizedSbom,
32        _matches: &ComponentMatches,
33    ) -> LicenseChangeSet {
34        let mut result = LicenseChangeSet::new();
35
36        // Collect all licenses from old SBOM
37        let mut old_licenses: HashMap<String, Vec<String>> = HashMap::new();
38        for (_id, comp) in &old.components {
39            for lic in &comp.licenses.declared {
40                old_licenses
41                    .entry(lic.expression.clone())
42                    .or_default()
43                    .push(comp.name.clone());
44            }
45        }
46
47        // Collect all licenses from new SBOM
48        let mut new_licenses: HashMap<String, Vec<String>> = HashMap::new();
49        for (_id, comp) in &new.components {
50            for lic in &comp.licenses.declared {
51                new_licenses
52                    .entry(lic.expression.clone())
53                    .or_default()
54                    .push(comp.name.clone());
55            }
56        }
57
58        // Find new licenses
59        for (license, components) in &new_licenses {
60            if !old_licenses.contains_key(license) {
61                result.new_licenses.push(LicenseChange {
62                    license: license.clone(),
63                    components: components.clone(),
64                    family: "Unknown".to_string(), // Would need license analysis
65                });
66            }
67        }
68
69        // Find removed licenses
70        for (license, components) in &old_licenses {
71            if !new_licenses.contains_key(license) {
72                result.removed_licenses.push(LicenseChange {
73                    license: license.clone(),
74                    components: components.clone(),
75                    family: "Unknown".to_string(),
76                });
77            }
78        }
79
80        result
81    }
82
83    fn name(&self) -> &'static str {
84        "LicenseChangeComputer"
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn test_license_change_computer_default() {
94        let computer = LicenseChangeComputer;
95        assert_eq!(computer.name(), "LicenseChangeComputer");
96    }
97
98    #[test]
99    fn test_empty_sboms() {
100        let computer = LicenseChangeComputer;
101        let old = NormalizedSbom::default();
102        let new = NormalizedSbom::default();
103        let matches = ComponentMatches::new();
104
105        let result = computer.compute(&old, &new, &matches);
106        assert!(result.is_empty());
107    }
108}