UUID::v5()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 1
c 2
b 0
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 2
crap 1
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 AssertionError;
15
use InvalidArgumentException;
16
use function base64_decode;
17
use function base64_encode;
18
use function chr;
19
use function ctype_digit;
20
use function ctype_xdigit;
21
use function dechex;
22
use function explode;
23
use function gethostbyname;
24
use function gettimeofday;
25
use function hex2bin;
26
use function hexdec;
27
use function in_array;
28
use function md5;
29
use function mt_rand;
30
use function preg_match;
31
use function random_bytes;
32
use function sha1;
33
use function sprintf;
34
use function str_replace;
35
use function strlen;
36
use function strtolower;
37
use function substr;
38
use function trim;
39
use function unpack;
40
use function vsprintf;
41
42
/**
43
 * Class UUID generates Universally Unique Identifiers following the RFC 4122.
44
 *
45
 * The 5 fields of the UUID v1
46
 *  - 32 bit, *time_low*
47
 *  - 16 bit, *time_mid*
48
 *  - 16 bit, *time_high_and_version*
49
 *  - 16 bit, (8 bits for *clock_seq_and_reserved* + 8 bits for *clock_seq_low*)
50
 *  - 48 bit, *node*
51
 *
52
 * @link    http://tools.ietf.org/html/rfc4122
53
 * @link    https://docs.python.org/2/library/uuid.html
54
 * @link    https://en.wikipedia.org/wiki/Universally_unique_identifier
55
 */
