Passed
Push — master ( 980804...08b834 )
by Mihail
09:48
created
Labels
Severity
1
<?php declare(strict_types=1);
2
3
/*
4
 * This file is part of the Koded package.
5
 *
6
 * (c) Mihail Binev <[email protected]>
7
 *
8
 * Please view the LICENSE distributed with this source code
9
 * for the full copyright and license information.
10
 *
11
 */
12
13
namespace Koded\Stdlib;
14
15
use InvalidArgumentException;
16
17
/**
18
 * Class UUID generates Universally Unique Identifiers following the RFC 4122.
19
 *
20
 * The 5 fields of the UUID v1
21
 *  - 32 bit, *time_low*
22
 *  - 16 bit, *time_mid*
23
 *  - 16 bit, *time_high_and_version*
24
 *  - 16 bit, (8 bits for *clock_seq_and_reserved* + 8 bits for *clock_seq_low*)
25
 *  - 48 bit, *node*
26
 *
27
 * @link    http://tools.ietf.org/html/rfc4122
28
 * @link    https://docs.python.org/2/library/uuid.html
29
 * @link    https://en.wikipedia.org/wiki/Universally_unique_identifier
30
 */
31
final class UUID
32
{
33
34
    /* @link http://tools.ietf.org/html/rfc4122#appendix-C */
35
36
    /**
37
     * When this namespace is specified, the name string
38
     * is a fully-qualified domain name.
39
     */
40
    const NAMESPACE_DNS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
41
42
    /**
43
     * When this namespace is specified, the name string is a URL.
44
     */
45
    const NAMESPACE_URL = '6ba7b811-9dad-11d1-80b4-00c04fd430c8';
46
47
    /**
48
     * When this namespace is specified, the name string is an ISO OID.
49
     */
50
    const NAMESPACE_OID = '6ba7b812-9dad-11d1-80b4-00c04fd430c8';
51
52
    /**
53
     * When this namespace is specified, the name string is
54
     * an X.500 DN in DER or a text output format.
55
     */
56
    const NAMESPACE_X500 = '6ba7b814-9dad-11d1-80b4-00c04fd430c8';
57
58
    /**
59
     * Regex pattern for UUIDs
60
     */
61
    const PATTERN = '[a-f0-9]{8}\-[a-f0-9]{4}\-[1|3|4|5][a-f0-9]{3}\-[a-f0-9]{4}\-[a-f0-9]{12}';
62
63
    /**
64
     * UUID v1 is generated from host (hardware address), clock sequence and
65
     * current time. This is very slow method.
66
     *
67
     * 0x01b21dd213814000 is a 100 nanoseconds interval between
68
     * UUID epoch and UNIX epoch datetime (15/10/1582 00:00:01 - 01/01/1970 00:00:01)
69
     *
70
     * @param int|string $address [optional] 48 bit number for the hardware address.
71
     *                            It can be an integer or hexadecimal string
72
     *
73
     * @return string UUID v1
74
     */
75 6
    public static function v1($address = null): string
76
    {
77 6
        static $matches = [[null], [null]];
78
79
        /**
80
         * Get the hardware address as a 48-bit positive integer.
81
         *
82
         * @param string $node [optional]
83
         *
84
         * @return null|string
85
         * @throws InvalidArgumentException
86
         */
87
        $node = function($node = null) use (&$matches) {
88 6
            if (null === $node) {
89 1
                if (empty($matches[1][0])) {
90
                    // Get MAC address (Linux server LAN)
91 1
                    $info = `ifconfig 2>&1` ?: '';
92
93
                    // Cache the info in $matches
94 1
                    preg_match_all('~[^:]([a-f0-9]{2}([:-])[a-f0-9]{2}(\2[a-f0-9]{2}){4})[^:]~i',
95 1
                        $info,
96 1
                        $matches,
97 1
                        PREG_PATTERN_ORDER
98
                    );
99
                }
100
101 1
                $node = $matches[1][0] ?? null;
102
103
                // Cannot identify host, fallback as in http://tools.ietf.org/html/rfc4122#section-4.5
104 1
                if (empty($node)) {
105
                    // @codeCoverageIgnoreStart
106
                    $node = sprintf('%06x%06x', mt_rand(0, 1 << 24), mt_rand(0, 1 << 24));
107
                    // @codeCoverageIgnoreEnd
108
                }
109
            }
110
111 6
            $node = str_replace([':', '-'], '', $node);
112
113 6
            if (ctype_digit($node)) {
114 1
                $node = sprintf('%012x', $node);
115
            }
116
117 6
            if (ctype_xdigit($node) && strlen($node) <= 12) {
118 3
                $node = strtolower(sprintf('%012s', $node));
119
            } else {
120 3
                throw new InvalidArgumentException('UUID invalid node value');
121
            }
122
123 3
            return $node;
124 6
        }; // end $node
125
126
        $clockSeq = function() {
127
            // Random 14-bit sequence number, http://tools.ietf.org/html/rfc4122#section-4.2.1.1
128 6
            return mt_rand(0, 1 << 14);
129 6
        };
130
131
        $uuidTime = function() {
132 6
            $time = gettimeofday();
133 6
            $time = ($time['sec'] * 10000000) + ($time['usec'] * 10) + 0x01b21dd213814000;
134
135
            return [
136 6
                'low' => sprintf('%08x', $time & 0xffffffff),
137 6
                'mid' => sprintf('%04x', ($time >> 32) & 0xffff),
138 6
                'high' => sprintf('%04x', ($time >> 48) & 0x0fff)
139
            ];
140 6
        };
141
142 6
        $uuidTime = $uuidTime();
143 6
        $clockSeq = $clockSeq();
144
145
        // Set to version 1
146 6
        $version = hexdec($uuidTime['high']) & 0x0fff;
147 6
        $version &= ~(0xf000);
148 6
        $version |= 1 << 12;
149
150
        // RFC 4122
151 6
        $variant = ($clockSeq >> 8) & 0x3f;
152 6
        $variant &= ~(0xc0);
153 6
        $variant |= 0x80;
154
155 6
        return sprintf('%08s-%04s-%04x-%02x%02x-%012s',
156 6
            $uuidTime['low'],
157 6
            $uuidTime['mid'],
158 6
            $version,
159 6
            $variant,
160 6
            $clockSeq & 0xff,
161 6
            $node($address)
162
        );
163
    }
164
165
    /**
166
     * UUID v3 (name based, MD5).
167
     *
168
     * @param string $namespace UUID namespace identifier
169
     * @param string $name      A name
170
     *
171
     * @return string UUID v3
172
     */
173 5
    public static function v3($namespace, $name): string
174
    {
175 5
        return UUID::fromName($namespace, $name, 3);
176
    }
177
178
    /**
179
     * Version 4, pseudo-random (xxxxxxxx-xxxx-4xxx-[8|9|a|b]xxx-xxxxxxxxxxxx)
180
     *
181
     * @return string 128bit of pseudo-random UUID
182
     * @see http://en.wikipedia.org/wiki/UUID#Version_4_.28random.29
183
     * @throws \Exception
184
     */
185 2
    public static function v4(): string
186
    {
187 2
        $bytes = unpack('n*', random_bytes(16));
188 2
        $bytes[4] = $bytes[4] & 0x0fff | 0x4000;
189 2
        $bytes[5] = $bytes[5] & 0x3fff | 0x8000;
190
191 2
        return vsprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', $bytes);
0 ignored issues
show
It seems like $bytes can also be of type false; however, parameter $args of vsprintf() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

191
        return vsprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', /** @scrutinizer ignore-type */ $bytes);
Loading history...
192
    }
193
194
    /**
195
     * UUID v5 (name based, SHA1).
196
     *
197
     * @param string $namespace UUID namespace identifier
198
     * @param string $name      A name
199
     *
200
     * @return string UUID v5
201
     */
202 4
    public static function v5($namespace, string $name): string
203
    {
204 4
        return UUID::fromName($namespace, $name, 5);
205
    }
206
207
    /**
208
     * Checks if a given UUID has valid format.
209
     *
210
     * @param string $uuid
211
     *
212
     * @return bool
213
     */
214 16
    public static function valid(string $uuid): bool
215
    {
216 16
        if (false === (bool)preg_match('/^' . UUID::PATTERN . '$/i', $uuid)) {
217 3
            return false;
218
        }
219
220 13
        if ('4' === $uuid[14] && false === in_array($uuid[19], ['8', '9', 'a', 'b'])) {
221 1
            return false;
222
        }
223
224 13
        return true;
225
    }
226
227
    /**
228
     * Checks if a given UUID has valid format and matches against the version.
229
     *
230
     * @param string $uuid
231
     * @param int    $version Check against the version 1, 3, 4 or 5
232
     *
233
     * @return bool
234
     */
235 13
    public static function matches(string $uuid, int $version = 4): bool
236
    {
237 13
        assert(in_array($version, [1, 3, 4, 5]), 'Expected UUID version 1, 3, 4 or 5');
238 12
        return UUID::valid($uuid);
239
    }
240
241
    /**
242
     * Creates the v3 and/or v5 UUID.
243
     *
244
     * @param string $namespace UUID namespace identifier (see UUID constants)
245
     * @param string $name      A name
246
     * @param int    $version   3 or 5
247
     *
248
     * @throws InvalidArgumentException
249
     * @return string UUID 3 or 5
250
     */
251 9
    private static function fromName(string $namespace, string $name, int $version): string
252
    {
253 9
        if (false === UUID::matches($namespace, $version)) {
254 2
            throw new InvalidArgumentException('Invalid UUID namespace ' . $namespace);
255
        }
256
257 7
        $hex = str_replace('-', '', $namespace);
258 7
        $bits = '';
259 7
        for ($i = 0, $len = strlen($hex); $i < $len; $i += 2) {
260 7
            $bits .= chr((int)hexdec($hex[$i] . $hex[$i + 1]));
261
        }
262
263 7
        $hash = $bits . $name;
264 7
        $hash = (3 === $version) ? md5($hash) : sha1($hash);
265
266 7
        return sprintf('%08s-%04s-%04x-%04x-%12s',
267 7
            substr($hash, 0, 8),
268 7
            substr($hash, 8, 4),
269 7
            (hexdec(substr($hash, 12, 4)) & 0x0fff) | (3 === $version ? 0x3000 : 0x5000),
270 7
            (hexdec(substr($hash, 16, 4)) & 0x3fff) | 0x8000,
271 7
            substr($hash, 20, 12)
272
        );
273
    }
274
}
275