Skip to main content

provenant/license_detection/spdx_mapping/
mod.rs

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