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_ diff --git a/src/Provider/Node/RandomNodeProvider.php b/src/Provider/Node/RandomNodeProvider.php index 6c81fdf..1018554 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 sprintf('%06x%06x', mt_rand(0, 0xffffff), mt_rand(0, 0xffffff)); + $node = hexdec(bin2hex(random_bytes(6))); + + // Set the multicast bit; see RFC 4122, section 4.5. + $node = $node | 0x010000000000; + + return str_pad(dechex($node), 12, '0', STR_PAD_LEFT); } } diff --git a/tests/Provider/Node/RandomNodeProviderTest.php b/tests/Provider/Node/RandomNodeProviderTest.php index 587fe3d..53b56bb 100644 --- a/tests/Provider/Node/RandomNodeProviderTest.php +++ b/tests/Provider/Node/RandomNodeProviderTest.php @@ -8,9 +8,6 @@ use AspectMock\Test as AspectMock; class RandomNodeProviderTest extends TestCase { - private $num = 16532480; - private $node = 'fc4400fc4400'; - protected function setUp() { $this->skipIfHhvm(); @@ -27,36 +24,79 @@ class RandomNodeProviderTest extends TestCase * @runInSeparateProcess * @preserveGlobalState disabled */ - public function testGetNodeUsesMtRand() + public function testGetNodeUsesRandomBytes() { - $mtRand = AspectMock::func('Ramsey\Uuid\Provider\Node', 'mt_rand', $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(); - $mtRand->verifyInvokedMultipleTimes(2, [0, 0xffffff]); + $randomBytes->verifyInvoked(6); } /** * @runInSeparateProcess * @preserveGlobalState disabled */ - public function testGetNodeFormatsRandomNumbersIntoHexString() + public function testGetNodeSetsMulticastBit() { - AspectMock::func('Ramsey\Uuid\Provider\Node', 'mt_rand', $this->num); - $sprintf = AspectMock::func('Ramsey\Uuid\Provider\Node', 'sprintf', $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(); - $sprintf->verifyInvoked(['%06x%06x', $this->num, $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', 'mt_rand', $this->num); - AspectMock::func('Ramsey\Uuid\Provider\Node', 'sprintf', $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); + } + + /** + * @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); + } + + public function testGetNodeAlwaysSetsMulticastBit() + { + $provider = new RandomNodeProvider(); + + $this->assertSame('010000000000', sprintf('%012x', hexdec($provider->getNode()) & 0x010000000000)); } }