pub mod error;
pub mod expression;
pub mod identifiers;
pub mod lexer;
mod licensee;
#[cfg(feature = "text")]
pub mod text;
pub use error::ParseError;
pub use expression::Expression;
use identifiers::{IS_COPYLEFT, IS_DEPRECATED, IS_FSF_LIBRE, IS_GNU, IS_OSI_APPROVED};
pub use lexer::ParseMode;
pub use licensee::Licensee;
use std::{
cmp::{self, Ordering},
fmt,
};
#[derive(Copy, Clone, Eq)]
pub struct LicenseId {
pub name: &'static str,
pub full_name: &'static str,
index: usize,
flags: u8,
}
impl PartialEq for LicenseId {
#[inline]
fn eq(&self, o: &Self) -> bool {
self.index == o.index
}
}
impl Ord for LicenseId {
#[inline]
fn cmp(&self, o: &Self) -> Ordering {
self.index.cmp(&o.index)
}
}
impl PartialOrd for LicenseId {
#[inline]
fn partial_cmp(&self, o: &Self) -> Option<Ordering> {
Some(self.cmp(o))
}
}
impl LicenseId {
#[inline]
#[must_use]
pub fn is_fsf_free_libre(self) -> bool {
self.flags & IS_FSF_LIBRE != 0
}
#[inline]
#[must_use]
pub fn is_osi_approved(self) -> bool {
self.flags & IS_OSI_APPROVED != 0
}
#[inline]
#[must_use]
pub fn is_deprecated(self) -> bool {
self.flags & IS_DEPRECATED != 0
}
#[inline]
#[must_use]
pub fn is_copyleft(self) -> bool {
self.flags & IS_COPYLEFT != 0
}
#[inline]
#[must_use]
pub fn is_gnu(self) -> bool {
self.flags & IS_GNU != 0
}
#[cfg(feature = "text")]
#[inline]
pub fn text(self) -> &'static str {
text::LICENSE_TEXTS[self.index].1
}
}
impl fmt::Debug for LicenseId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name)
}
}
#[derive(Copy, Clone, Eq)]
pub struct ExceptionId {
pub name: &'static str,
index: usize,
flags: u8,
}
impl PartialEq for ExceptionId {
#[inline]
fn eq(&self, o: &Self) -> bool {
self.index == o.index
}
}
impl Ord for ExceptionId {
#[inline]
fn cmp(&self, o: &Self) -> Ordering {
self.index.cmp(&o.index)
}
}
impl PartialOrd for ExceptionId {
#[inline]
fn partial_cmp(&self, o: &Self) -> Option<Ordering> {
Some(self.cmp(o))
}
}
impl ExceptionId {
#[inline]
#[must_use]
pub fn is_deprecated(self) -> bool {
self.flags & IS_DEPRECATED != 0
}
#[cfg(feature = "text")]
#[inline]
pub fn text(self) -> &'static str {
text::EXCEPTION_TEXTS[self.index].1
}
}
impl fmt::Debug for ExceptionId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct LicenseReq {
pub license: LicenseItem,
pub exception: Option<ExceptionId>,
}
impl From<LicenseId> for LicenseReq {
fn from(id: LicenseId) -> Self {
let (id, or_later) = if id.is_gnu() {
let (or_later, name) = id
.name
.strip_suffix("-or-later")
.map_or((false, id.name), |name| (true, name));
let root = name.strip_suffix("-only").unwrap_or(name);
(
license_id(root).expect("Unable to find root GNU license"),
or_later,
)
} else {
(id, false)
};
Self {
license: LicenseItem::Spdx { id, or_later },
exception: None,
}
}
}
impl fmt::Display for LicenseReq {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
self.license.fmt(f)?;
if let Some(ref exe) = self.exception {
write!(f, " WITH {}", exe.name)?;
}
Ok(())
}
}
#[derive(Debug, Clone, Eq)]
pub enum LicenseItem {
Spdx {
id: LicenseId,
or_later: bool,
},
Other {
doc_ref: Option<String>,
lic_ref: String,
},
}
impl LicenseItem {
#[must_use]
pub fn id(&self) -> Option<LicenseId> {
match self {
Self::Spdx { id, .. } => Some(*id),
Self::Other { .. } => None,
}
}
}
impl Ord for LicenseItem {
fn cmp(&self, o: &Self) -> Ordering {
match (self, o) {
(
Self::Spdx {
id: a,
or_later: la,
},
Self::Spdx {
id: b,
or_later: lb,
},
) => match a.cmp(b) {
Ordering::Equal => la.cmp(lb),
o => o,
},
(
Self::Other {
doc_ref: ad,
lic_ref: al,
},
Self::Other {
doc_ref: bd,
lic_ref: bl,
},
) => match ad.cmp(bd) {
Ordering::Equal => al.cmp(bl),
o => o,
},
(Self::Spdx { .. }, Self::Other { .. }) => Ordering::Less,
(Self::Other { .. }, Self::Spdx { .. }) => Ordering::Greater,
}
}
}
impl PartialOrd for LicenseItem {
#[allow(clippy::non_canonical_partial_ord_impl)]
fn partial_cmp(&self, o: &Self) -> Option<Ordering> {
match (self, o) {
(Self::Spdx { id: a, .. }, Self::Spdx { id: b, .. }) => a.partial_cmp(b),
(
Self::Other {
doc_ref: ad,
lic_ref: al,
},
Self::Other {
doc_ref: bd,
lic_ref: bl,
},
) => match ad.cmp(bd) {
Ordering::Equal => al.partial_cmp(bl),
o => Some(o),
},
(Self::Spdx { .. }, Self::Other { .. }) => Some(cmp::Ordering::Less),
(Self::Other { .. }, Self::Spdx { .. }) => Some(cmp::Ordering::Greater),
}
}
}
impl PartialEq for LicenseItem {
fn eq(&self, o: &Self) -> bool {
matches!(self.partial_cmp(o), Some(cmp::Ordering::Equal))
}
}
impl fmt::Display for LicenseItem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
match self {
LicenseItem::Spdx { id, or_later } => {
id.name.fmt(f)?;
if *or_later {
if id.is_gnu() && id.is_deprecated() {
f.write_str("-or-later")?;
} else if !id.is_gnu() {
f.write_str("+")?;
}
}
Ok(())
}
LicenseItem::Other {
doc_ref: Some(d),
lic_ref: l,
} => write!(f, "DocumentRef-{d}:LicenseRef-{l}"),
LicenseItem::Other {
doc_ref: None,
lic_ref: l,
} => write!(f, "LicenseRef-{l}"),
}
}
}
#[inline]
#[must_use]
pub fn license_id(name: &str) -> Option<LicenseId> {
let name = name.trim_end_matches('+');
identifiers::LICENSES
.binary_search_by(|lic| lic.0.cmp(name))
.map(|index| {
let (name, full_name, flags) = identifiers::LICENSES[index];
LicenseId {
name,
full_name,
index,
flags,
}
})
.ok()
}
#[inline]
#[must_use]
pub fn imprecise_license_id(name: &str) -> Option<(LicenseId, usize)> {
for (prefix, correct_name) in identifiers::IMPRECISE_NAMES {
if let Some(name_prefix) = name.as_bytes().get(0..prefix.len()) {
if prefix.as_bytes().eq_ignore_ascii_case(name_prefix) {
return license_id(correct_name).map(|lic| (lic, prefix.len()));
}
}
}
None
}
#[inline]
#[must_use]
pub fn exception_id(name: &str) -> Option<ExceptionId> {
identifiers::EXCEPTIONS
.binary_search_by(|exc| exc.0.cmp(name))
.map(|index| {
let (name, flags) = identifiers::EXCEPTIONS[index];
ExceptionId { name, index, flags }
})
.ok()
}
#[inline]
#[must_use]
pub fn license_version() -> &'static str {
identifiers::VERSION
}
#[cfg(test)]
mod test {
use super::LicenseItem;
use crate::{Expression, license_id};
#[test]
fn gnu_or_later_display() {
let gpl_or_later = LicenseItem::Spdx {
id: license_id("GPL-3.0").unwrap(),
or_later: true,
};
let gpl_or_later_in_id = LicenseItem::Spdx {
id: license_id("GPL-3.0-or-later").unwrap(),
or_later: true,
};
let gpl_or_later_parsed = Expression::parse("GPL-3.0-or-later").unwrap();
let non_gnu_or_later = LicenseItem::Spdx {
id: license_id("Apache-2.0").unwrap(),
or_later: true,
};
assert_eq!(gpl_or_later.to_string(), "GPL-3.0-or-later");
assert_eq!(gpl_or_later_parsed.to_string(), "GPL-3.0-or-later");
assert_eq!(gpl_or_later_in_id.to_string(), "GPL-3.0-or-later");
assert_eq!(non_gnu_or_later.to_string(), "Apache-2.0+");
}
}