Skip to content

Commit

Permalink
Add license text feature (#43)
Browse files Browse the repository at this point in the history
* Use git clone rather than doing HTTP reqeusts

* Add feature to include full license text

* Rename expression/mod => expression

* Add exception text as well

* Add license minimization

* Add docs
  • Loading branch information
Jake-Shadle authored Oct 21, 2021
1 parent 3bd2231 commit 779b720
Show file tree
Hide file tree
Showing 528 changed files with 38,152 additions and 238 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
- run: rustup component add clippy
- run: cargo fetch
- name: cargo clippy
run: cargo clippy --all-targets -- -D warnings
run: cargo clippy --all-targets --all-features -- -D warnings

deny-check:
name: cargo-deny check
Expand All @@ -45,7 +45,7 @@ jobs:
override: true
- run: cargo fetch
- name: cargo check
run: cargo check --all-targets
run: cargo check --all-targets --all-features

test:
name: Test
Expand All @@ -61,5 +61,5 @@ jobs:
override: true
- run: cargo fetch
- name: cargo build
run: cargo build --tests
- run: cargo test
run: cargo build --tests --all-features
- run: cargo test --all-features
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/target
update/target
**/*.rs.bk
Cargo.lock
Cargo.lock
spdx-data
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ readme = "README.md"
keywords = ["spdx", "license"]
license = "MIT OR Apache-2.0"
repository = "https://github.com/EmbarkStudios/spdx"

include = ["Cargo.toml", "LICENSE-APACHE", "LICENSE-MIT", "src/**/*"]

[features]
# Includes the full canonical text of each license
text = []

[dependencies]
# In most cases expressions are quite small so we can avoid heap allocations
smallvec = "1.7"
Expand Down
2 changes: 1 addition & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::{error::Error, fmt};
/// or identifier
#[derive(Debug, PartialEq)]
pub struct ParseError<'a> {
/// The string that was parsed
/// The string that was attempting to be parsed
pub original: &'a str,
/// The range of characters in the original string that result
/// in this error
Expand Down
2 changes: 2 additions & 0 deletions src/expression/mod.rs → src/expression.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
mod minimize;
mod parser;

use crate::LicenseReq;
pub use minimize::MinimizeError;
use smallvec::SmallVec;
use std::fmt;

Expand Down
125 changes: 125 additions & 0 deletions src/expression/minimize.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
use crate::{LicenseReq, Licensee};
use std::fmt;

/// Errors that can occur when trying to minimize the requirements for an [`Expression`]
#[derive(Debug, PartialEq)]
pub enum MinimizeError {
/// More than `64` unique licensees satisfied a requirement in the [`Expression`]
TooManyRequirements(usize),
/// The list of licensees did not fully satisfy the requirements in the [`Expression`]
RequirementsUnmet,
}

impl fmt::Display for MinimizeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TooManyRequirements(n) => write!(
f,
"the license expression required {} licensees which exceeds the limit of 64",
n
),
Self::RequirementsUnmet => {
f.write_str("the expression was not satisfied by the provided list of licensees")
}
}
}
}

impl std::error::Error for MinimizeError {
fn description(&self) -> &str {
match self {
Self::TooManyRequirements(_) => "too many requirements in license expression",
Self::RequirementsUnmet => {
"the expression was not satisfied by the provided list of licensees"
}
}
}
}

impl super::Expression {
/// Given a set of [`Licensee`]s, attempts to find the minimum number that
/// satisfy this [`Expression`]. The list of licensees should be given in
/// priority order, eg, if you wish to accept the `Apache-2.0` license if
/// it is available and the `MIT` if not, putting `Apache-2.0` before `MIT`
/// will cause the ubiquitous `Apache-2.0 OR MIT` expression to minimize to
/// just `Apache-2.0` as only only 1 of the licenses is required, and the
/// `Apache-2.0` has priority.
///
/// # Errors
///
/// This method will fail if more than 64 unique licensees are satisfied by
/// this expression, but such a case is unlikely to say the least in a real
/// world scenario. The list of licensees must also actually satisfy this
/// expression, otherwise it can't be minimized.
///
/// # Example
///
/// ```
/// let expr = spdx::Expression::parse("Apache-2.0 OR MIT").unwrap();
///
/// let apache_licensee = spdx::Licensee::parse("Apache-2.0").unwrap();
/// assert_eq!(
/// expr.minimized_requirements([&apache_licensee, &spdx::Licensee::parse("MIT").unwrap()]).unwrap(),
/// vec![apache_licensee.into_req()],
/// );
/// ```
pub fn minimized_requirements<'lic>(
&self,
accepted: impl IntoIterator<Item = &'lic Licensee>,
) -> Result<Vec<LicenseReq>, MinimizeError> {
let found_set = {
let mut found_set = smallvec::SmallVec::<[Licensee; 5]>::new();

for lic in accepted {
if !found_set.contains(lic)
&& self.requirements().any(|ereq| lic.satisfies(&ereq.req))
{
found_set.push(lic.clone());
}
}

if found_set.len() > 64 {
return Err(MinimizeError::TooManyRequirements(found_set.len()));
}

// Ensure that the licensees provided actually _can_ be accepted by
// this expression
if !self.evaluate(|ereq| found_set.iter().any(|lic| lic.satisfies(ereq))) {
return Err(MinimizeError::RequirementsUnmet);
}

found_set
};

let set_size = (1 << found_set.len()) as u64;

for mask in 1..=set_size {
let eval_res = self.evaluate(|req| {
for (ind, lic) in found_set.iter().enumerate() {
if mask & (1 << ind) != 0 && lic.satisfies(req) {
return true;
}
}

false
});

if eval_res {
return Ok(found_set
.into_iter()
.enumerate()
.filter_map(|(ind, lic)| {
if mask & (1 << ind) != 0 {
Some(lic.into_req())
} else {
None
}
})
.collect());
}
}

// This should be impossible, but would rather not panic
Ok(found_set.into_iter().map(|lic| lic.into_req()).collect())
}
}
29 changes: 22 additions & 7 deletions src/identifiers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* DO NOT MODIFY
*
* cargo run --manifest-path update/Cargo.toml -- v<version> > src/identifiers.rs
*/
*/

