/// Error types pub mod error; pub mod expression; /// Auto-generated lists of license identifiers and exception identifiers pub mod identifiers; /// Contains types for lexing an SPDX license expression pub mod lexer; mod licensee; /// Auto-generated full canonical text of each license #[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, }; /// 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, Eq)] pub struct LicenseId { /// The short identifier for the license pub name: &'static str, /// The full name of the license 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 { Some(self.cmp(o)) } } 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.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.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.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.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.flags & IS_GNU != 0 } /// 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.index].1 } } impl fmt::Debug for LicenseId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.name) } } /// Unique identifier for a particular exception /// /// ``` /// let exception_id = spdx::exception_id("LLVM-exception").unwrap(); /// assert!(!exception_id.is_deprecated()); /// ``` #[derive(Copy, Clone, Eq)] pub struct ExceptionId { /// The short identifier for the exception 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 { Some(self.cmp(o)) } } 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.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.index].1 } } impl fmt::Debug for ExceptionId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.name) } } /// Represents a single license requirement, which 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 exception allowed for this license, as specified following /// the `WITH` operator pub exception: Option, } impl From for LicenseReq { fn from(id: LicenseId) -> Self { // We need to special case GNU licenses because reasons 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); // If the root, eg GPL-2.0 licenses, which are currently deprecated, // are actually removed we will need to add them manually, but that // should only occur on a major revision of the SPDX license list, // so for now we should be fine with this ( 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(()) } } /// 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, Eq)] pub enum LicenseItem { /// A regular SPDX license id Spdx { 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, }, Other { /// 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. doc_ref: Option, /// Purpose: 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. lic_ref: String, }, } 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 { 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 { 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-{}:LicenseRef-{}", d, l), LicenseItem::Other { doc_ref: None, lic_ref: l, } => write!(f, "LicenseRef-{}", l), } } } /// Attempts to find a [`LicenseId`] for the string. 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 { 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() } /// 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 /// [`identifiers::IMPRECISE_NAMES`](identifiers/constant.IMPRECISE_NAMES.html) /// for the list of invalid names, and the valid license identifiers they are /// paired with. /// /// ``` /// assert!(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 { identifiers::EXCEPTIONS .binary_search_by(|exc| exc.0.cmp(name)) .map(|index| { let (name, flags) = identifiers::EXCEPTIONS[index]; ExceptionId { name, index, flags } }) .ok() } /// Returns the version number of the SPDX list from which /// the license and exception identifiers are sourced from /// /// ``` /// assert_eq!(spdx::license_version(), "3.26.0"); /// ``` #[inline] #[must_use] pub fn license_version() -> &'static str { identifiers::VERSION } #[cfg(test)] mod test { use super::LicenseItem; use crate::{license_id, Expression}; #[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+"); } }