use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum SpdxLicense {
MIT,
Apache2,
BSD2Clause,
BSD3Clause,
ISC,
Unlicense,
CC0,
WTFPL,
Zlib,
LGPL21,
LGPL21Plus,
LGPL3,
LGPL3Plus,
MPL2,
EPL1,
EPL2,
GPL2,
GPL2Plus,
GPL3,
GPL3Plus,
AGPL3,
AGPL3Plus,
Proprietary,
Unknown(String),
Custom(String),
}
impl SpdxLicense {
pub fn parse_license(s: &str) -> Self {
let normalized = s.trim().to_uppercase();
match normalized.as_str() {
"MIT" => SpdxLicense::MIT,
"APACHE-2.0" | "APACHE 2.0" | "APACHE2" => SpdxLicense::Apache2,
"BSD-2-CLAUSE" | "BSD 2-CLAUSE" => SpdxLicense::BSD2Clause,
"BSD-3-CLAUSE" | "BSD 3-CLAUSE" => SpdxLicense::BSD3Clause,
"ISC" => SpdxLicense::ISC,
"UNLICENSE" | "THE UNLICENSE" => SpdxLicense::Unlicense,
"CC0-1.0" | "CC0" => SpdxLicense::CC0,
"WTFPL" => SpdxLicense::WTFPL,
"ZLIB" => SpdxLicense::Zlib,
"LGPL-2.1" | "LGPL-2.1-ONLY" => SpdxLicense::LGPL21,
"LGPL-2.1+" | "LGPL-2.1-OR-LATER" => SpdxLicense::LGPL21Plus,
"LGPL-3.0" | "LGPL-3.0-ONLY" => SpdxLicense::LGPL3,
"LGPL-3.0+" | "LGPL-3.0-OR-LATER" => SpdxLicense::LGPL3Plus,
"MPL-2.0" => SpdxLicense::MPL2,
"EPL-1.0" => SpdxLicense::EPL1,
"EPL-2.0" => SpdxLicense::EPL2,
"GPL-2.0" | "GPL-2.0-ONLY" => SpdxLicense::GPL2,
"GPL-2.0+" | "GPL-2.0-OR-LATER" => SpdxLicense::GPL2Plus,
"GPL-3.0" | "GPL-3.0-ONLY" => SpdxLicense::GPL3,
"GPL-3.0+" | "GPL-3.0-OR-LATER" => SpdxLicense::GPL3Plus,
"AGPL-3.0" | "AGPL-3.0-ONLY" => SpdxLicense::AGPL3,
"AGPL-3.0+" | "AGPL-3.0-OR-LATER" => SpdxLicense::AGPL3Plus,
"PROPRIETARY" | "COMMERCIAL" => SpdxLicense::Proprietary,
_ => SpdxLicense::Unknown(s.to_string()),
}
}
pub fn to_spdx(&self) -> &str {
match self {
SpdxLicense::MIT => "MIT",
SpdxLicense::Apache2 => "Apache-2.0",
SpdxLicense::BSD2Clause => "BSD-2-Clause",
SpdxLicense::BSD3Clause => "BSD-3-Clause",
SpdxLicense::ISC => "ISC",
SpdxLicense::Unlicense => "Unlicense",
SpdxLicense::CC0 => "CC0-1.0",
SpdxLicense::WTFPL => "WTFPL",
SpdxLicense::Zlib => "Zlib",
SpdxLicense::LGPL21 => "LGPL-2.1-only",
SpdxLicense::LGPL21Plus => "LGPL-2.1-or-later",
SpdxLicense::LGPL3 => "LGPL-3.0-only",
SpdxLicense::LGPL3Plus => "LGPL-3.0-or-later",
SpdxLicense::MPL2 => "MPL-2.0",
SpdxLicense::EPL1 => "EPL-1.0",
SpdxLicense::EPL2 => "EPL-2.0",
SpdxLicense::GPL2 => "GPL-2.0-only",
SpdxLicense::GPL2Plus => "GPL-2.0-or-later",
SpdxLicense::GPL3 => "GPL-3.0-only",
SpdxLicense::GPL3Plus => "GPL-3.0-or-later",
SpdxLicense::AGPL3 => "AGPL-3.0-only",
SpdxLicense::AGPL3Plus => "AGPL-3.0-or-later",
SpdxLicense::Proprietary => "Proprietary",
SpdxLicense::Unknown(s) => s,
SpdxLicense::Custom(s) => s,
}
}
pub fn is_permissive(&self) -> bool {
matches!(
self,
SpdxLicense::MIT
| SpdxLicense::Apache2
| SpdxLicense::BSD2Clause
| SpdxLicense::BSD3Clause
| SpdxLicense::ISC
| SpdxLicense::Unlicense
| SpdxLicense::CC0
| SpdxLicense::WTFPL
| SpdxLicense::Zlib
)
}
pub fn is_copyleft(&self) -> bool {
matches!(
self,
SpdxLicense::GPL2
| SpdxLicense::GPL2Plus
| SpdxLicense::GPL3
| SpdxLicense::GPL3Plus
| SpdxLicense::AGPL3
| SpdxLicense::AGPL3Plus
)
}
pub fn is_weak_copyleft(&self) -> bool {
matches!(
self,
SpdxLicense::LGPL21
| SpdxLicense::LGPL21Plus
| SpdxLicense::LGPL3
| SpdxLicense::LGPL3Plus
| SpdxLicense::MPL2
| SpdxLicense::EPL1
| SpdxLicense::EPL2
)
}
pub fn matches_pattern(&self, pattern: &str) -> bool {
let pattern_upper = pattern.to_uppercase();
let spdx = self.to_spdx().to_uppercase();
if pattern_upper.ends_with('*') {
let prefix = &pattern_upper[..pattern_upper.len() - 1];
spdx.starts_with(prefix)
} else if pattern_upper.contains('*') {
let parts: Vec<&str> = pattern_upper.split('*').collect();
if parts.len() == 2 {
spdx.starts_with(parts[0]) && spdx.ends_with(parts[1])
} else {
spdx == pattern_upper
}
} else {
spdx == pattern_upper
}
}
}
impl fmt::Display for SpdxLicense {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_spdx())
}
}
impl Default for SpdxLicense {
fn default() -> Self {
SpdxLicense::Unknown("Unknown".to_string())
}
}
#[derive(Debug, Clone)]
pub enum SpdxExpression {
License(SpdxLicense),
Or(Box<SpdxExpression>, Box<SpdxExpression>),
And(Box<SpdxExpression>, Box<SpdxExpression>),
With(Box<SpdxExpression>, String), }
impl SpdxExpression {
pub fn all_licenses(&self) -> Vec<&SpdxLicense> {
match self {
SpdxExpression::License(l) => vec![l],
SpdxExpression::Or(a, b) | SpdxExpression::And(a, b) => {
let mut result = a.all_licenses();
result.extend(b.all_licenses());
result
}
SpdxExpression::With(expr, _) => expr.all_licenses(),
}
}
pub fn any_matches(&self, pattern: &str) -> bool {
self.all_licenses()
.iter()
.any(|l| l.matches_pattern(pattern))
}
}
pub fn parse_spdx_expression(s: &str) -> SpdxExpression {
let s = s.trim();
if let Some(with_pos) = s.to_uppercase().find(" WITH ") {
let license_part = &s[..with_pos];
let exception = &s[with_pos + 6..];
return SpdxExpression::With(
Box::new(parse_spdx_expression(license_part)),
exception.trim().to_string(),
);
}
if let Some(or_pos) = s.to_uppercase().find(" OR ") {
let left = &s[..or_pos];
let right = &s[or_pos + 4..];
return SpdxExpression::Or(
Box::new(parse_spdx_expression(left)),
Box::new(parse_spdx_expression(right)),
);
}
if let Some(and_pos) = s.to_uppercase().find(" AND ") {
let left = &s[..and_pos];
let right = &s[and_pos + 5..];
return SpdxExpression::And(
Box::new(parse_spdx_expression(left)),
Box::new(parse_spdx_expression(right)),
);
}
let s = s.trim_start_matches('(').trim_end_matches(')').trim();
SpdxExpression::License(SpdxLicense::parse_license(s))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_spdx_from_str() {
assert_eq!(SpdxLicense::parse_license("MIT"), SpdxLicense::MIT);
assert_eq!(SpdxLicense::parse_license("Apache-2.0"), SpdxLicense::Apache2);
assert_eq!(SpdxLicense::parse_license("GPL-3.0"), SpdxLicense::GPL3);
}
#[test]
fn test_spdx_is_permissive() {
assert!(SpdxLicense::MIT.is_permissive());
assert!(SpdxLicense::Apache2.is_permissive());
assert!(!SpdxLicense::GPL3.is_permissive());
}
#[test]
fn test_spdx_is_copyleft() {
assert!(SpdxLicense::GPL3.is_copyleft());
assert!(SpdxLicense::AGPL3.is_copyleft());
assert!(!SpdxLicense::MIT.is_copyleft());
}
#[test]
fn test_pattern_matching() {
assert!(SpdxLicense::GPL3.matches_pattern("GPL-*"));
assert!(SpdxLicense::LGPL3.matches_pattern("LGPL-*"));
assert!(!SpdxLicense::MIT.matches_pattern("GPL-*"));
}
#[test]
fn test_parse_expression() {
let expr = parse_spdx_expression("MIT OR Apache-2.0");
let licenses = expr.all_licenses();
assert_eq!(licenses.len(), 2);
let expr = parse_spdx_expression("MIT AND BSD-3-Clause");
let licenses = expr.all_licenses();
assert_eq!(licenses.len(), 2);
}
}