From 1d37529091c09b17396ef21d86e9617e163e763f Mon Sep 17 00:00:00 2001 From: Joel Edwards Date: Fri, 12 Jan 2018 23:30:12 -0700 Subject: [PATCH] Cleaned up coerce behavior - followed the convention for including a regex - ignore digit sequences longer than 16 characters - explain expected behavior more carefully in README - update tests to reflect expected behavior --- README.md | 4 ++++ semver.js | 27 ++++++++++++++++--------- test/coerce.js | 55 +++++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 69 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 521aa458..951c5395 100644 --- a/README.md +++ b/README.md @@ -382,3 +382,7 @@ consumes all remaining characters which satisfy at least a partial semver Longer versions are simply truncated (`4.6.3.9.2-alpha2` becomes `4.6.3`). All surrounding text is simply ignored (`v3.4 replaces v3.3.1` becomes `3.4.0`). Only text which lacks digits will fail coercion (`version one` is not valid). +The maximum length for any semver component considered for coercion is 16 characters; +longer components will be ignored (`10000000000000000.4.7.4` becomes `4.7.4`). +The maximum value for any semver component is `Integer.MAX_SAFE_INTEGER || (2**53 - 1)`; +higher value components are invalid (`9999999999999999.4.7.4` is likely invalid). diff --git a/semver.js b/semver.js index e5e1312f..9cf9f6e5 100644 --- a/semver.js +++ b/semver.js @@ -21,6 +21,9 @@ exports.SEMVER_SPEC_VERSION = '2.0.0'; var MAX_LENGTH = 256; var MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991; +// Max safe segment length for coercion. +var MAX_SAFE_COMPONENT_LENGTH = 16; + // The actual regexps go on exports.re var re = exports.re = []; var src = exports.src = []; @@ -156,6 +159,15 @@ src[XRANGE] = '^' + src[GTLT] + '\\s*' + src[XRANGEPLAIN] + '$'; var XRANGELOOSE = R++; src[XRANGELOOSE] = '^' + src[GTLT] + '\\s*' + src[XRANGEPLAINLOOSE] + '$'; +// Coercion. +// Extract anything that could conceivably be a part of a valid semver +var COERCE = R++; +src[COERCE] = '(?:^|[^\\d])' + + '(\\d{1,' + MAX_SAFE_COMPONENT_LENGTH + '})' + + '(?:\\.(\\d{1,' + MAX_SAFE_COMPONENT_LENGTH + '}))?' + + '(?:\\.(\\d{1,' + MAX_SAFE_COMPONENT_LENGTH + '}))?' + + '(?:$|[^\\d])'; + // Tilde ranges. // Meaning is "reasonably at or greater than" var LONETILDE = R++; @@ -1298,20 +1310,15 @@ function intersects(r1, r2, loose) { exports.coerce = coerce; function coerce(version) { if (version instanceof SemVer) - return version + return version; if (typeof version !== 'string') - return null - - if (version.length > MAX_LENGTH) return null; - var match = version.match(/([vV]?\d+(?:[.]\d+){0,2})/) - if (match == null) - return null + var match = version.match(re[COERCE]); - var parts = match[0].split(/[.]/) - var semver = ['0', '0', '0'].map(function (ph, idx) { return parts[idx] || ph } ).join('.') + if (match == null) + return null; - return parse(semver) + return parse((match[1] || '0') + '.' + (match[2] || '0') + '.' + (match[3] || '0')); } diff --git a/test/coerce.js b/test/coerce.js index 73e408f6..86a7891b 100644 --- a/test/coerce.js +++ b/test/coerce.js @@ -2,11 +2,15 @@ var tap = require('tap'); var test = tap.test; var semver = require('../semver.js'); var coerce = semver.coerce; +var valid = semver.valid; -test('\ncoerce tests', function(t) { - var tooLong = '123' + '.1'.repeat(127); - var justRight = '12' + '.1'.repeat(127); +function r(text) { + return function (count) { + return text.repeat(count); + }; +} +test('\ncoerce tests', function(t) { // Expected to be null (cannot be coerced). [ null, @@ -15,13 +19,22 @@ test('\ncoerce tests', function(t) { '', '.', 'version one', - tooLong + r('9')(16), + r('1')(17), + 'a' + r('9')(16), + 'a' + r('1')(17), + r('9')(16) + 'a', + r('1')(17) + 'a', + r('9')(16) + '.4.7.4', + r('9')(16) + '.' + r('2')(16) + '.' + r('3')(16), + r('1')(16) + '.' + r('9')(16) + '.' + r('3')(16), + r('1')(16) + '.' + r('2')(16) + '.' + r('9')(16) ].forEach(function (input) { var msg = 'coerce(' + input + ') should be null' t.same(coerce(input), null, msg) }); - // Expected to be the same. + // Expected to be the valid. [ [semver.parse('1.2.3'), '1.2.3'], ['.1', '1.0.0'], @@ -49,6 +62,8 @@ test('\ncoerce tests', function(t) { ['v1.2', '1.2.0'], ['v1.2.3', '1.2.3'], ['v1.2.3.4', '1.2.3'], + [' 1', '1.0.0'], + ['1 ', '1.0.0'], ['1 0', '1.0.0'], ['1 1', '1.0.0'], ['1.1 1', '1.1.0'], @@ -65,7 +80,30 @@ test('\ncoerce tests', function(t) { ['v2', '2.0.0'], ['v3.4 replaces v3.3.1', '3.4.0'], ['4.6.3.9.2-alpha2', '4.6.3'], - [justRight, '12.1.1'] + [r('1')(17) + '.2', '2.0.0'], + [r('1')(17) + '.2.3', '2.3.0'], + ['1.' + r('2')(17) + '.3', '1.0.0'], + ['1.2.' + r('3')(17), '1.2.0'], + [r('1')(17) + '.2.3.4', '2.3.4'], + ['1.' + r('2')(17) + '.3.4', '1.0.0'], + ['1.2.' + r('3')(17) + '.4', '1.2.0'], + [r('1')(17) + '.' + r('2')(16) + '.' + r('3')(16), + r('2')(16) + '.' + r('3')(16) + '.0'], + [r('1')(16) + '.' + r('2')(17) + '.' + r('3')(16), + r('1')(16) + '.0.0'], + [r('1')(16) + '.' + r('2')(16) + '.' + r('3')(17), + r('1')(16) + '.' + r('2')(16) + '.0'], + ['11' + r('.1')(126), '11.1.1'], + [r('1')(16), r('1')(16) + '.0.0'], + ['a' + r('1')(16), r('1')(16) + '.0.0'], + [r('1')(16) + '.2.3.4', r('1')(16) + '.2.3'], + ['1.' + r('2')(16) + '.3.4', '1.' + r('2')(16) + '.3'], + ['1.2.' + r('3')(16) + '.4', '1.2.' + r('3')(16)], + [r('1')(16) + '.' + r('2')(16) + '.' + r('3')(16), + r('1')(16) + '.' + r('2')(16) + '.' + r('3')(16)], + ['1.2.3.' + r('4')(252) + '.5', '1.2.3'], + ['1.2.3.' + r('4')(1024), '1.2.3'], + [r('1')(17) + '.4.7.4', '4.7.4'] ].forEach(function (tuple) { var input = tuple[0] var expected = tuple[1] @@ -73,5 +111,8 @@ test('\ncoerce tests', function(t) { t.same((coerce(input) || {}).version, expected, msg) }); + t.same(valid(coerce('42.6.7.9.3-alpha')), '42.6.7') + t.same(valid(coerce('v2')), '2.0.0') + t.done(); -}); \ No newline at end of file +});