blob: dd99596b1e4416e29f68cd6e503904e5cce21773 [file] [log] [blame] [edit]
// BEGIN - Embark standard lints v6 for Rust 1.55+
// do not change or add/remove here, but one can add exceptions after this section
// for more info see: <https://github.com/EmbarkStudios/rust-ecosystem/issues/59>
#![deny(unsafe_code)]
#![warn(
clippy::all,
clippy::await_holding_lock,
clippy::char_lit_as_u8,
clippy::checked_conversions,
clippy::dbg_macro,
clippy::debug_assert_with_mut_call,
clippy::doc_markdown,
clippy::empty_enum,
clippy::enum_glob_use,
clippy::exit,
clippy::expl_impl_clone_on_copy,
clippy::explicit_deref_methods,
clippy::explicit_into_iter_loop,
clippy::fallible_impl_from,
clippy::filter_map_next,
clippy::flat_map_option,
clippy::float_cmp_const,
clippy::fn_params_excessive_bools,
clippy::from_iter_instead_of_collect,
clippy::if_let_mutex,
clippy::implicit_clone,
clippy::imprecise_flops,
clippy::inefficient_to_string,
clippy::invalid_upcast_comparisons,
clippy::large_digit_groups,
clippy::large_stack_arrays,
clippy::large_types_passed_by_value,
clippy::let_unit_value,
clippy::linkedlist,
clippy::lossy_float_literal,
clippy::macro_use_imports,
clippy::manual_ok_or,
clippy::map_err_ignore,
clippy::map_flatten,
clippy::map_unwrap_or,
clippy::match_on_vec_items,
clippy::match_same_arms,
clippy::match_wild_err_arm,
clippy::match_wildcard_for_single_variants,
clippy::mem_forget,
clippy::mismatched_target_os,
clippy::missing_enforced_import_renames,
clippy::mut_mut,
clippy::mutex_integer,
clippy::needless_borrow,
clippy::needless_continue,
clippy::needless_for_each,
clippy::option_option,
clippy::path_buf_push_overwrite,
clippy::ptr_as_ptr,
clippy::rc_mutex,
clippy::ref_option_ref,
clippy::rest_pat_in_fully_bound_structs,
clippy::same_functions_in_if_condition,
clippy::semicolon_if_nothing_returned,
clippy::single_match_else,
clippy::string_add_assign,
clippy::string_add,
clippy::string_lit_as_bytes,
clippy::string_to_string,
clippy::todo,
clippy::trait_duplication_in_bounds,
clippy::unimplemented,
clippy::unnested_or_patterns,
clippy::unused_self,
clippy::useless_transmute,
clippy::verbose_file_reads,
clippy::zero_sized_map_values,
future_incompatible,
nonstandard_style,
rust_2018_idioms
)]
// END - Embark standard lints v6 for Rust 1.55+
// crate-specific exceptions:
/// 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<Ordering> {
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<Ordering> {
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<ExceptionId>,
}
impl From<LicenseId> 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<String>,
/// 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<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-{}: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<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()
}
/// 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<ExceptionId> {
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.24.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+");
}
}