VersionParser.php 19 KB

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