Passed
Branch master (a02de7)
by Mihail
02:35
created

UUID::v5()   A

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