diff --git a/classes/range.js b/classes/range.js index a7d6556..cc62092 100644 --- a/classes/range.js +++ b/classes/range.js @@ -143,10 +143,18 @@ class Range { if (loose) { // in loose mode, throw out any that are not valid comparators - rangeList = rangeList.filter(comp => { + const filtered = rangeList.filter(comp => { debug('loose invalid filter', comp, this.options) return !!comp.match(re[t.COMPARATORLOOSE]) }) + // dropping invalid comparators must not leave a range that matches + // everything: `* (4.1.x 4.1.2)` would collapse to the leftover `*`. + // if only the any comparator survives, treat the list as invalid. + if (filtered.length !== rangeList.length && + filtered.every(comp => comp === '')) { + return [] + } + rangeList = filtered } debug('range list', rangeList) diff --git a/test/classes/range.js b/test/classes/range.js index d012e50..4678a80 100644 --- a/test/classes/range.js +++ b/test/classes/range.js @@ -3,6 +3,7 @@ const { test } = require('tap') const Range = require('../../classes/range') const Comparator = require('../../classes/comparator') +const satisfies = require('../../functions/satisfies') const rangeIntersection = require('../fixtures/range-intersection.js') const rangeInclude = require('../fixtures/range-include.js') @@ -38,6 +39,25 @@ test('throw for empty comparator set, even in loose mode', t => { t.end() }) +test('loose mode does not drop comparators down to a match-everything range', t => { + // `(4.1.x` and `4.1.2)` used to be filtered out, collapsing the range to the + // `*` that was left over, so a typo silently matched every version + t.throws(() => new Range('* (4.1.x 4.1.2)', { loose: true }), + TypeError('Invalid SemVer Range: * (4.1.x 4.1.2)')) + t.equal(satisfies('9.9.9', '* (4.1.x 4.1.2)', { loose: true }), false, + 'typo range does not match an unrelated version') + t.equal(satisfies('4.1.2', '* (4.1.x 4.1.2)', { loose: true }), false, + 'typo range does not match the version it looks like it means') + // only the collapse to `*` is rejected. loose mode still tolerates junk + // sitting next to a comparator that does parse, and still tolerates a `||` + // branch that is invalid as a whole + t.equal(new Range('>=1.2.3 &&&', { loose: true }).range, '>=1.2.3', + 'junk beside a valid comparator is still dropped') + t.equal(new Range('1.2.3 || (foo)', { loose: true }).range, '1.2.3', + 'wholly invalid union branch is still tolerated') + t.end() +}) + test('convert comparator to range', t => { const c = new Comparator('>=1.2.3') const r = new Range(c) diff --git a/test/fixtures/range-parse.js b/test/fixtures/range-parse.js index 1c69c96..daab29f 100644 --- a/test/fixtures/range-parse.js +++ b/test/fixtures/range-parse.js @@ -148,4 +148,12 @@ module.exports = [ ['1.2.3+sha256.' + 'a'.repeat(200) + '.' + 'b'.repeat(200), '1.2.3', { loose: true }], ['1.2.3+' + 'a'.repeat(251) + ' || 2.0.0+' + 'b'.repeat(251), '1.2.3||2.0.0', { loose: true }], + // dropping unparseable comparators must not leave a match-everything range + ['* (4.1.x 4.1.2)', null], + ['* (4.1.x 4.1.2)', null, { loose: true }], + ['* (foo)', null, { loose: true }], + // but loose mode still tolerates junk next to a real comparator, and still + // tolerates a `||` branch that is invalid as a whole + ['>=1.2.3 &&&', '>=1.2.3', { loose: true }], + ['1.2.3 || (foo)', '1.2.3', { loose: true }], ]