provenant/license_detection/spdx_mapping/mod.rs
1//! SPDX license key mapping for license expressions.
2//!
3//! This module provides mapping between ScanCode license keys and SPDX license
4//! identifiers. It loads the mapping data from License objects and provides
5//! functions to convert license expressions from ScanCode keys to SPDX keys.
6//!
7//! Based on the Python ScanCode Toolkit implementation:
8//! - `build_spdx_license_expression()` in `reference/scancode-toolkit/src/licensedcode/cache.py`
9//! - License.spdx_license_key in `reference/scancode-toolkit/src/licensedcode/models.py`
10
11use std::collections::HashMap;
12
13use crate::license_detection::expression::{
14 LicenseExpression, expression_to_string, parse_expression,
15};
16use crate::license_detection::models::License;
17
18/// Mapping between ScanCode and SPDX license keys.
19///
20/// This structure enables conversion of license expressions from ScanCode-specific
21/// license keys (lowercase, e.g., "mit", "gpl-2.0-plus") to SPDX license identifiers
22/// (case-sensitive, e.g., "MIT", "GPL-2.0-or-later") and vice versa.
23#[derive(Debug, Clone)]
24pub struct SpdxMapping {
25 /// Mapping from ScanCode license key to SPDX license key.
26 ///
27 /// Keys are lowercase ScanCode license keys. Values are SPDX license identifiers.
28 scancode_to_spdx: HashMap<String, String>,
29}
30
31impl SpdxMapping {
32 /// Build an SPDX mapping from a slice of License objects.
33 ///
34 /// This function extracts the `spdx_license_key` field from each License
35 /// and builds the two-way mapping. For licenses without an SPDX equivalent,
36 /// they are mapped to `LicenseRef-scancode-<key>` format.
37 ///
38 /// # Arguments
39 /// * `licenses` - Slice of License objects to build mapping from
40 ///
41 /// # Returns
42 /// A SpdxMapping with populated mappings
43 pub fn build_from_licenses(licenses: &[License]) -> Self {
44 let mut scancode_to_spdx = HashMap::new();
45
46 for license in licenses {
47 let scancode_key = &license.key;
48
49 if let Some(spdx_key) = &license.spdx_license_key {
50 scancode_to_spdx.insert(scancode_key.clone(), spdx_key.clone());
51 } else {
52 let licenseref_key = format!("LicenseRef-scancode-{}", scancode_key);
53 scancode_to_spdx.insert(scancode_key.clone(), licenseref_key.clone());
54 }
55 }
56
57 Self { scancode_to_spdx }
58 }
59
60 /// Convert a ScanCode license key to its SPDX equivalent.
61 ///
62 /// # Arguments
63 /// * `scancode_key` - Lowercase ScanCode license key (e.g., "mit", "gpl-2.0-plus")
64 ///
65 /// # Returns
66 /// Option containing SPDX license identifier, or None if key not found
67 pub fn scancode_to_spdx(&self, scancode_key: &str) -> Option<String> {
68 self.scancode_to_spdx.get(scancode_key).cloned()
69 }
70
71 /// Convert a license expression from ScanCode keys to SPDX keys.
72 ///
73 /// This function parses the expression, replaces each license key with its SPDX
74 /// equivalent, and serializes the result back to a string.
75 ///
76 /// # Arguments
77 /// * `scancode_expr` - License expression string with ScanCode keys
78 ///
79 /// # Returns
80 /// String containing the expression with SPDX keys, or parse error
81 ///
82 /// # Example
83 /// ```
84 /// use provenant::license_detection::spdx_mapping::build_spdx_mapping;
85 /// use provenant::license_detection::models::License;
86 ///
87 /// let licenses = vec![
88 /// License {
89 /// key: "mit".to_string(),
90 /// name: "MIT License".to_string(),
91 /// spdx_license_key: Some("MIT".to_string()),
92 /// ..Default::default()
93 /// },
94 /// License {
95 /// key: "gpl-2.0-plus".to_string(),
96 /// name: "GPL 2.0+".to_string(),
97 /// ..Default::default()
98 /// },
99 /// ];
100 /// let mapping = build_spdx_mapping(&licenses);
101 /// let spdx_expr = mapping.expression_scancode_to_spdx("mit OR gpl-2.0-plus").unwrap();
102 /// assert_eq!(spdx_expr, "MIT OR LicenseRef-scancode-gpl-2.0-plus");
103 /// ```
104 pub fn expression_scancode_to_spdx(&self, scancode_expr: &str) -> Result<String, String> {
105 let parsed = parse_expression(scancode_expr).map_err(|e| format!("Parse error: {}", e))?;
106 let converted = self.convert_expression_to_spdx(&parsed);
107 Ok(expression_to_string(&converted))
108 }
109
110 /// Internal function to convert a LicenseExpression from ScanCode to SPDX keys.
111 fn convert_expression_to_spdx(&self, expr: &LicenseExpression) -> LicenseExpression {
112 match expr {
113 LicenseExpression::License(key) => {
114 if let Some(spdx_key) = self.scancode_to_spdx(key) {
115 if spdx_key.starts_with("LicenseRef-") {
116 LicenseExpression::LicenseRef(spdx_key)
117 } else {
118 LicenseExpression::License(spdx_key)
119 }
120 } else {
121 LicenseExpression::LicenseRef(format!("LicenseRef-scancode-{}", key))
122 }
123 }
124 LicenseExpression::LicenseRef(key) => {
125 if let Some(spdx_key) = self.scancode_to_spdx(key) {
126 LicenseExpression::LicenseRef(spdx_key)
127 } else {
128 LicenseExpression::LicenseRef(key.clone())
129 }
130 }
131 LicenseExpression::And { left, right } => LicenseExpression::And {
132 left: Box::new(self.convert_expression_to_spdx(left)),
133 right: Box::new(self.convert_expression_to_spdx(right)),
134 },
135 LicenseExpression::Or { left, right } => LicenseExpression::Or {
136 left: Box::new(self.convert_expression_to_spdx(left)),
137 right: Box::new(self.convert_expression_to_spdx(right)),
138 },
139 LicenseExpression::With { left, right } => LicenseExpression::With {
140 left: Box::new(self.convert_expression_to_spdx(left)),
141 right: Box::new(self.convert_expression_to_spdx(right)),
142 },
143 }
144 }
145}
146
147/// Build an SPDX mapping from a slice of License objects.
148///
149/// Convenience function that creates a new SpdxMapping instance.
150///
151/// # Arguments
152/// * `licenses` - Slice of License objects to build mapping from
153///
154/// # Returns
155/// A SpdxMapping with populated mappings
156pub fn build_spdx_mapping(licenses: &[License]) -> SpdxMapping {
157 SpdxMapping::build_from_licenses(licenses)
158}
159
160#[cfg(test)]
161mod test;