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
5 changes: 3 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: ['4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14']
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
check-latest: true
Expand Down
52 changes: 9 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,48 +1,14 @@
`satisfies(SPDX license expression, array of approved licenses)`

Approved licenses may be simple license identifiers like `MIT`, plus-ranges like `EPL-2.0+`, or licenses with exceptions like `Apache-2.0 WITH LLVM`. They may _not_ be compound expressions using `AND` or `OR`.

```javascript
var assert = require('assert')
var satisfies = require('spdx-satisfies')

assert(satisfies('MIT', 'MIT'))

assert(satisfies('MIT', '(ISC OR MIT)'))
assert(satisfies('Zlib', '(ISC OR (MIT OR Zlib))'))
assert(!satisfies('GPL-3.0', '(ISC OR MIT)'))

assert(satisfies('GPL-2.0', 'GPL-2.0+'))
assert(satisfies('GPL-3.0', 'GPL-2.0+'))
assert(satisfies('GPL-1.0+', 'GPL-2.0+'))
assert(!satisfies('GPL-1.0', 'GPL-2.0+'))
assert(satisfies('GPL-2.0-only', 'GPL-2.0-only'))
assert(satisfies('GPL-3.0-only', 'GPL-2.0+'))
assert(satisfies('LGPL-3.0-only', 'LGPL-3.0-or-later'))
assert(satisfies('GPL-2.0', 'GPL-2.0+'))
assert(satisfies('GPL-2.0-only', 'GPL-2.0+'))
assert(satisfies('GPL-2.0', 'GPL-2.0-or-later'))

assert(!satisfies(
'GPL-2.0',
'GPL-2.0+ WITH Bison-exception-2.2'
))

assert(satisfies(
'GPL-3.0 WITH Bison-exception-2.2',
'GPL-2.0+ WITH Bison-exception-2.2'
))

assert(satisfies('(MIT OR GPL-2.0)', '(ISC OR MIT)'))
assert(satisfies('(MIT AND GPL-2.0)', '(MIT AND GPL-2.0)'))
assert(satisfies('MIT AND GPL-2.0 AND ISC', 'MIT AND GPL-2.0 AND ISC'))
assert(satisfies('MIT AND GPL-2.0 AND ISC', 'ISC AND GPL-2.0 AND MIT'))
assert(satisfies('(MIT OR GPL-2.0) AND ISC', 'MIT AND ISC'))
assert(satisfies('MIT AND ISC', '(MIT OR GPL-2.0) AND ISC'))
assert(satisfies('MIT AND ISC', '(MIT AND GPL-2.0) OR ISC'))
assert(satisfies('(MIT OR Apache-2.0) AND (ISC OR GPL-2.0)', 'Apache-2.0 AND ISC'))
assert(satisfies('(MIT OR Apache-2.0) AND (ISC OR GPL-2.0)', 'Apache-2.0 OR ISC'))
assert(satisfies('(MIT AND GPL-2.0)', '(MIT OR GPL-2.0)'))
assert(satisfies('(MIT AND GPL-2.0)', '(GPL-2.0 AND MIT)'))
assert(satisfies('MIT', '(GPL-2.0 OR MIT) AND (MIT OR ISC)'))
assert(satisfies('MIT AND ICU', '(MIT AND GPL-2.0) OR (ISC AND (Apache-2.0 OR ICU))'))
assert(!satisfies('(MIT AND GPL-2.0)', '(ISC OR GPL-2.0)'))
assert(!satisfies('MIT AND (GPL-2.0 OR ISC)', 'MIT'))
assert(!satisfies('(MIT OR Apache-2.0) AND (ISC OR GPL-2.0)', 'MIT'))
assert(satisfies('MIT', ['MIT', 'ISC', 'BSD-2-Clause', 'Apache-2.0']))
assert(satisfies('GPL-2.0 OR MIT', ['MIT']))
assert(!satisfies('GPL-2.0 AND MIT', ['MIT']))
assert(satisfies('GPL-3.0', ['GPL-2.0+']))
assert(!satisfies('GPL-1.0', ['GPL-2.0+']))
```
34 changes: 34 additions & 0 deletions examples.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"returnTrue": [
["MIT", ["MIT"]],
["MIT", ["ISC", "MIT"]],
["Zlib", ["ISC", "MIT", "Zlib"]],
["GPL-2.0", ["GPL-2.0+"]],
["GPL-3.0", ["GPL-2.0+"]],
["GPL-1.0+", ["GPL-2.0+"]],
["GPL-2.0-only", ["GPL-2.0-only"]],
["GPL-3.0-only", ["GPL-2.0+"]],
["LGPL-3.0-only", ["LGPL-3.0-or-later"]],
["GPL-2.0", ["GPL-2.0+"]],
["GPL-2.0-only", ["GPL-2.0+"]],
["GPL-2.0", ["GPL-2.0-or-later"]],
["GPL-3.0 WITH Bison-exception-2.2", ["GPL-2.0+ WITH Bison-exception-2.2"]],
["(MIT OR GPL-2.0)", ["ISC", "MIT"]],
["(MIT OR Apache-2.0) AND (ISC OR GPL-2.0)", ["Apache-2.0", "ISC"]],
["(MIT AND GPL-2.0)", ["MIT", "GPL-2.0"]]
],

