VersionParser.php 19 KB

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