VersionParser.php 21 KB

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