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: if `mit` maps to SPDX `MIT` and `gpl-2.0-plus` has no SPDX key,
86 /// then `mit OR gpl-2.0-plus` becomes
87 /// `LicenseRef-scancode-gpl-2.0-plus OR MIT`.
88 pub fn expression_scancode_to_spdx(&self, scancode_expr: &str) -> Result<String, String> {
89 let parsed = parse_expression(scancode_expr).map_err(|e| format!("Parse error: {}", e))?;
90 let converted = simplify_expression(&self.convert_expression_to_spdx(&parsed));
91 Ok(expression_to_string(&converted))
92 }
93
94 /// Internal function to convert a LicenseExpression from ScanCode to SPDX keys.
95 fn convert_expression_to_spdx(&self, expr: &LicenseExpression) -> LicenseExpression {
96 match expr {
97 LicenseExpression::License(key) => {
98 if let Some(spdx_key) = self.scancode_to_spdx(key) {
99 if spdx_key.starts_with("LicenseRef-") {
100 LicenseExpression::LicenseRef(spdx_key)
101 } else {
102 LicenseExpression::License(spdx_key)
103 }
104 } else {
105 LicenseExpression::LicenseRef(format!("LicenseRef-scancode-{}", key))
106 }
107 }
108 LicenseExpression::LicenseRef(key) => {
109 if let Some(spdx_key) = self.scancode_to_spdx(key) {
110 LicenseExpression::LicenseRef(spdx_key)
111 } else {
112 LicenseExpression::LicenseRef(key.clone())
113 }
114 }
115 LicenseExpression::And { left, right } => LicenseExpression::And {
116 left: Box::new(self.convert_expression_to_spdx(left)),
117 right: Box::new(self.convert_expression_to_spdx(right)),
118 },
119 LicenseExpression::Or { left, right } => LicenseExpression::Or {
120 left: Box::new(self.convert_expression_to_spdx(left)),
121 right: Box::new(self.convert_expression_to_spdx(right)),
122 },
123 LicenseExpression::With { left, right } => LicenseExpression::With {
124 left: Box::new(self.convert_expression_to_spdx(left)),
125 right: Box::new(self.convert_expression_to_spdx(right)),
126 },
127 }
128 }
129}
130
131/// Build an SPDX mapping from a slice of License objects.
132///
133/// Convenience function that creates a new SpdxMapping instance.
134///
135/// # Arguments
136/// * `licenses` - Slice of License objects to build mapping from
137///
138/// # Returns
139/// A SpdxMapping with populated mappings
140pub fn build_spdx_mapping(licenses: &[License]) -> SpdxMapping {
141 SpdxMapping::build_from_licenses(licenses)
142}
143
144#[cfg(test)]
145mod test;