Skip to main content

provenant/license_detection/spdx_mapping/
mod.rs

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