"returnFalse": [
["GPL-3.0", ["ISC", "MIT"]],
["GPL-1.0", ["GPL-2.0+"]],
["GPL-2.0", ["GPL-2.0+ WITH Bison-exception-2.2"]],
["(MIT AND GPL-2.0)", ["ISC", "GPL-2.0"]],
["MIT AND (GPL-2.0 OR ISC)", ["MIT"]],
["(MIT OR Apache-2.0) AND (ISC OR GPL-2.0)", ["MIT"]]
],

"throwErrors": [
["MIT AND ISC", ["(MIT OR GPL-2.0) AND ISC"]],
["MIT AND ISC", ["(MIT AND GPL-2.0)", "ISC"]]
]
}
50 changes: 27 additions & 23 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ var compare = require('spdx-compare')
var parse = require('spdx-expression-parse')
var ranges = require('spdx-ranges')

var rangesAreCompatible = function (first, second) {
function rangesAreCompatible (first, second) {
return (
first.license === second.license ||
ranges.some(function (range) {
Expand All @@ -26,15 +26,15 @@ function licenseInRange (license, range) {
)
}

var identifierInRange = function (identifier, range) {
function identifierInRange (identifier, range) {
return (
identifier.license === range.license ||
compare.gt(identifier.license, range.license) ||
compare.eq(identifier.license, range.license)
)
}

var licensesAreCompatible = function (first, second) {
function licensesAreCompatible (first, second) {
if (first.exception !== second.exception) {
return false
} else if (second.hasOwnProperty('license')) {
Expand All @@ -58,7 +58,7 @@ var licensesAreCompatible = function (first, second) {
}
}

function normalizeGPLIdentifiers (argument) {
function replaceGPLOnlyOrLaterWithRanges (argument) {
var license = argument.license
if (license) {
if (endsWith(license, '-or-later')) {
Expand All @@ -69,8 +69,8 @@ function normalizeGPLIdentifiers (argument) {
delete argument.plus
}
} else if (argument.left && argument.right) {
argument.left = normalizeGPLIdentifiers(argument.left)
argument.right = normalizeGPLIdentifiers(argument.right)
argument.left = replaceGPLOnlyOrLaterWithRanges(argument.left)
argument.right = replaceGPLOnlyOrLaterWithRanges(argument.right)
}
return argument
}
Expand All @@ -81,7 +81,13 @@ function endsWith (string, substring) {

function licenseString (e) {
if (e.hasOwnProperty('noassertion')) return 'NOASSERTION'
if (e.license) return `${e.license}${e.plus ? '+' : ''}${e.exception ? ` WITH ${e.exception}` : ''}`
if (e.license) {
return (
e.license +
(e.plus ? '+' : '') +
(e.exception ? ('WITH ' + e.exception) : '')
)
}
}

// Expand the given expression into an equivalent array where each member is an array of licenses AND'd
Expand All @@ -92,15 +98,6 @@ function expand (expression) {
return sort(expandInner(expression))
}

// Flatten the given expression into an array of all licenses mentioned in the expression.
function flatten (expression) {
var expanded = expandInner(expression)
var flattened = expanded.reduce(function (result, clause) {
return Object.assign(result, clause)
}, {})
return sort([flattened])[0]
}

function expandInner (expression) {
if (!expression.conjunction) return [{ [licenseString(expression)]: expression }]
if (expression.conjunction === 'or') return expandInner(expression.left).concat(expandInner(expression.right))
Expand All @@ -123,16 +120,23 @@ function sort (licenseList) {
})
}

function isANDCompatible (one, two) {
return one.every(function (o) {
return two.some(function (t) { return licensesAreCompatible(o, t) })
function isANDCompatible (parsedExpression, parsedLicenses) {
return parsedExpression.every(function (element) {
return parsedLicenses.some(function (approvedLicense) {
return licensesAreCompatible(element, approvedLicense)
})
})
}

function satisfies (first, second) {
var one = expand(normalizeGPLIdentifiers(parse(first)))
var two = flatten(normalizeGPLIdentifiers(parse(second)))
return one.some(function (o) { return isANDCompatible(o, two) })
function satisfies (spdxExpression, arrayOfLicenses) {
var parsedExpression = expand(replaceGPLOnlyOrLaterWithRanges(parse(spdxExpression)))
var parsedLicenses = arrayOfLicenses.map(function (l) { return replaceGPLOnlyOrLaterWithRanges(parse(l)) })
for (const parsed of parsedLicenses) {
if (parsed.hasOwnProperty('conjunction')) {
throw new Error('Approved licenses cannot be AND or OR expressions.')
}
}
return parsedExpression.some(function (o) { return isANDCompatible(o, parsedLicenses) })
}

module.exports = satisfies
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"spdx-ranges": "^2.0.0"
},
"devDependencies": {
"defence-cli": "^2.0.1",
"defence-cli": "^3.0.1",
"replace-require-self": "^1.1.1",
"standard": "^11.0.0"
},
Expand All @@ -34,7 +34,9 @@
"index.js"
],
"scripts": {
"test": "defence -i javascript README.md | replace-require-self | node",
"test": "npm run test:suite && npm run test:readme",
"test:suite": "node test.js",
"test:readme": "defence -i javascript README.md | replace-require-self | node",
"lint": "standard"
}
}
51 changes: 51 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
var assert = require('assert')
var examples = require('./examples.json')
var satisfies = require('./')

var failed = false

function write (string) { process.stdout.write(string) }

function label (example) {
write('satisfies(' + JSON.stringify(example[0]) + ', ' + JSON.stringify(example[1]) + ')')
}

examples.returnTrue.forEach(function (example) {
label(example)
try {
assert(satisfies(example[0], example[1]) === true)
} catch (error) {
failed = true
write(' did not return true\n')
return
}
write(' returned true\n')
})

// False Examples
examples.returnFalse.forEach(function (example) {
label(example)
try {
assert(satisfies(example[0], example[1]) === false)
} catch (error) {
failed = true
write(' did not return false\n')
return
}
write(' returned false\n')
})

// Invalid License Arrays
examples.throwErrors.forEach(function (example) {
label(example)
try {
satisfies(example[0], example[1])
} catch (error) {
write(' threw an exception\n')
return
}
failed = true
write(' did not throw an exception\n')
})

process.exit(failed ? 1 : 0)