#![cfg_attr(docsrs, feature(doc_cfg))]
#![deny(missing_docs)]
#![doc = include_str!("../README.md")]
#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
pub mod error;
pub mod expression;
#[allow(missing_docs)]
pub mod identifiers;
pub mod lexer;
mod licensee;
#[cfg(feature = "detection")]
pub mod detection;
#[cfg(feature = "text")]
#[allow(missing_docs)]
pub mod text;
use alloc::{boxed::Box, string::String};
use core::{
cmp::{self, Ordering},
fmt,
};
pub use error::ParseError;
pub use expression::Expression;
pub use lexer::ParseMode;
pub use licensee::Licensee;
pub mod flags {
pub type Type = u8;
pub const IS_FSF_LIBRE: Type = 0x1;
pub const IS_OSI_APPROVED: Type = 0x2;
pub const IS_DEPRECATED: Type = 0x4;
pub const IS_COPYLEFT: Type = 0x8;
pub const IS_GNU: Type = 0x10;
}
pub struct License {
pub name: &'static str,
pub full_name: &'static str,
pub index: usize,
pub flags: flags::Type,
}
#[derive(Copy, Clone)]
pub struct LicenseId {
l: &'static License,
}
impl PartialEq for LicenseId {
#[inline]
fn eq(&self, o: &Self) -> bool {
self.l.index == o.l.index
}
}
impl Eq for LicenseId {}
impl Ord for LicenseId {
#[inline]
fn cmp(&self, o: &Self) -> Ordering {
self.l.index.cmp(&o.l.index)
}
}
impl PartialOrd for LicenseId {
#[inline]
fn partial_cmp(&self, o: &Self) -> Option<Ordering> {
Some(self.cmp(o))
}
}
impl core::ops::Deref for LicenseId {
type Target = License;
#[inline]
fn deref(&self) -> &Self::Target {
self.l
}
}
impl LicenseId {
#[inline]
#[must_use]
pub fn is_fsf_free_libre(self) -> bool {
self.l.flags & flags::IS_FSF_LIBRE != 0
}
#[inline]
#[must_use]
pub fn is_osi_approved(self) -> bool {
self.l.flags & flags::IS_OSI_APPROVED != 0
}
#[inline]
#[must_use]
pub fn is_deprecated(self) -> bool {
self.l.flags & flags::IS_DEPRECATED != 0
}
#[inline]
#[must_use]
pub fn is_copyleft(self) -> bool {
self.l.flags & flags::IS_COPYLEFT != 0
}
#[inline]
#[must_use]
pub fn is_gnu(self) -> bool {
self.l.flags & flags::IS_GNU != 0
}
#[inline]
pub fn version(self) -> Option<&'static str> {
self.l
.name
.split('-')
.find(|comp| comp.chars().all(|c| c == '.' || c.is_ascii_digit()))
}
#[inline]
pub fn base(self) -> &'static str {
self.l.name.split_once('-').map_or(self.l.name, |(n, _)| n)
}
#[cfg(feature = "text")]
#[inline]
pub fn text(self) -> &'static str {
text::LICENSE_TEXTS[self.l.index].1
}
}
impl fmt::Debug for LicenseId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.l.name)
}
}
pub struct Exception {
pub name: &'static str,
pub index: usize,
pub flags: flags::Type,
}
#[derive(Copy, Clone)]
pub struct ExceptionId {
e: &'static Exception,
}
impl PartialEq for ExceptionId {
#[inline]
fn eq(&self, o: &Self) -> bool {
self.e.index == o.e.index
}
}
impl Eq for ExceptionId {}
impl Ord for ExceptionId {
#[inline]
fn cmp(&self, o: &Self) -> Ordering {
self.e.index.cmp(&o.e.index)
}
}
impl PartialOrd for ExceptionId {
#[inline]
fn partial_cmp(&self, o: &Self) -> Option<Ordering> {
Some(self.cmp(o))
}
}
impl core::ops::Deref for ExceptionId {
type Target = Exception;
#[inline]
fn deref(&self) -> &Self::Target {
self.e
}
}
impl ExceptionId {
#[inline]
#[must_use]
pub fn is_deprecated(self) -> bool {
self.e.flags & flags::IS_DEPRECATED != 0
}
#[cfg(feature = "text")]
#[inline]
pub fn text(self) -> &'static str {
text::EXCEPTION_TEXTS[self.e.index].1
}
}
impl fmt::Debug for ExceptionId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.e.name)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct LicenseReq {
pub license: LicenseItem,
pub addition: Option<AdditionItem>,
}
impl From<LicenseId> for LicenseReq {
fn from(id: LicenseId) -> Self {
Self {
license: LicenseItem::Spdx {
id,
or_later: false,
},
addition: 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.addition {
write!(f, " WITH {exe}")?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct LicenseRef {
pub doc_ref: Option<String>,
pub lic_ref: String,
}
impl fmt::Display for LicenseRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
match (&self.doc_ref, &self.lic_ref) {
(Some(d), a) => write!(f, "DocumentRef-{d}:LicenseRef-{a}"),
(None, a) => write!(f, "LicenseRef-{a}"),
}
}
}
#[derive(Debug, Clone)]
pub enum LicenseItem {
Spdx {
id: LicenseId,
or_later: bool,
},
Other(Box<LicenseRef>),
}
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(a), Self::Other(b)) => a.cmp(b),
(Self::Spdx { .. }, Self::Other { .. }) => Ordering::Less,
(Self::Other { .. }, Self::Spdx { .. }) => Ordering::Greater,
}
}
}
#[allow(clippy::non_canonical_partial_ord_impl)]
impl PartialOrd for LicenseItem {
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(a), Self::Other(b)) => a.partial_cmp(b),
(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 Eq for LicenseItem {}
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(refs) => refs.fmt(f),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct AdditionRef {
pub doc_ref: Option<String>,
pub add_ref: String,
}
impl fmt::Display for AdditionRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
match (&self.doc_ref, &self.add_ref) {
(Some(d), a) => write!(f, "DocumentRef-{d}:AdditionRef-{a}"),
(None, a) => write!(f, "AdditionRef-{a}"),
}
}
}
#[derive(Debug, Clone)]
pub enum AdditionItem {
Spdx(ExceptionId),
Other(Box<AdditionRef>),
}
impl AdditionItem {
#[must_use]
pub fn id(&self) -> Option<ExceptionId> {
match self {
Self::Spdx(id) => Some(*id),
Self::Other { .. } => None,
}
}
}
impl Ord for AdditionItem {
fn cmp(&self, o: &Self) -> Ordering {
match (self, o) {
(Self::Spdx(a), Self::Spdx(b)) => match a.cmp(b) {
Ordering::Equal => a.cmp(b),
o => o,
},
(Self::Other(a), Self::Other(b)) => a.cmp(b),
(Self::Spdx(_), Self::Other { .. }) => Ordering::Less,
(Self::Other { .. }, Self::Spdx(_)) => Ordering::Greater,
}
}
}
#[allow(clippy::non_canonical_partial_ord_impl)]
impl PartialOrd for AdditionItem {
fn partial_cmp(&self, o: &Self) -> Option<Ordering> {
match (self, o) {
(Self::Spdx(a), Self::Spdx(b)) => a.partial_cmp(b),
(Self::Other(a), Self::Other(b)) => a.partial_cmp(b),
(Self::Spdx(_), Self::Other { .. }) => Some(cmp::Ordering::Less),
(Self::Other { .. }, Self::Spdx(_)) => Some(cmp::Ordering::Greater),
}
}
}
impl PartialEq for AdditionItem {
fn eq(&self, o: &Self) -> bool {
matches!(self.partial_cmp(o), Some(cmp::Ordering::Equal))
}
}
impl Eq for AdditionItem {}
impl fmt::Display for AdditionItem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
match self {
AdditionItem::Spdx(id) => id.name.fmt(f),
AdditionItem::Other(refs) => refs.fmt(f),
}
}
}
#[inline]
#[must_use]
pub fn license_id(name: &str) -> Option<LicenseId> {
let name = name.trim_end_matches('+');
identifiers::LICENSES
.binary_search_by(|lic| lic.name.cmp(name))
.map(|index| LicenseId {
l: &identifiers::LICENSES[index],
})
.ok()
}
#[inline]
#[must_use]
pub fn gnu_license_id(base: &str, or_later: bool) -> Option<LicenseId> {
if base.ends_with("-only") || base.ends_with("-or-later") {
license_id(base)
} else {
let mut v = smallvec::SmallVec::<[u8; 32]>::new();
v.resize(base.len() + if or_later { 9 } else { 5 }, 0);
v[..base.len()].copy_from_slice(base.as_bytes());
if or_later {
v[base.len()..].copy_from_slice(b"-or-later");
} else {
v[base.len()..].copy_from_slice(b"-only");
}
let Ok(s) = core::str::from_utf8(v.as_slice()) else {
return None;
};
license_id(s)
}
}
#[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.name.cmp(name))
.map(|index| ExceptionId {
e: &identifiers::EXCEPTIONS[index],
})
.ok()
}
#[inline]
#[must_use]
pub fn license_version() -> &'static str {
identifiers::VERSION
}
#[cfg(test)]
mod test {
use super::LicenseItem;
use crate::{Expression, license_id};
use alloc::string::ToString;
#[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+");
}
}