diff --git a/src/Uuid/Uuid.php b/src/Uuid/Uuid.php index 584095b14..b3366eaab 100644 --- a/src/Uuid/Uuid.php +++ b/src/Uuid/Uuid.php @@ -29,6 +29,7 @@ final class Uuid public const UUID_TYPE_NAME = 1; // Deprecated alias public const UUID_TYPE_RANDOM = 4; public const UUID_TYPE_SHA1 = 5; + public const UUID_TYPE_TIME_V6 = 6; public const UUID_TYPE_NULL = -1; public const UUID_TYPE_INVALID = -42; @@ -51,6 +52,8 @@ public static function uuid_create($uuid_type = \UUID_TYPE_DEFAULT) case self::UUID_TYPE_NAME: case self::UUID_TYPE_TIME: return self::uuid_generate_time(); + case self::UUID_TYPE_TIME_V6: + return self::uuid_generate_time_v6(); case self::UUID_TYPE_DCE: case self::UUID_TYPE_RANDOM: case self::UUID_TYPE_DEFAULT: @@ -269,24 +272,31 @@ public static function uuid_time($uuid) } $parsed = self::parse($uuid); + $time = null; - if (self::UUID_TYPE_TIME !== ($parsed['version'] ?? null)) { - if (80000 > \PHP_VERSION_ID) { - return false; - } + switch($parsed['version'] ?? null) { + case self::UUID_TYPE_TIME_V6: + $time = $parsed['time']; + $time = '0' . substr($time, -8) . substr($time, 4, 4) . substr($time, 1, 3); + case self::UUID_TYPE_TIME: + $time = $time ?: $parsed['time']; - throw new \ValueError('uuid_time(): Argument #1 ($uuid) UUID DCE TIME expected'); - } + if (\PHP_INT_SIZE >= 8) { + return intdiv(hexdec($time) - self::TIME_OFFSET_INT, 10000000); + } - if (\PHP_INT_SIZE >= 8) { - return intdiv(hexdec($parsed['time']) - self::TIME_OFFSET_INT, 10000000); - } + $time = str_pad(hex2bin($time), 8, "\0", \STR_PAD_LEFT); + $time = self::binaryAdd($time, self::TIME_OFFSET_COM); + $time[0] = $time[0] & "\x7F"; - $time = str_pad(hex2bin($parsed['time']), 8, "\0", \STR_PAD_LEFT); - $time = self::binaryAdd($time, self::TIME_OFFSET_COM); - $time[0] = $time[0] & "\x7F"; + return (int) substr(self::toDecimal($time), 0, -7); + default: + if (80000 > \PHP_VERSION_ID) { + return false; + } - return (int) substr(self::toDecimal($time), 0, -7); + throw new \ValueError('uuid_time(): Argument #1 ($uuid) UUID DCE TIME expected'); + } } public static function uuid_mac($uuid) @@ -437,6 +447,52 @@ private static function uuid_generate_time() ); } + /** + * @see https://www.rfc-editor.org/rfc/rfc9562.html#section-5.6 + */ + private static function uuid_generate_time_v6() + { + $time = microtime(false); + $time = substr($time, 11).substr($time, 2, 7); + + if (\PHP_INT_SIZE >= 8) { + $time = str_pad(dechex($time + self::TIME_OFFSET_INT), 16, '0', \STR_PAD_LEFT); + } else { + $time = str_pad(self::toBinary($time), 8, "\0", \STR_PAD_LEFT); + $time = self::binaryAdd($time, self::TIME_OFFSET_BIN); + $time = bin2hex($time); + } + + $clockSeq = random_int(0, 0x3FFF); + + // rfc9562 discourages using static node for v6 + $node = sprintf('%06x%06x', + random_int(0, 0xFFFFFF) | 0x010000, + random_int(0, 0xFFFFFF) + ); + + return sprintf('%08s-%04s-6%03s-%04x-%012s', + // 32 bits for "time_high" + substr($time, 1, 8), + + // 16 bits for "time_mid" + substr($time, 9, 4), + + // 16 bits for "time_low_and_version", + // four most significant bits holds version number 6 + substr($time, 13, 3), + + // 16 bits: + // * 8 bits for "clk_seq_hi_res", + // * 8 bits for "clk_seq_low", + // two most significant bits holds zero and one for variant DCE1.1 + $clockSeq | 0x8000, + + // 48 bits for "node" + $node + ); + } + private static function isValid($uuid) { return (bool) preg_match('{^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$}Di', $uuid); diff --git a/src/Uuid/bootstrap.php b/src/Uuid/bootstrap.php index 6d8545b3a..5aab3af34 100644 --- a/src/Uuid/bootstrap.php +++ b/src/Uuid/bootstrap.php @@ -52,6 +52,9 @@ if (!defined('UUID_TYPE_SHA1')) { define('UUID_TYPE_SHA1', 5); } +if (!defined('UUID_TYPE_TIME_V6')) { + define('UUID_TYPE_TIME_V6', 6); +} if (!defined('UUID_TYPE_NULL')) { define('UUID_TYPE_NULL', -1); } diff --git a/src/Uuid/bootstrap80.php b/src/Uuid/bootstrap80.php index d6c592fe8..43ca66f8e 100644 --- a/src/Uuid/bootstrap80.php +++ b/src/Uuid/bootstrap80.php @@ -44,6 +44,9 @@ if (!defined('UUID_TYPE_SHA1')) { define('UUID_TYPE_SHA1', 5); } +if (!defined('UUID_TYPE_TIME_V6')) { + define('UUID_TYPE_TIME_V6', 6); +} if (!defined('UUID_TYPE_NULL')) { define('UUID_TYPE_NULL', -1); } diff --git a/tests/Uuid/UuidTest.php b/tests/Uuid/UuidTest.php index c84bef9fc..fa6df1dbc 100644 --- a/tests/Uuid/UuidTest.php +++ b/tests/Uuid/UuidTest.php @@ -23,7 +23,8 @@ public function testCreate() public function testCreateTime() { - $this->assertMatchesRegularExpression('{^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$}', uuid_create(\UUID_TYPE_TIME)); + $this->assertMatchesRegularExpression('{^[0-9a-f]{8}-[0-9a-f]{4}-1[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$}', uuid_create(\UUID_TYPE_TIME)); + $this->assertMatchesRegularExpression('{^[0-9a-f]{8}-[0-9a-f]{4}-6[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$}', uuid_create(\UUID_TYPE_TIME_V6)); } public function testGenerateMd5() @@ -81,6 +82,7 @@ public static function provideCreateNoOverlapTests(): array return [ [Uuid::UUID_TYPE_RANDOM], [Uuid::UUID_TYPE_TIME], + [Uuid::UUID_TYPE_TIME_V6], ]; } @@ -190,6 +192,8 @@ public static function provideTypeTest(): array [Uuid::UUID_TYPE_RANDOM, 'fa83b381-328c-46b8-8c90-4e9ba47dfa4b'], [Uuid::UUID_TYPE_TIME, 'dbc6260f-e9cc-11e9-8dac-9cb6d0897f07'], [Uuid::UUID_TYPE_TIME, '6fec1e70-fb1f-11e9-81dc-b52d3e41ad26'], + [Uuid::UUID_TYPE_TIME_V6, '26092b71-bc5f-6d06-9bce-59bbad3c99ad'], + [Uuid::UUID_TYPE_TIME_V6, '18058902-5fe0-633c-a72d-e9e7a022f779'] ]; } @@ -239,6 +243,8 @@ public static function provideTimeTest(): array return [ [1572444805, '6fec1e70-fb1f-11e9-81dc-b52d3e41ad26'], [1572445677, '77ffc38a-fb21-11e9-b46a-3c7de2fa99cb'], + [4910517298, '26092b71-bc5f-6d06-9bce-59bbad3c99ad'], + [-1400916268, '180588fb-6471-6e20-96f3-b175966dcfeb'] ]; }