use std::fmt;
const MAX_SPDX_ID_CHARS: usize = 128;
const MAX_LICENSE_ENTRIES_JOINED: usize = 8;
fn is_syntactically_valid_spdx_id(id: &str) -> bool {
!id.is_empty()
&& id.len() <= MAX_SPDX_ID_CHARS
&& id
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '+'))
}
#[must_use]
pub fn filter_valid_spdx_ids(ids: Vec<String>) -> Vec<String> {
ids.into_iter()
.filter(|id| {
let valid = is_syntactically_valid_spdx_id(id);
if !valid {
tracing::warn!(
identifier = %id,
"license policy: dropping invalid SPDX identifier"
);
}
valid
})
.collect()
}
#[non_exhaustive]
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct LicensePolicy {
pub allow: Vec<String>,
pub deny: Vec<String>,
}
impl LicensePolicy {
#[must_use]
pub fn new(allow: Vec<String>, deny: Vec<String>) -> Self {
Self {
allow: filter_valid_spdx_ids(allow),
deny: filter_valid_spdx_ids(deny),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.allow.is_empty() && self.deny.is_empty()
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ViolationReason {
Denied,
NotAllowed,
}
impl fmt::Display for ViolationReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Denied => "denied by policy",
Self::NotAllowed => "not on the allowed license list",
})
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LicenseViolation {
pub license: String,
pub reason: ViolationReason,
}
#[must_use]
pub fn evaluate(license: &[String], policy: &LicensePolicy) -> Option<LicenseViolation> {
if license.is_empty() {
return None;
}
if let Some(denied) = license.iter().find(|l| contains_ci(&policy.deny, l)) {
return Some(LicenseViolation {
license: denied.clone(),
reason: ViolationReason::Denied,
});
}
if !policy.allow.is_empty() && !license.iter().any(|l| contains_ci(&policy.allow, l)) {
return Some(LicenseViolation {
license: join_capped(license, MAX_LICENSE_ENTRIES_JOINED),
reason: ViolationReason::NotAllowed,
});
}
None
}
pub const MAX_POM_LICENSE_NAME_RAW_CHARS: usize = 128;
const KNOWN_POM_LICENSE_NAMES: &[(&str, &[&str])] = &[
("The Apache Software License, Version 2.0", &["Apache-2.0"]),
("The Apache License, Version 2.0", &["Apache-2.0"]),
("Apache License, Version 2.0", &["Apache-2.0"]),
("Apache License Version 2.0", &["Apache-2.0"]),
("Apache License 2.0", &["Apache-2.0"]),
("Apache Software License - Version 2.0", &["Apache-2.0"]),
("Apache 2.0", &["Apache-2.0"]),
("Apache 2", &["Apache-2.0"]),
("Apache-2.0 License", &["Apache-2.0"]),
("Apache-2.0", &["Apache-2.0"]),
("MIT License", &["MIT"]),
("The MIT License", &["MIT"]),
("MIT License (MIT)", &["MIT"]),
("The MIT License (MIT)", &["MIT"]),
("MIT", &["MIT"]),
("BSD 3-Clause License", &["BSD-3-Clause"]),
("The BSD 3-Clause License", &["BSD-3-Clause"]),
("3-Clause BSD License", &["BSD-3-Clause"]),
("BSD 3-Clause", &["BSD-3-Clause"]),
("BSD 3-clause New License", &["BSD-3-Clause"]),
("New BSD License", &["BSD-3-Clause"]),
("The New BSD License", &["BSD-3-Clause"]),
("Eclipse Distribution License - v 1.0", &["BSD-3-Clause"]),
(
"Eclipse Distribution License (New BSD License)",
&["BSD-3-Clause"],
),
("BSD-3-Clause", &["BSD-3-Clause"]),
("BSD 2-Clause License", &["BSD-2-Clause"]),
("2-Clause BSD License", &["BSD-2-Clause"]),
("Simplified BSD License", &["BSD-2-Clause"]),
("BSD-2-Clause", &["BSD-2-Clause"]),
(
"GNU General Public License v2",
&["GPL-2.0", "GPL-2.0-only", "GPL-2.0-or-later"],
),
(
"GNU General Public License v2.0",
&["GPL-2.0", "GPL-2.0-only", "GPL-2.0-or-later"],
),
(
"GNU General Public License, Version 2",
&["GPL-2.0", "GPL-2.0-only", "GPL-2.0-or-later"],
),
(
"GNU General Public License Version 2",
&["GPL-2.0", "GPL-2.0-only", "GPL-2.0-or-later"],
),
("GPLv2", &["GPL-2.0", "GPL-2.0-only", "GPL-2.0-or-later"]),
("GPL v2", &["GPL-2.0", "GPL-2.0-only", "GPL-2.0-or-later"]),
("GPL-2.0", &["GPL-2.0", "GPL-2.0-only", "GPL-2.0-or-later"]),
("GPL2 w/ CPE", &["GPL-2.0-with-classpath-exception"]),
(
"GNU General Public License, version 2 (GPL2), with the classpath exception",
&["GPL-2.0-with-classpath-exception"],
),
(
"GNU General Public License v3",
&["GPL-3.0", "GPL-3.0-only", "GPL-3.0-or-later"],
),
(
"GNU General Public License v3.0",
&["GPL-3.0", "GPL-3.0-only", "GPL-3.0-or-later"],
),
(
"GNU General Public License, Version 3",
&["GPL-3.0", "GPL-3.0-only", "GPL-3.0-or-later"],
),
("GPLv3", &["GPL-3.0", "GPL-3.0-only", "GPL-3.0-or-later"]),
("GPL v3", &["GPL-3.0", "GPL-3.0-only", "GPL-3.0-or-later"]),
("GPL-3.0", &["GPL-3.0", "GPL-3.0-only", "GPL-3.0-or-later"]),
(
"GNU Affero General Public License v3.0",
&["AGPL-3.0", "AGPL-3.0-only", "AGPL-3.0-or-later"],
),
(
"GNU Affero General Public License v3",
&["AGPL-3.0", "AGPL-3.0-only", "AGPL-3.0-or-later"],
),
(
"AGPLv3",
&["AGPL-3.0", "AGPL-3.0-only", "AGPL-3.0-or-later"],
),
(
"AGPL-3.0",
&["AGPL-3.0", "AGPL-3.0-only", "AGPL-3.0-or-later"],
),
(
"GNU Lesser General Public License v2.1",
&["LGPL-2.1", "LGPL-2.1-only", "LGPL-2.1-or-later"],
),
(
"GNU Lesser General Public License, Version 2.1",
&["LGPL-2.1", "LGPL-2.1-only", "LGPL-2.1-or-later"],
),
(
"LGPLv2.1",
&["LGPL-2.1", "LGPL-2.1-only", "LGPL-2.1-or-later"],
),
(
"LGPL 2.1",
&["LGPL-2.1", "LGPL-2.1-only", "LGPL-2.1-or-later"],
),
(
"LGPL-2.1",
&["LGPL-2.1", "LGPL-2.1-only", "LGPL-2.1-or-later"],
),
(
"GNU Lesser General Public License v3",
&["LGPL-3.0", "LGPL-3.0-only", "LGPL-3.0-or-later"],
),
(
"GNU Lesser General Public License v3.0",
&["LGPL-3.0", "LGPL-3.0-only", "LGPL-3.0-or-later"],
),
(
"GNU Lesser General Public License, Version 3",
&["LGPL-3.0", "LGPL-3.0-only", "LGPL-3.0-or-later"],
),
(
"LGPLv3",
&["LGPL-3.0", "LGPL-3.0-only", "LGPL-3.0-or-later"],
),
(
"LGPL 3.0",
&["LGPL-3.0", "LGPL-3.0-only", "LGPL-3.0-or-later"],
),
(
"LGPL-3.0",
&["LGPL-3.0", "LGPL-3.0-only", "LGPL-3.0-or-later"],
),
("Eclipse Public License - v 1.0", &["EPL-1.0"]),
("Eclipse Public License - Version 1.0", &["EPL-1.0"]),
("Eclipse Public License, Version 1.0", &["EPL-1.0"]),
("Eclipse Public License 1.0", &["EPL-1.0"]),
("Eclipse Public License v1.0", &["EPL-1.0"]),
("EPL-1.0", &["EPL-1.0"]),
("Eclipse Public License - v 2.0", &["EPL-2.0"]),
("Eclipse Public License v. 2.0", &["EPL-2.0"]),
("Eclipse Public License v2.0", &["EPL-2.0"]),
("Eclipse Public License 2.0", &["EPL-2.0"]),
("EPL-2.0", &["EPL-2.0"]),
("Mozilla Public License 2.0", &["MPL-2.0"]),
("Mozilla Public License Version 2.0", &["MPL-2.0"]),
("Mozilla Public License, Version 2.0", &["MPL-2.0"]),
("MPL 2.0", &["MPL-2.0"]),
("MPL-2.0", &["MPL-2.0"]),
("Mozilla Public License 1.1", &["MPL-1.1"]),
("MPL 1.1", &["MPL-1.1"]),
("MPL-1.1", &["MPL-1.1"]),
(
"Common Development and Distribution License (CDDL) v1.0",
&["CDDL-1.0"],
),
(
"COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0",
&["CDDL-1.0"],
),
("CDDL 1.0", &["CDDL-1.0"]),
("CDDL 1.1", &["CDDL-1.1"]),
(
"CDDL + GPLv2 with classpath exception",
&["CDDL-1.1", "GPL-2.0-with-classpath-exception"],
),
("ISC License", &["ISC"]),
("ISC", &["ISC"]),
];
fn collapse_whitespace(s: &str) -> String {
s.split_whitespace().collect::<Vec<_>>().join(" ")
}
#[must_use]
pub fn normalize_pom_license_name(raw: &str) -> Vec<String> {
if raw.len() > MAX_POM_LICENSE_NAME_RAW_CHARS {
return Vec::new();
}
let canonical = collapse_whitespace(raw.trim());
if canonical.is_empty() {
return Vec::new();
}
if let Some((_, ids)) = KNOWN_POM_LICENSE_NAMES
.iter()
.find(|(variant, _)| variant.eq_ignore_ascii_case(&canonical))
{
return ids.iter().map(|id| (*id).to_string()).collect();
}
if is_syntactically_valid_spdx_id(&canonical) && canonical.bytes().any(|b| b.is_ascii_digit()) {
return vec![canonical];
}
Vec::new()
}
#[must_use]
pub fn normalize_pom_license_names(raw: &[String]) -> Vec<String> {
normalize_pom_license_names_checked(raw).0
}
#[must_use]
pub fn normalize_pom_license_names_checked(raw: &[String]) -> (Vec<String>, bool) {
let mut normalized = Vec::new();
let mut all_matched = true;
for name in raw {
let ids = normalize_pom_license_name(name);
if ids.is_empty() {
all_matched = false;
tracing::debug!(
license = %name,
"license policy: gradle POM license name did not normalize to a known SPDX identifier"
);
}
for id in ids {
if !normalized.contains(&id) {
normalized.push(id);
}
}
}
(normalized, all_matched)
}
#[must_use]
pub fn resolve_license_entries(
source: crate::LicenseSource,
raw: &[String],
) -> (Vec<String>, bool) {
match source {
crate::LicenseSource::PomFreeText => normalize_pom_license_names_checked(raw),
crate::LicenseSource::RegistryDeclaredSpdx
| crate::LicenseSource::FetchedDeclaredSpdx
| crate::LicenseSource::DetectedSpdx => (raw.to_vec(), true),
}
}
#[must_use]
pub fn resolve_license_entries_for_display(
source: crate::LicenseSource,
raw: &[String],
) -> Vec<String> {
match source {
crate::LicenseSource::PomFreeText => {
let mut resolved = Vec::new();
for name in raw {
let ids = normalize_pom_license_name(name);
let display_ids: Vec<String> = if ids.is_empty() {
vec![name.clone()]
} else if ids_are_synonyms(&ids) {
ids.into_iter().take(1).collect()
} else {
ids
};
for id in display_ids {
if !resolved.contains(&id) {
resolved.push(id);
}
}
}
resolved
}
crate::LicenseSource::RegistryDeclaredSpdx
| crate::LicenseSource::FetchedDeclaredSpdx
| crate::LicenseSource::DetectedSpdx => raw.to_vec(),
}
}
fn ids_are_synonyms(ids: &[String]) -> bool {
match ids.split_first() {
Some((first, rest)) => rest.iter().all(|id| id.starts_with(first.as_str())),
None => true,
}
}
fn contains_ci(haystack: &[String], needle: &str) -> bool {
haystack.iter().any(|h| h.eq_ignore_ascii_case(needle))
}
fn join_capped(licenses: &[String], max_entries: usize) -> String {
let shown = licenses.len().min(max_entries);
let mut joined = licenses.get(..shown).unwrap_or(licenses).join(", ");
let remaining = licenses.len() - shown;
if remaining > 0 {
joined.push_str(&format!(" (+{remaining} more)"));
}
joined
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_identifiers_are_kept() {
let cleaned = filter_valid_spdx_ids(vec![
"MIT".to_string(),
"Apache-2.0".to_string(),
"GPL-3.0-or-later".to_string(),
"LicenseRef-Acme-Custom".to_string(),
]);
assert_eq!(cleaned.len(), 4);
}
#[test]
fn empty_identifier_is_dropped() {
assert!(filter_valid_spdx_ids(vec![String::new()]).is_empty());
}
#[test]
fn whitespace_identifier_is_dropped() {
assert!(filter_valid_spdx_ids(vec!["MIT OR Apache-2.0".to_string()]).is_empty());
}
#[test]
fn overlong_identifier_is_dropped() {
let overlong = "A".repeat(MAX_SPDX_ID_CHARS + 1);
assert!(filter_valid_spdx_ids(vec![overlong]).is_empty());
}
#[test]
fn max_length_identifier_is_kept() {
let exact = "A".repeat(MAX_SPDX_ID_CHARS);
assert_eq!(filter_valid_spdx_ids(vec![exact.clone()]), vec![exact]);
}
#[test]
fn valid_and_invalid_entries_are_partitioned() {
let cleaned = filter_valid_spdx_ids(vec!["MIT".to_string(), "???".to_string()]);
assert_eq!(cleaned, vec!["MIT".to_string()]);
}
#[test]
fn new_drops_invalid_entries_from_both_lists() {
let policy = LicensePolicy::new(
vec!["MIT".to_string(), "bad id!".to_string()],
vec!["GPL-3.0".to_string(), String::new()],
);
assert_eq!(policy.allow, vec!["MIT".to_string()]);
assert_eq!(policy.deny, vec!["GPL-3.0".to_string()]);
}
#[test]
fn default_policy_is_empty() {
assert!(LicensePolicy::default().is_empty());
}
#[test]
fn policy_with_only_allow_is_not_empty() {
assert!(!LicensePolicy::new(vec!["MIT".to_string()], vec![]).is_empty());
}
#[test]
fn policy_with_only_deny_is_not_empty() {
assert!(!LicensePolicy::new(vec![], vec!["GPL-3.0".to_string()]).is_empty());
}
#[test]
fn evaluate_against_empty_policy_never_violates() {
let policy = LicensePolicy::default();
assert!(evaluate(&["GPL-3.0".to_string()], &policy).is_none());
}
#[test]
fn evaluate_unknown_license_never_violates() {
let policy = LicensePolicy::new(vec!["MIT".to_string()], vec!["GPL-3.0".to_string()]);
assert!(evaluate(&[], &policy).is_none());
}
#[test]
fn evaluate_denied_license_violates() {
let policy = LicensePolicy::new(vec![], vec!["GPL-3.0".to_string()]);
let violation = evaluate(&["GPL-3.0".to_string()], &policy).unwrap();
assert_eq!(violation.reason, ViolationReason::Denied);
assert_eq!(violation.license, "GPL-3.0");
}
#[test]
fn evaluate_denied_match_is_case_insensitive() {
let policy = LicensePolicy::new(vec![], vec!["gpl-3.0".to_string()]);
let violation = evaluate(&["GPL-3.0".to_string()], &policy).unwrap();
assert_eq!(violation.reason, ViolationReason::Denied);
}
#[test]
fn evaluate_not_allowed_license_violates() {
let policy = LicensePolicy::new(vec!["MIT".to_string()], vec![]);
let violation = evaluate(&["ISC".to_string()], &policy).unwrap();
assert_eq!(violation.reason, ViolationReason::NotAllowed);
assert_eq!(violation.license, "ISC");
}
#[test]
fn evaluate_allowed_license_is_compliant() {
let policy = LicensePolicy::new(vec!["MIT".to_string()], vec![]);
assert!(evaluate(&["MIT".to_string()], &policy).is_none());
}
#[test]
fn evaluate_allowed_match_is_case_insensitive() {
let policy = LicensePolicy::new(vec!["mit".to_string()], vec![]);
assert!(evaluate(&["MIT".to_string()], &policy).is_none());
}
#[test]
fn evaluate_no_allow_list_means_no_restriction_beyond_deny() {
let policy = LicensePolicy::new(vec![], vec!["GPL-3.0".to_string()]);
assert!(evaluate(&["Anything-Not-Denied".to_string()], &policy).is_none());
}
#[test]
fn evaluate_deny_wins_when_license_matches_both_lists() {
let policy = LicensePolicy::new(vec!["MIT".to_string()], vec!["MIT".to_string()]);
let violation = evaluate(&["MIT".to_string()], &policy).unwrap();
assert_eq!(violation.reason, ViolationReason::Denied);
}
#[test]
fn evaluate_multi_license_denied_if_any_entry_matches_deny() {
let policy = LicensePolicy::new(vec![], vec!["GPL-3.0".to_string()]);
let violation = evaluate(&["MIT".to_string(), "GPL-3.0".to_string()], &policy).unwrap();
assert_eq!(violation.reason, ViolationReason::Denied);
assert_eq!(violation.license, "GPL-3.0");
}
#[test]
fn evaluate_multi_license_allowed_if_any_entry_matches_allow() {
let policy = LicensePolicy::new(vec!["MIT".to_string()], vec![]);
assert!(evaluate(&["MIT".to_string(), "GPL-3.0".to_string()], &policy).is_none());
}
#[test]
fn evaluate_multi_license_not_allowed_if_no_entry_matches_allow() {
let policy = LicensePolicy::new(vec!["MIT".to_string()], vec![]);
let violation =
evaluate(&["GPL-3.0".to_string(), "AGPL-3.0".to_string()], &policy).unwrap();
assert_eq!(violation.reason, ViolationReason::NotAllowed);
assert_eq!(violation.license, "GPL-3.0, AGPL-3.0");
}
fn assert_all_normalize_to(variants: &[&str], expected: &[&str]) {
let expected: Vec<String> = expected.iter().map(|s| (*s).to_string()).collect();
for variant in variants {
assert_eq!(
normalize_pom_license_name(variant),
expected,
"variant: {variant}"
);
}
}
#[test]
fn normalizes_apache_2_0_variants() {
assert_all_normalize_to(
&[
"The Apache Software License, Version 2.0",
"Apache License, Version 2.0",
"Apache License 2.0",
"Apache 2.0",
"Apache-2.0",
],
&["Apache-2.0"],
);
}
#[test]
fn normalizes_mit_variants() {
assert_all_normalize_to(
&[
"MIT License",
"The MIT License",
"The MIT License (MIT)",
"MIT",
],
&["MIT"],
);
}
#[test]
fn normalizes_bsd_3_clause_variants() {
assert_all_normalize_to(
&[
"BSD 3-Clause License",
"3-Clause BSD License",
"BSD-3-Clause",
"Eclipse Distribution License - v 1.0",
],
&["BSD-3-Clause"],
);
}
#[test]
fn normalizes_bsd_2_clause_variants() {
assert_all_normalize_to(
&[
"BSD 2-Clause License",
"Simplified BSD License",
"BSD-2-Clause",
],
&["BSD-2-Clause"],
);
}
#[test]
fn normalizes_gpl_2_0_variants_without_confusing_lgpl() {
assert_all_normalize_to(
&[
"GNU General Public License v2",
"GNU General Public License v2.0",
"GPL-2.0",
"GPLv2",
],
&["GPL-2.0", "GPL-2.0-only", "GPL-2.0-or-later"],
);
assert_all_normalize_to(
&["GNU Lesser General Public License v2.1", "LGPL-2.1"],
&["LGPL-2.1", "LGPL-2.1-only", "LGPL-2.1-or-later"],
);
}
#[test]
fn normalizes_gpl_3_0_variants_without_confusing_lgpl_or_agpl() {
assert_all_normalize_to(
&[
"GNU General Public License v3",
"GNU General Public License v3.0",
"GPL-3.0",
"GPLv3",
],
&["GPL-3.0", "GPL-3.0-only", "GPL-3.0-or-later"],
);
assert_all_normalize_to(
&["GNU Lesser General Public License v3", "LGPL-3.0"],
&["LGPL-3.0", "LGPL-3.0-only", "LGPL-3.0-or-later"],
);
assert_all_normalize_to(
&["GNU Affero General Public License v3", "AGPL-3.0"],
&["AGPL-3.0", "AGPL-3.0-only", "AGPL-3.0-or-later"],
);
}
#[test]
fn normalizes_gpl_2_0_classpath_exception_as_a_distinct_id() {
assert_all_normalize_to(
&["GPL2 w/ CPE", "GPL-2.0-with-classpath-exception"],
&["GPL-2.0-with-classpath-exception"],
);
assert_ne!(
normalize_pom_license_name("GPL2 w/ CPE"),
normalize_pom_license_name("GPLv2")
);
}
#[test]
fn normalizes_epl_variants() {
assert_all_normalize_to(
&[
"Eclipse Public License - v 1.0",
"Eclipse Public License 1.0",
"EPL-1.0",
],
&["EPL-1.0"],
);
assert_all_normalize_to(
&[
"Eclipse Public License - v 2.0",
"Eclipse Public License v2.0",
"Eclipse Public License 2.0",
"EPL-2.0",
],
&["EPL-2.0"],
);
}
#[test]
fn normalizes_mpl_variants() {
assert_all_normalize_to(
&[
"Mozilla Public License 2.0",
"Mozilla Public License Version 2.0",
"MPL-2.0",
],
&["MPL-2.0"],
);
assert_all_normalize_to(&["Mozilla Public License 1.1", "MPL-1.1"], &["MPL-1.1"]);
}
#[test]
fn normalizes_isc_variants() {
assert_all_normalize_to(&["ISC License", "ISC"], &["ISC"]);
}
#[test]
fn normalizes_cddl_and_dual_classpath_license() {
assert_all_normalize_to(
&[
"Common Development and Distribution License (CDDL) v1.0",
"CDDL 1.0",
],
&["CDDL-1.0"],
);
assert_all_normalize_to(&["CDDL 1.1"], &["CDDL-1.1"]);
assert_all_normalize_to(
&["CDDL + GPLv2 with classpath exception"],
&["CDDL-1.1", "GPL-2.0-with-classpath-exception"],
);
}
#[test]
fn normalize_is_case_insensitive_and_whitespace_collapsing() {
assert_eq!(
normalize_pom_license_name("mit license"),
vec!["MIT".to_string()]
);
assert_eq!(
normalize_pom_license_name(" Apache License, Version 2.0 "),
vec!["Apache-2.0".to_string()]
);
}
#[test]
fn already_spdx_shaped_free_text_passes_through() {
for id in [
"SSPL-1.0",
"BUSL-1.1",
"EUPL-1.2",
"GPL-3.0-or-later",
"GPL-2.0-only",
] {
assert_eq!(
normalize_pom_license_name(id),
vec![id.to_string()],
"id: {id}"
);
}
}
#[test]
fn bare_versionless_terms_never_pass_through() {
for term in ["GPL", "BSD", "Apache", "MPL"] {
assert!(normalize_pom_license_name(term).is_empty(), "term: {term}");
}
}
#[test]
fn unrecognized_free_text_fails_closed() {
assert!(normalize_pom_license_name("Some Bespoke Corporate License").is_empty());
assert!(normalize_pom_license_name("").is_empty());
}
#[test]
fn oversized_raw_value_is_rejected_before_collapsing() {
let oversized = format!("{}Apache-2.0", " ".repeat(MAX_POM_LICENSE_NAME_RAW_CHARS));
assert!(oversized.len() > MAX_POM_LICENSE_NAME_RAW_CHARS);
assert!(normalize_pom_license_name(&oversized).is_empty());
}
#[test]
fn normalize_names_drops_only_unmatched_entries() {
let normalized = normalize_pom_license_names(&[
"The Apache Software License, Version 2.0".to_string(),
"Some Bespoke Corporate License".to_string(),
"MIT License".to_string(),
]);
assert_eq!(
normalized,
vec!["Apache-2.0".to_string(), "MIT".to_string()]
);
}
#[test]
fn normalize_names_all_unmatched_yields_empty() {
assert!(normalize_pom_license_names(&["Unknown License Text".to_string()]).is_empty());
}
#[test]
fn normalize_names_deduplicates_overlapping_ids() {
let normalized = normalize_pom_license_names(&["GPL-3.0".to_string(), "GPLv3".to_string()]);
assert_eq!(
normalized,
vec![
"GPL-3.0".to_string(),
"GPL-3.0-only".to_string(),
"GPL-3.0-or-later".to_string()
]
);
}
#[test]
fn normalize_names_checked_reports_full_coverage() {
let (ids, all_matched) = normalize_pom_license_names_checked(&["MIT License".to_string()]);
assert_eq!(ids, vec!["MIT".to_string()]);
assert!(all_matched);
}
#[test]
fn normalize_names_checked_reports_partial_coverage() {
let (ids, all_matched) = normalize_pom_license_names_checked(&[
"MIT License".to_string(),
"Some Bespoke Corporate License".to_string(),
]);
assert_eq!(ids, vec!["MIT".to_string()]);
assert!(!all_matched);
}
#[test]
fn normalize_names_checked_reports_zero_coverage() {
let (ids, all_matched) =
normalize_pom_license_names_checked(&["Some Bespoke Corporate License".to_string()]);
assert!(ids.is_empty());
assert!(!all_matched);
}
#[test]
fn normalize_names_agrees_with_checked_variant() {
let raw = vec!["MIT License".to_string(), "GPLv3".to_string()];
assert_eq!(
normalize_pom_license_names(&raw),
normalize_pom_license_names_checked(&raw).0
);
}
#[test]
fn known_pom_license_names_has_no_duplicate_keys() {
for (i, (key_a, _)) in KNOWN_POM_LICENSE_NAMES.iter().enumerate() {
for (key_b, _) in &KNOWN_POM_LICENSE_NAMES[i + 1..] {
assert!(
!key_a.eq_ignore_ascii_case(key_b),
"duplicate (case-insensitive) table key: {key_a:?}"
);
}
}
}
#[test]
fn known_pom_license_names_synonym_entries_list_base_id_first() {
const DISJUNCTIVE_KEYS: &[&str] = &["CDDL + GPLv2 with classpath exception"];
for &(key, ids) in KNOWN_POM_LICENSE_NAMES {
let ids_owned: Vec<String> = ids.iter().map(|id| (*id).to_string()).collect();
let expected_disjunctive = DISJUNCTIVE_KEYS.contains(&key);
assert_eq!(
ids_are_synonyms(&ids_owned),
!expected_disjunctive,
"{key:?} -> {ids:?}: expected {}, ids_are_synonyms said {}",
if expected_disjunctive {
"disjunctive (base id not first, or a new genuine OR)"
} else {
"synonyms (base id first)"
},
ids_are_synonyms(&ids_owned)
);
}
}
#[test]
fn violation_reason_display_text() {
assert_eq!(ViolationReason::Denied.to_string(), "denied by policy");
assert_eq!(
ViolationReason::NotAllowed.to_string(),
"not on the allowed license list"
);
}
}