Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Accept an AST and a whitelist Array of AST leaves #11

Closed
wants to merge 3 commits into from
Closed
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
245 changes: 210 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,44 +1,219 @@
```javascript
var assert = require('assert')
var satisfies = require('spdx-satisfies')
```

This package exports a single function of two arguments:

1. an Object representing an SPDX expression

assert(satisfies('MIT', 'MIT'))
2. an Array of Objects, each in the form of a leaf in an SPDX expression data structure

assert(satisfies('MIT', '(ISC OR MIT)'))
assert(satisfies('Zlib', '(ISC OR (MIT OR Zlib))'))
assert(!satisfies('GPL-3.0', '(ISC OR MIT)'))
```javascript
assert(
satisfies(
{license: 'MIT'},
[{license: '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+'))
The schema for SPDX expression data structures is the same returned by [spdx-expression-parse](https://www.npmjs.com/package/spdx-expression-parse).

```javascript
var parse = require('spdx-expression-parse')

assert(satisfies(
parse('MIT'),
[parse('ISC'), parse('MIT')]
))

assert(satisfies(
{license: 'Zlib'},
[
{license: 'ISC'},
{license: 'MIT'},
{license: 'Zlib'}
]
))

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'))
{license: 'GPL-3.0'},
[
{license: 'ISC'},
{license: 'MIT'}
]
))


assert(satisfies(
{license: 'GPL-2.0'},
[{license: 'GPL-2.0', plus: true}]
))

assert(satisfies(
{license: 'GPL-3.0'},
[{license: 'GPL-2.0', plus: true}]
))

assert(satisfies(
{license: 'GPL-1.0', plus: true},
[{license: 'GPL-2.0', plus: true}]
))

assert(!satisfies(
{license: 'GPL-1.0'},
[{license: 'GPL-2.0', plus: true}]
))

assert(satisfies(
{license: 'GPL-2.0-only'},
[{license: 'GPL-2.0-only'}]
))

assert(satisfies(
{license: 'GPL-3.0-only'},
[{license: 'GPL-2.0', plus: true}]
))

assert(!satisfies(
{license: 'GPL-2.0'},
[
{
license: 'GPL-2.0',
plus: true,
exception: 'Bison-exception-2.2'
}
]
))

assert(satisfies(
{
license: 'GPL-3.0',
exception: 'Bison-exception-2.2'
},
[
{
license: 'GPL-2.0',
plus: true,
exception: 'Bison-exception-2.2'
}
]
))

assert(satisfies(
// (MIT OR GPL-2.0)
{
left: {license: 'MIT'},
conjunction: 'or',
right: {license: 'GPL-2.0'}
},
[
{license: 'ISC'},
{license: 'MIT'}
]
))

assert(satisfies(
// ((MIT OR Apache-2.0) AND (ISC OR GPL-2.0))
{
left: {
left: {license: 'MIT'},
conjunction: 'or',
right: {license: 'Apache-2.0'}
},
conjunction: 'and',
right: {
left: {license: 'ISC'},
conjunction: 'or',
right: {license: 'GPL-2.0'}
}
},
[
{license: 'Apache-2.0'},
{license: 'ISC'}
]
))

assert(satisfies(
// (MIT AND GPL-2.0)
{
left: {license: 'MIT'},
conjunction: 'and',
right: {license: 'GPL-2.0'}
},
[
{license: 'MIT'},
{license: 'GPL-2.0'}
]
))

assert(!satisfies(
// (MIT AND GPL-2.0)
{
left: {license: 'MIT'},
conjunction: 'and',
right: {license: 'GPL-2.0'}
},
[
{license: 'ISC'},
{license: 'GPL-2.0'}
]
))

assert(!satisfies(
// (MIT AND (GPL-2.0 OR ISC))
{
left: {license: 'MIT'},
conjunction: 'and',
right: {
left: {license: 'GPL-2.0'},
conjunction: 'or',
right: {license: 'ISC'}
}
},
[{license: 'MIT'}]
))