pub const IS_FSF_LIBRE: u8 = 0x1;
pub const IS_OSI_APPROVED: u8 = 0x2;
Expand Down Expand Up @@ -697,8 +697,8 @@ pub const LICENSES: &[(&str, &str, u8)] = &[
),
(
"GFDL-1.1-invariants",
r#"GNU Free Documentation License v1.1"#,
IS_DEPRECATED | IS_FSF_LIBRE | IS_COPYLEFT | IS_GNU,
r#"GNU Free Documentation License v1.1 only - invariants"#,
IS_COPYLEFT | IS_GNU,
),
(
"GFDL-1.1-invariants-only",
Expand All @@ -710,6 +710,11 @@ pub const LICENSES: &[(&str, &str, u8)] = &[
r#"GNU Free Documentation License v1.1 or later - invariants"#,
IS_COPYLEFT | IS_GNU,
),
(
"GFDL-1.1-no-invariants",
r#"GNU Free Documentation License v1.1 only - no invariants"#,
IS_COPYLEFT | IS_GNU,
),
(
"GFDL-1.1-no-invariants-only",
r#"GNU Free Documentation License v1.1 only - no invariants"#,
Expand Down Expand Up @@ -737,8 +742,8 @@ pub const LICENSES: &[(&str, &str, u8)] = &[
),
(
"GFDL-1.2-invariants",
r#"GNU Free Documentation License v1.2"#,
IS_DEPRECATED | IS_FSF_LIBRE | IS_COPYLEFT | IS_GNU,
r#"GNU Free Documentation License v1.2 only - invariants"#,
IS_COPYLEFT | IS_GNU,
),
(
"GFDL-1.2-invariants-only",
Expand All @@ -750,6 +755,11 @@ pub const LICENSES: &[(&str, &str, u8)] = &[
r#"GNU Free Documentation License v1.2 or later - invariants"#,
IS_COPYLEFT | IS_GNU,
),
(
"GFDL-1.2-no-invariants",
r#"GNU Free Documentation License v1.2 only - no invariants"#,
IS_COPYLEFT | IS_GNU,
),
(
"GFDL-1.2-no-invariants-only",
r#"GNU Free Documentation License v1.2 only - no invariants"#,
Expand Down Expand Up @@ -777,8 +787,8 @@ pub const LICENSES: &[(&str, &str, u8)] = &[
),
(
"GFDL-1.3-invariants",
r#"GNU Free Documentation License v1.3"#,
IS_DEPRECATED | IS_FSF_LIBRE | IS_COPYLEFT | IS_GNU,
r#"GNU Free Documentation License v1.3 only - invariants"#,
IS_COPYLEFT | IS_GNU,
),
(
"GFDL-1.3-invariants-only",
Expand All @@ -790,6 +800,11 @@ pub const LICENSES: &[(&str, &str, u8)] = &[
r#"GNU Free Documentation License v1.3 or later - invariants"#,
IS_COPYLEFT | IS_GNU,
),
(
"GFDL-1.3-no-invariants",
r#"GNU Free Documentation License v1.3 only - no invariants"#,
IS_COPYLEFT | IS_GNU,
),
(
"GFDL-1.3-no-invariants-only",
r#"GNU Free Documentation License v1.3 only - no invariants"#,
Expand Down
25 changes: 25 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ 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;
Expand Down Expand Up @@ -181,6 +184,17 @@ impl LicenseId {
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 {
Expand Down Expand Up @@ -234,6 +248,17 @@ impl ExceptionId {
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 {
Expand Down
Loading

0 comments on commit 779b720

Please sign in to comment.