56
final class UUID
57
{
58
    /* @link http://tools.ietf.org/html/rfc4122#appendix-C */
59
60
    /**
61
     * When this namespace is specified, the name string
62
     * is a fully-qualified domain name.
63
     */
64
    public const NAMESPACE_DNS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
65
66
    /**
67
     * When this namespace is specified, the name string is a URL.
68
     */
69
    public const NAMESPACE_URL = '6ba7b811-9dad-11d1-80b4-00c04fd430c8';
70
71
    /**
72
     * When this namespace is specified, the name string is an ISO OID.
73
     */
74
    public const NAMESPACE_OID = '6ba7b812-9dad-11d1-80b4-00c04fd430c8';
75
76
    /**
77
     * When this namespace is specified, the name string is
78
     * an X.500 DN in DER or a text output format.
79
     */
80
    public const NAMESPACE_X500 = '6ba7b814-9dad-11d1-80b4-00c04fd430c8';
81
82
    /**
83
     * Regex pattern for UUIDs
84
     */
85
    public const PATTERN = '[a-f0-9]{8}\-[a-f0-9]{4}\-[1345][a-f0-9]{3}\-[a-f0-9]{4}\-[a-f0-9]{12}';
86
87
    /**
88
     * Generates a UUID based on the MD5 hash of a namespace
89
     * identifier (which is a UUID) and a name (which is a string).
90
     *
91
     * @param string $namespace UUID namespace identifier
92
     * @param string $name      A name
93
     *
94
     * @return string UUID v3
95
     */
96 5
    public static function v3(string $namespace, string $name): string
97
    {
98 5
        return UUID::fromName($namespace, $name, 3);
99
    }
100
101
    /**
102
     * Version 4, pseudo-random UUID
103
     * xxxxxxxx-xxxx-4xxx-[8|9|a|b]xxx-xxxxxxxxxxxx
104
     *
105
     * @return string 128bit of pseudo-random UUID
106
     * @throws \Exception
107
     *@see http://en.wikipedia.org/wiki/UUID#Version_4_.28random.29
108
     */
109 2
    public static function v4(): string
110
    {
111 2
        $bytes = unpack('n*', random_bytes(16));
112 2
        $bytes[4] = $bytes[4] & 0x0fff | 0x4000;
113 2
        $bytes[5] = $bytes[5] & 0x3fff | 0x8000;
114 2
        return vsprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', $bytes);
115
    }
116
117
    /**
118
     * Generates a UUID based on the SHA-1 hash of a namespace
119
     * identifier (which is a UUID) and a name (which is a string).
120
     *
121
     * @param string $namespace UUID namespace identifier
122
     * @param string $name      A name
123
     *
124
     * @return string UUID v5
125
     */
126 4
    public static function v5(string $namespace, string $name): string
127
    {
128 4
        return UUID::fromName($namespace, $name, 5);
129
    }
130
131
    /**
132
     * Checks if a given UUID has valid format.
133
     *
134
     * @param string $uuid
135
     *
136
     * @return bool
137
     */
138 23
    public static function valid(string $uuid): bool
139
    {
140 23
        if (false === (bool)preg_match('/^' . UUID::PATTERN . '$/i', $uuid)) {
141 5
            return false;
142
        }
143 19
        if ('4' === $uuid[14]) {
144 9
            return in_array($uuid[19], ['8', '9', 'a', 'b']);
145
        }
146 10
        return true;
147
    }
148
149
    /**
150
     * Checks if a given UUID has valid format and matches against the version.
151
     *
152
     * @param string $uuid
153
     * @param int    $version Check against the version 1, 3, 4 or 5
154
     *
155
     * @return bool
156
     */
157 13
    public static function matches(string $uuid, int $version = 4): bool
158
    {
159 13
        assert(in_array($version, [1, 3, 4, 5]), new AssertionError("Expected UUID version 1, 3, 4 or 5 (got $version)"));
160 12
        return UUID::valid($uuid);
161
    }
162
163
    /**
164
     * UUID v1 is generated from host (hardware) address, clock sequence and
165
     * current time. This is very slow method.
166
     *
167
     * @param string|int|null $address [optional] 48 bit number for the hardware address.
168
     *                                 It can be an integer or hexadecimal string
169
     *
170
     * @return string UUID v1
171
     */
172 7
    public static function v1(string|int $address = null): string
173
    {
174 7
        static $node, $clockSeq, $lastTimestamp;
175
176
        /**
177
         * If $node is not initialized, it will try to
178
         * get the network address of the machine,
179
         * or fallback to random generated hex string.
180
         * @return string
181
         */
182 7
        $fetchAddress = static function() use (&$node): string {
183 1
            if ($node) {
184
                return $node;
185
            }
186 1
            if ($node = `hostname -i 2> /dev/null`) {
187 1
                return $node = vsprintf('%02x%02x%02x%02x', explode('.', $node));
188
            }
189
            // @codeCoverageIgnoreStart
190
            if ($node = `hostname 2> /dev/null`) {
191
                // macOS
192
                $node = gethostbyname(trim($node));
193
                return $node = vsprintf('%02x%02x%02x%02x', explode('.', $node));
194
            }
195
            // @codeCoverageIgnoreEnd
196
197
            // Cannot identify IP or host, fallback as described in
198
            // http://tools.ietf.org/html/rfc4122#section-4.5
199
            // https://en.wikipedia.org/wiki/MAC_address#Unicast_vs._multicast_(I/G_bit)
200
            // @codeCoverageIgnoreStart
201
            return $node = dechex(mt_rand(0, 1 << 48) | (1 << 40));
202
            // @codeCoverageIgnoreEnd
203 7
        };
204
205
        /**
206
         * Transform the address into hexadecimal string
207
         * as spatially unique node identifier.
208
         * @param string|int|null $address [optional]
209
         * @return string
210
         */
211 7
        $nodeIdentifier = static function(string|int $address = null) use ($fetchAddress): string {
212 7
            $address = null !== $address
213 6
                ? str_replace([':', '-', '.'], '', (string)$address)
214 1
                : $fetchAddress();
215
216 7
            if (ctype_digit($address)) {
217 2
                return sprintf('%012x', $address);
218
            }
219 5
            if (ctype_xdigit($address) && strlen($address) <= 12) {
220 2
                return strtolower($address);
221
            }
222 3
            throw new InvalidArgumentException('UUID invalid node value');
223 7
        };
224
225
        /**
226
         * Convert UNIX epoch in nanoseconds to Gregorian epoch
227
         * (15/10/1582 00:00:00 - 01/01/1970 00:00:00)
228
         * @return int[]
229
         */
230 7
        $fromUnixNano = static function() use (&$lastTimestamp) {
231 7
            $ts = gettimeofday();
232 7
            $ts = ($ts['sec'] * 10000000) + ($ts['usec'] * 10) + 0x01b21dd213814000;
233 7
            if ($lastTimestamp && $ts <= $lastTimestamp) {
234
                $ts = $lastTimestamp + 1;
235
            }
236 7
            $lastTimestamp = $ts;
237 7
            return [
238
                // timestamp low field
239 7
                $ts & 0xffffffff,
240
                // timestamp middle field
241 7
                ($ts >> 32) & 0xffff,
242
                // timestamp high field with version number
243 7
                (($ts >> 48) & 0x0fff) | (1 << 12)
244 7
            ];
245 7
        };
246
247 7
        if (!$clockSeq) {
248
            // Random 14-bit sequence number
249
            // http://tools.ietf.org/html/rfc4122#section-4.2.1.1
250 1
            $clockSeq = mt_rand(0, 1 << 14);
251
        }
252 7
        return vsprintf('%08x-%04x-%04x-%02x%02x-%012s', [
253 7
            ...$fromUnixNano(),
254 7
            $clockSeq & 0xff,
255 7
            ($clockSeq >> 8) & 0x3f,
256 7
            $nodeIdentifier($address)
257 7
        ]);
258
    }
259
260
    /**
261
     * Creates a base64 string out of the UUID.
262
     *
263
     * @param string $uuid UUID string
264
     *
265
     * @return string base64 encoded string
266
     */
267 4
    public static function toBase64(string $uuid): string
268
    {
269 4
        if (false === UUID::valid($uuid)) {
270 1
            throw new InvalidArgumentException('Invalid UUID ' . $uuid);
271
        }
272 3
        return str_replace(['/', '+', '='], ['-', '_', ''],
273 3
            base64_encode(hex2bin(str_replace('-', '', $uuid)))
274 3
        );
275
    }
276
277
    /**
278
     * Converts a base64 string to UUID.
279
     *
280
     * @param string $base64
281
     *
282
     * @return string UUID string
283
     */
284 7
    public static function fromBase64(string $base64): string
285
    {
286 7
        $uuid = base64_decode(str_replace(
287 7
            ['-', '_', '='], ['/', '+', ''], $base64) . '=='
288 7
        );
289 7
        if (!preg_match('//u', $uuid)) {
290 3
            $uuid = vsprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', unpack('n*', $uuid));
291
        }
292 7
        if (UUID::valid($uuid)) {
293 6
            return $uuid;
294
        }
295 1
        throw new InvalidArgumentException(
296 1
            'Failed to convert base 64 string to UUID');
297
    }
298
299
    /**
300
     * Creates a v3 or v5 UUID.
301
     *
302
     * @param string $namespace UUID namespace identifier (see UUID constants)
303
     * @param string $name      A name
304
     * @param int    $version   3 or 5
305
     *
306
     * @throws InvalidArgumentException
307
     * @return string UUID 3 or 5
308
     */
309 9
    private static function fromName(string $namespace, string $name, int $version): string
310
    {
311 9
        if (false === UUID::matches($namespace, $version)) {
312 2
            throw new InvalidArgumentException('Invalid UUID namespace ' . $namespace);
313
        }
314 7
        $hex = str_replace('-', '', $namespace);
315 7
        $bits = '';
316 7
        for ($i = 0, $len = strlen($hex); $i < $len; $i += 2) {
317 7
            $bits .= chr((int)hexdec($hex[$i] . $hex[$i + 1]));
318
        }
319 7
        $hash = $bits . $name;
320 7
        $hash = (3 === $version) ? md5($hash) : sha1($hash);
321 7
        return sprintf('%08s-%04s-%04x-%04x-%12s',
322 7
            substr($hash, 0, 8),
323 7
            substr($hash, 8, 4),
324 7
            (hexdec(substr($hash, 12, 4)) & 0x0fff) | (3 === $version ? 0x3000 : 0x5000),
325 7
            (hexdec(substr($hash, 16, 4)) & 0x3fff) | 0x8000,
326 7
            substr($hash, 20, 12)
327 7
        );
328
    }
329
}
330