use crate::license_detection::models::LoadedLicense;
const SCANCODE_LICENSEREF_PREFIX: &str = "LicenseRef-scancode-";
const SPDX_CANONICALIZATION_EXEMPT_KEYS: &[&str] = &[
"tgc-spec-license-v2",
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SpdxKeyCanonicalization {
pub license_key: String,
pub previous_spdx_license_key: String,
pub canonical_spdx_license_key: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SpdxKeyCanonicalizationReport {
pub canonicalized_spdx_keys: Vec<SpdxKeyCanonicalization>,
}
impl SpdxKeyCanonicalizationReport {
pub fn is_empty(&self) -> bool {
self.canonicalized_spdx_keys.is_empty()
}
}
pub fn canonicalize_license_spdx_keys(
licenses: &mut [LoadedLicense],
) -> SpdxKeyCanonicalizationReport {
let mut report = SpdxKeyCanonicalizationReport::default();
for license in licenses.iter_mut() {
if SPDX_CANONICALIZATION_EXEMPT_KEYS.contains(&license.key.as_str()) {
continue;
}
let Some(spdx) = license.spdx_license_key.as_deref() else {
continue;
};
if !spdx.starts_with(SCANCODE_LICENSEREF_PREFIX) {
continue;
}
let canonical = format!("{SCANCODE_LICENSEREF_PREFIX}{}", license.key);
if spdx == canonical {
continue;
}
let previous = spdx.to_string();
license
.other_spdx_license_keys
.retain(|k| k != &canonical && k != &previous);
license.other_spdx_license_keys.push(previous.clone());
license.spdx_license_key = Some(canonical.clone());
report
.canonicalized_spdx_keys
.push(SpdxKeyCanonicalization {
license_key: license.key.clone(),
previous_spdx_license_key: previous,
canonical_spdx_license_key: canonical,
});
}
report
}
#[cfg(test)]
mod tests {
use super::*;
fn license(key: &str, spdx: Option<&str>, other: &[&str]) -> LoadedLicense {
LoadedLicense {
key: key.to_string(),
spdx_license_key: spdx.map(str::to_string),
other_spdx_license_keys: other.iter().map(|s| s.to_string()).collect(),
..Default::default()
}
}
#[test]
fn promotes_canonical_form_and_demotes_squashed_primary() {
let mut licenses = vec![license(
"openssl-exception-lgpl-3.0-plus",
Some("LicenseRef-scancode-openssl-exception-lgpl3.0plus"),
&["LicenseRef-scancode-openssl-exception-lgpl-3.0-plus"],
)];
let report = canonicalize_license_spdx_keys(&mut licenses);
assert_eq!(
licenses[0].spdx_license_key.as_deref(),
Some("LicenseRef-scancode-openssl-exception-lgpl-3.0-plus")
);
assert_eq!(
licenses[0].other_spdx_license_keys,
vec!["LicenseRef-scancode-openssl-exception-lgpl3.0plus".to_string()]
);
assert_eq!(report.canonicalized_spdx_keys.len(), 1);
}
#[test]
fn adds_previous_primary_when_canonical_not_already_present() {
let mut licenses = vec![license(
"gradle-enterprise-sla-2022-11-08",
Some("LicenseRef-scancode-gradle-enterprise-sla-2022-11-"),
&[],
)];
canonicalize_license_spdx_keys(&mut licenses);
assert_eq!(
licenses[0].spdx_license_key.as_deref(),
Some("LicenseRef-scancode-gradle-enterprise-sla-2022-11-08")
);
assert_eq!(
licenses[0].other_spdx_license_keys,
vec!["LicenseRef-scancode-gradle-enterprise-sla-2022-11-".to_string()]
);
}
#[test]
fn exempts_key_typo_so_correct_spdx_key_is_not_regressed() {
let mut licenses = vec![license(
"tgc-spec-license-v2",
Some("LicenseRef-scancode-tcg-spec-license-v2"),
&[],
)];
let report = canonicalize_license_spdx_keys(&mut licenses);
assert_eq!(licenses[0].key, "tgc-spec-license-v2");
assert_eq!(
licenses[0].spdx_license_key.as_deref(),
Some("LicenseRef-scancode-tcg-spec-license-v2"),
"correct SPDX key must be preserved, not regressed to the typo"
);
assert!(licenses[0].other_spdx_license_keys.is_empty());
assert!(report.is_empty());
}
#[test]
fn leaves_canonical_and_real_spdx_keys_untouched() {
let mut licenses = vec![
license("mit", Some("MIT"), &[]),
license(
"some-ref",
Some("LicenseRef-scancode-some-ref"),
&["LicenseRef-scancode-old-alias"],
),
license("no-spdx", None, &[]),
];
let report = canonicalize_license_spdx_keys(&mut licenses);
assert!(report.is_empty());
assert_eq!(licenses[0].spdx_license_key.as_deref(), Some("MIT"));
assert_eq!(
licenses[1].other_spdx_license_keys,
vec!["LicenseRef-scancode-old-alias".to_string()]
);
}
}