8000 [Uid] Add UidFactory to create Ulid and Uuid from timestamps and rand… · symfony/symfony@421431c · GitHub
[go: up one dir, main page]

Skip to content

Commit 421431c

Browse files
committed
[Uid] Add UidFactory to create Ulid and Uuid from timestamps and randomness/nodes
1 parent c82567b commit 421431c

File tree

7 files changed

+313
-42
lines changed

7 files changed

+313
-42
lines changed

src/Symfony/Component/Uid/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
-----
66

77
* made UUIDv6 always return truly random node fields to prevent leaking the MAC of the host
8+
* added UidFactory to create Ulid and Uuid from timestamps and randomness/nodes
89

910
5.1.0
1011
-----
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Uid\Tests;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Uid\UidFactory;
16+
17+
final class UidFactoryTest extends TestCase
18+
{
19+
public function testCreateUlidWithoutArguments()
20+
{
21+
(new UidFactory())->createUlid();
22+
23+
$this->addToAssertionCount(1);
24+
}
25+
26+
/**
27+
* @dataProvider provideCreateUlid
28+
*/
29+
public function testCreateUlid(?float $expectedTime, ?string $expectedRandomness, ?string $timestamp, ?string $randomness)
30+
{
31+
$ulid = (new UidFactory())->createUlid($timestamp, $randomness);
32+
33+
if (null === $expectedTime && null === $expectedRandomness) {
34+
$this->addToAssertionCount(1);
35+
36+
return;
37+
}
38+
39+
if (null !== $expectedTime) {
40+
$this->assertSame($expectedTime, $ulid->getTime());
41+
}
42+
43+
if (null !== $expectedRandomness) {
44+
$this->assertStringEndsWith($expectedRandomness, $ulid->toBase32());
45+
}
46+
}
47+
48+
public function provideCreateUlid()
49+
{
50+
foreach ([
51+
[null, null],
52+
[0.0, '0'],
53+
] as [$expectedTimestamp, $timestamp]) {
54+
foreach ([
55+
null,
56+
'0000000000000000',
57+
'ZZZZZZZZZZZZZZZZ',
58+
'2345ABCDEFGHJKMN',
59+
] as $randomness) {
60+
yield [$expectedTimestamp, $randomness, $timestamp, $randomness];
61+
}
62+
}
63+
}
64+
65+
/**
66+
* @dataProvider provideCreateUuid
67+
*/
68+
public function testCreateUuidV1(?float $expectedTime, ?string $expectedNode, ?string $timestamp, ?string $node)
69+
{
70+
$uuid = (new UidFactory())->createUuidV1($timestamp, $node);
71+
72+
if (null === $expectedTime && null === $expectedNode) {
73+
$this->addToAssertionCount(1);
74+
75+
return;
76+
}
77+
78+
if (null !== $expectedTime) {
79+
$this->assertSame($expectedTime, $uuid->getTime());
80+
}
81+
82+
if (null !== $expectedNode) {
83+
$this->assertSame($expectedNode, $uuid->getNode());
84+
}
85+
}
86+
87+
/**
88+
* @dataProvider provideCreateUuid
89+
*/
90+
public function testCreateUuidV4(?float $expectedTime, ?string $expectedNode, ?string $timestamp, ?string $node)
91+
{
92+
$uuid = (new UidFactory())->createUuidV4($timestamp, $node);
93+
94+
if (null === $expectedTime && null === $expectedNode) {
95+
$this->addToAssertionCount(1);
96+
97+
return;
98+
}
99+
100+
if (null !== $expectedTime) {
101+
$this->assertSame($expectedTime, $uuid->getTime());
102+
}
103+
104+
if (null !== $expectedNode) {
105+
$this->assertSame($expectedNode, $uuid->getNode());
106+
}
107+
}
108+
109+
/**
110+
* @dataProvider provideCreateUuid
111+
*/
112+
public function testCreateUuidV6(?float $expectedTime, ?string $expectedNode, ?string $timestamp, ?string $node)
113+
{
114+
$uuid = (new UidFactory())->createUuidV6($timestamp, $node);
115+
116+
if (null === $expectedTime && null === $expectedNode) {
117+
$this->addToAssertionCount(1);
118+
119+
return;
120+
}
121+
122+
if (null !== $expectedTime) {
123+
$this->assertSame($expectedTime, $uuid->getTime());
124+
}
125+
126+
if (null !== $expectedNode) {
127+
$this->assertSame($expectedNode, $uuid->getNode());
128+
}
129+
}
130+
131+
public function provideCreateUuid()
132+
{
133+
foreach ([
134+
[-12219292800.0, '0'],
135+
[0, (string) 0x01b21dd213814000],
136+
[103072857660.6847, '999999999999999999999'],
137+
] as [$expectedTimestamp, $timestamp]) {
138+
foreach ([
139+
null,
140+
'000000000000',
141+
'ffffffffffff',
142+
'370a6f305c0d',
143+
] as $node) {
144+
yield [$expectedTimestamp, $node, $timestamp, $node];
145+
}
146+
}
147+
}
148+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Uid;
13+
14+
final class UidFactory
15+
{
16+
/**
17+
* @param string|null $timestamp Milliseconds since the Unix epoch 1970-01-01 00:00:00
18+
* @param string|null $randomness 16 characters of the Base32 dictionary (0000000000000000 to ZZZZZZZZZZZZZZZZ)
19+
*/
20+
public function createUlid(string $timestamp = null, string $randomness = null): Ulid
21+
{
22+
return new Ulid(Ulid::generate($timestamp, $randomness));
23+
}
24+
25+
/**
26+
* @param string|null $timestamp Count of 100-nanosecond intervals since the UUID epoch 1582-10-15 00:00:00
27+
* @param string|null $node 12 characters of the Base16 (hexadecimal) dictionary (000000000000 to ffffffffffff)
28+
*/
29+
public function createUuidV1(string $timestamp = null, string $node = null): UuidV1
30+
{
31+
return new UuidV1(UuidV1::generate($timestamp, $node));
32+
}
33+
34+
/**
35+
* @param string|null $timestamp Count of 100-nanosecond intervals since the UUID epoch 1582-10-15 00:00:00
36+
* @param string|null $node 12 characters of the Base16 (hexadecimal) dictionary (000000000000 to ffffffffffff)
37+
*/
38+
public function createUuidV4(string $timestamp = null, string $node = null): UuidV4
39+
{
40+
return new UuidV4(UuidV4::generate($timestamp, $node));
41+
}
42+
43+
/**
44+
* @param string|null $timestamp Count of 100-nanosecond intervals since the UUID epoch 1582-10-15 00:00:00
45+
* @param string|null $node 12 characters of the Base16 (hexadecimal) dictionary (000000000000 to ffffffffffff)
46+
*/
47+
public function createUuidV6(string $timestamp = null, string $node = null): UuidV6
48+
{
49+
return new UuidV6(UuidV6::generate($timestamp, $node));
50+
}
51+
}

src/Symfony/Component/Uid/Ulid.php

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -121,30 +121,37 @@ public function getTime(): float
121121
return BinaryUtil::toBase(hex2bin($time), BinaryUtil::BASE10) / 1000;
122122
}
123123

