use crate::{
error::{ParseError, Reason},
lexer::{Lexer, Token},
ExceptionId, LicenseItem, LicenseReq,
};
use std::fmt;
/// A convenience wrapper for a license and optional exception that can be
/// checked against a license requirement to see if it satisfies the requirement
/// placed by a license holder
///
/// ```
/// let licensee = spdx::Licensee::parse("GPL-2.0").unwrap();
///
/// assert!(licensee.satisfies(&spdx::LicenseReq::from(spdx::license_id("GPL-2.0-only").unwrap())));
/// ```
#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone)]
pub struct Licensee {
inner: LicenseReq,
}
impl fmt::Display for Licensee {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.inner.fmt(f)
}
}
impl Licensee {
/// Creates a licensee from its component parts. Note that use of SPDX's
/// `or_later` is completely ignored for licensees as it only applies
/// to the license holder(s), not the licensee
#[must_use]
pub fn new(license: LicenseItem, exception: Option<ExceptionId>) -> Self {
if let LicenseItem::Spdx { or_later, .. } = &license {
debug_assert!(!or_later);
}
Self {
inner: LicenseReq { license, exception },
}
}
/// Parses an simplified version of an SPDX license expression that can
/// contain at most 1 valid SDPX license with an optional exception joined
/// by a `WITH`.
///
/// ```
/// use spdx::Licensee;
///
/// // Normal single license
/// Licensee::parse("MIT").unwrap();
///
/// // SPDX allows license identifiers outside of the official license list
/// // via the LicenseRef- prefix
/// Licensee::parse("LicenseRef-My-Super-Extra-Special-License").unwrap();
///
/// // License and exception
/// Licensee::parse("Apache-2.0 WITH LLVM-exception").unwrap();
///
/// // `+` is only allowed to be used by license requirements from the license holder
/// Licensee::parse("Apache-2.0+").unwrap_err();
///
/// Licensee::parse("GPL-2.0").unwrap();
///
/// // GNU suffix license (GPL, AGPL, LGPL, GFDL) must not contain the suffix
/// Licensee::parse("GPL-3.0-or-later").unwrap_err();
///
/// // GFDL licenses are only allowed to contain the `invariants` suffix
/// Licensee::parse("GFDL-1.3-invariants").unwrap();
/// ```
pub fn parse(original: &str) -> Result<Self, ParseError> {
let mut lexer = Lexer::new(original);
let license = {
let lt = lexer.next().ok_or_else(|| ParseError {
original: original.to_owned(),
span: 0..original.len(),
reason: Reason::Empty,
})??;
match lt.token {
Token::Spdx(id) => {
// If we have one of the GNU licenses which use the `-only`
// or `-or-later` suffixes return an error rather than
// silently truncating, the `-only` and `-or-later` suffixes
// are for the license holder(s) to specify what license(s)
// they can be licensed under, not for the licensee,
// similarly to the `+`
if id.is_gnu() {
let is_only = original.ends_with("-only");
let or_later = original.ends_with("-or-later");
if is_only || or_later {
return Err(ParseError {
original: original.to_owned(),
span: if is_only {
original.len() - 5..original.len()
} else {
original.len() - 9..original.len()
},
reason: Reason::Unexpected(&["<bare-gnu-license>"]),
});
}
// GFDL has `no-invariants` and `invariants` variants, we
// treat `no-invariants` as invalid, just the same as
// only, it would be the same as a bare GFDL-<version>.
// However, the `invariants`...variant we do allow since
// it is a modifier on the license...and should therefore
// by a WITH exception but GNU licenses are the worst
if original.starts_with("GFDL") && original.contains("-no-invariants") {
return Err(ParseError {
original: original.to_owned(),
span: 8..original.len(),
reason: Reason::Unexpected(&["<bare-gfdl-license>"]),
});
}
}
LicenseItem::Spdx {
id,
or_later: false,
}
}
Token::LicenseRef { doc_ref, lic_ref } => LicenseItem::Other {
doc_ref: doc_ref.map(String::from),
lic_ref: lic_ref.to_owned(),
},
_ => {
return Err(ParseError {
original: original.to_owned(),
span: lt.span,
reason: Reason::Unexpected(&["<license>"]),
})
}
}
};
let exception = match lexer.next() {
None => None,
Some(lt) => {
let lt = lt?;
match lt.token {
Token::With => {
let lt = lexer.next().ok_or(ParseError {
original: original.to_owned(),
span: lt.span,
reason: Reason::Empty,
})??;
match lt.token {
Token::Exception(exc) => Some(exc),
_ => {
return Err(ParseError {
original: original.to_owned(),
span: lt.span,
reason: Reason::Unexpected(&["<exception>"]),
})
}
}
}
_ => {
return Err(ParseError {
original: original.to_owned(),
span: lt.span,
reason: Reason::Unexpected(&["WITH"]),
})
}
}
}
};
Ok(Licensee {
inner: LicenseReq { license, exception },
})
}
/// Determines whether the specified license requirement is satisfied by
/// this license (+exception)
///
/// ```
/// let licensee = spdx::Licensee::parse("Apache-2.0 WITH LLVM-exception").unwrap();
///
/// assert!(licensee.satisfies(&spdx::LicenseReq {
/// license: spdx::LicenseItem::Spdx {
/// id: spdx::license_id("Apache-2.0").unwrap(),
/// // Means the license holder is fine with Apache-2.0 or higher
/// or_later: true,
/// },
/// exception: spdx::exception_id("LLVM-exception"),
/// }));
/// ```
#[must_use]
pub fn satisfies(&self, req: &LicenseReq) -> bool {
match (&self.inner.license, &req.license) {
(LicenseItem::Spdx { id: a, .. }, LicenseItem::Spdx { id: b, or_later }) => {
if a.index != b.index {
if *or_later {
let (a_name, a_gfdl_invariants) = if a.name.starts_with("GFDL") {
a.name
.strip_suffix("-invariants")
.map_or((a.name, false), |name| (name, true))
} else {
(a.name, false)
};
let (b_name, b_gfdl_invariants) = if b.name.starts_with("GFDL") {
b.name
.strip_suffix("-invariants")
.map_or((b.name, false), |name| (name, true))
} else {
(b.name, false)
};
if a_gfdl_invariants != b_gfdl_invariants {
return false;
}
// Many of the SPDX identifiers end with `-<version number>`,
// so chop that off and ensure the base strings match, and if so,
// just a do a lexical compare, if this "allowed license" is >,
// then we satisfed the license requirement
let a_test_name = &a_name[..a_name.rfind('-').unwrap_or(a_name.len())];
let b_test_name = &b_name[..b_name.rfind('-').unwrap_or(b_name.len())];
if a_test_name != b_test_name || a_name < b_name {
return false;
}
} else {
return false;
}
}
}
(
LicenseItem::Other {
doc_ref: doc_a,
lic_ref: lic_a,
},
LicenseItem::Other {
doc_ref: doc_b,
lic_ref: lic_b,
},
) => {
if doc_a != doc_b || lic_a != lic_b {
return false;
}
}
_ => return false,
}
req.exception == self.inner.exception
}
#[must_use]
pub fn into_req(self) -> LicenseReq {
self.inner
}
}
impl PartialOrd<LicenseReq> for Licensee {
#[inline]
fn partial_cmp(&self, o: &LicenseReq) -> Option<std::cmp::Ordering> {
self.inner.partial_cmp(o)
}
}
impl PartialEq<LicenseReq> for Licensee {
#[inline]
fn eq(&self, o: &LicenseReq) -> bool {
self.inner.eq(o)
}
}
impl AsRef<LicenseReq> for Licensee {
#[inline]
fn as_ref(&self) -> &LicenseReq {
&self.inner
}
}
#[cfg(test)]
mod test {
use crate::{exception_id, license_id, LicenseItem, LicenseReq, Licensee};
const LICENSEES: &[&str] = &[
"LicenseRef-Embark-Proprietary",
"BSD-2-Clause",
"Apache-2.0 WITH LLVM-exception",
"BSD-2-Clause-FreeBSD",
"BSL-1.0",
"Zlib",
"CC0-1.0",
"FTL",
"ISC",
"MIT",
"MPL-2.0",
"BSD-3-Clause",
"Unicode-DFS-2016",
"Unlicense",
"Apache-2.0",
];
#[test]
fn handles_or_later() {
let mut licensees: Vec<_> = LICENSEES
.iter()
.map(|l| Licensee::parse(l).unwrap())
.collect();
licensees.sort();
let mpl_id = license_id("MPL-2.0").unwrap();
let req = LicenseReq {
license: LicenseItem::Spdx {
id: mpl_id,
or_later: true,
},
exception: None,
};
// Licensees can't have the `or_later`
assert!(licensees.binary_search_by(|l| l.inner.cmp(&req)).is_err());
match &licensees[licensees
.binary_search_by(|l| l.partial_cmp(&req).unwrap())
.unwrap()]
.inner
.license
{
LicenseItem::Spdx { id, .. } => assert_eq!(*id, mpl_id),
o @ LicenseItem::Other { .. } => panic!("unexpected {:?}", o),
}
}
#[test]
fn handles_exceptions() {
let mut licensees: Vec<_> = LICENSEES
.iter()
.map(|l| Licensee::parse(l).unwrap())
.collect();
licensees.sort();
let apache_id = license_id("Apache-2.0").unwrap();
let llvm_exc = exception_id("LLVM-exception").unwrap();
let req = LicenseReq {
license: LicenseItem::Spdx {
id: apache_id,
or_later: false,
},
exception: Some(llvm_exc),
};
assert_eq!(
&req,
&licensees[licensees
.binary_search_by(|l| l.partial_cmp(&req).unwrap())
.unwrap()]
.inner
);
}
#[test]
fn handles_license_ref() {
let mut licensees: Vec<_> = LICENSEES
.iter()
.map(|l| Licensee::parse(l).unwrap())
.collect();
licensees.sort();
let req = LicenseReq {
license: LicenseItem::Other {
doc_ref: None,
lic_ref: "Embark-Proprietary".to_owned(),
},
exception: None,
};
assert_eq!(
&req,
&licensees[licensees
.binary_search_by(|l| l.partial_cmp(&req).unwrap())
.unwrap()]
.inner
);
}
#[test]
fn handles_close() {
let mut licensees: Vec<_> = LICENSEES
.iter()
.map(|l| Licensee::parse(l).unwrap())
.collect();
licensees.sort();
for id in &["BSD-2-Clause", "BSD-2-Clause-FreeBSD"] {
let lic_id = license_id(id).unwrap();
let req = LicenseReq {
license: LicenseItem::Spdx {
id: lic_id,
or_later: true,
},
exception: None,
};
// Licensees can't have the `or_later`
assert!(licensees.binary_search_by(|l| l.inner.cmp(&req)).is_err());
match &licensees[licensees
.binary_search_by(|l| l.partial_cmp(&req).unwrap())
.unwrap()]
.inner
.license
{
LicenseItem::Spdx { id, .. } => assert_eq!(*id, lic_id),
o @ LicenseItem::Other { .. } => panic!("unexpected {:?}", o),
}
}
}
}