| // 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+"); |
| } |
| } |