From aab08bb08a87bd749dc5b64587a8053bb219d958 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Fri, 22 Sep 2017 11:52:03 -0500 Subject: [PATCH 1/6] Use random_bytes() to generate a random node --- src/Provider/Node/RandomNodeProvider.php | 2 +- .../Provider/Node/RandomNodeProviderTest.php | 22 ++++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/Provider/Node/RandomNodeProvider.php b/src/Provider/Node/RandomNodeProvider.php index 6c81fdf..0af65d5 100644 --- a/src/Provider/Node/RandomNodeProvider.php +++ b/src/Provider/Node/RandomNodeProvider.php @@ -31,6 +31,6 @@ class RandomNodeProvider implements NodeProviderInterface */ public function getNode() { - return sprintf('%06x%06x', mt_rand(0, 0xffffff), mt_rand(0, 0xffffff)); + return bin2hex(random_bytes(6)); } } diff --git a/tests/Provider/Node/RandomNodeProviderTest.php b/tests/Provider/Node/RandomNodeProviderTest.php index 587fe3d..d292c26 100644 --- a/tests/Provider/Node/RandomNodeProviderTest.php +++ b/tests/Provider/Node/RandomNodeProviderTest.php @@ -8,11 +8,13 @@ use AspectMock\Test as AspectMock; class RandomNodeProviderTest extends TestCase { - private $num = 16532480; - private $node = 'fc4400fc4400'; + private $num; + private $node = '38a675685d50'; protected function setUp() { + $this->num = pack('H*', base_convert(decbin(3892974093781), 2, 16)); + $this->skipIfHhvm(); parent::setUp(); } @@ -27,12 +29,12 @@ class RandomNodeProviderTest extends TestCase * @runInSeparateProcess * @preserveGlobalState disabled */ - public function testGetNodeUsesMtRand() + public function testGetNodeUsesRandomBytes() { - $mtRand = AspectMock::func('Ramsey\Uuid\Provider\Node', 'mt_rand', $this->num); + $randomBytes = AspectMock::func('Ramsey\Uuid\Provider\Node', 'random_bytes', $this->num); $provider = new RandomNodeProvider(); $provider->getNode(); - $mtRand->verifyInvokedMultipleTimes(2, [0, 0xffffff]); + $randomBytes->verifyInvoked(6); } /** @@ -41,11 +43,11 @@ class RandomNodeProviderTest extends TestCase */ public function testGetNodeFormatsRandomNumbersIntoHexString() { - AspectMock::func('Ramsey\Uuid\Provider\Node', 'mt_rand', $this->num); - $sprintf = AspectMock::func('Ramsey\Uuid\Provider\Node', 'sprintf', $this->node); + AspectMock::func('Ramsey\Uuid\Provider\Node', 'random_bytes', $this->num); + $bin2Hex = AspectMock::func('Ramsey\Uuid\Provider\Node', 'bin2hex', $this->node); $provider = new RandomNodeProvider(); $provider->getNode(); - $sprintf->verifyInvoked(['%06x%06x', $this->num, $this->num]); + $bin2Hex->verifyInvoked($this->num); } /** @@ -54,8 +56,8 @@ class RandomNodeProviderTest extends TestCase */ public function testGetNodeReturnsHexString() { - AspectMock::func('Ramsey\Uuid\Provider\Node', 'mt_rand', $this->num); - AspectMock::func('Ramsey\Uuid\Provider\Node', 'sprintf', $this->node); + AspectMock::func('Ramsey\Uuid\Provider\Node', 'random_bytes', $this->num); + AspectMock::func('Ramsey\Uuid\Provider\Node', 'bin2hex', $this->node); $provider = new RandomNodeProvider(); $this->assertEquals($this->node, $provider->getNode()); } From 1ec0826a40c0c419232f637205c2734045ccb818 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Fri, 22 Sep 2017 12:48:45 -0500 Subject: [PATCH 2/6] =?UTF-8?q?Set=20the=20multicast=20bit=20for=20random?= =?UTF-8?q?=20nodes,=20according=20to=20RFC=204122,=20=C2=A74.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Borrows the idea from cf1c98141462a9fc8386f5dad7ec84ee4ef8880c to properly set the multicast bit to the "least significant bit of the first octet of the node ID." When merged, this will close #171 and #170 --- src/Provider/Node/RandomNodeProvider.php | 7 +++- .../Provider/Node/RandomNodeProviderTest.php | 42 ++++++++++++------- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/Provider/Node/RandomNodeProvider.php b/src/Provider/Node/RandomNodeProvider.php index 0af65d5..69383ec 100644 --- a/src/Provider/Node/RandomNodeProvider.php +++ b/src/Provider/Node/RandomNodeProvider.php @@ -31,6 +31,11 @@ class RandomNodeProvider implements NodeProviderInterface */ public function getNode() { - return bin2hex(random_bytes(6)); + $node = hexdec(bin2hex(random_bytes(6))); + + // Set the multicast bit; see RFC 4122, section 4.5. + $node = $node | 0x010000000000; + + return dechex($node); } } diff --git a/tests/Provider/Node/RandomNodeProviderTest.php b/tests/Provider/Node/RandomNodeProviderTest.php index d292c26..99bc5e6 100644 --- a/tests/Provider/Node/RandomNodeProviderTest.php +++ b/tests/Provider/Node/RandomNodeProviderTest.php @@ -8,13 +8,8 @@ use AspectMock\Test as AspectMock; class RandomNodeProviderTest extends TestCase { - private $num; - private $node = '38a675685d50'; - protected function setUp() { - $this->num = pack('H*', base_convert(decbin(3892974093781), 2, 16)); - $this->skipIfHhvm(); parent::setUp(); } @@ -31,7 +26,9 @@ class RandomNodeProviderTest extends TestCase */ public function testGetNodeUsesRandomBytes() { - $randomBytes = AspectMock::func('Ramsey\Uuid\Provider\Node', 'random_bytes', $this->num); + $bytes = pack('H*', base_convert(decbin(3892974093781), 2, 16)); + + $randomBytes = AspectMock::func('Ramsey\Uuid\Provider\Node', 'random_bytes', $bytes); $provider = new RandomNodeProvider(); $provider->getNode(); $randomBytes->verifyInvoked(6); @@ -41,24 +38,39 @@ class RandomNodeProviderTest extends TestCase * @runInSeparateProcess * @preserveGlobalState disabled */ - public function testGetNodeFormatsRandomNumbersIntoHexString() + public function testGetNodeSetsMulticastBit() { - AspectMock::func('Ramsey\Uuid\Provider\Node', 'random_bytes', $this->num); - $bin2Hex = AspectMock::func('Ramsey\Uuid\Provider\Node', 'bin2hex', $this->node); + $bytes = pack('H*', base_convert(decbin(3892974093781), 2, 16)); + $expectedBytesHex = '38a675685d50'; + $decimal = 62287585500496; + $expectedNode = '39a675685d50'; + + AspectMock::func('Ramsey\Uuid\Provider\Node', 'random_bytes', $bytes); + $hexDec = AspectMock::func('Ramsey\Uuid\Provider\Node', 'hexdec', $decimal); $provider = new RandomNodeProvider(); - $provider->getNode(); - $bin2Hex->verifyInvoked($this->num); + + $this->assertSame($expectedNode, $provider->getNode()); + $hexDec->verifyInvoked($expectedBytesHex); } /** * @runInSeparateProcess * @preserveGlobalState disabled */ - public function testGetNodeReturnsHexString() + public function testGetNodeAlreadyHasMulticastBit() { - AspectMock::func('Ramsey\Uuid\Provider\Node', 'random_bytes', $this->num); - AspectMock::func('Ramsey\Uuid\Provider\Node', 'bin2hex', $this->node); + $bytes = pack('H*', base_convert(decbin(4492974093781), 2, 16)); + $expectedBytesHex = '4161a1ff5d50'; + $decimal = 71887585500496; + + // We expect the same hex value for the node. + $expectedNode = $expectedBytesHex; + + AspectMock::func('Ramsey\Uuid\Provider\Node', 'random_bytes', $bytes); + $hexDec = AspectMock::func('Ramsey\Uuid\Provider\Node', 'hexdec', $decimal); $provider = new RandomNodeProvider(); - $this->assertEquals($this->node, $provider->getNode()); + + $this->assertSame($expectedNode, $provider->getNode()); + $hexDec->verifyInvoked($expectedBytesHex); } } From 5c38d41ccbe751c9a891067f298f2cac6c373db0 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Fri, 22 Sep 2017 13:03:41 -0500 Subject: [PATCH 3/6] Use sprintf() to prefix zeros on the returned node hex string --- src/Provider/Node/RandomNodeProvider.php | 2 +- .../Provider/Node/RandomNodeProviderTest.php | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/Provider/Node/RandomNodeProvider.php b/src/Provider/Node/RandomNodeProvider.php index 69383ec..7f17c5a 100644 --- a/src/Provider/Node/RandomNodeProvider.php +++ b/src/Provider/Node/RandomNodeProvider.php @@ -36,6 +36,6 @@ class RandomNodeProvider implements NodeProviderInterface // Set the multicast bit; see RFC 4122, section 4.5. $node = $node | 0x010000000000; - return dechex($node); + return sprintf('%012x', $node); } } diff --git a/tests/Provider/Node/RandomNodeProviderTest.php b/tests/Provider/Node/RandomNodeProviderTest.php index 99bc5e6..5816322 100644 --- a/tests/Provider/Node/RandomNodeProviderTest.php +++ b/tests/Provider/Node/RandomNodeProviderTest.php @@ -73,4 +73,23 @@ class RandomNodeProviderTest extends TestCase $this->assertSame($expectedNode, $provider->getNode()); $hexDec->verifyInvoked($expectedBytesHex); } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testGetNodeSetsMulticastBitForLowNodeValue() + { + $bytes = pack('H*', base_convert(decbin(1), 2, 16)); + $expectedBytesHex = '10'; + $decimal = 16; + $expectedNode = '010000000010'; + + AspectMock::func('Ramsey\Uuid\Provider\Node', 'random_bytes', $bytes); + $hexDec = AspectMock::func('Ramsey\Uuid\Provider\Node', 'hexdec', $decimal); + $provider = new RandomNodeProvider(); + + $this->assertSame($expectedNode, $provider->getNode()); + $hexDec->verifyInvoked($expectedBytesHex); + } } From dced71b87afb070aede3f4da51a43a1a3270fd31 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Fri, 22 Sep 2017 14:27:30 -0500 Subject: [PATCH 4/6] Use str_pad() and dechex() instead of sprint() This is a micro-optimization that improves memory usage when generating large quantities of UUIDs. See #159 and #160. --- src/Provider/Node/RandomNodeProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Provider/Node/RandomNodeProvider.php b/src/Provider/Node/RandomNodeProvider.php index 7f17c5a..1018554 100644 --- a/src/Provider/Node/RandomNodeProvider.php +++ b/src/Provider/Node/RandomNodeProvider.php @@ -36,6 +36,6 @@ class RandomNodeProvider implements NodeProviderInterface // Set the multicast bit; see RFC 4122, section 4.5. $node = $node | 0x010000000000; - return sprintf('%012x', $node); + return str_pad(dechex($node), 12, '0', STR_PAD_LEFT); } } From 749b30e3f95c7576fac02356c89808b5b9abf0b5 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Fri, 22 Sep 2017 15:45:24 -0500 Subject: [PATCH 5/6] Add random node test that asserts multicast bit against random values --- tests/Provider/Node/RandomNodeProviderTest.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/Provider/Node/RandomNodeProviderTest.php b/tests/Provider/Node/RandomNodeProviderTest.php index 5816322..53b56bb 100644 --- a/tests/Provider/Node/RandomNodeProviderTest.php +++ b/tests/Provider/Node/RandomNodeProviderTest.php @@ -92,4 +92,11 @@ class RandomNodeProviderTest extends TestCase $this->assertSame($expectedNode, $provider->getNode()); $hexDec->verifyInvoked($expectedBytesHex); } + + public function testGetNodeAlwaysSetsMulticastBit() + { + $provider = new RandomNodeProvider(); + + $this->assertSame('010000000000', sprintf('%012x', hexdec($provider->getNode()) & 0x010000000000)); + } } From 45cffe822057a09e05f7bd09ec5fb88eeecd2334 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Fri, 22 Sep 2017 15:35:45 -0500 Subject: [PATCH 6/6] Add change log notes for 3.7.1 release --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58eaf90..18edcdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # ramsey/uuid Changelog +## 3.7.1 + + * Use `random_bytes()` when generating random nodes + * Set the multicast bit for random nodes, according to RFC 4122, ยง4.5, [#170](https://github.com/ramsey/uuid/pull/170), [#171](https://github.com/ramsey/uuid/pull/171), [#182](https://github.com/ramsey/uuid/pull/182) + ## 3.7.0 _Released: 2017-08-04_