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

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