assert(!satisfies(
// (MIT OR Apache-2.0) AND (ISC OR GPL-2.0)
{
left: {
left: {license: 'MIT'},
conjunction: 'or',
right: {license: 'Apache-2.0'}
},
conjunction: 'and',
right: {
left: {license: 'ISC'},
conjunction: 'or',
right: {license: 'GPL-2.0'}
}
},
[{license: 'MIT'}]
))
```

The exported function does a few naive type checks on arguments. Do not rely on it for rigorous validation.

```javascript
assert.throws(function () {
satisfies('MIT', [parse('MIT')])
}, /first argument/)

assert.throws(function () {
satisfies({invalid: 'AST'}, [parse('MIT')])
}, /first argument/)

assert.throws(function () {
satisfies(parse('MIT'), parse('MIT'))
}, /second argument/)

assert.throws(function () {
satisfies(parse('MIT'), parse('MIT'))
}, /second argument/)

assert.throws(function () {
satisfies(parse('MIT'), [{invalid: 'leaf'}])
}, /second argument/)
```
84 changes: 28 additions & 56 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
var compare = require('spdx-compare')
var parse = require('spdx-expression-parse')
var ranges = require('spdx-ranges')

module.exports = function (first, second) {
if (!Array.isArray(second)) throw new Error('second argument must be an Array')
if (second.some(function (element) {
return !element.hasOwnProperty('license')
})) throw new Error('second argument must contain license expression AST leaves')
if (
!first.hasOwnProperty('license') &&
!first.hasOwnProperty('conjunction')
) throw new Error('first argument must be a license expression AST')
var terms = normalizeGPLIdentifiers(first)
var whitelist = second.map(function (element) {
return normalizeGPLIdentifiers(element)
})
return recurse(terms, whitelist)
}

var rangesAreCompatible = function (first, second) {
return (
first.license === second.license ||
Expand Down Expand Up @@ -79,60 +94,17 @@ function endsWith (string, substring) {
return string.indexOf(substring) === string.length - 1
}

function licenseString (e) {
if (e.hasOwnProperty('noassertion')) return 'NOASSERTION'
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
// together and the members are OR'd together. For example, `(MIT OR ISC) AND GPL-3.0` expands to
// `[[GPL-3.0 AND MIT], [ISC AND MIT]]`. Note that within each array of licenses, the entries are
// normalized (sorted) by license name.
function expand (expression) {
return sort(Array.from(expandInner(expression)))
}

// Flatten the given expression into an array of all licenses mentioned in the expression.
function flatten (expression) {
const expanded = Array.from(expandInner(expression))
const 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))
if (expression.conjunction === 'and') {
var left = expandInner(expression.left)
var right = expandInner(expression.right)
return left.reduce(function (result, l) {
right.forEach(function (r) { result.push(Object.assign({}, l, r)) })
return result
}, [])
function recurse (terms, whitelist) {
var conjunction = terms.conjunction
if (!conjunction) {
return whitelist.some(function (whitelisted) {
return licensesAreCompatible(terms, whitelisted)
})
} else if (conjunction === 'or') {
return recurse(terms.left, whitelist) || recurse(terms.right, whitelist)
} else if (conjunction === 'and') {
return recurse(terms.left, whitelist) && recurse(terms.right, whitelist)
} else {
throw new Error('invalid terms')
}
}

function sort (licenseList) {
var sortedLicenseLists = licenseList
.filter(function (e) { return Object.keys(e).length })
.map(function (e) { return Object.keys(e).sort() })
return sortedLicenseLists.map(function (list, i) {
return list.map(function (license) { return licenseList[i][license] })
})
}

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

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) })
}

module.exports = satisfies
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@
],
"dependencies": {
"spdx-compare": "^1.0.0",
"spdx-expression-parse": "^3.0.0",
"spdx-ranges": "^2.0.0"
},
"devDependencies": {
"defence-cli": "^2.0.1",
"replace-require-self": "^1.1.1",
"spdx-expression-parse": "^3.0.0",
"standard": "^11.0.0"
},
"keywords": [
Expand Down