0
|
1 <?php
|
|
2
|
|
3 /*
|
|
4 * This file is part of composer/semver.
|
|
5 *
|
|
6 * (c) Composer <https://github.com/composer>
|
|
7 *
|
|
8 * For the full copyright and license information, please view
|
|
9 * the LICENSE file that was distributed with this source code.
|
|
10 */
|
|
11
|
|
12 namespace Composer\Semver;
|
|
13
|
|
14 use Composer\Semver\Constraint\ConstraintInterface;
|
|
15 use Composer\Semver\Constraint\EmptyConstraint;
|
|
16 use Composer\Semver\Constraint\MultiConstraint;
|
|
17 use Composer\Semver\Constraint\Constraint;
|
|
18
|
|
19 /**
|
|
20 * Version parser.
|
|
21 *
|
|
22 * @author Jordi Boggiano <j.boggiano@seld.be>
|
|
23 */
|
|
24 class VersionParser
|
|
25 {
|
|
26 /**
|
|
27 * Regex to match pre-release data (sort of).
|
|
28 *
|
|
29 * Due to backwards compatibility:
|
|
30 * - Instead of enforcing hyphen, an underscore, dot or nothing at all are also accepted.
|
|
31 * - Only stabilities as recognized by Composer are allowed to precede a numerical identifier.
|
|
32 * - Numerical-only pre-release identifiers are not supported, see tests.
|
|
33 *
|
|
34 * |--------------|
|
|
35 * [major].[minor].[patch] -[pre-release] +[build-metadata]
|
|
36 *
|
|
37 * @var string
|
|
38 */
|
|
39 private static $modifierRegex = '[._-]?(?:(stable|beta|b|RC|alpha|a|patch|pl|p)((?:[.-]?\d+)*+)?)?([.-]?dev)?';
|
|
40
|
|
41 /** @var array */
|
|
42 private static $stabilities = array('stable', 'RC', 'beta', 'alpha', 'dev');
|
|
43
|
|
44 /**
|
|
45 * Returns the stability of a version.
|
|
46 *
|
|
47 * @param string $version
|
|
48 *
|
|
49 * @return string
|
|
50 */
|
|
51 public static function parseStability($version)
|
|
52 {
|
|
53 $version = preg_replace('{#.+$}i', '', $version);
|
|
54
|
|
55 if ('dev-' === substr($version, 0, 4) || '-dev' === substr($version, -4)) {
|
|
56 return 'dev';
|
|
57 }
|
|
58
|
|
59 preg_match('{' . self::$modifierRegex . '(?:\+.*)?$}i', strtolower($version), $match);
|
|
60 if (!empty($match[3])) {
|
|
61 return 'dev';
|
|
62 }
|
|
63
|
|
64 if (!empty($match[1])) {
|
|
65 if ('beta' === $match[1] || 'b' === $match[1]) {
|
|
66 return 'beta';
|
|
67 }
|
|
68 if ('alpha' === $match[1] || 'a' === $match[1]) {
|
|
69 return 'alpha';
|
|
70 }
|
|
71 if ('rc' === $match[1]) {
|
|
72 return 'RC';
|
|
73 }
|
|
74 }
|
|
75
|
|
76 return 'stable';
|
|
77 }
|
|
78
|
|
79 /**
|
|
80 * @param string $stability
|
|
81 *
|
|
82 * @return string
|
|
83 */
|
|
84 public static function normalizeStability($stability)
|
|
85 {
|
|
86 $stability = strtolower($stability);
|
|
87
|
|
88 return $stability === 'rc' ? 'RC' : $stability;
|
|
89 }
|
|
90
|
|
91 /**
|
|
92 * Normalizes a version string to be able to perform comparisons on it.
|
|
93 *
|
|
94 * @param string $version
|
|
95 * @param string $fullVersion optional complete version string to give more context
|
|
96 *
|
|
97 * @throws \UnexpectedValueException
|
|
98 *
|
|
99 * @return string
|
|
100 */
|
|
101 public function normalize($version, $fullVersion = null)
|
|
102 {
|
|
103 $version = trim($version);
|
|
104 if (null === $fullVersion) {
|
|
105 $fullVersion = $version;
|
|
106 }
|
|
107
|
|
108 // strip off aliasing
|
|
109 if (preg_match('{^([^,\s]++) ++as ++([^,\s]++)$}', $version, $match)) {
|
|
110 $version = $match[1];
|
|
111 }
|
|
112
|
|
113 // match master-like branches
|
|
114 if (preg_match('{^(?:dev-)?(?:master|trunk|default)$}i', $version)) {
|
|
115 return '9999999-dev';
|
|
116 }
|
|
117
|
|
118 // if requirement is branch-like, use full name
|
|
119 if ('dev-' === strtolower(substr($version, 0, 4))) {
|
|
120 return 'dev-' . substr($version, 4);
|
|
121 }
|
|
122
|
|
123 // strip off build metadata
|
|
124 if (preg_match('{^([^,\s+]++)\+[^\s]++$}', $version, $match)) {
|
|
125 $version = $match[1];
|
|
126 }
|
|
127
|
|
128 // match classical versioning
|
|
129 if (preg_match('{^v?(\d{1,5})(\.\d++)?(\.\d++)?(\.\d++)?' . self::$modifierRegex . '$}i', $version, $matches)) {
|
|
130 $version = $matches[1]
|
|
131 . (!empty($matches[2]) ? $matches[2] : '.0')
|
|
132 . (!empty($matches[3]) ? $matches[3] : '.0')
|
|
133 . (!empty($matches[4]) ? $matches[4] : '.0');
|
|
134 $index = 5;
|
|
135 // match date(time) based versioning
|
|
136 } elseif (preg_match('{^v?(\d{4}(?:[.:-]?\d{2}){1,6}(?:[.:-]?\d{1,3})?)' . self::$modifierRegex . '$}i', $version, $matches)) {
|
|
137 $version = preg_replace('{\D}', '.', $matches[1]);
|
|
138 $index = 2;
|
|
139 }
|
|
140
|
|
141 // add version modifiers if a version was matched
|
|
142 if (isset($index)) {
|
|
143 if (!empty($matches[$index])) {
|
|
144 if ('stable' === $matches[$index]) {
|
|
145 return $version;
|
|
146 }
|
|
147 $version .= '-' . $this->expandStability($matches[$index]) . (!empty($matches[$index + 1]) ? ltrim($matches[$index + 1], '.-') : '');
|
|
148 }
|
|
149
|
|
150 if (!empty($matches[$index + 2])) {
|
|
151 $version .= '-dev';
|
|
152 }
|
|
153
|
|
154 return $version;
|
|
155 }
|
|
156
|
|
157 // match dev branches
|
|
158 if (preg_match('{(.*?)[.-]?dev$}i', $version, $match)) {
|
|
159 try {
|
|
160 return $this->normalizeBranch($match[1]);
|
|
161 } catch (\Exception $e) {
|
|
162 }
|
|
163 }
|
|
164
|
|
165 $extraMessage = '';
|
|
166 if (preg_match('{ +as +' . preg_quote($version) . '$}', $fullVersion)) {
|
|
167 $extraMessage = ' in "' . $fullVersion . '", the alias must be an exact version';
|
|
168 } elseif (preg_match('{^' . preg_quote($version) . ' +as +}', $fullVersion)) {
|
|
169 $extraMessage = ' in "' . $fullVersion . '", the alias source must be an exact version, if it is a branch name you should prefix it with dev-';
|
|
170 }
|
|
171
|
|
172 throw new \UnexpectedValueException('Invalid version string "' . $version . '"' . $extraMessage);
|
|
173 }
|
|
174
|
|
175 /**
|
|
176 * Extract numeric prefix from alias, if it is in numeric format, suitable for version comparison.
|
|
177 *
|
|
178 * @param string $branch Branch name (e.g. 2.1.x-dev)
|
|
179 *
|
|
180 * @return string|false Numeric prefix if present (e.g. 2.1.) or false
|
|
181 */
|
|
182 public function parseNumericAliasPrefix($branch)
|
|
183 {
|
|
184 if (preg_match('{^(?P<version>(\d++\\.)*\d++)(?:\.x)?-dev$}i', $branch, $matches)) {
|
|
185 return $matches['version'] . '.';
|
|
186 }
|
|
187
|
|
188 return false;
|
|
189 }
|
|
190
|
|
191 /**
|
|
192 * Normalizes a branch name to be able to perform comparisons on it.
|
|
193 *
|
|
194 * @param string $name
|
|
195 *
|
|
196 * @return string
|
|
197 */
|
|
198 public function normalizeBranch($name)
|
|
199 {
|
|
200 $name = trim($name);
|
|
201
|
|
202 if (in_array($name, array('master', 'trunk', 'default'))) {
|
|
203 return $this->normalize($name);
|
|
204 }
|
|
205
|
|
206 if (preg_match('{^v?(\d++)(\.(?:\d++|[xX*]))?(\.(?:\d++|[xX*]))?(\.(?:\d++|[xX*]))?$}i', $name, $matches)) {
|
|
207 $version = '';
|
|
208 for ($i = 1; $i < 5; ++$i) {
|
|
209 $version .= isset($matches[$i]) ? str_replace(array('*', 'X'), 'x', $matches[$i]) : '.x';
|
|
210 }
|
|
211
|
|
212 return str_replace('x', '9999999', $version) . '-dev';
|
|
213 }
|
|
214
|
|
215 return 'dev-' . $name;
|
|
216 }
|
|
217
|
|
218 /**
|
|
219 * Parses a constraint string into MultiConstraint and/or Constraint objects.
|
|
220 *
|
|
221 * @param string $constraints
|
|
222 *
|
|
223 * @return ConstraintInterface
|
|
224 */
|
|
225 public function parseConstraints($constraints)
|
|
226 {
|
|
227 $prettyConstraint = $constraints;
|
|
228
|
|
229 if (preg_match('{^([^,\s]*?)@(' . implode('|', self::$stabilities) . ')$}i', $constraints, $match)) {
|
|
230 $constraints = empty($match[1]) ? '*' : $match[1];
|
|
231 }
|
|
232
|
|
233 if (preg_match('{^(dev-[^,\s@]+?|[^,\s@]+?\.x-dev)#.+$}i', $constraints, $match)) {
|
|
234 $constraints = $match[1];
|
|
235 }
|
|
236
|
|
237 $orConstraints = preg_split('{\s*\|\|?\s*}', trim($constraints));
|
|
238 $orGroups = array();
|
|
239 foreach ($orConstraints as $constraints) {
|
|
240 $andConstraints = preg_split('{(?<!^|as|[=>< ,]) *(?<!-)[, ](?!-) *(?!,|as|$)}', $constraints);
|
|
241 if (count($andConstraints) > 1) {
|
|
242 $constraintObjects = array();
|
|
243 foreach ($andConstraints as $constraint) {
|
|
244 foreach ($this->parseConstraint($constraint) as $parsedConstraint) {
|
|
245 $constraintObjects[] = $parsedConstraint;
|
|
246 }
|
|
247 }
|
|
248 } else {
|
|
249 $constraintObjects = $this->parseConstraint($andConstraints[0]);
|
|
250 }
|
|
251
|
|
252 if (1 === count($constraintObjects)) {
|
|
253 $constraint = $constraintObjects[0];
|
|
254 } else {
|
|
255 $constraint = new MultiConstraint($constraintObjects);
|
|
256 }
|
|
257
|
|
258 $orGroups[] = $constraint;
|
|
259 }
|
|
260
|
|
261 if (1 === count($orGroups)) {
|
|
262 $constraint = $orGroups[0];
|
|
263 } elseif (2 === count($orGroups)
|
|
264 // parse the two OR groups and if they are contiguous we collapse
|
|
265 // them into one constraint
|
|
266 && $orGroups[0] instanceof MultiConstraint
|
|
267 && $orGroups[1] instanceof MultiConstraint
|
|
268 && 2 === count($orGroups[0]->getConstraints())
|
|
269 && 2 === count($orGroups[1]->getConstraints())
|
|
270 && ($a = (string) $orGroups[0])
|
|
271 && substr($a, 0, 3) === '[>=' && (false !== ($posA = strpos($a, '<', 4)))
|
|
272 && ($b = (string) $orGroups[1])
|
|
273 && substr($b, 0, 3) === '[>=' && (false !== ($posB = strpos($b, '<', 4)))
|
|
274 && substr($a, $posA + 2, -1) === substr($b, 4, $posB - 5)
|
|
275 ) {
|
|
276 $constraint = new MultiConstraint(array(
|
|
277 new Constraint('>=', substr($a, 4, $posA - 5)),
|
|
278 new Constraint('<', substr($b, $posB + 2, -1)),
|
|
279 ));
|
|
280 } else {
|
|
281 $constraint = new MultiConstraint($orGroups, false);
|
|
282 }
|
|
283
|
|
284 $constraint->setPrettyString($prettyConstraint);
|
|
285
|
|
286 return $constraint;
|
|
287 }
|
|
288
|
|
289 /**
|
|
290 * @param string $constraint
|
|
291 *
|
|
292 * @throws \UnexpectedValueException
|
|
293 *
|
|
294 * @return array
|
|
295 */
|
|
296 private function parseConstraint($constraint)
|
|
297 {
|
|
298 if (preg_match('{^([^,\s]+?)@(' . implode('|', self::$stabilities) . ')$}i', $constraint, $match)) {
|
|
299 $constraint = $match[1];
|
|
300 if ($match[2] !== 'stable') {
|
|
301 $stabilityModifier = $match[2];
|
|
302 }
|
|
303 }
|
|
304
|
|
305 if (preg_match('{^v?[xX*](\.[xX*])*$}i', $constraint)) {
|
|
306 return array(new EmptyConstraint());
|
|
307 }
|
|
308
|
|
309 $versionRegex = 'v?(\d++)(?:\.(\d++))?(?:\.(\d++))?(?:\.(\d++))?' . self::$modifierRegex . '(?:\+[^\s]+)?';
|
|
310
|
|
311 // Tilde Range
|
|
312 //
|
|
313 // Like wildcard constraints, unsuffixed tilde constraints say that they must be greater than the previous
|
|
314 // version, to ensure that unstable instances of the current version are allowed. However, if a stability
|
|
315 // suffix is added to the constraint, then a >= match on the current version is used instead.
|
|
316 if (preg_match('{^~>?' . $versionRegex . '$}i', $constraint, $matches)) {
|
|
317 if (substr($constraint, 0, 2) === '~>') {
|
|
318 throw new \UnexpectedValueException(
|
|
319 'Could not parse version constraint ' . $constraint . ': ' .
|
|
320 'Invalid operator "~>", you probably meant to use the "~" operator'
|
|
321 );
|
|
322 }
|
|
323
|
|
324 // Work out which position in the version we are operating at
|
|
325 if (isset($matches[4]) && '' !== $matches[4]) {
|
|
326 $position = 4;
|
|
327 } elseif (isset($matches[3]) && '' !== $matches[3]) {
|
|
328 $position = 3;
|
|
329 } elseif (isset($matches[2]) && '' !== $matches[2]) {
|
|
330 $position = 2;
|
|
331 } else {
|
|
332 $position = 1;
|
|
333 }
|
|
334
|
|
335 // Calculate the stability suffix
|
|
336 $stabilitySuffix = '';
|
|
337 if (!empty($matches[5])) {
|
|
338 $stabilitySuffix .= '-' . $this->expandStability($matches[5]) . (!empty($matches[6]) ? $matches[6] : '');
|
|
339 }
|
|
340
|
|
341 if (!empty($matches[7])) {
|
|
342 $stabilitySuffix .= '-dev';
|
|
343 }
|
|
344
|
|
345 if (!$stabilitySuffix) {
|
|
346 $stabilitySuffix = '-dev';
|
|
347 }
|
|
348
|
|
349 $lowVersion = $this->manipulateVersionString($matches, $position, 0) . $stabilitySuffix;
|
|
350 $lowerBound = new Constraint('>=', $lowVersion);
|
|
351
|
|
352 // For upper bound, we increment the position of one more significance,
|
|
353 // but highPosition = 0 would be illegal
|
|
354 $highPosition = max(1, $position - 1);
|
|
355 $highVersion = $this->manipulateVersionString($matches, $highPosition, 1) . '-dev';
|
|
356 $upperBound = new Constraint('<', $highVersion);
|
|
357
|
|
358 return array(
|
|
359 $lowerBound,
|
|
360 $upperBound,
|
|
361 );
|
|
362 }
|
|
363
|
|
364 // Caret Range
|
|
365 //
|
|
366 // Allows changes that do not modify the left-most non-zero digit in the [major, minor, patch] tuple.
|
|
367 // In other words, this allows patch and minor updates for versions 1.0.0 and above, patch updates for
|
|
368 // versions 0.X >=0.1.0, and no updates for versions 0.0.X
|
|
369 if (preg_match('{^\^' . $versionRegex . '($)}i', $constraint, $matches)) {
|
|
370 // Work out which position in the version we are operating at
|
|
371 if ('0' !== $matches[1] || '' === $matches[2]) {
|
|
372 $position = 1;
|
|
373 } elseif ('0' !== $matches[2] || '' === $matches[3]) {
|
|
374 $position = 2;
|
|
375 } else {
|
|
376 $position = 3;
|
|
377 }
|
|
378
|
|
379 // Calculate the stability suffix
|
|
380 $stabilitySuffix = '';
|
|
381 if (empty($matches[5]) && empty($matches[7])) {
|
|
382 $stabilitySuffix .= '-dev';
|
|
383 }
|
|
384
|
|
385 $lowVersion = $this->normalize(substr($constraint . $stabilitySuffix, 1));
|
|
386 $lowerBound = new Constraint('>=', $lowVersion);
|
|
387
|
|
388 // For upper bound, we increment the position of one more significance,
|
|
389 // but highPosition = 0 would be illegal
|
|
390 $highVersion = $this->manipulateVersionString($matches, $position, 1) . '-dev';
|
|
391 $upperBound = new Constraint('<', $highVersion);
|
|
392
|
|
393 return array(
|
|
394 $lowerBound,
|
|
395 $upperBound,
|
|
396 );
|
|
397 }
|
|
398
|
|
399 // X Range
|
|
400 //
|
|
401 // Any of X, x, or * may be used to "stand in" for one of the numeric values in the [major, minor, patch] tuple.
|
|
402 // A partial version range is treated as an X-Range, so the special character is in fact optional.
|
|
403 if (preg_match('{^v?(\d++)(?:\.(\d++))?(?:\.(\d++))?(?:\.[xX*])++$}', $constraint, $matches)) {
|
|
404 if (isset($matches[3]) && '' !== $matches[3]) {
|
|
405 $position = 3;
|
|
406 } elseif (isset($matches[2]) && '' !== $matches[2]) {
|
|
407 $position = 2;
|
|
408 } else {
|
|
409 $position = 1;
|
|
410 }
|
|
411
|
|
412 $lowVersion = $this->manipulateVersionString($matches, $position) . '-dev';
|
|
413 $highVersion = $this->manipulateVersionString($matches, $position, 1) . '-dev';
|
|
414
|
|
415 if ($lowVersion === '0.0.0.0-dev') {
|
|
416 return array(new Constraint('<', $highVersion));
|
|
417 }
|
|
418
|
|
419 return array(
|
|
420 new Constraint('>=', $lowVersion),
|
|
421 new Constraint('<', $highVersion),
|
|
422 );
|
|
423 }
|
|
424
|
|
425 // Hyphen Range
|
|
426 //
|
|
427 // Specifies an inclusive set. If a partial version is provided as the first version in the inclusive range,
|
|
428 // then the missing pieces are replaced with zeroes. If a partial version is provided as the second version in
|
|
429 // the inclusive range, then all versions that start with the supplied parts of the tuple are accepted, but
|
|
430 // nothing that would be greater than the provided tuple parts.
|
|
431 if (preg_match('{^(?P<from>' . $versionRegex . ') +- +(?P<to>' . $versionRegex . ')($)}i', $constraint, $matches)) {
|
|
432 // Calculate the stability suffix
|
|
433 $lowStabilitySuffix = '';
|
|
434 if (empty($matches[6]) && empty($matches[8])) {
|
|
435 $lowStabilitySuffix = '-dev';
|
|
436 }
|
|
437
|
|
438 $lowVersion = $this->normalize($matches['from']);
|
|
439 $lowerBound = new Constraint('>=', $lowVersion . $lowStabilitySuffix);
|
|
440
|
|
441 $empty = function ($x) {
|
|
442 return ($x === 0 || $x === '0') ? false : empty($x);
|
|
443 };
|
|
444
|
|
445 if ((!$empty($matches[11]) && !$empty($matches[12])) || !empty($matches[14]) || !empty($matches[16])) {
|
|
446 $highVersion = $this->normalize($matches['to']);
|
|
447 $upperBound = new Constraint('<=', $highVersion);
|
|
448 } else {
|
|
449 $highMatch = array('', $matches[10], $matches[11], $matches[12], $matches[13]);
|
|
450 $highVersion = $this->manipulateVersionString($highMatch, $empty($matches[11]) ? 1 : 2, 1) . '-dev';
|
|
451 $upperBound = new Constraint('<', $highVersion);
|
|
452 }
|
|
453
|
|
454 return array(
|
|
455 $lowerBound,
|
|
456 $upperBound,
|
|
457 );
|
|
458 }
|
|
459
|
|
460 // Basic Comparators
|
|
461 if (preg_match('{^(<>|!=|>=?|<=?|==?)?\s*(.*)}', $constraint, $matches)) {
|
|
462 try {
|
|
463 $version = $this->normalize($matches[2]);
|
|
464
|
|
465 if (!empty($stabilityModifier) && $this->parseStability($version) === 'stable') {
|
|
466 $version .= '-' . $stabilityModifier;
|
|
467 } elseif ('<' === $matches[1] || '>=' === $matches[1]) {
|
|
468 if (!preg_match('/-' . self::$modifierRegex . '$/', strtolower($matches[2]))) {
|
|
469 if (substr($matches[2], 0, 4) !== 'dev-') {
|
|
470 $version .= '-dev';
|
|
471 }
|
|
472 }
|
|
473 }
|
|
474
|
|
475 return array(new Constraint($matches[1] ?: '=', $version));
|
|
476 } catch (\Exception $e) {
|
|
477 }
|
|
478 }
|
|
479
|
|
480 $message = 'Could not parse version constraint ' . $constraint;
|
|
481 if (isset($e)) {
|
|
482 $message .= ': ' . $e->getMessage();
|
|
483 }
|
|
484
|
|
485 throw new \UnexpectedValueException($message);
|
|
486 }
|
|
487
|
|
488 /**
|
|
489 * Increment, decrement, or simply pad a version number.
|
|
490 *
|
|
491 * Support function for {@link parseConstraint()}
|
|
492 *
|
|
493 * @param array $matches Array with version parts in array indexes 1,2,3,4
|
|
494 * @param int $position 1,2,3,4 - which segment of the version to increment/decrement
|
|
495 * @param int $increment
|
|
496 * @param string $pad The string to pad version parts after $position
|
|
497 *
|
|
498 * @return string The new version
|
|
499 */
|
|
500 private function manipulateVersionString($matches, $position, $increment = 0, $pad = '0')
|
|
501 {
|
|
502 for ($i = 4; $i > 0; --$i) {
|
|
503 if ($i > $position) {
|
|
504 $matches[$i] = $pad;
|
|
505 } elseif ($i === $position && $increment) {
|
|
506 $matches[$i] += $increment;
|
|
507 // If $matches[$i] was 0, carry the decrement
|
|
508 if ($matches[$i] < 0) {
|
|
509 $matches[$i] = $pad;
|
|
510 --$position;
|
|
511
|
|
512 // Return null on a carry overflow
|
|
513 if ($i === 1) {
|
|
514 return;
|
|
515 }
|
|
516 }
|
|
517 }
|
|
518 }
|
|
519
|
|
520 return $matches[1] . '.' . $matches[2] . '.' . $matches[3] . '.' . $matches[4];
|
|
521 }
|
|
522
|
|
523 /**
|
|
524 * Expand shorthand stability string to long version.
|
|
525 *
|
|
526 * @param string $stability
|
|
527 *
|
|
528 * @return string
|
|
529 */
|
|
530 private function expandStability($stability)
|
|
531 {
|
|
532 $stability = strtolower($stability);
|
|
533
|
|
534 switch ($stability) {
|
|
535 case 'a':
|
|
536 return 'alpha';
|
|
537 case 'b':
|
|
538 return 'beta';
|
|
539 case 'p':
|
|
540 case 'pl':
|
|
541 return 'patch';
|
|
542 case 'rc':
|
|
543 return 'RC';
|
|
544 default:
|
|
545 return $stability;
|
|
546 }
|
|
547 }
|
|
548 }
|