1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
// SPDX-FileCopyrightText: Provenant contributors
// SPDX-License-Identifier: Apache-2.0
//! Declared-license name aliases.
//!
//! Maps bare / informal license names (e.g. `"Apache"`, `"PSF"`) that are not
//! valid SPDX expressions to a canonical license key, for use **only** when
//! normalizing a package's declared license field. These aliases are never
//! consulted during file-content license detection and are not part of the
//! license index, so they cannot produce file-content false positives.
//!
//! The list — and the justification for every entry — lives in the
//! checked-in config at
//! `resources/license_detection/declared_license_aliases.toml`, intentionally
//! out of the code so the curation stays transparent and reviewable. This
//! module only loads and indexes it.
use std::collections::HashMap;
use std::sync::LazyLock;
use serde::Deserialize;
const DECLARED_LICENSE_ALIASES_TEXT: &str =
include_str!("../../resources/license_detection/declared_license_aliases.toml");
#[derive(Debug, Deserialize)]
struct AliasFile {
#[serde(default)]
alias: Vec<AliasEntry>,
}
#[derive(Debug, Deserialize)]
struct AliasEntry {
names: Vec<String>,
expression: String,
// `reason` is intentionally not deserialized: it is required documentation
// in the TOML (enforced by a test), but the runtime only needs the mapping.
}
/// Lower-cased declared name -> canonical license key.
static DECLARED_LICENSE_ALIASES: LazyLock<HashMap<String, String>> = LazyLock::new(|| {
let parsed: AliasFile = toml::from_str(DECLARED_LICENSE_ALIASES_TEXT)
.expect("declared_license_aliases.toml must be valid TOML");
let mut map = HashMap::new();
for entry in parsed.alias {
for name in entry.names {
map.insert(name.trim().to_ascii_lowercase(), entry.expression.clone());
}
}
map
});
/// Resolve a declared license statement to a canonical license key via the
/// curated alias table, or `None` if it is not an aliased name.
///
/// Matching is case-insensitive on the trimmed statement; only exact bare-name
/// matches resolve (this is not a substring or fuzzy match).
pub(crate) fn declared_license_alias(statement: &str) -> Option<&'static str> {
let key = statement.trim().to_ascii_lowercase();
DECLARED_LICENSE_ALIASES.get(&key).map(String::as_str)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn loads_and_indexes_aliases() {
assert_eq!(declared_license_alias("apache"), Some("apache-2.0"));
assert_eq!(declared_license_alias("Apache"), Some("apache-2.0"));
assert_eq!(declared_license_alias(" APACHE "), Some("apache-2.0"));
assert_eq!(declared_license_alias("boost"), Some("boost-1.0"));
assert_eq!(declared_license_alias("PSF"), Some("python"));
assert_eq!(declared_license_alias("Python"), Some("python"));
}
#[test]
fn unaliased_names_return_none() {
// Deliberately-excluded ambiguous names must not resolve here.
assert_eq!(declared_license_alias("LGPL"), None);
assert_eq!(declared_license_alias("MPL"), None);
assert_eq!(declared_license_alias("EPL"), None);
// Not a bare alias name.
assert_eq!(declared_license_alias("Apache License, Version 2.0"), None);
assert_eq!(declared_license_alias(""), None);
}
#[test]
fn every_alias_entry_has_a_nonempty_reason() {
// Enforce the transparency contract: each curated mapping must justify
// itself. Parsed generically so the runtime struct can ignore `reason`.
let value: toml::Value =
toml::from_str(DECLARED_LICENSE_ALIASES_TEXT).expect("aliases TOML parses");
let aliases = value
.get("alias")
.and_then(toml::Value::as_array)
.expect("at least one [[alias]] entry");
assert!(!aliases.is_empty());
for entry in aliases {
let names = entry.get("names").and_then(toml::Value::as_array);
assert!(
names.is_some_and(|n| !n.is_empty()),
"every alias needs at least one name: {entry:?}"
);
assert!(
entry
.get("expression")
.and_then(toml::Value::as_str)
.is_some(),
"every alias needs an expression: {entry:?}"
);
let reason = entry.get("reason").and_then(toml::Value::as_str);
assert!(
reason.is_some_and(|r| r.trim().len() >= 20),
"every alias needs a substantive `reason`: {entry:?}"
);
}
}
#[test]
fn every_alias_expression_resolves_in_the_license_index() {
// Enforce the schema contract documented in the TOML: each `expression`
// must be a real license key the index can resolve, so a typo such as
// "apache-2" instead of "apache-2.0" fails loudly here.
for expression in DECLARED_LICENSE_ALIASES.values() {
assert!(
crate::parsers::license_normalization::normalize_declared_license_key(expression)
.is_some(),
"alias expression {expression:?} does not resolve in the license index"
);
}
}
}