Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

<!-- next-header -->
## [Unreleased] - ReleaseDate
### Changed
- [PR#80](https://github.com/EmbarkStudios/spdx/pull/80) changed how `Licensee::satisfies` works for GNU licenses again, it now requires that the license ids match exactly. This is incredibly pedantic but means it's up to consumers if the want to have a smarter comparison, I just don't want to have to care about GNU licenses, ever.

### Fixed
- [PR#80](https://github.com/EmbarkStudios/spdx/pull/80) reverted a change introduced in [PR#78] that would auto-fixup GNU licenses to their non-deprecated forms eg. `GPL-2.0` => `GPL-2.0-only`. This is no longer done, resolving [#79](https://github.com/EmbarkStudios/spdx/issues/79).

## [0.11.0] - 2025-08-08
### Changed
- [PR#78] removed `ParseMode::allow_lower_case_operators`, newer revisions of the SPDX spec allow all lower-case operators, making the option pointless.
Expand Down
24 changes: 7 additions & 17 deletions src/expression/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,26 +196,16 @@ impl Expression {
match &lt.token {
Token::Spdx(id) => match last_token {
None | Some(Token::And | Token::Or | Token::OpenParen) => {
let id = if id.is_gnu() {
crate::gnu_license_id(id.name, false).ok_or_else(|| ParseError {
if !mode.allow_deprecated && id.is_deprecated() {
return Err(ParseError {
original: original.to_owned(),
span: lt.span.clone(),
reason: Reason::UnknownLicense,
})?
} else {
if !mode.allow_deprecated && id.is_deprecated() {
return Err(ParseError {
original: original.to_owned(),
span: lt.span,
reason: Reason::DeprecatedLicenseId,
});
}

*id
};
span: lt.span,
reason: Reason::DeprecatedLicenseId,
});
}

expr_queue.push(ExprNode::Req(ExpressionReq {
req: LicenseReq::from(id),
req: LicenseReq::from(*id),
span: lt.span.start as u32..lt.span.end as u32,
}));
}
Expand Down
44 changes: 2 additions & 42 deletions src/licensee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use std::fmt;
///
/// ```
/// let licensee = spdx::Licensee::parse("GPL-2.0-or-later").unwrap();
/// let req = spdx::LicenseReq::from(spdx::license_id("GPL-2.0-only").unwrap());
/// let req = spdx::LicenseReq::from(spdx::license_id("GPL-2.0-or-later").unwrap());
///
/// assert!(licensee.satisfies(&req));
/// ```
Expand Down Expand Up @@ -89,7 +89,7 @@ impl Licensee {
})??;

match lt.token {
Token::Spdx(mut id) => {
Token::Spdx(id) => {
if !mode.allow_deprecated && id.is_deprecated() {
return Err(ParseError {
original: original.to_owned(),
Expand All @@ -98,15 +98,6 @@ impl Licensee {
});
}

if id.is_gnu() && !id.name.ends_with("-or-later") && !id.name.ends_with("-only")
{
id = crate::gnu_license_id(id.name, false).ok_or_else(|| ParseError {
original: original.to_owned(),
span: lt.span,
reason: Reason::UnknownLicense,
})?;
}

LicenseItem::Spdx {
id,
or_later: false,
Expand Down Expand Up @@ -211,37 +202,6 @@ impl Licensee {
_ => return false,
}
}
} else if a.is_gnu() && b.is_gnu() {
let abn = a.base();
let bbn = b.base();

if abn != bbn {
return false;
}

// GFDL has the annoying -no -invariants...variants, both
// sides have to agree on all or none
if abn == "GFDL"
&& a.name.contains("-invariants") ^ b.name.contains("-invariants")
|| a.name.contains("-no-") ^ b.name.contains("-no-")
{
return false;
}

let Some(av) = a.version() else {
return false;
};
let Some(bv) = b.version() else {
return false;
};

if b.name.ends_with("-or-later") {
if av < bv {
return false;
}
} else if abn != bbn || av != bv {
return false;
}
} else {
return false;
}
Expand Down
183 changes: 83 additions & 100 deletions tests/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ fn complex() {
false && (false || false) => |req| exact!(req, "Apache-2.0"),
true && (false || false) => |req| exact!(req, "MIT"),
true && (false || true) => |req| exact!(req, "MIT") || exact!(req, "BSD-3-Clause"),
true && (true || false) => |req| exact!(req, "MIT") || exact!(req, "LGPL-3.0-or-later"),
true && (true || false) => |req| exact!(req, "MIT") || exact!(req, "LGPL-2.1-or-later"),
]);
}

Expand Down Expand Up @@ -171,24 +171,13 @@ fn or_later() {
fn lgpl_only() {
check!("LGPL-2.1-only" => [
false => |req| exact!(req, "LGPL-2.0"),
true => |req| exact!(req, "LGPL-2.1"),
false => |req| exact!(req, "LGPL-2.1"),
true => |req| exact!(req, "LGPL-2.1-only"),
false => |req| exact!(req, "LGPL-3.0"),
//false => |req| exact!(req, "LGPL-4.0"),
]);
}

#[test]
fn gpl_or_later() {
check!("GPL-3.0-or-later" => [
false => |req| exact!(req, "GPL-1.0"),
false => |req| exact!(req, "GPL-2.0"),
true => |req| exact!(req, "GPL-3.0-only"),
true => |req| exact!(req, "GPL-3.0"),
true => |req| exact!(req, "GPL-3.0-or-later"),
//true => |req| exact!(req, "GPL-4.0"),
]);
}

#[test]
fn gpl_or_later_plus_strict() {
spdx::Expression::parse("GPL-2.0+").unwrap_err();
Expand All @@ -201,104 +190,95 @@ fn gpl_or_later_plus_lax() {

#[test]
fn gpl_pedantic() {
// | Licensee | GPL-1.0-only | GPL-1.0-or-later | GPL-2.0-only | GPL-2.0-or-later | GPL-3.0-only | GPL-3.0-or-later |
// | ----------------- | -- | -- | -- | -- | -- | -- |
// | GPL-1.0-only | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
// | GPL-1.0-or-later | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
// | GPL-2.0-only | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
// | GPL-2.0-or-later | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
// | GPL-3.0-only | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ |
// | GPL-3.0-or-later | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ |

const ONE_ONLY: &str = "GPL-1.0-only";
const ONE_LATER: &str = "GPL-1.0-or-later";
const TWO_ONLY: &str = "GPL-2.0-only";
const TWO_LATER: &str = "GPL-2.0-or-later";
const THREE_ONLY: &str = "GPL-3.0-only";
const THREE_LATER: &str = "GPL-3.0-or-later";

const COLUMNS: &[&str] = &[
ONE_ONLY,
ONE_LATER,
TWO_ONLY,
TWO_LATER,
THREE_ONLY,
THREE_LATER,
];

let table = [
(
ONE_ONLY,
[
(ONE_ONLY, true),
(ONE_LATER, true),
(TWO_ONLY, false),
(TWO_LATER, false),
(THREE_ONLY, false),
(THREE_LATER, false),
],
),
(
ONE_LATER,
[
(ONE_ONLY, true),
(ONE_LATER, true),
(TWO_ONLY, false),
(TWO_LATER, false),
(THREE_ONLY, false),
(THREE_LATER, false),
],
),
(
TWO_ONLY,
[
(ONE_ONLY, false),
(ONE_LATER, true),
(TWO_ONLY, true),
(TWO_LATER, true),
(THREE_ONLY, false),
(THREE_LATER, false),
],
),
(
TWO_LATER,
[
(ONE_ONLY, false),
(ONE_LATER, true),
(TWO_ONLY, true),
(TWO_LATER, true),
(THREE_ONLY, false),
(THREE_LATER, false),
],
),
(
THREE_ONLY,
[
(ONE_ONLY, false),
(ONE_LATER, true),
(TWO_ONLY, false),
(TWO_LATER, true),
(THREE_ONLY, true),
(THREE_LATER, true),
],
),
(
THREE_LATER,
[
(ONE_ONLY, false),
(ONE_LATER, true),
(TWO_ONLY, false),
(TWO_LATER, true),
(THREE_ONLY, true),
(THREE_LATER, true),
],
),
(ONE_ONLY, [true, false, false, false, false, false]),
(ONE_LATER, [false, true, false, false, false, false]),
(TWO_ONLY, [false, false, true, false, false, false]),
(TWO_LATER, [false, false, false, true, false, false]),
(THREE_ONLY, [false, false, false, false, true, false]),
(THREE_LATER, [false, false, false, false, false, true]),
];

fn header(s: &mut String) {
s.push_str("| Licensee | GPL-1.0-only | GPL-1.0-or-later | GPL-2.0-only | GPL-2.0-or-later | GPL-3.0-only | GPL-3.0-or-later |\n");
s.push_str("| ---------------- | ------------ | ---------------- | ------------ | ---------------- | ------------ | ---------------- |\n");
}

fn fill(s: &mut String, c: &str, len: usize) {
for _ in 0..len - c.len() {
s.push(' ');
}
}

fn lic(s: &mut String, lic: &str) {
s.push_str("| ");
s.push_str(lic);
fill(s, lic, 16);
s.push_str(" | ");
}

for (licensee, items) in table {
let mut expected = String::new();
let exp = &mut expected;
header(exp);

let mut actual = String::new();
let act = &mut actual;
header(act);

lic(exp, licensee);
lic(act, licensee);

let lic = spdx::Licensee::parse(licensee).unwrap();

for (req, passes) in items {
let mut fail = false;
for (col, passes) in COLUMNS.iter().zip(items.iter()) {
let req = spdx::LicenseReq {
license: spdx::LicenseItem::Spdx {
id: spdx::license_id(req).unwrap(),
id: spdx::license_id(col).unwrap(),
or_later: false,
},
exception: None,
};

assert_eq!(lic.satisfies(&req), passes);
exp.push_str(if *passes { "+" } else { "-" });
fill(exp, " ", col.len());

exp.push_str(" | ");

let satisfies = lic.satisfies(&req);

act.push_str(if satisfies { "+" } else { "-" });
fill(act, " ", col.len());
act.push_str(" | ");

fail |= *passes != satisfies;
}

exp.push('\n');
act.push('\n');

if fail {
panic!(
"{}",
similar_asserts::SimpleDiff::from_str(exp, act, "expected", "calculated")
);
}
}
}
Expand All @@ -307,11 +287,11 @@ fn gpl_pedantic() {
fn gfdl() {
check!("GFDL-1.2-or-later" => [
false => |req| exact!(req, "GFDL-1.1"),
true => |req| exact!(req, "GFDL-1.2"),
true => |req| exact!(req, "GFDL-1.3"),
false => |req| exact!(req, "GFDL-1.2"),
false => |req| exact!(req, "GFDL-1.3"),
false => |req| exact!(req, "GFDL-1.1-or-later"),
true => |req| exact!(req, "GFDL-1.2-or-later"),
true => |req| exact!(req, "GFDL-1.3-or-later"),
false => |req| exact!(req, "GFDL-1.3-or-later"),
]);

check!("GFDL-1.2-invariants-or-later" => [
Expand All @@ -320,23 +300,24 @@ fn gfdl() {
false => |req| exact!(req, "GFDL-1.2"),
true => |req| exact!(req, "GFDL-1.2-invariants-or-later"),
false => |req| exact!(req, "GFDL-1.3"),
true => |req| exact!(req, "GFDL-1.3-invariants-only"),
false => |req| exact!(req, "GFDL-1.3-invariants-only"),
]);

check_lax!("GFDL-1.1-invariants+" => [
false => |req| exact!(req, "GFDL-1.1"),
true => |req| exact!(req, "GFDL-1.1-invariants-or-later"),
false => |req| exact!(req, "GFDL-1.2"),
true => |req| exact!(req, "GFDL-1.2-invariants-or-later"),
false => |req| exact!(req, "GFDL-1.2-invariants-or-later"),
false => |req| exact!(req, "GFDL-1.3"),
true => |req| exact!(req, "GFDL-1.3-invariants-or-later"),
false => |req| exact!(req, "GFDL-1.3-invariants-or-later"),
]);

check!("GFDL-1.2-invariants" => [
false => |req| exact!(req, "GFDL-1.1"),
false => |req| exact!(req, "GFDL-1.1-invariants"),
false => |req| exact!(req, "GFDL-1.2"),
true => |req| exact!(req, "GFDL-1.2-invariants"),
false => |req| exact!(req, "GFDL-1.2-invariants-only"),
false => |req| exact!(req, "GFDL-1.3"),
false => |req| exact!(req, "GFDL-1.3-invariants"),
]);
Expand All @@ -345,7 +326,8 @@ fn gfdl() {
false => |req| exact!(req, "GFDL-1.1"),
false => |req| exact!(req, "GFDL-1.1-invariants"),
false => |req| exact!(req, "GFDL-1.2"),
true => |req| exact!(req, "GFDL-1.2-invariants"),
false => |req| exact!(req, "GFDL-1.2-invariants"),
true => |req| exact!(req, "GFDL-1.2-invariants-only"),
false => |req| exact!(req, "GFDL-1.3"),
false => |req| exact!(req, "GFDL-1.3-invariants"),
]);
Expand All @@ -355,7 +337,8 @@ fn gfdl() {
false => |req| exact!(req, "GFDL-1.1-invariants"),
false => |req| exact!(req, "GFDL-1.2"),
false => |req| exact!(req, "GFDL-1.2-invariants"),
true => |req| exact!(req, "GFDL-1.3"),
false => |req| exact!(req, "GFDL-1.3"),
true => |req| exact!(req, "GFDL-1.3-only"),
false => |req| exact!(req, "GFDL-1.3-invariants"),
]);
}
Expand Down