diff --git a/README.md b/README.md index 571fa20..78c3787 100644 --- a/README.md +++ b/README.md @@ -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/) ``` diff --git a/index.js b/index.js index f6c5a37..f3d22b9 100644 --- a/index.js +++ b/index.js @@ -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 || @@ -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 diff --git a/package.json b/package.json index 31db82c..57f98d8 100644 --- a/package.json +++ b/package.json @@ -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": [