Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
50 / 50
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
Semver
100.00% covered (success)
100.00%
50 / 50
100.00% covered (success)
100.00%
2 / 2
23
100.00% covered (success)
100.00%
1 / 1
 parse
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
9
 compare
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
14
1<?php
2/**
3 * Simple semver version handling.
4 *
5 * We use this instead of something like `composer/semver` to avoid
6 * plugins needing to include yet-another dependency package. The
7 * amount of code we need here is pretty small.
8 *
9 * We use this instead of PHP's `version_compare()` because that doesn't
10 * handle prerelease versions in the way anyone other than PHP devs would
11 * expect, and silently breaks on various unexpected input.
12 *
13 * @package automattic/jetpack-assets
14 */
15
16namespace Automattic\Jetpack\Assets;
17
18use InvalidArgumentException;
19
20/**
21 * Simple semver version handling.
22 */
23class Semver {
24    /**
25     * Parse a semver version.
26     *
27     * @param string $version Version.
28     * @return array With components:
29     *  - major: (int) Major version.
30     *  - minor: (int) Minor version.
31     *  - patch: (int) Patch version.
32     *  - version: (string) Major.minor.patch.
33     *  - prerelease: (string|null) Pre-release string.
34     *  - buildinfo: (string|null) Build metadata string.
35     * @throws InvalidArgumentException If the version number is not in a recognized format.
36     */
37    public static function parse( $version ) {
38        // This is slightly looser than the official version from semver.org, in that leading zeros are allowed.
39        if ( ! preg_match( '/^(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<prerelease>(?:[0-9a-zA-Z-]+)(?:\.(?:[0-9a-zA-Z-]+))*))?(?:\+(?P<buildinfo>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/', $version, $m ) ) {
40            throw new InvalidArgumentException( "Version number \"$version\" is not in a recognized format." );
41        }
42        $info = array(
43            'major'      => (int) $m['major'],
44            'minor'      => (int) $m['minor'],
45            'patch'      => (int) $m['patch'],
46            'version'    => sprintf( '%d.%d.%d', $m['major'], $m['minor'], $m['patch'] ),
47            'prerelease' => isset( $m['prerelease'] ) && '' !== $m['prerelease'] ? $m['prerelease'] : null,
48            'buildinfo'  => isset( $m['buildinfo'] ) && '' !== $m['buildinfo'] ? $m['buildinfo'] : null,
49        );
50
51        if ( null !== $info['prerelease'] ) {
52            $sep        = '';
53            $prerelease = '';
54            foreach ( explode( '.', $info['prerelease'] ) as $part ) {
55                if ( ctype_digit( $part ) ) {
56                    $part = (int) $part;
57                }
58                $prerelease .= $sep . $part;
59                $sep         = '.';
60            }
61            $info['prerelease'] = $prerelease;
62        }
63
64        return $info;
65    }
66
67    /**
68     * Compare two version numbers.
69     *
70     * @param string $a First version.
71     * @param string $b Second version.
72     * @return int Less than, equal to, or greater than 0 depending on whether `$a` is less than, equal to, or greater than `$b`.
73     * @throws InvalidArgumentException If the version numbers are not in a recognized format.
74     */
75    public static function compare( $a, $b ) {
76        $aa = self::parse( $a );
77        $bb = self::parse( $b );
78        if ( $aa['major'] !== $bb['major'] ) {
79            return $aa['major'] - $bb['major'];
80        }
81        if ( $aa['minor'] !== $bb['minor'] ) {
82            return $aa['minor'] - $bb['minor'];
83        }
84        if ( $aa['patch'] !== $bb['patch'] ) {
85            return $aa['patch'] - $bb['patch'];
86        }
87
88        if ( null === $aa['prerelease'] ) {
89            return null === $bb['prerelease'] ? 0 : 1;
90        }
91        if ( null === $bb['prerelease'] ) {
92            return -1;
93        }
94
95        $aaa = explode( '.', $aa['prerelease'] );
96        $bbb = explode( '.', $bb['prerelease'] );
97        $al  = count( $aaa );
98        $bl  = count( $bbb );
99        for ( $i = 0; $i < $al && $i < $bl; $i++ ) {
100            $a = $aaa[ $i ];
101            $b = $bbb[ $i ];
102            if ( ctype_digit( $a ) ) {
103                if ( ctype_digit( $b ) ) {
104                    if ( (int) $a !== (int) $b ) {
105                        return (int) $a - (int) $b;
106                    }
107                } else {
108                    return -1;
109                }
110            } elseif ( ctype_digit( $b ) ) {
111                return 1;
112            } else {
113                $tmp = strcmp( $a, $b );
114                if ( 0 !== $tmp ) {
115                    return $tmp;
116                }
117            }
118        }
119        return $al - $bl;
120    }
121}