From 432fdf5b2a55385ae9f5cc02c58af67e45912ed3 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 7 Sep 2022 17:52:46 +0200 Subject: [PATCH] [Uid] Add UuidV7 and UuidV8 --- .../Routing/Requirement/Requirement.php | 4 +- src/Symfony/Component/Uid/CHANGELOG.md | 1 + .../Component/Uid/Factory/UuidFactory.php | 2 +- src/Symfony/Component/Uid/README.md | 3 + .../Tests/Command/InspectUuidCommandTest.php | 56 ++++++++- src/Symfony/Component/Uid/Tests/UuidTest.php | 26 ++++ src/Symfony/Component/Uid/Ulid.php | 22 ++-- src/Symfony/Component/Uid/Uuid.php | 12 ++ src/Symfony/Component/Uid/UuidV1.php | 8 +- src/Symfony/Component/Uid/UuidV7.php | 114 ++++++++++++++++++ src/Symfony/Component/Uid/UuidV8.php | 27 +++++ .../Component/Validator/Constraints/Uuid.php | 4 + .../Validator/Constraints/UuidValidator.php | 2 +- .../Tests/Constraints/UuidValidatorTest.php | 4 +- 14 files changed, 259 insertions(+), 26 deletions(-) create mode 100644 src/Symfony/Component/Uid/UuidV7.php create mode 100644 src/Symfony/Component/Uid/UuidV8.php diff --git a/src/Symfony/Component/Routing/Requirement/Requirement.php b/src/Symfony/Component/Routing/Requirement/Requirement.php index ccba5ec41d32..54ad86b61018 100644 --- a/src/Symfony/Component/Routing/Requirement/Requirement.php +++ b/src/Symfony/Component/Routing/Requirement/Requirement.php @@ -25,10 +25,12 @@ enum Requirement public const UID_BASE58 = '[1-9A-HJ-NP-Za-km-z]{22}'; public const UID_RFC4122 = '[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}'; public const ULID = '[0-7][0-9A-HJKMNP-TV-Z]{25}'; - public const UUID = '[0-9a-f]{8}-[0-9a-f]{4}-[13-6][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}'; + public const UUID = '[0-9a-f]{8}-[0-9a-f]{4}-[13-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}'; public const UUID_V1 = '[0-9a-f]{8}-[0-9a-f]{4}-1[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}'; public const UUID_V3 = '[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}'; public const UUID_V4 = '[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}'; public const UUID_V5 = '[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}'; public const UUID_V6 = '[0-9a-f]{8}-[0-9a-f]{4}-6[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}'; + public const UUID_V7 = '[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}'; + public const UUID_V8 = '[0-9a-f]{8}-[0-9a-f]{4}-8[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}'; } diff --git a/src/Symfony/Component/Uid/CHANGELOG.md b/src/Symfony/Component/Uid/CHANGELOG.md index 7b5142ba5c7d..b82133751c0a 100644 --- a/src/Symfony/Component/Uid/CHANGELOG.md +++ b/src/Symfony/Component/Uid/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 6.2 --- + * Add `UuidV7` and `UuidV8` * Add `TimeBasedUidInterface` to describe UIDs that embed a timestamp * Add `MaxUuid` and `MaxUlid` diff --git a/src/Symfony/Component/Uid/Factory/UuidFactory.php b/src/Symfony/Component/Uid/Factory/UuidFactory.php index aac35386e95f..0255d51bf37b 100644 --- a/src/Symfony/Component/Uid/Factory/UuidFactory.php +++ b/src/Symfony/Component/Uid/Factory/UuidFactory.php @@ -44,7 +44,7 @@ public function __construct(string|int $defaultClass = UuidV6::class, string|int $this->nameBasedNamespace = $nameBasedNamespace; } - public function create(): UuidV6|UuidV4|UuidV1 + public function create(): Uuid { $class = $this->defaultClass; diff --git a/src/Symfony/Component/Uid/README.md b/src/Symfony/Component/Uid/README.md index 2ec5b761bc7b..ce0fb18612d6 100644 --- a/src/Symfony/Component/Uid/README.md +++ b/src/Symfony/Component/Uid/README.md @@ -3,6 +3,9 @@ Uid Component The UID component provides an object-oriented API to generate and represent UIDs. +It provides implementations for UUIDs version 1 and versions 3 to 8, +for ULIDs and for related factories. + Resources --------- diff --git a/src/Symfony/Component/Uid/Tests/Command/InspectUuidCommandTest.php b/src/Symfony/Component/Uid/Tests/Command/InspectUuidCommandTest.php index f581b37a7a47..c9061b5a861d 100644 --- a/src/Symfony/Component/Uid/Tests/Command/InspectUuidCommandTest.php +++ b/src/Symfony/Component/Uid/Tests/Command/InspectUuidCommandTest.php @@ -82,16 +82,16 @@ public function testUnknown() EOF , $commandTester->getDisplay(true)); - $this->assertSame(0, $commandTester->execute(['uuid' => '461cc9b9-2397-7dba-91e9-33af4c63f7ec'])); + $this->assertSame(0, $commandTester->execute(['uuid' => '461cc9b9-2397-adba-91e9-33af4c63f7ec'])); $this->assertSame(<<getDisplay(true)); + } + + public function testV7() + { + $commandTester = new CommandTester(new InspectUuidCommand()); + + $this->assertSame(0, $commandTester->execute(['uuid' => '017f22e2-79b0-7cc3-98c4-dc0c0c07398f'])); + $this->assertSame(<<getDisplay(true)); + } + + public function testV8() + { + $commandTester = new CommandTester(new InspectUuidCommand()); + + $this->assertSame(0, $commandTester->execute(['uuid' => '017f22e2-79b0-8cc3-98c4-dc0c0c07398f'])); + $this->assertSame(<<getDisplay(true)); } diff --git a/src/Symfony/Component/Uid/Tests/UuidTest.php b/src/Symfony/Component/Uid/Tests/UuidTest.php index 93c2340ff937..3ac925aa4194 100644 --- a/src/Symfony/Component/Uid/Tests/UuidTest.php +++ b/src/Symfony/Component/Uid/Tests/UuidTest.php @@ -22,11 +22,13 @@ use Symfony\Component\Uid\UuidV4; use Symfony\Component\Uid\UuidV5; use Symfony\Component\Uid\UuidV6; +use Symfony\Component\Uid\UuidV7; class UuidTest extends TestCase { private const A_UUID_V1 = 'd9e7a184-5d5b-11ea-a62a-3499710062d0'; private const A_UUID_V4 = 'd6b3345b-2905-4048-a83c-b5988e765d98'; + private const A_UUID_V7 = '017f22e2-79b0-7cc3-98c4-dc0c0c07398f'; /** * @dataProvider provideInvalidUuids @@ -69,6 +71,8 @@ public function provideInvalidVariant(): iterable yield ['8dac64d3-937a-4e7c-fa1d-d5d6c06a61f5']; yield ['8dac64d3-937a-5e7c-fa1d-d5d6c06a61f5']; yield ['8dac64d3-937a-6e7c-fa1d-d5d6c06a61f5']; + yield ['8dac64d3-937a-7e7c-fa1d-d5d6c06a61f5']; + yield ['8dac64d3-937a-8e7c-fa1d-d5d6c06a61f5']; } public function testConstructorWithValidUuid() @@ -134,6 +138,28 @@ public function testV6IsSeeded() $this->assertNotSame(substr($uuidV1, 24), substr($uuidV6, 24)); } + public function testV7() + { + $uuid = Uuid::fromString(self::A_UUID_V7); + + $this->assertInstanceOf(UuidV7::class, $uuid); + $this->assertSame(1645557742, $uuid->getDateTime()->getTimeStamp()); + + $prev = UuidV7::generate(); + + for ($i = 0; $i < 25; ++$i) { + $uuid = UuidV7::generate(); + $now = gmdate('Y-m-d H:i'); + $this->assertGreaterThan($prev, $uuid); + $prev = $uuid; + } + + $this->assertTrue(Uuid::isValid($uuid)); + $uuid = Uuid::fromString($uuid); + $this->assertInstanceOf(UuidV7::class, $uuid); + $this->assertSame($now, $uuid->getDateTime()->format('Y-m-d H:i')); + } + public function testBinary() { $uuid = new UuidV4(self::A_UUID_V4); diff --git a/src/Symfony/Component/Uid/Ulid.php b/src/Symfony/Component/Uid/Ulid.php index 6a9b21cf4cc5..81610a00d421 100644 --- a/src/Symfony/Component/Uid/Ulid.php +++ b/src/Symfony/Component/Uid/Ulid.php @@ -152,15 +152,15 @@ public static function generate(\DateTimeInterface $time = null): string if ($time > self::$time || (null !== $mtime && $time !== self::$time)) { randomize: - $r = unpack('nr1/nr2/nr3/nr4/nr', random_bytes(10)); - $r['r1'] |= ($r['r'] <<= 4) & 0xF0000; - $r['r2'] |= ($r['r'] <<= 4) & 0xF0000; - $r['r3'] |= ($r['r'] <<= 4) & 0xF0000; - $r['r4'] |= ($r['r'] <<= 4) & 0xF0000; - unset($r['r']); - self::$rand = array_values($r); + $r = unpack('n*', random_bytes(10)); + $r[1] |= ($r[5] <<= 4) & 0xF0000; + $r[2] |= ($r[5] <<= 4) & 0xF0000; + $r[3] |= ($r[5] <<= 4) & 0xF0000; + $r[4] |= ($r[5] <<= 4) & 0xF0000; + unset($r[5]); + self::$rand = $r; self::$time = $time; - } elseif ([0xFFFFF, 0xFFFFF, 0xFFFFF, 0xFFFFF] === self::$rand) { + } elseif ([1 => 0xFFFFF, 0xFFFFF, 0xFFFFF, 0xFFFFF] === self::$rand) { if (\PHP_INT_SIZE >= 8 || 10 > \strlen($time = self::$time)) { $time = (string) (1 + $time); } elseif ('999999999' === $mtime = substr($time, -9)) { @@ -171,7 +171,7 @@ public static function generate(\DateTimeInterface $time = null): string goto randomize; } else { - for ($i = 3; $i >= 0 && 0xFFFFF === self::$rand[$i]; --$i) { + for ($i = 4; $i > 0 && 0xFFFFF === self::$rand[$i]; --$i) { self::$rand[$i] = 0; } @@ -192,10 +192,10 @@ public static function generate(\DateTimeInterface $time = null): string return strtr(sprintf('%010s%04s%04s%04s%04s', $time, - base_convert(self::$rand[0], 10, 32), base_convert(self::$rand[1], 10, 32), base_convert(self::$rand[2], 10, 32), - base_convert(self::$rand[3], 10, 32) + base_convert(self::$rand[3], 10, 32), + base_convert(self::$rand[4], 10, 32) ), 'abcdefghijklmnopqrstuv', 'ABCDEFGHJKMNPQRSTVWXYZ'); } } diff --git a/src/Symfony/Component/Uid/Uuid.php b/src/Symfony/Component/Uid/Uuid.php index 8f763c6588a4..da0bfcb39957 100644 --- a/src/Symfony/Component/Uid/Uuid.php +++ b/src/Symfony/Component/Uid/Uuid.php @@ -83,6 +83,8 @@ public static function fromString(string $uuid): static UuidV4::TYPE => new UuidV4($uuid), UuidV5::TYPE => new UuidV5($uuid), UuidV6::TYPE => new UuidV6($uuid), + UuidV7::TYPE => new UuidV7($uuid), + UuidV8::TYPE => new UuidV8($uuid), default => new self($uuid), }; } @@ -118,6 +120,16 @@ final public static function v6(): UuidV6 return new UuidV6(); } + final public static function v7(): UuidV7 + { + return new UuidV7(); + } + + final public static function v8(string $uuid): UuidV8 + { + return new UuidV8($uuid); + } + public static function isValid(string $uuid): bool { if (!preg_match('{^[0-9a-f]{8}(?:-[0-9a-f]{4}){2}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$}Di', $uuid)) { diff --git a/src/Symfony/Component/Uid/UuidV1.php b/src/Symfony/Component/Uid/UuidV1.php index 045f667b4c52..8c0379211374 100644 --- a/src/Symfony/Component/Uid/UuidV1.php +++ b/src/Symfony/Component/Uid/UuidV1.php @@ -20,7 +20,7 @@ class UuidV1 extends Uuid implements TimeBasedUidInterface { protected const TYPE = 1; - private static ?string $clockSeq = null; + private static string $clockSeq; public function __construct(string $uuid = null) { @@ -49,13 +49,13 @@ public static function generate(\DateTimeInterface $time = null, Uuid $node = nu if ($node) { // use clock_seq from the node $seq = substr($node->uid, 19, 4); - } else { + } elseif (!$seq = self::$clockSeq ?? '') { // generate a static random clock_seq to prevent any collisions with the real one $seq = substr($uuid, 19, 4); - while (null === self::$clockSeq || $seq === self::$clockSeq) { + do { self::$clockSeq = sprintf('%04x', random_int(0, 0x3FFF) | 0x8000); - } + } while ($seq === self::$clockSeq); $seq = self::$clockSeq; } diff --git a/src/Symfony/Component/Uid/UuidV7.php b/src/Symfony/Component/Uid/UuidV7.php new file mode 100644 index 000000000000..718b35ff9ade --- /dev/null +++ b/src/Symfony/Component/Uid/UuidV7.php @@ -0,0 +1,114 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Uid; + +/** + * A v7 UUID is lexicographically sortable and contains a 48-bit timestamp and 74 extra unique bits. + * + * Within the same millisecond, monotonicity is ensured by incrementing the random part by a random increment. + * + * @author Nicolas Grekas + */ +class UuidV7 extends Uuid implements TimeBasedUidInterface +{ + protected const TYPE = 7; + + private static string $time = ''; + private static array $rand = []; + private static string $seed; + private static array $seedParts; + private static int $seedIndex = 0; + + public function __construct(string $uuid = null) + { + if (null === $uuid) { + $this->uid = static::generate(); + } else { + parent::__construct($uuid, true); + } + } + + public function getDateTime(): \DateTimeImmutable + { + $time = substr($this->uid, 0, 8).substr($this->uid, 9, 4); + $time = \PHP_INT_SIZE >= 8 ? (string) hexdec($time) : BinaryUtil::toBase(hex2bin($time), BinaryUtil::BASE10); + + if (4 > \strlen($time)) { + $time = '000'.$time; + } + + return \DateTimeImmutable::createFromFormat('U.v', substr_replace($time, '.', -3, 0)); + } + + public static function generate(\DateTimeInterface $time = null): string + { + if (null === $mtime = $time) { + $time = microtime(false); + $time = substr($time, 11).substr($time, 2, 3); + } elseif (0 > $time = $time->format('Uv')) { + throw new \InvalidArgumentException('The timestamp must be positive.'); + } + + if ($time > self::$time || (null !== $mtime && $time !== self::$time)) { + randomize: + self::$rand = unpack('n*', isset(self::$seed) ? random_bytes(10) : self::$seed = random_bytes(16)); + self::$rand[1] &= 0x03FF; + self::$time = $time; + } else { + if (!self::$seedIndex) { + $s = unpack('l*', self::$seed = hash('sha512', self::$seed, true)); + $s[] = ($s[1] >> 8 & 0xFF0000) | ($s[2] >> 16 & 0xFF00) | ($s[3] >> 24 & 0xFF); + $s[] = ($s[4] >> 8 & 0xFF0000) | ($s[5] >> 16 & 0xFF00) | ($s[6] >> 24 & 0xFF); + $s[] = ($s[7] >> 8 & 0xFF0000) | ($s[8] >> 16 & 0xFF00) | ($s[9] >> 24 & 0xFF); + $s[] = ($s[10] >> 8 & 0xFF0000) | ($s[11] >> 16 & 0xFF00) | ($s[12] >> 24 & 0xFF); + $s[] = ($s[13] >> 8 & 0xFF0000) | ($s[14] >> 16 & 0xFF00) | ($s[15] >> 24 & 0xFF); + self::$seedParts = $s; + self::$seedIndex = 21; + } + + self::$rand[5] = 0xFFFF & $carry = self::$rand[5] + (self::$seedParts[self::$seedIndex--] & 0xFFFFFF); + self::$rand[4] = 0xFFFF & $carry = self::$rand[4] + ($carry >> 16); + self::$rand[3] = 0xFFFF & $carry = self::$rand[3] + ($carry >> 16); + self::$rand[2] = 0xFFFF & $carry = self::$rand[2] + ($carry >> 16); + self::$rand[1] += $carry >> 16; + + if (0xFC00 & self::$rand[1]) { + if (\PHP_INT_SIZE >= 8 || 10 > \strlen($time = self::$time)) { + $time = (string) (1 + $time); + } elseif ('999999999' === $mtime = substr($time, -9)) { + $time = (1 + substr($time, 0, -9)).'000000000'; + } else { + $time = substr_replace($time, str_pad(++$mtime, 9, '0', \STR_PAD_LEFT), -9); + } + + goto randomize; + } + + $time = self::$time; + } + + if (\PHP_INT_SIZE >= 8) { + $time = base_convert($time, 10, 16); + } else { + $time = bin2hex(BinaryUtil::fromBase($time, BinaryUtil::BASE10)); + } + + return substr_replace(sprintf('%012s-%04x-%04x-%04x%04x%04x', + $time, + 0x7000 | (self::$rand[1] << 2) | (self::$rand[2] >> 14), + 0x8000 | (self::$rand[2] & 0x3FFF), + self::$rand[3], + self::$rand[4], + self::$rand[5], + ), '-', 8, 0); + } +} diff --git a/src/Symfony/Component/Uid/UuidV8.php b/src/Symfony/Component/Uid/UuidV8.php new file mode 100644 index 000000000000..c194a6f699ee --- /dev/null +++ b/src/Symfony/Component/Uid/UuidV8.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Uid; + +/** + * A v8 UUID has no explicit requirements except embedding its version + variant bits. + * + * @author Nicolas Grekas + */ +class UuidV8 extends Uuid +{ + protected const TYPE = 8; + + public function __construct(string $uuid) + { + parent::__construct($uuid, true); + } +} diff --git a/src/Symfony/Component/Validator/Constraints/Uuid.php b/src/Symfony/Component/Validator/Constraints/Uuid.php index 82b790578378..89067fdd4788 100644 --- a/src/Symfony/Component/Validator/Constraints/Uuid.php +++ b/src/Symfony/Component/Validator/Constraints/Uuid.php @@ -51,6 +51,8 @@ class Uuid extends Constraint public const V4_RANDOM = 4; public const V5_SHA1 = 5; public const V6_SORTABLE = 6; + public const V7_SORTABLE = 7; + public const V8_CUSTOM = 8; public const ALL_VERSIONS = [ self::V1_MAC, @@ -59,6 +61,8 @@ class Uuid extends Constraint self::V4_RANDOM, self::V5_SHA1, self::V6_SORTABLE, + self::V7_SORTABLE, + self::V8_CUSTOM, ]; /** diff --git a/src/Symfony/Component/Validator/Constraints/UuidValidator.php b/src/Symfony/Component/Validator/Constraints/UuidValidator.php index af4ae413a9b9..c4ccfb27dee9 100644 --- a/src/Symfony/Component/Validator/Constraints/UuidValidator.php +++ b/src/Symfony/Component/Validator/Constraints/UuidValidator.php @@ -35,7 +35,7 @@ class UuidValidator extends ConstraintValidator // Roughly speaking: // x = any hexadecimal character - // M = any allowed version {1..6} + // M = any allowed version {1..8} // N = any allowed variant {8, 9, a, b} public const STRICT_LENGTH = 36; diff --git a/src/Symfony/Component/Validator/Tests/Constraints/UuidValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/UuidValidatorTest.php index 347980eecea7..9a1e0183b7f0 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/UuidValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/UuidValidatorTest.php @@ -82,6 +82,8 @@ public function getValidStrictUuids() ['456daEFb-5AA6-41B5-8DBC-068B05A8B201'], // Version 4 UUID in mixed case ['456daEFb-5AA6-41B5-8DBC-068B05A8B201', [Uuid::V4_RANDOM]], ['1eb01932-4c0b-6570-aa34-d179cdf481ae', [Uuid::V6_SORTABLE]], + ['216fff40-98d9-71e3-a5e2-0800200c9a66', [Uuid::V7_SORTABLE]], + ['216fff40-98d9-81e3-a5e2-0800200c9a66', [Uuid::V8_CUSTOM]], ]; } @@ -159,8 +161,6 @@ public function getInvalidStrictUuids() ['216fff40-98d9-11e3-a5e2-0800200c9a6', Uuid::TOO_SHORT_ERROR], ['216fff40-98d9-11e3-a5e2-0800200c9a666', Uuid::TOO_LONG_ERROR], ['216fff40-98d9-01e3-a5e2-0800200c9a66', Uuid::INVALID_VERSION_ERROR], - ['216fff40-98d9-71e3-a5e2-0800200c9a66', Uuid::INVALID_VERSION_ERROR], - ['216fff40-98d9-81e3-a5e2-0800200c9a66', Uuid::INVALID_VERSION_ERROR], ['216fff40-98d9-91e3-a5e2-0800200c9a66', Uuid::INVALID_VERSION_ERROR], ['216fff40-98d9-a1e3-a5e2-0800200c9a66', Uuid::INVALID_VERSION_ERROR], ['216fff40-98d9-b1e3-a5e2-0800200c9a66', Uuid::INVALID_VERSION_ERROR], pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy