8000 feature #53060 [Uid] Add `UuidV1::toV6()`, `UuidV1::toV7()` and `Uuid… · symfony/symfony@e85fa2c · GitHub
[go: up one dir, main page]

Skip to content

Commit e85fa2c

Browse files
feature #53060 [Uid] Add UuidV1::toV6(), UuidV1::toV7() and UuidV6::toV7() (fancyweb, nicolas-grekas)
This PR was merged into the 7.1 branch. Discussion ---------- [Uid] Add `UuidV1::toV6()`, `UuidV1::toV7()` and `UuidV6::toV7()` | Q | A | ------------- | --- | Branch? | 7.1 | Bug fix? | no | New feature? | yes | Deprecations? | false | Issues | - | License | MIT The goal is to help working with new UUIDv6 & v7 versions when you receive legacy UUIDv1. UUIDv6 and v1 are 100% compatible since v6 is just a "reordering" of v1. UUIDv7 and v1 are not strictly compatible for several reasons: the timestamp precision is not the same. Also, there is no clock seq or variant or node in v7 but full randomness instead. And maybe others? However, I believe we can still technically do the conversion (timestamp -> timestamp, clock seq + variant + node -> randomness) with at least one concession: the sub-milliseconds v1 timestamp precision is lost since v7 doesn't support it. My proposed v1 to v7 implementation has 2 issues: * sub-microsecond entropy is entirely lost * monotonicity is lost? I'm opening this PR so that `@nicolas`-grekas gets triggered by the inefficiency of my code and finds a better solution 😁 I'm kidding, I actually know he already has an idea because he explained it to me during the Brussels HackDay but I couldn't understand it well enough to be able to implement it 😅 Commits ------- 9ee1437 [Uid] Add `UuidV1::toV6()`, `UuidV1::toV7()` and `UuidV6::toV7()` 93f1812 [Uid] Add `UuidV6::fromV1()` and `UuidV7::fromV1()` methods
2 parents 82dc312 + 9ee1437 commit e85fa2c

File tree

5 files changed

+102
-2
lines changed

5 files changed

+102
-2
lines changed

src/Symfony/Component/Uid/BinaryUtil.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,10 @@ public static function add(string $a, string $b): string
118118

119119
/**
120120
* @param string $time Count of 100-nanosecond intervals since the UUID epoch 1582-10-15 00:00:00 in hexadecimal
121+
*
122+
* @return string Count of 100-nanosecond intervals since the UUID epoch 1582-10-15 00:00:00 as a numeric string
121123
*/
122-
public static function hexToDateTime(string $time): \DateTimeImmutable
124+
public static function hexToNumericString(string $time): string
123125
{
124126
if (\PHP_INT_SIZE >= 8) {
125127
$time = (string) (hexdec($time) - self::TIME_OFFSET_INT);
@@ -140,7 +142,17 @@ public static function hexToDateTime(string $time): \DateTimeImmutable
140142
$time = '-' === $time[0] ? '-'.str_pad(substr($time, 1), 8, '0', \STR_PAD_LEFT) : str_pad($time, 8, '0', \STR_PAD_LEFT);
141143
}
142144

143-
return \DateTimeImmutable::createFromFormat('U.u?', substr_replace($time, '.', -7, 0));
145+
return $time;
146+
}
147+
148+
/**
149+
* Sub-microseconds are lost since they are not handled by \DateTimeImmutable.
150+
*
151+
* @param string $time Count of 100-nanosecond intervals since the UUID epoch 1582-10-15 00:00:00 in hexadecimal
152+
*/
153+
public static function hexToDateTime(string $time): \DateTimeImmutable
154+
{
155+
return \DateTimeImmutable::createFromFormat('U.u?', substr_replace(self::hexToNumericString($time), '.', -7, 0));
144156
}
145157

