Skip to content

Commit

Permalink
Merge pull request #17 from jslicense/array
Browse files Browse the repository at this point in the history
Change API to `satisfies(SPDX Expression, Array of Approved Licenses)`
  • Loading branch information
kemitchell authored Oct 3, 2023
2 parents bb284e1 + 07d4686 commit 9c2a4f8
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 70 deletions.
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)

0 comments on commit 9c2a4f8

Please sign in to comment.