blob: d6b6a416a1d3ab7116902f2a93b4292142e30672 [file] [log] [blame] [edit]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![deny(missing_docs)]
#![doc = include_str!("../README.md")]
#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
/// Error types
pub mod error;
pub mod expression;
/// Auto-generated lists of license identifiers and exception identifiers
#[allow(missing_docs)]
pub mod identifiers;
/// Contains types for lexing an SPDX license expression
pub mod lexer;
mod licensee;
/// Allows analysis of text to determine if it resembles a license
#[cfg(feature = "detection")]
pub mod detection;
/// Auto-generated full canonical text of each license
#[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;
/// Flags that can apply to licenses and/or license exceptions
pub mod flags {
/// Inner type of the flags
pub type Type = u8;
/// Whether the license is listed as free by the [Free Software Foundation](https://www.gnu.org/licenses/license-list.en.html)
pub const IS_FSF_LIBRE: Type = 0x1;
/// Whether the license complies with the Open Source Definition as determined by the [Open Source Initiative](https://opensource.org/licenses)
pub const IS_OSI_APPROVED: Type = 0x2;
/// Whether the license or exception has been deprecated and should no longer be used
pub const IS_DEPRECATED: Type = 0x4;
/// Whether the license is considered copyleft
pub const IS_COPYLEFT: Type = 0x8;
/// Whether the license is a GNU license
pub const IS_GNU: Type = 0x10;
}
/// An SPDX license
pub struct License {
/// The short identifier for the license
pub name: &'static str,
/// The full name of the license
pub full_name: &'static str,
/// The index in the full license list where this license is positioned
pub index: usize,
/// The flags for this license
pub flags: flags::Type,
}
/// Unique identifier for a particular license
///
/// ```
/// let bsd = spdx::license_id("BSD-3-Clause").unwrap();
///
/// assert!(
/// bsd.is_fsf_free_libre()
/// && bsd.is_osi_approved()
/// && !bsd.is_deprecated()
/// && !bsd.is_copyleft()
/// );
/// ```
#[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 {
/// Returns true if the license is [considered free by the FSF](https://www.gnu.org/licenses/license-list.en.html)
///
/// ```
/// assert!(spdx::license_id("GPL-2.0-only").unwrap().is_fsf_free_libre());
/// ```
#[inline]
#[must_use]
pub fn is_fsf_free_libre(self) -> bool {
self.l.flags & flags::IS_FSF_LIBRE != 0
}
/// Returns true if the license is [OSI approved](https://opensource.org/licenses)
///
/// ```
/// assert!(spdx::license_id("MIT").unwrap().is_osi_approved());
/// ```
#[inline]
#[must_use]
pub fn is_osi_approved(self) -> bool {
self.l.flags & flags::IS_OSI_APPROVED != 0
}
/// Returns true if the license is deprecated
///
/// ```
/// assert!(spdx::license_id("wxWindows").unwrap().is_deprecated());
/// ```
#[inline]
#[must_use]
pub fn is_deprecated(self) -> bool {
self.l.flags & flags::IS_DEPRECATED != 0
}
/// Returns true if the license is [copyleft](https://en.wikipedia.org/wiki/Copyleft)
///
/// ```
/// assert!(spdx::license_id("LGPL-3.0-or-later").unwrap().is_copyleft());
/// ```
#[inline]
#[must_use]
pub fn is_copyleft(self) -> bool {
self.l.flags & flags::IS_COPYLEFT != 0
}
/// Returns true if the license is a [GNU license](https://www.gnu.org/licenses/identify-licenses-clearly.html),
/// which operate differently than all other SPDX license identifiers
///
/// ```
/// assert!(spdx::license_id("AGPL-3.0-only").unwrap().is_gnu());
/// ```
#[inline]
#[must_use]
pub fn is_gnu(self) -> bool {
self.l.flags & flags::IS_GNU != 0
}
/// Retrieves the version of the license ID, if any
///
/// ```
/// assert_eq!(spdx::license_id("GPL-2.0-only").unwrap().version().unwrap(), "2.0");
/// assert_eq!(spdx::license_id("BSD-3-Clause").unwrap().version().unwrap(), "3");
/// assert!(spdx::license_id("Aladdin").unwrap().version().is_none());
/// ```
#[inline]
pub fn version(self) -> Option<&'static str> {
self.l
.name
.split('-')
.find(|comp| comp.chars().all(|c| c == '.' || c.is_ascii_digit()))
}
/// The base name of the license
///
/// ```
/// assert_eq!(spdx::license_id("GPL-2.0-only").unwrap().base(), "GPL");
/// assert_eq!(spdx::license_id("MIT").unwrap().base(), "MIT");
/// ```
#[inline]
pub fn base(self) -> &'static str {
self.l.name.split_once('-').map_or(self.l.name, |(n, _)| n)
}
/// Attempts to retrieve the license text
///
/// ```
/// assert!(spdx::license_id("GFDL-1.3-invariants").unwrap().text().contains("Invariant Sections"))
/// ```
#[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)
}
}
/// An SPDX exception
pub struct Exception {
/// The name of the exception
pub name: &'static str,
/// The index in the full exception list where this exception is positioned
pub index: usize,
/// The flags for the exception
pub flags: flags::Type,
}
/// Unique identifier for a particular exception
///
/// ```
/// let exception_id = spdx::exception_id("LLVM-exception").unwrap();
/// assert!(!exception_id.is_deprecated());
/// ```
#[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 {
/// Returns true if the exception is deprecated
///
/// ```
/// assert!(spdx::exception_id("Nokia-Qt-exception-1.1").unwrap().is_deprecated());
/// ```
#[inline]
#[must_use]
pub fn is_deprecated(self) -> bool {
self.e.flags & flags::IS_DEPRECATED != 0
}
/// Attempts to retrieve the license exception text
///
/// ```
/// assert!(spdx::exception_id("LLVM-exception").unwrap().text().contains("LLVM Exceptions to the Apache 2.0 License"));
/// ```
#[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)
}
}
/// Represents a single license requirement.
///
/// The requirement must include a valid [`LicenseItem`], and may allow current
/// and future versions of the license, and may also allow for a specific exception
///
/// While they can be constructed manually, most of the time these will
/// be parsed and combined in an [Expression]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct LicenseReq {
/// The license
pub license: LicenseItem,
/// The additional text for this license, as specified following
/// the `WITH` operator
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(())
}
}
/// SPDX allows the use of `LicenseRef-<user supplied string>` to provide
/// arbitrary licenses that aren't a part of the official SPDX license list
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct LicenseRef {
/// Identify any external SPDX documents referenced within this SPDX document.
///
/// See the [spec](https://spdx.org/spdx-specification-21-web-version#h.h430e9ypa0j9) for
/// more details.
pub doc_ref: Option<String>,
/// Provide a locally unique identifier to refer to licenses that are not found on the SPDX License List.
///
/// See the [spec](https://spdx.org/spdx-specification-21-web-version#h.4f1mdlm) for
/// more details.
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}"),
}
}
}
/// A single license term in a license expression, according to the SPDX spec.
///
/// This can be either an SPDX license, which is mapped to a [`LicenseId`] from
/// a valid SPDX short identifier, or else a document and/or license ref
#[derive(Debug, Clone)]
pub enum LicenseItem {
/// A regular SPDX license id
Spdx {
/// The license identifier
id: LicenseId,
/// Indicates the license had a `+`, allowing the licensee to license
/// the software under either the specific version, or any later versions
or_later: bool,
},
/// SPDX allows the use of `LicenseRef-<user supplied string>` to provide
/// arbitrary licenses that aren't a part of the official SPDX license list
Other(Box<LicenseRef>),
}
impl LicenseItem {
/// Returns the license identifier, if it is a recognized SPDX license and not
/// a license referencer
#[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),
}
}
}
/// A user supplied `AddtionRef-<user string>` to specify additional text to
/// associate with a license that falls outside the SPDX license list
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct AdditionRef {
/// Purpose: Identify any external SPDX documents referenced within this SPDX document.
/// See the [spec](https://spdx.org/spdx-specification-21-web-version#h.h430e9ypa0j9) for
/// more details.
pub doc_ref: Option<String>,
/// Purpose: Provide a locally unique identifier to refer to additional text that are not found on the SPDX License List.
/// See the [spec](https://spdx.org/spdx-specification-21-web-version#h.4f1mdlm) for
/// more details.
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}"),
}
}
}
/// A single addition term in a addition expression, according to the SPDX spec.
///
/// This can be either an SPDX license exception, which is mapped to a [`ExceptionId`]
/// from a valid SPDX short identifier, or else a document and/or addition ref
#[derive(Debug, Clone)]
pub enum AdditionItem {
/// A regular SPDX license exception id
Spdx(ExceptionId),
/// A user supplied `AddtionRef-<user string>` to specify additional text to
/// associate with a license that falls outside the SPDX license list
Other(Box<AdditionRef>),
}
impl AdditionItem {
/// Returns the license exception identifier, if it is a recognized SPDX license exception
/// and not a license exception referencer
#[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),
}
}
}
/// Attempts to find a [`LicenseId`] given a short id.
///
/// Note that any `+` at the end is trimmed when searching for a match.
///
/// ```
/// assert!(spdx::license_id("MIT").is_some());
/// assert!(spdx::license_id("BitTorrent-1.1+").is_some());
/// ```
#[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()
}
/// Attempts to find a GNU license from its base name.
///
/// GNU licenses are "special", unlike every other license in the SPDX list, they
/// have (in _most_ cases) a bare variant which is deprecated, eg. GPL-2.0, an
/// `-only` variant which acts like every other license, and an `-or-later`
/// variant which acts as if `+` was applied.
#[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 {
// Unreachable, but whatever
return None;
};
license_id(s)
}
}
/// Find license partially matching the name, e.g. "apache" => "Apache-2.0"
///
/// Returns length (in bytes) of the string matched. Garbage at the end is
/// ignored. See [`crate::identifiers::IMPRECISE_NAMES`] for the list of invalid
/// names, and the valid license identifiers they are mapped to.
///
/// ```
/// assert_eq!(
/// spdx::imprecise_license_id("simplified bsd license").unwrap().0,
/// spdx::license_id("BSD-2-Clause").unwrap()
/// );
/// ```
#[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
}
/// Attempts to find an [`ExceptionId`] for the string
///
/// ```
/// assert!(spdx::exception_id("LLVM-exception").is_some());
/// ```
#[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()
}
/// Returns the version number of the SPDX list from which
/// the license and exception identifiers are sourced from
#[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+");
}
}