diff --git a/src/Symfony/Component/Uid/BinaryUtil.php b/src/Symfony/Component/Uid/BinaryUtil.php index 8fd19d8674af0..fd158345546d3 100644 --- a/src/Symfony/Component/Uid/BinaryUtil.php +++ b/src/Symfony/Component/Uid/BinaryUtil.php @@ -118,8 +118,10 @@ public static function add(string $a, string $b): string /** * @param string $time Count of 100-nanosecond intervals since the UUID epoch 1582-10-15 00:00:00 in hexadecimal + * + * @return string Count of 100-nanosecond intervals since the UUID epoch 1582-10-15 00:00:00 as a numeric string */ - public static function hexToDateTime(string $time): \DateTimeImmutable + public static function hexToNumericString(string $time): string { if (\PHP_INT_SIZE >= 8) { $time = (string) (hexdec($time) - self::TIME_OFFSET_INT); @@ -140,7 +142,17 @@ public static function hexToDateTime(string $time): \DateTimeImmutable $time = '-' === $time[0] ? '-'.str_pad(substr($time, 1), 8, '0', \STR_PAD_LEFT) : str_pad($time, 8, '0', \STR_PAD_LEFT); } - return \DateTimeImmutable::createFromFormat('U.u?', substr_replace($time, '.', -7, 0)); + return $time; + } + + /** + * Sub-microseconds are lost since they are not handled by \DateTimeImmutable. + * + * @param string $time Count of 100-nanosecond intervals since the UUID epoch 1582-10-15 00:00:00 in hexadecimal + */ + public static function hexToDateTime(string $time): \DateTimeImmutable + { + return \DateTimeImmutable::createFromFormat('U.u?', substr_replace(self::hexToNumericString($time), '.', -7, 0)); } /** diff --git a/src/Symfony/Component/Uid/CHANGELOG.md b/src/Symfony/Component/Uid/CHANGELOG.md index b82133751c0a9..dd28dd6a54c49 100644 --- a/src/Symfony/Component/Uid/CHANGELOG.md +++ b/src/Symfony/Component/Uid/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + + * Add `UuidV1::toV6()`, `UuidV1::toV7()` and `UuidV6::toV7()` + 6.2 --- diff --git a/src/Symfony/Component/Uid/Tests/UuidTest.php b/src/Symfony/Component/Uid/Tests/UuidTest.php index 5e05b89f6e395..297f85cb99993 100644 --- a/src/Symfony/Component/Uid/Tests/UuidTest.php +++ b/src/Symfony/Component/Uid/Tests/UuidTest.php @@ -427,4 +427,48 @@ public function testFromStringBase58Padding() { $this->assertInstanceOf(Uuid::class, Uuid::fromString('111111111u9QRyVM94rdmZ')); } + + public function testV1ToV6() + { + $uuidV1 = new UuidV1('8189d3de-9670-11ee-b9d1-0242ac120002'); + $uuidV6 = $uuidV1->toV6(); + + $this->assertEquals($uuidV1->getDateTime(), $uuidV6->getDateTime()); + $this->assertSame($uuidV1->getNode(), $uuidV6->getNode()); + $this->assertEquals($uuidV6, $uuidV1->toV6()); + } + + public function testV1ToV7BeforeUnixEpochThrows() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Cannot convert UUID to v7: its timestamp is before the Unix epoch.'); + + (new UuidV1('9aba8000-ff00-11b0-b3db-3b3fc83afdfc'))->toV7(); // Timestamp is 1969-01-01 00:00:00.0000000 + } + + public function testV1ToV7() + { + $uuidV1 = new UuidV1('eb248d80-ea4f-11ec-9d2a-839425e6fb88'); + $sameUuidV1100NanosecondsLater = new UuidV1('eb248d81-ea4f-11ec-9d2a-839425e6fb88'); + $uuidV7 = $uuidV1->toV7(); + $sameUuidV7100NanosecondsLater = $sameUuidV1100NanosecondsLater->toV7(); + + $this->assertSame($uuidV1->getDateTime()->format('Uv'), $uuidV7->getDateTime()->format('Uv')); + $this->assertEquals($uuidV7, $uuidV1->toV7()); + $this->assertNotEquals($uuidV7, $sameUuidV7100NanosecondsLater); + $this->assertSame(hexdec('0'.substr($uuidV7, -2)) + 1, hexdec('0'.substr($sameUuidV7100NanosecondsLater, -2))); + } + + public function testV1ToV7WhenExtraTimeEntropyOverflows() + { + $uuidV1 = new UuidV1('10e7718f-2d4f-11be-bfed-cdd35907e584'); + $sameUuidV1100NanosecondsLater = new UuidV1('10e77190-2d4f-11be-bfed-cdd35907e584'); + $uuidV7 = $uuidV1->toV7(); + $sameUuidV7100NanosecondsLater = $sameUuidV1100NanosecondsLater->toV7(); + + $this->assertSame($uuidV1->getDateTime()->format('Uv'), $uuidV7->getDateTime()->format('Uv')); + $this->assertEquals($uuidV7, $uuidV1->toV7()); + $this->assertNotEquals($uuidV7, $sameUuidV7100NanosecondsLater); + $this->assertSame(hexdec('0'.substr($uuidV7, -2)) + 1, hexdec('0'.substr($sameUuidV7100NanosecondsLater, -2))); + } } diff --git a/src/Symfony/Component/Uid/UuidV1.php b/src/Symfony/Component/Uid/UuidV1.php index 8c03792113741..4bb24dacee87d 100644 --- a/src/Symfony/Component/Uid/UuidV1.php +++ b/src/Symfony/Component/Uid/UuidV1.php @@ -41,6 +41,18 @@ public function getNode(): string return uuid_mac($this->uid); } + public function toV6(): UuidV6 + { + $uuid = $this->uid; + + return new UuidV6(substr($uuid, 15, 3).substr($uuid, 9, 4).$uuid[0].'-'.substr($uuid, 1, 4).'-6'.substr($uuid, 5, 3).substr($uuid, 18, 6).substr($uuid, 24)); + } + + public function toV7(): UuidV7 + { + return $this->toV6()->toV7(); + } + public static function generate(\DateTimeInterface $time = null, Uuid $node = null): string { $uuid = !$time || !$node ? uuid_create(static::TYPE) : parent::NIL; diff --git a/src/Symfony/Component/Uid/UuidV6.php b/src/Symfony/Component/Uid/UuidV6.php index 1cecf408d9177..cb213ce774208 100644 --- a/src/Symfony/Component/Uid/UuidV6.php +++ b/src/Symfony/Component/Uid/UuidV6.php @@ -43,6 +43,33 @@ public function getNode(): string return substr($this->uid, 24); } + public function toV7(): UuidV7 + { + $uuid = $this->uid; + $time = BinaryUtil::hexToNumericString('0'.substr($uuid, 0, 8).substr($uuid, 9, 4).substr($uuid, 15, 3)); + if ('-' === $time[0]) { + throw new \InvalidArgumentException('Cannot convert UUID to v7: its timestamp is before the Unix epoch.'); + } + + $ms = \strlen($time) > 4 ? substr($time, 0, -4) : '0'; + $time = dechex(10000 * hexdec(substr($uuid, 20, 3)) + substr($time, -4)); + + if (\strlen($time) > 6) { + $uuid[29] = dechex(hexdec($uuid[29]) ^ hexdec($time[0])); + $time = substr($time, 1); + } + + return new UuidV7(substr_replace(sprintf( + '%012s-7%s-%s%s-%s%06s', + \PHP_INT_SIZE >= 8 ? dechex($ms) : bin2hex(BinaryUtil::fromBase($ms, BinaryUtil::BASE10)), + substr($uuid, -6, 3), + $uuid[19], + substr($uuid, -3), + substr($uuid, -12, 6), + $time + ), '-', 8, 0)); + } + public static function generate(\DateTimeInterface $time = null, Uuid $node = null): string { $uuidV1 = UuidV1::generate($time, $node);