124-
private static function generate(): string
124+
/**
125+
* @internal
126+
*/
127+
final public static function generate(string $time = null, string $randomness = null): string
125128
{
126-
$time = microtime(false);
127-
$time = substr($time, 11).substr($time, 2, 3);
128-
129-
if ($time !== self::$time) {
130-
$r = unpack('nr1/nr2/nr3/nr4/nr', random_bytes(10));
131-
$r['r1'] |= ($r['r'] <<= 4) & 0xF0000;
132-
$r['r2'] |= ($r['r'] <<= 4) & 0xF0000;
133-
$r['r3'] |= ($r['r'] <<= 4) & 0xF0000;
134-
$r['r4'] |= ($r['r'] <<= 4) & 0xF0000;
135-
unset($r['r']);
136-
self::$rand = array_values($r);
137-
self::$time = $time;
138-
} elseif ([0xFFFFF, 0xFFFFF, 0xFFFFF, 0xFFFFF] === self::$rand) {
139-
usleep(100);
140-
141-
return self::generate();
142-
} else {
143-
for ($i = 3; $i >= 0 && 0xFFFFF === self::$rand[$i]; --$i) {
144-
self::$rand[$i] = 0;
145-
}
129+
if (null === $time) {
130+
$time = microtime(false);
131+
$time = substr($time, 11).substr($time, 2, 3);
132+
}
146133

147-
++self::$rand[$i];
134+
if (null == $randomness) {
135+
if ($time !== self::$time) {
136+
$r = unpack('nr1/nr2/nr3/nr4/nr', random_bytes(10));
137+
$r['r1'] |= ($r['r'] <<= 4) & 0xF0000;
138+
$r['r2'] |= ($r['r'] <<= 4) & 0xF0000;
139+
$r['r3'] |= ($r['r'] <<= 4) & 0xF0000;
140+
$r['r4'] |= ($r['r'] <<= 4) & 0xF0000;
141+
unset($r['r']);
142+
self::$rand = array_values($r);
143+
self::$time = $time;
144+
} elseif ([0xFFFFF, 0xFFFFF, 0xFFFFF, 0xFFFFF] === self::$rand) {
145+
usleep(100);
146+
147+
return self::generate();
148+
} else {
149+
for ($i = 3; $i >= 0 && 0xFFFFF === self::$rand[$i]; --$i) {
150+
self::$rand[$i] = 0;
151+
}
152+
153+
++self::$rand[$i];
154+
}
148155
}
149156

150157
if (\PHP_INT_SIZE >= 8) {
@@ -158,6 +165,10 @@ private static function generate(): string
158165
);
159166
}
160167

168+
if (null !== $randomness) {
169+
return strtr(sprintf('%010s', $time).$randomness, 'abcdefghijklmnopqrstuv', 'ABCDEFGHJKMNPQRSTVWXYZ');
170+
}
171+
161172
return strtr(sprintf('%010s%04s%04s%04s%04s',
162173
$time,
163174
base_convert(self::$rand[0], 10, 32),

src/Symfony/Component/Uid/UuidV1.php

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class UuidV1 extends Uuid
2525
public function __construct(string $uuid = null)
2626
{
2727
if (null === $uuid) {
28-
$this->uid = uuid_create(static::TYPE);
28+
$this->uid = self::generate();
2929
} else {
3030
parent::__construct($uuid);
3131
}
@@ -42,4 +42,28 @@ public function getNode(): string
4242
{
4343
return uuid_mac($this->uid);
4444
}
45+
46+
/**
47+
* @internal
48+
*/
49+
final public static function generate(string $timestamp = null, string $node = null): string
50+
{
51+
$uuid = uuid_create(static::TYPE);
52+
53+
if (null !== $timestamp) {
54+
if (\PHP_INT_SIZE >= 8) {
55+
$time = str_pad(dechex($timestamp), 16, '0', \STR_PAD_LEFT);
56+
} else {
57+
$time = bin2hex(str_pad(BinaryUtil::fromBase($timestamp, BinaryUtil::BASE10), 16, "\0", \STR_PAD_LEFT));
58+
}
59+
60+
$uuid = substr($time, 8).'-'.substr($time, 4, 4).'-1'.substr($time, 1, 3).substr($uuid, 18);
61+
}
62+
63+
if (null !== $node) {
64+
$uuid = substr($uuid, 0, 24).$node;
65+
}
66+
67+
return $uuid;
68+
}
4569
}

src/Symfony/Component/Uid/UuidV4.php

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,38 @@ class UuidV4 extends Uuid
2525
public function __construct(string $uuid = null)
2626
{
2727
if (null === $uuid) {
28+
$this->uid = self::generate();
29+
} else {
30+
parent::__construct($uuid);
31+
}
32+
}
33+
34+
public function getTime(): float
35+
{
36+
$time = '0'.substr($this->uid, 15, 3).substr($this->uid, 9, 4).substr($this->uid, 0, 8);
37+
38+
return BinaryUtil::timeToFloat($time);
39+
}
40+
41+
public function getNode(): string
42+
{
43+
return substr($this->uid, 24);
44+
}
45+
46+
/**
47+
* @internal
48+
*/
49+
public static function generate(string $timestamp = null, string $node = null): string
50+
{
51+
if (null === $timestamp && null === $node) {
2852
$uuid = random_bytes(16);
2953
$uuid[6] = $uuid[6] & "\x0F" | "\x4F";
3054
$uuid[8] = $uuid[8] & "\x3F" | "\x80";
3155
$uuid = bin2hex($uuid);
3256

33-
$this->uid = substr($uuid, 0, 8).'-'.substr($uuid, 8, 4).'-'.substr($uuid, 12, 4).'-'.substr($uuid, 16, 4).'-'.substr($uuid, 20, 12);
34-
} else {
35-
parent::__construct($uuid);
57+
return substr($uuid, 0, 8).'-'.substr($uuid, 8, 4).'-'.substr($uuid, 12, 4).'-'.substr($uuid, 16, 4).'-'.substr($uuid, 20, 12);
3658
}
59+
60+
return substr($uuid = UuidV1::generate($timestamp, $node), 0, 14).'4'.substr($uuid, 15);
3761
}
3862
}

0 commit comments

Comments
 (0)
0