Skip to main content

sbom_tools/diff/changes/
licenses.rs

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