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 |