146158
/**

src/Symfony/Component/Uid/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
7.1
5+
---
6+
7+
* Add `UuidV1::toV6()`, `UuidV1::toV7()` and `UuidV6::toV7()`
8+
49
6.2
510
---
611

src/Symfony/Component/Uid/Tests/UuidTest.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,4 +427,48 @@ public function testFromStringBase58Padding()
427427
{
428428
$this->assertInstanceOf(Uuid::class, Uuid::fromString('111111111u9QRyVM94rdmZ'));
429429
}
430+
431+
public function testV1ToV6()
432+
{
433+
$uuidV1 = new UuidV1('8189d3de-9670-11ee-b9d1-0242ac120002');
434+
$uuidV6 = $uuidV1->toV6();
435+
436+
$this->assertEquals($uuidV1->getDateTime(), $uuidV6->getDateTime());
437+
$this->assertSame($uuidV1->getNode(), $uuidV6->getNode());
438+
$this->assertEquals($uuidV6, $uuidV1->toV6());
439+
}
440+
441+
public function testV1ToV7BeforeUnixEpochThrows()
442+
{
443+
$this->expectException(\InvalidArgumentException::class);
444+
$this->expectExceptionMessage('Cannot convert UUID to v7: its timestamp is before the Unix epoch.');
445+
446+
(new UuidV1('9aba8000-ff00-11b0-b3db-3b3fc83afdfc'))->toV7(); // Timestamp is 1969-01-01 00:00:00.0000000
447+
}
448+
449+
public function testV1ToV7()
450+
{
451+
$uuidV1 = new UuidV1('eb248d80-ea4f-11ec-9d2a-839425e6fb88');
452+
$sameUuidV1100NanosecondsLater = new UuidV1('eb248d81-ea4f-11ec-9d2a-839425e6fb88');
453+
$uuidV7 = $uuidV1->toV7();
454+
$sameUuidV7100NanosecondsLater = $sameUuidV1100NanosecondsLater->toV7();
455+
456+
$this->assertSame($uuidV1->getDateTime()->format('Uv'), $uuidV7->getDateTime()->format('Uv'));
457+
$this->assertEquals($uuidV7, $uuidV1->toV7());
458+
$this->assertNotEquals($uuidV7, $sameUuidV7100NanosecondsLater);
459+
$this->assertSame(hexdec('0'.substr($uuidV7, -2)) + 1, hexdec('0'.substr($sameUuidV7100NanosecondsLater, -2)));
460+
}
461+
462+
public function testV1ToV7WhenExtraTimeEntropyOverflows()
463+
{
464+
$uuidV1 = new UuidV1('10e7718f-2d4f-11be-bfed-cdd35907e584');
465+
$sameUuidV1100NanosecondsLater = new UuidV1('10e77190-2d4f-11be-bfed-cdd35907e584');
466+
$uuidV7 = $uuidV1->toV7();
467+
$sameUuidV7100NanosecondsLater = $sameUuidV1100NanosecondsLater->toV7();
468+
469+
$this->assertSame($uuidV1->getDateTime()->format('Uv'), $uuidV7->getDateTime()->format('Uv'));
470+
$this->assertEquals($uuidV7, $uuidV1->toV7());
471+
$this->assertNotEquals($uuidV7, $sameUuidV7100NanosecondsLater);
472+
$this->assertSame(hexdec('0'.substr($uuidV7, -2)) + 1, hexdec('0'.substr($sameUuidV7100NanosecondsLater, -2)));
473+
}
430474
}

src/Symfony/Component/Uid/UuidV1.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,18 @@ public function getNode(): string
4141
return uuid_mac($this->uid);
4242
}
4343

44+
public function toV6(): UuidV6
45+
{
46+
$uuid = $this->uid;
47+
48+
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));
49+
}
50+
51+
public function toV7(): UuidV7
52+
{
53+
return $this->toV6()->toV7();
54+
}
55+
4456
public static function generate(\DateTimeInterface $time = null, Uuid $node = null): string
4557
{
4658
$uuid = !$time || !$node ? uuid_create(static::TYPE) : parent::NIL;

src/Symfony/Component/Uid/UuidV6.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,33 @@ public function getNode(): string
4343
return substr($this->uid, 24);
4444
}
4545

46+
public function toV7(): UuidV7
47+
{
48+
$uuid = $this->uid;
49+
$time = BinaryUtil::hexToNumericString('0'.substr($uuid, 0, 8).substr($uuid, 9, 4).substr($uuid, 15, 3));
50+
if ('-' === $time[0]) {
51+
throw new \InvalidArgumentException('Cannot convert UUID to v7: its timestamp is before the Unix epoch.');
52+
}
53+
54+
$ms = \strlen($time) > 4 ? substr($time, 0, -4) : '0';
55+
$time = dechex(10000 * hexdec(substr($uuid, 20, 3)) + substr($time, -4));
56+
57+
if (\strlen($time) > 6) {
58+
$uuid[29] = dechex(hexdec($uuid[29]) ^ hexdec($time[0]));
59+
$time = substr($time, 1);
60+
}
61+
62+
return new UuidV7(substr_replace(sprintf(
63+
'%012s-7%s-%s%s-%s%06s',
64+
\PHP_INT_SIZE >= 8 ? dechex($ms) : bin2hex(BinaryUtil::fromBase($ms, BinaryUtil::BASE10)),
65+
substr($uuid, -6, 3),
66+
$uuid[19],
67+
substr($uuid, -3),
68+
substr($uuid, -12, 6),
69+
$time
70+
), '-', 8, 0));
71+
}
72+
4673
public static function generate(\DateTimeInterface $time = null, Uuid $node = null): string
4774
{
4875
$uuidV1 = UuidV1::generate($time, $node);

0 commit comments

Comments
 (0)
0