VersionParser.php 22 KB

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