PageRenderTime 25ms CodeModel.GetById 33ms RepoModel.GetById 0ms app.codeStats 0ms

/plugins/CMS/src/Core/Package/Composer/Package/Version/VersionParser.php

http://github.com/QuickAppsCMS/QuickApps-CMS
PHP | 579 lines | 378 code | 75 blank | 126 comment | 92 complexity | 10c3011e4a102d5e6bf169b16df112a1 MD5 | raw file
Possible License(s): LGPL-2.1, MPL-2.0-no-copyleft-exception, GPL-3.0
  1. <?php
  2. /**
  3. * Licensed under The GPL-3.0 License
  4. * For full copyright and license information, please see the LICENSE.txt
  5. * Redistributions of files must retain the above copyright notice.
  6. *
  7. * @since 1.0.0
  8. * @author Christopher Castro <chris@quickapps.es>
  9. * @link http://www.quickappscms.org
  10. * @license http://opensource.org/licenses/gpl-3.0.html GPL-3.0 License
  11. */
  12. namespace CMS\Core\Package\Composer\Package\Version;
  13. use CMS\Core\Package\Composer\Package\BasePackage;
  14. use CMS\Core\Package\Composer\Package\Link;
  15. use CMS\Core\Package\Composer\Package\LinkConstraint\EmptyConstraint;
  16. use CMS\Core\Package\Composer\Package\LinkConstraint\MultiConstraint;
  17. use CMS\Core\Package\Composer\Package\LinkConstraint\VersionConstraint;
  18. use CMS\Core\Package\Composer\Package\PackageInterface;
  19. /**
  20. * Version parser
  21. *
  22. * @author Jordi Boggiano <j.boggiano@seld.be>
  23. */
  24. class VersionParser
  25. {
  26. private static $modifierRegex = '[._-]?(?:(stable|beta|b|RC|alpha|a|patch|pl|p)(?:[.-]?(\d+))?)?([.-]?dev)?';
  27. /**
  28. * Returns the stability of a version
  29. *
  30. * @param string $version Version
  31. * @return string
  32. */
  33. public static function parseStability($version)
  34. {
  35. $version = preg_replace('{#.+$}i', '', $version);
  36. if ('dev-' === substr($version, 0, 4) || '-dev' === substr($version, -4)) {
  37. return 'dev';
  38. }
  39. preg_match('{' . self::$modifierRegex . '$}i', strtolower($version), $match);
  40. if (!empty($match[3])) {
  41. return 'dev';
  42. }
  43. if (!empty($match[1])) {
  44. if ('beta' === $match[1] || 'b' === $match[1]) {
  45. return 'beta';
  46. }
  47. if ('alpha' === $match[1] || 'a' === $match[1]) {
  48. return 'alpha';
  49. }
  50. if ('rc' === $match[1]) {
  51. return 'RC';
  52. }
  53. }
  54. return 'stable';
  55. }
  56. /**
  57. * normalizeStability().
  58. *
  59. * @param string $stability stability
  60. * @return string
  61. */
  62. public static function normalizeStability($stability)
  63. {
  64. $stability = strtolower($stability);
  65. return $stability === 'rc' ? 'RC' : $stability;
  66. }
  67. /**
  68. * formatVersion().
  69. *
  70. * @param PackageInterface $package Package
  71. * @param bool $truncate Whether to truncate or not
  72. * @return string
  73. */
  74. public static function formatVersion(PackageInterface $package, $truncate = true)
  75. {
  76. if (!$package->isDev() || !in_array($package->getSourceType(), ['hg', 'git'])) {
  77. return $package->getPrettyVersion();
  78. }
  79. // if source reference is a sha1 hash -- truncate
  80. if ($truncate && strlen($package->getSourceReference()) === 40) {
  81. return $package->getPrettyVersion() . ' ' . substr($package->getSourceReference(), 0, 7);
  82. }
  83. return $package->getPrettyVersion() . ' ' . $package->getSourceReference();
  84. }
  85. /**
  86. * Normalizes a version string to be able to perform comparisons on it
  87. *
  88. * @param string $version Version
  89. * @param string $fullVersion optional complete version string to give more context
  90. * @throws \UnexpectedValueException
  91. * @return string
  92. */
  93. public function normalize($version, $fullVersion = null)
  94. {
  95. $version = trim($version);
  96. if (null === $fullVersion) {
  97. $fullVersion = $version;
  98. }
  99. // ignore aliases and just assume the alias is required instead of the source
  100. if (preg_match('{^([^,\s]+) +as +([^,\s]+)$}', $version, $match)) {
  101. $version = $match[1];
  102. }
  103. // ignore build metadata
  104. if (preg_match('{^([^,\s+]+)\+[^\s]+$}', $version, $match)) {
  105. $version = $match[1];
  106. }
  107. // match master-like branches
  108. if (preg_match('{^(?:dev-)?(?:master|trunk|default)$}i', $version)) {
  109. return '9999999-dev';
  110. }
  111. if ('dev-' === strtolower(substr($version, 0, 4))) {
  112. return 'dev-' . substr($version, 4);
  113. }
  114. // match classical versioning
  115. if (preg_match('{^v?(\d{1,3})(\.\d+)?(\.\d+)?(\.\d+)?' . self::$modifierRegex . '$}i', $version, $matches)) {
  116. $version = $matches[1] . (!empty($matches[2]) ? $matches[2] : '.0') . (!empty($matches[3]) ? $matches[3] : '.0') . (!empty($matches[4]) ? $matches[4] : '.0');
  117. $index = 5;
  118. } elseif (preg_match('{^v?(\d{4}(?:[.:-]?\d{2}){1,6}(?:[.:-]?\d{1,3})?)' . self::$modifierRegex . '$}i', $version, $matches)) {
  119. // match date-based versioning
  120. $version = preg_replace('{\D}', '-', $matches[1]);
  121. $index = 2;
  122. } elseif (preg_match('{^v?(\d{4,})(\.\d+)?(\.\d+)?(\.\d+)?' . self::$modifierRegex . '$}i', $version, $matches)) {
  123. $version = $matches[1] . (!empty($matches[2]) ? $matches[2] : '.0') . (!empty($matches[3]) ? $matches[3] : '.0') . (!empty($matches[4]) ? $matches[4] : '.0');
  124. $index = 5;
  125. }
  126. // add version modifiers if a version was matched
  127. if (isset($index)) {
  128. if (!empty($matches[$index])) {
  129. if ('stable' === $matches[$index]) {
  130. return $version;
  131. }
  132. $version .= '-' . $this->expandStability($matches[$index]) . (!empty($matches[$index + 1]) ? $matches[$index + 1] : '');
  133. }
  134. if (!empty($matches[$index + 2])) {
  135. $version .= '-dev';
  136. }
  137. return $version;
  138. }
  139. // match dev branches
  140. if (preg_match('{(.*?)[.-]?dev$}i', $version, $match)) {
  141. try {
  142. return $this->normalizeBranch($match[1]);
  143. } catch (\Exception $e) {
  144. }
  145. }
  146. $extraMessage = '';
  147. if (preg_match('{ +as +' . preg_quote($version) . '$}', $fullVersion)) {
  148. $extraMessage = ' in "' . $fullVersion . '", the alias must be an exact version';
  149. } elseif (preg_match('{^' . preg_quote($version) . ' +as +}', $fullVersion)) {
  150. $extraMessage = ' in "' . $fullVersion . '", the alias source must be an exact version, if it is a branch name you should prefix it with dev-';
  151. }
  152. throw new \UnexpectedValueException('Invalid version string "' . $version . '"' . $extraMessage);
  153. }
  154. /**
  155. * Extract numeric prefix from alias, if it is in numeric format, suitable for
  156. * version comparison
  157. *
  158. * @param string $branch Branch name (e.g. 2.1.x-dev)
  159. * @return string|false Numeric prefix if present (e.g. 2.1.) or false
  160. */
  161. public function parseNumericAliasPrefix($branch)
  162. {
  163. if (preg_match('/^(?P<version>(\d+\\.)*\d+)(?:\.x)?-dev$/i', $branch, $matches)) {
  164. return $matches['version'] . ".";
  165. }
  166. return false;
  167. }
  168. /**
  169. * Normalizes a branch name to be able to perform comparisons on it
  170. *
  171. * @param string $name Name
  172. * @return string
  173. */
  174. public function normalizeBranch($name)
  175. {
  176. $name = trim($name);
  177. if (in_array($name, [
  178. 'master',
  179. 'trunk',
  180. 'default'
  181. ])) {
  182. return $this->normalize($name);
  183. }
  184. if (preg_match('#^v?(\d+)(\.(?:\d+|[xX*]))?(\.(?:\d+|[xX*]))?(\.(?:\d+|[xX*]))?$#i', $name, $matches)) {
  185. $version = '';
  186. for ($i = 1; $i < 5; $i++) {
  187. $version .= isset($matches[$i]) ? str_replace([
  188. '*',
  189. 'X'
  190. ], 'x', $matches[$i]) : '.x';
  191. }
  192. return str_replace('x', '9999999', $version) . '-dev';
  193. }
  194. return 'dev-' . $name;
  195. }
  196. /**
  197. * @param string $source source package name
  198. * @param string $sourceVersion source package version (pretty version ideally)
  199. * @param string $description link description (e.g. requires, replaces, ..)
  200. * @param array $links array of package name => constraint mappings
  201. * @return Link[]
  202. */
  203. public function parseLinks($source, $sourceVersion, $description, $links)
  204. {
  205. $res = [];
  206. foreach ($links as $target => $constraint) {
  207. if ('self.version' === $constraint) {
  208. $parsedConstraint = $this->parseConstraints($sourceVersion);
  209. } else {
  210. $parsedConstraint = $this->parseConstraints($constraint);
  211. }
  212. $res[strtolower($target)] = new Link($source, $target, $parsedConstraint, $description, $constraint);
  213. }
  214. return $res;
  215. }
  216. /**
  217. * Parses as constraint string into LinkConstraint objects
  218. *
  219. * @param string $constraints Constraints
  220. * @return \Composer\Package\LinkConstraint\LinkConstraintInterface
  221. */
  222. public function parseConstraints($constraints)
  223. {
  224. $prettyConstraint = $constraints;
  225. if (preg_match('{^([^,\s]*?)@(' . implode('|', array_keys(BasePackage::$stabilities)) . ')$}i', $constraints, $match)) {
  226. $constraints = empty($match[1]) ? '*' : $match[1];
  227. }
  228. if (preg_match('{^(dev-[^,\s@]+?|[^,\s@]+?\.x-dev)#.+$}i', $constraints, $match)) {
  229. $constraints = $match[1];
  230. }
  231. $orConstraints = preg_split('{\s*\|\|?\s*}', trim($constraints));
  232. $orGroups = [];
  233. foreach ($orConstraints as $constraints) {
  234. $andConstraints = preg_split('{(?<!^|as|[=>< ,]) *(?<!-)[, ](?!-) *(?!,|as|$)}', $constraints);
  235. if (count($andConstraints) > 1) {
  236. $constraintObjects = [];
  237. foreach ($andConstraints as $constraint) {
  238. $constraintObjects = array_merge($constraintObjects, $this->parseConstraint($constraint));
  239. }
  240. } else {
  241. $constraintObjects = $this->parseConstraint($andConstraints[0]);
  242. }
  243. if (1 === count($constraintObjects)) {
  244. $constraint = $constraintObjects[0];
  245. } else {
  246. $constraint = new MultiConstraint($constraintObjects);
  247. }
  248. $orGroups[] = $constraint;
  249. }
  250. if (1 === count($orGroups)) {
  251. $constraint = $orGroups[0];
  252. } else {
  253. $constraint = new MultiConstraint($orGroups, false);
  254. }
  255. $constraint->setPrettyString($prettyConstraint);
  256. return $constraint;
  257. }
  258. /**
  259. * parseConstraint().
  260. *
  261. * @param string $constraint Constraint
  262. * @return array
  263. */
  264. public function parseConstraint($constraint)
  265. {
  266. if (preg_match('{^([^,\s]+?)@(' . implode('|', array_keys(BasePackage::$stabilities)) . ')$}i', $constraint, $match)) {
  267. $constraint = $match[1];
  268. if ($match[2] !== 'stable') {
  269. $stabilityModifier = $match[2];
  270. }
  271. }
  272. if (preg_match('{^[xX*](\.[xX*])*$}i', $constraint)) {
  273. return [
  274. new EmptyConstraint
  275. ];
  276. }
  277. $versionRegex = '(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?' . self::$modifierRegex;
  278. // match tilde constraints
  279. // like wildcard constraints, unsuffixed tilde constraints say that they must be greater than the previous
  280. // version, to ensure that unstable instances of the current version are allowed.
  281. // however, if a stability suffix is added to the constraint, then a >= match on the current version is
  282. // used instead
  283. if (preg_match('{^~>?' . $versionRegex . '$}i', $constraint, $matches)) {
  284. if (substr($constraint, 0, 2) === '~>') {
  285. throw new \UnexpectedValueException('Could not parse version constraint ' . $constraint . ': ' . 'Invalid operator "~>", you probably meant to use the "~" operator');
  286. }
  287. // Work out which position in the version we are operating at
  288. if (isset($matches[4]) && '' !== $matches[4]) {
  289. $position = 4;
  290. } elseif (isset($matches[3]) && '' !== $matches[3]) {
  291. $position = 3;
  292. } elseif (isset($matches[2]) && '' !== $matches[2]) {
  293. $position = 2;
  294. } else {
  295. $position = 1;
  296. }
  297. // Calculate the stability suffix
  298. $stabilitySuffix = '';
  299. if (!empty($matches[5])) {
  300. $stabilitySuffix .= '-' . $this->expandStability($matches[5]) . (!empty($matches[6]) ? $matches[6] : '');
  301. }
  302. if (!empty($matches[7])) {
  303. $stabilitySuffix .= '-dev';
  304. }
  305. if (!$stabilitySuffix) {
  306. $stabilitySuffix = "-dev";
  307. }
  308. $lowVersion = $this->manipulateVersionString($matches, $position, 0) . $stabilitySuffix;
  309. $lowerBound = new VersionConstraint('>=', $lowVersion);
  310. // For upper bound, we increment the position of one more significance,
  311. // but highPosition = 0 would be illegal
  312. $highPosition = max(1, $position - 1);
  313. $highVersion = $this->manipulateVersionString($matches, $highPosition, 1) . '-dev';
  314. $upperBound = new VersionConstraint('<', $highVersion);
  315. return [
  316. $lowerBound,
  317. $upperBound
  318. ];
  319. }
  320. // match caret constraints
  321. if (preg_match('{^\^' . $versionRegex . '($)}i', $constraint, $matches)) {
  322. // Work out which position in the version we are operating at
  323. if ('0' !== $matches[1] || '' === $matches[2]) {
  324. $position = 1;
  325. } elseif ('0' !== $matches[2] || '' === $matches[3]) {
  326. $position = 2;
  327. } else {
  328. $position = 3;
  329. }
  330. // Calculate the stability suffix
  331. $stabilitySuffix = '';
  332. if (empty($matches[5]) && empty($matches[7])) {
  333. $stabilitySuffix .= '-dev';
  334. }
  335. $lowVersion = $this->normalize(substr($constraint . $stabilitySuffix, 1));
  336. $lowerBound = new VersionConstraint('>=', $lowVersion);
  337. // For upper bound, we increment the position of one more significance,
  338. // but highPosition = 0 would be illegal
  339. $highVersion = $this->manipulateVersionString($matches, $position, 1) . '-dev';
  340. $upperBound = new VersionConstraint('<', $highVersion);
  341. return [
  342. $lowerBound,
  343. $upperBound
  344. ];
  345. }
  346. // match wildcard constraints
  347. if (preg_match('{^(\d+)(?:\.(\d+))?(?:\.(\d+))?\.[xX*]$}', $constraint, $matches)) {
  348. if (isset($matches[3]) && '' !== $matches[3]) {
  349. $position = 3;
  350. } elseif (isset($matches[2]) && '' !== $matches[2]) {
  351. $position = 2;
  352. } else {
  353. $position = 1;
  354. }
  355. $lowVersion = $this->manipulateVersionString($matches, $position) . "-dev";
  356. $highVersion = $this->manipulateVersionString($matches, $position, 1) . "-dev";
  357. if ($lowVersion === "0.0.0.0-dev") {
  358. return [
  359. new VersionConstraint('<', $highVersion)
  360. ];
  361. }
  362. return [
  363. new VersionConstraint('>=', $lowVersion),
  364. new VersionConstraint('<', $highVersion)
  365. ];
  366. }
  367. // match hyphen constraints
  368. if (preg_match('{^(?P<from>' . $versionRegex . ') +- +(?P<to>' . $versionRegex . ')($)}i', $constraint, $matches)) {
  369. // Calculate the stability suffix
  370. $lowStabilitySuffix = '';
  371. if (empty($matches[6]) && empty($matches[8])) {
  372. $lowStabilitySuffix = '-dev';
  373. }
  374. $lowVersion = $this->normalize($matches['from']);
  375. $lowerBound = new VersionConstraint('>=', $lowVersion . $lowStabilitySuffix);
  376. $highVersion = $matches[10];
  377. if ((!empty($matches[11]) && !empty($matches[12])) || !empty($matches[14]) || !empty($matches[16])) {
  378. $highVersion = $this->normalize($matches['to']);
  379. $upperBound = new VersionConstraint('<=', $highVersion);
  380. } else {
  381. $highMatch = [
  382. '',
  383. $matches[10],
  384. $matches[11],
  385. $matches[12],
  386. $matches[13]
  387. ];
  388. $highVersion = $this->manipulateVersionString($highMatch, empty($matches[11]) ? 1 : 2, 1) . '-dev';
  389. $upperBound = new VersionConstraint('<', $highVersion);
  390. }
  391. return [
  392. $lowerBound,
  393. $upperBound
  394. ];
  395. }
  396. // match operators constraints
  397. if (preg_match('{^(<>|!=|>=?|<=?|==?)?\s*(.*)}', $constraint, $matches)) {
  398. try {
  399. $version = $this->normalize($matches[2]);
  400. if (!empty($stabilityModifier) && $this->parseStability($version) === 'stable') {
  401. $version .= '-' . $stabilityModifier;
  402. } elseif ('<' === $matches[1]) {
  403. if (!preg_match('/-' . self::$modifierRegex . '$/', strtolower($matches[2]))) {
  404. $version .= '-dev';
  405. }
  406. }
  407. return [
  408. new VersionConstraint($matches[1] ?: '=', $version)
  409. ];
  410. } catch (\Exception $e) {
  411. // no-catch
  412. }
  413. }
  414. $message = 'Could not parse version constraint ' . $constraint;
  415. if (isset($e)) {
  416. $message .= ': ' . $e->getMessage();
  417. }
  418. throw new \UnexpectedValueException($message);
  419. }
  420. /**
  421. * Increment, decrement, or simply pad a version number.
  422. *
  423. * Support function for {@link parseConstraint()}
  424. *
  425. * @param array $matches Array with version parts in array indexes 1,2,3,4
  426. * @param int $position 1,2,3,4 - which segment of the version to decrement
  427. * @param int $increment Increment
  428. * @param string $pad The string to pad version parts after $position
  429. * @return string The new version
  430. */
  431. public function manipulateVersionString($matches, $position, $increment = 0, $pad = '0')
  432. {
  433. for ($i = 4; $i > 0; $i--) {
  434. if ($i > $position) {
  435. $matches[$i] = $pad;
  436. } elseif ($i == $position && $increment) {
  437. $matches[$i] += $increment;
  438. // If $matches[$i] was 0, carry the decrement
  439. if ($matches[$i] < 0) {
  440. $matches[$i] = $pad;
  441. $position--;
  442. // Return null on a carry overflow
  443. if ($i == 1) {
  444. return;
  445. }
  446. }
  447. }
  448. }
  449. return $matches[1] . '.' . $matches[2] . '.' . $matches[3] . '.' . $matches[4];
  450. }
  451. /**
  452. * expandStability().
  453. *
  454. * @param string $stability Stability
  455. * @return string
  456. */
  457. public function expandStability($stability)
  458. {
  459. $stability = strtolower($stability);
  460. switch ($stability) {
  461. case 'a':
  462. return 'alpha';
  463. case 'b':
  464. return 'beta';
  465. case 'p':
  466. case 'pl':
  467. return 'patch';
  468. case 'rc':
  469. return 'RC';
  470. default:
  471. return $stability;
  472. }
  473. }
  474. /**
  475. * Parses a name/version pairs and returns an array of pairs + the
  476. *
  477. * @param array $pairs a set of package/version pairs separated by ":", "=" or " "
  478. * @return array[] array of arrays containing a name and (if provided) a version
  479. */
  480. public function parseNameVersionPairs(array $pairs)
  481. {
  482. $pairs = array_values($pairs);
  483. $result = [];
  484. for ($i = 0, $count = count($pairs); $i < $count; $i++) {
  485. $pair = preg_replace('{^([^=: ]+)[=: ](.*)$}', '$1 $2', trim($pairs[$i]));
  486. if (false === strpos($pair, ' ') && isset($pairs[$i + 1]) && false === strpos($pairs[$i + 1], '/')) {
  487. $pair .= ' ' . $pairs[$i + 1];
  488. $i++;
  489. }
  490. if (strpos($pair, ' ')) {
  491. list($name, $version) = explode(" ", $pair, 2);
  492. $result[] = [
  493. 'name' => $name,
  494. 'version' => $version
  495. ];
  496. } else {
  497. $result[] = [
  498. 'name' => $pair
  499. ];
  500. }
  501. }
  502. return $result;
  503. }
  504. }