use std::collections::HashMap;
use crate::models::LicenseDetection;
pub(super) fn spdx_expression_mirroring_key(
key_expression: &str,
detections: &[LicenseDetection],
) -> Option<String> {
let (_, token_to_spdx) = detection_token_maps(detections);
render_license_expression(key_expression, &token_to_spdx, true)
}
pub(super) fn detection_token_maps(
detections: &[LicenseDetection],
) -> (HashMap<String, String>, HashMap<String, String>) {
let mut token_to_key: HashMap<String, String> = HashMap::new();
let mut token_to_spdx: HashMap<String, String> = HashMap::new();
for detection in detections {
let pairs = std::iter::once((
detection.license_expression.as_str(),
detection.license_expression_spdx.as_str(),
))
.chain(detection.matches.iter().map(|match_item| {
(
match_item.license_expression.as_str(),
match_item.license_expression_spdx.as_str(),
)
}));
for (key_expression, spdx_expression) in pairs {
let keys = license_tokens(key_expression);
let spdxes = license_tokens(spdx_expression);
if keys.len() != spdxes.len() {
continue;
}
for (key, spdx) in keys.into_iter().zip(spdxes) {
if key.is_empty() || spdx.is_empty() {
continue;
}
token_to_key
.entry(key.to_ascii_lowercase())
.or_insert_with(|| key.to_string());
token_to_key
.entry(spdx.to_ascii_lowercase())
.or_insert_with(|| key.to_string());
token_to_spdx
.entry(key.to_ascii_lowercase())
.or_insert_with(|| spdx.to_string());
token_to_spdx
.entry(spdx.to_ascii_lowercase())
.or_insert_with(|| spdx.to_string());
}
}
}
(token_to_key, token_to_spdx)
}
pub(super) fn render_license_expression(
expression: &str,
token_map: &HashMap<String, String>,
strict: bool,
) -> Option<String> {
let mut rendered = String::with_capacity(expression.len());
for token in tokenize_license_expression(expression) {
match token {
ExpressionToken::Operator(text) => rendered.push_str(text),
ExpressionToken::License(key) => match token_map.get(&key.to_ascii_lowercase()) {
Some(mapped) => rendered.push_str(mapped),
None if strict => return None,
None => rendered.push_str(key),
},
}
}
Some(rendered)
}
enum ExpressionToken<'a> {
Operator(&'a str),
License(&'a str),
}
fn tokenize_license_expression(expression: &str) -> Vec<ExpressionToken<'_>> {
let is_license_char = |c: char| c.is_alphanumeric() || matches!(c, '-' | '.' | '_' | '+' | ':');
let mut tokens = Vec::new();
let mut rest = expression;
while !rest.is_empty() {
let boundary = rest.find(is_license_char).unwrap_or(rest.len());
if boundary > 0 {
tokens.push(ExpressionToken::Operator(&rest[..boundary]));
rest = &rest[boundary..];
continue;
}
let end = rest
.find(|c: char| !is_license_char(c))
.unwrap_or(rest.len());
let word = &rest[..end];
if is_expression_operator(word) {
tokens.push(ExpressionToken::Operator(word));
} else {
tokens.push(ExpressionToken::License(word));
}
rest = &rest[end..];
}
tokens
}
fn is_expression_operator(word: &str) -> bool {
matches!(word.to_ascii_uppercase().as_str(), "AND" | "OR" | "WITH")
}
fn license_tokens(expression: &str) -> Vec<&str> {
tokenize_license_expression(expression)
.into_iter()
.filter_map(|token| match token {
ExpressionToken::License(license) => Some(license),
ExpressionToken::Operator(_) => None,
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn detection(key: &str, spdx: &str) -> LicenseDetection {
LicenseDetection {
license_expression: key.to_string(),
license_expression_spdx: spdx.to_string(),
matches: Vec::new(),
detection_log: Vec::new(),
identifier: String::new(),
}
}
#[test]
fn mirrors_or_choice_without_collapsing_to_and() {
let detections = vec![detection("mit OR apache-2.0", "MIT OR Apache-2.0")];
assert_eq!(
spdx_expression_mirroring_key("mit OR apache-2.0", &detections).as_deref(),
Some("MIT OR Apache-2.0"),
);
}
#[test]
fn mirrors_key_structure_when_combined_from_separate_detections() {
let detections = vec![
detection("mit OR apache-2.0", "MIT OR Apache-2.0"),
detection("bsd-new", "BSD-3-Clause"),
];
assert_eq!(
spdx_expression_mirroring_key("(mit OR apache-2.0) AND bsd-new", &detections)
.as_deref(),
Some("(MIT OR Apache-2.0) AND BSD-3-Clause"),
);
}
#[test]
fn yields_absent_field_rather_than_leaking_key_form_text() {
let detections = vec![
detection("mit", "MIT"),
detection("proprietary-license", ""),
];
assert_eq!(
spdx_expression_mirroring_key("mit AND proprietary-license", &detections),
None,
);
}
#[test]
fn resolves_a_key_already_spelled_in_spdx_form() {
let detections = vec![detection("bsl-1.1 AND mpl-2.0", "BUSL-1.1 AND MPL-2.0")];
assert_eq!(
spdx_expression_mirroring_key("BUSL-1.1 AND MPL-2.0", &detections).as_deref(),
Some("BUSL-1.1 AND MPL-2.0"),
);
}
}