Add NameGeneratorInterface and generators for v3 and v5 UUIDs

This commit is contained in:
Ben Ramsey
2020-02-08 13:12:20 -06:00
parent e5357f2c8a
commit 0bff9e8660
17 changed files with 525 additions and 8 deletions
+7
View File
@@ -12,6 +12,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
* Add `Uuid::fromDateTime()` to create version 1 UUIDs from instances of
`\DateTimeInterface`.
* Add `Generator\NameGeneratorInterface` to support alternate methods of
generating bytes for version 3 and version 5 name-based UUID. By default,
ramsey/uuid uses the `Generator\DefaultNameGenerator`, which uses the standard
algorithm this library has used since the beginning. You may choose to use the
new `Generator\PeclUuidNameGenerator` to make use of the new
`uuid_generate_md5()` and `uuid_generate_sha1()` functions in ext-uuid version
1.1.0.
### Changed
+25
View File
@@ -0,0 +1,25 @@
<?php
/**
* This file is part of the ramsey/uuid library
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @copyright Copyright (c) Ben Ramsey <ben@benramsey.com>
* @license http://opensource.org/licenses/MIT MIT
*/
declare(strict_types=1);
namespace Ramsey\Uuid\Exception;
use RuntimeException as PhpRuntimeException;
/**
* Thrown to indicate that an error occurred while attempting to hash a
* namespace and name
*/
class NameException extends PhpRuntimeException
{
}
+34
View File
@@ -26,6 +26,10 @@ use Ramsey\Uuid\Converter\Time\PhpTimeConverter;
use Ramsey\Uuid\Converter\TimeConverterInterface;
use Ramsey\Uuid\Generator\DceSecurityGenerator;
use Ramsey\Uuid\Generator\DceSecurityGeneratorInterface;
use Ramsey\Uuid\Generator\NameGeneratorFactory;
use Ramsey\Uuid\Generator\NameGeneratorInterface;
use Ramsey\Uuid\Generator\PeclUuidNameGenerator;
use Ramsey\Uuid\Generator\PeclUuidRandomGenerator;
use Ramsey\Uuid\Generator\PeclUuidTimeGenerator;
use Ramsey\Uuid\Generator\RandomGeneratorFactory;
use Ramsey\Uuid\Generator\RandomGeneratorInterface;
@@ -92,6 +96,11 @@ class FeatureSet
*/
private $dceSecurityGenerator;
/**
* @var NameGeneratorInterface
*/
private $nameGenerator;
/**
* @var NodeProviderInterface
*/
@@ -154,6 +163,7 @@ class FeatureSet
$this->builder = $this->buildUuidBuilder($useGuids);
$this->codec = $this->buildCodec($useGuids);
$this->nodeProvider = $this->buildNodeProvider();
$this->nameGenerator = $this->buildNameGenerator();
$this->randomGenerator = $this->buildRandomGenerator();
$this->setTimeProvider(new SystemTimeProvider());
$this->setDceSecurityProvider(new SystemDceSecurityProvider());
@@ -192,6 +202,14 @@ class FeatureSet
return $this->dceSecurityGenerator;
}
/**
* Returns the name generator configured for this environment
*/
public function getNameGenerator(): NameGeneratorInterface
{
return $this->nameGenerator;
}
/**
* Returns the node provider configured for this environment
*/
@@ -329,6 +347,10 @@ class FeatureSet
*/
private function buildRandomGenerator(): RandomGeneratorInterface
{
if ($this->enablePecl) {
return new PeclUuidRandomGenerator();
}
return (new RandomGeneratorFactory())->getGenerator();
}
@@ -351,6 +373,18 @@ class FeatureSet
))->getGenerator();
}
/**
* Returns a name generator configured for this environment
*/
private function buildNameGenerator(): NameGeneratorInterface
{
if ($this->enablePecl) {
return new PeclUuidNameGenerator();
}
return (new NameGeneratorFactory())->getGenerator();
}
/**
* Returns a time converter configured for this environment
*/
+42
View File
@@ -0,0 +1,42 @@
<?php
/**
* This file is part of the ramsey/uuid library
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @copyright Copyright (c) Ben Ramsey <ben@benramsey.com>
* @license http://opensource.org/licenses/MIT MIT
*/
declare(strict_types=1);
namespace Ramsey\Uuid\Generator;
use Ramsey\Uuid\Exception\NameException;
use Ramsey\Uuid\UuidInterface;
use function hash;
/**
* DefaultNameGenerator generates strings of binary data based on a namespace,
* name, and hashing algorithm
*/
class DefaultNameGenerator implements NameGeneratorInterface
{
public function generate(UuidInterface $ns, string $name, string $hashAlgorithm): string
{
/** @var string|bool $bytes */
$bytes = @hash($hashAlgorithm, $ns->getBytes() . $name, true);
if ($bytes === false) {
throw new NameException(sprintf(
'Unable to hash namespace and name with algorithm \'%s\'',
$hashAlgorithm
));
}
return (string) $bytes;
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
/**
* This file is part of the ramsey/uuid library
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @copyright Copyright (c) Ben Ramsey <ben@benramsey.com>
* @license http://opensource.org/licenses/MIT MIT
*/
declare(strict_types=1);
namespace Ramsey\Uuid\Generator;
/**
* NameGeneratorFactory retrieves a default name generator, based on the
* environment
*/
class NameGeneratorFactory
{
/**
* Returns a default name generator, based on the current environment
*/
public function getGenerator(): NameGeneratorInterface
{
return new DefaultNameGenerator();
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
/**
* This file is part of the ramsey/uuid library
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @copyright Copyright (c) Ben Ramsey <ben@benramsey.com>
* @license http://opensource.org/licenses/MIT MIT
*/
declare(strict_types=1);
namespace Ramsey\Uuid\Generator;
use Ramsey\Uuid\UuidInterface;
/**
* A name generator generates strings of binary data created by hashing together
* a namespace with a name, according to a hashing algorithm
*/
interface NameGeneratorInterface
{
/**
* Generate a binary string from a namespace and name hashed together with
* the specified hashing algorithm
*
* @param UuidInterface $ns The namespace
* @param string $name The name to use for creating a UUID
* @param string $hashAlgorithm The hashing algorithm to use
*
* @return string A binary string
*/
public function generate(UuidInterface $ns, string $name, string $hashAlgorithm): string;
}
+53
View File
@@ -0,0 +1,53 @@
<?php
/**
* This file is part of the ramsey/uuid library
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @copyright Copyright (c) Ben Ramsey <ben@benramsey.com>
* @license http://opensource.org/licenses/MIT MIT
*/
declare(strict_types=1);
namespace Ramsey\Uuid\Generator;
use Ramsey\Uuid\Exception\NameException;
use Ramsey\Uuid\UuidInterface;
use function sprintf;
use function uuid_generate_md5;
use function uuid_generate_sha1;
use function uuid_parse;
/**
* PeclUuidNameGenerator generates strings of binary data from a namespace and a
* name, using ext-uuid
*
* @link https://pecl.php.net/package/uuid ext-uuid
*/
class PeclUuidNameGenerator implements NameGeneratorInterface
{
public function generate(UuidInterface $ns, string $name, string $hashAlgorithm): string
{
switch ($hashAlgorithm) {
case 'md5':
$uuid = (string) uuid_generate_md5($ns->toString(), $name);
break;
case 'sha1':
$uuid = (string) uuid_generate_sha1($ns->toString(), $name);
break;
default:
throw new NameException(sprintf(
'Unable to hash namespace and name with algorithm \'%s\'',
$hashAlgorithm
));
}
return (string) uuid_parse($uuid);
}
}
+1 -1
View File
@@ -23,7 +23,7 @@ class RandomGeneratorFactory
/**
* Returns a default random generator, based on the current environment
*/
public static function getGenerator(): RandomGeneratorInterface
public function getGenerator(): RandomGeneratorInterface
{
return new RandomBytesGenerator();
}
+28 -3
View File
@@ -21,6 +21,7 @@ use Ramsey\Uuid\Converter\NumberConverterInterface;
use Ramsey\Uuid\Converter\TimeConverterInterface;
use Ramsey\Uuid\Generator\DceSecurityGeneratorInterface;
use Ramsey\Uuid\Generator\DefaultTimeGenerator;
use Ramsey\Uuid\Generator\NameGeneratorInterface;
use Ramsey\Uuid\Generator\RandomGeneratorInterface;
use Ramsey\Uuid\Generator\TimeGeneratorInterface;
use Ramsey\Uuid\Provider\NodeProviderInterface;
@@ -30,7 +31,6 @@ use Ramsey\Uuid\Type\IntegerValue;
use Ramsey\Uuid\Type\Time;
use Ramsey\Uuid\Validator\ValidatorInterface;
use function hash;
use function pack;
use function str_pad;
use function strtolower;
@@ -52,6 +52,11 @@ class UuidFactory implements UuidFactoryInterface
*/
private $dceSecurityGenerator;
/**
* @var NameGeneratorInterface
*/
private $nameGenerator;
/**
* @var NodeProviderInterface
*/
@@ -96,6 +101,7 @@ class UuidFactory implements UuidFactoryInterface
$this->codec = $features->getCodec();
$this->dceSecurityGenerator = $features->getDceSecurityGenerator();
$this->nameGenerator = $features->getNameGenerator();
$this->nodeProvider = $features->getNodeProvider();
$this->numberConverter = $features->getNumberConverter();
$this->randomGenerator = $features->getRandomGenerator();
@@ -123,6 +129,25 @@ class UuidFactory implements UuidFactoryInterface
$this->codec = $codec;
}
/**
* Returns the name generator used by this factory
*/
public function getNameGenerator(): NameGeneratorInterface
{
return $this->nameGenerator;
}
/**
* Sets the name generator to use for this factory
*
* @param NameGeneratorInterface $nameGenerator A generator to generate
* binary data, based on a namespace and name
*/
public function setNameGenerator(NameGeneratorInterface $nameGenerator): void
{
$this->nameGenerator = $nameGenerator;
}
/**
* Returns the node provider used by this factory
*/
@@ -357,10 +382,10 @@ class UuidFactory implements UuidFactoryInterface
private function uuidFromNsAndName($ns, string $name, int $version, string $hashAlgorithm): UuidInterface
{
if (!($ns instanceof UuidInterface)) {
$ns = $this->codec->decode($ns);
$ns = $this->fromString($ns);
}
$bytes = hash($hashAlgorithm, $ns->getBytes() . $name, true);
$bytes = $this->nameGenerator->generate($ns, $name, $hashAlgorithm);
return $this->uuidFromBytesAndVersion(substr($bytes, 0, 16), $version);
}
+16
View File
@@ -8,6 +8,8 @@ use Mockery;
use Ramsey\Uuid\Builder\FallbackBuilder;
use Ramsey\Uuid\Converter\TimeConverterInterface;
use Ramsey\Uuid\FeatureSet;
use Ramsey\Uuid\Generator\DefaultNameGenerator;
use Ramsey\Uuid\Generator\PeclUuidTimeGenerator;
use Ramsey\Uuid\Guid\GuidBuilder;
use Ramsey\Uuid\Validator\ValidatorInterface;
@@ -43,4 +45,18 @@ class FeatureSetTest extends TestCase
$this->assertInstanceOf(TimeConverterInterface::class, $featureSet->getTimeConverter());
}
public function testDefaultNameGeneratorIsSelected(): void
{
$featureSet = new FeatureSet();
$this->assertInstanceOf(DefaultNameGenerator::class, $featureSet->getNameGenerator());
}
public function testPeclUuidTimeGeneratorIsSelected(): void
{
$featureSet = new FeatureSet(false, false, false, false, true);
$this->assertInstanceOf(PeclUuidTimeGenerator::class, $featureSet->getTimeGenerator());
}
}
@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace Ramsey\Uuid\Test\Generator;
use Ramsey\Uuid\Exception\NameException;
use Ramsey\Uuid\Generator\DefaultNameGenerator;
use Ramsey\Uuid\Test\TestCase;
use Ramsey\Uuid\Uuid;
use function hash;
class DefaultNameGeneratorTest extends TestCase
{
/**
* @dataProvider provideNamesForHashingTest
*/
public function testDefaultNameGeneratorHashesName(string $ns, string $name, string $algorithm): void
{
$namespace = Uuid::fromString($ns);
$expectedBytes = hash($algorithm, $namespace->getBytes() . $name, true);
$generator = new DefaultNameGenerator();
$this->assertSame($expectedBytes, $generator->generate($namespace, $name, $algorithm));
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingTraversableTypeHintSpecification
*/
public function provideNamesForHashingTest(): array
{
return [
[
'ns' => Uuid::NAMESPACE_URL,
'name' => 'https://example.com/foobar',
'algorithm' => 'md5',
],
[
'ns' => Uuid::NAMESPACE_URL,
'name' => 'https://example.com/foobar',
'algorithm' => 'sha1',
],
[
'ns' => Uuid::NAMESPACE_URL,
'name' => 'https://example.com/foobar',
'algorithm' => 'sha256',
],
[
'ns' => Uuid::NAMESPACE_OID,
'name' => '1.3.6.1.4.1.343',
'algorithm' => 'sha1',
],
[
'ns' => Uuid::NAMESPACE_OID,
'name' => '1.3.6.1.4.1.52627',
'algorithm' => 'md5',
],
[
'ns' => 'd988ae29-674e-48e7-b93c-2825e2a96fbe',
'name' => 'foobar',
'algorithm' => 'sha1',
],
];
}
public function testGenerateThrowsException(): void
{
$namespace = Uuid::fromString('cd998804-c661-4264-822c-00cada75a87b');
$generator = new DefaultNameGenerator();
$this->expectException(NameException::class);
$this->expectExceptionMessage(
'Unable to hash namespace and name with algorithm \'aBadAlgorithm\''
);
$generator->generate($namespace, 'a test name', 'aBadAlgorithm');
}
}
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Ramsey\Uuid\Test\Generator;
use Ramsey\Uuid\Generator\DefaultNameGenerator;
use Ramsey\Uuid\Generator\NameGeneratorFactory;
use Ramsey\Uuid\Test\TestCase;
class NameGeneratorFactoryTest extends TestCase
{
public function testGetGenerator(): void
{
$factory = new NameGeneratorFactory();
$this->assertInstanceOf(DefaultNameGenerator::class, $factory->getGenerator());
}
}
@@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
namespace Ramsey\Uuid\Test\Generator;
use Ramsey\Uuid\BinaryUtils;
use Ramsey\Uuid\Exception\NameException;
use Ramsey\Uuid\Generator\PeclUuidNameGenerator;
use Ramsey\Uuid\Test\TestCase;
use Ramsey\Uuid\Uuid;
use function hash;
use function pack;
use function substr;
use function substr_replace;
use function unpack;
class PeclUuidNameGeneratorTest extends TestCase
{
/**
* @dataProvider provideNamesForHashingTest
*/
public function testPeclUuidNameGeneratorHashesName(string $ns, string $name, string $algorithm): void
{
$namespace = Uuid::fromString($ns);
$version = $algorithm === 'md5' ? 3 : 5;
$expectedBytes = substr(hash($algorithm, $namespace->getBytes() . $name, true), 0, 16);
// Need to add the version and variant, since ext-uuid already includes
// these in the values returned.
$timeHi = (int) unpack('n*', substr($expectedBytes, 6, 2))[1];
$timeHiAndVersion = pack('n*', BinaryUtils::applyVersion($timeHi, $version));
$clockSeqHi = (int) unpack('n*', substr($expectedBytes, 8, 2))[1];
$clockSeqHiAndReserved = pack('n*', BinaryUtils::applyVariant($clockSeqHi));
$expectedBytes = substr_replace($expectedBytes, $timeHiAndVersion, 6, 2);
$expectedBytes = substr_replace($expectedBytes, $clockSeqHiAndReserved, 8, 2);
$generator = new PeclUuidNameGenerator();
$generatedBytes = $generator->generate($namespace, $name, $algorithm);
$this->assertSame(
$expectedBytes,
$generatedBytes,
'Expected: ' . bin2hex($expectedBytes) . '; Received: ' . bin2hex($generatedBytes)
);
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingTraversableTypeHintSpecification
*/
public function provideNamesForHashingTest(): array
{
return [
[
'ns' => Uuid::NAMESPACE_URL,
'name' => 'https://example.com/foobar',
'algorithm' => 'md5',
],
[
'ns' => Uuid::NAMESPACE_URL,
'name' => 'https://example.com/foobar',
'algorithm' => 'sha1',
],
[
'ns' => Uuid::NAMESPACE_OID,
'name' => '1.3.6.1.4.1.343',
'algorithm' => 'sha1',
],
[
'ns' => Uuid::NAMESPACE_OID,
'name' => '1.3.6.1.4.1.52627',
'algorithm' => 'md5',
],
[
'ns' => 'd988ae29-674e-48e7-b93c-2825e2a96fbe',
'name' => 'foobar',
'algorithm' => 'sha1',
],
];
}
public function testGenerateThrowsException(): void
{
$namespace = Uuid::fromString('cd998804-c661-4264-822c-00cada75a87b');
$generator = new PeclUuidNameGenerator();
$this->expectException(NameException::class);
$this->expectExceptionMessage(
'Unable to hash namespace and name with algorithm \'aBadAlgorithm\''
);
$generator->generate($namespace, 'a test name', 'aBadAlgorithm');
}
}
@@ -12,7 +12,7 @@ class RandomGeneratorFactoryTest extends TestCase
{
public function testFactoryReturnsRandomBytesGenerator(): void
{
$generator = RandomGeneratorFactory::getGenerator();
$generator = (new RandomGeneratorFactory)->getGenerator();
$this->assertInstanceOf(RandomBytesGenerator::class, $generator);
}
+23
View File
@@ -15,6 +15,8 @@ use Ramsey\Uuid\Converter\NumberConverterInterface;
use Ramsey\Uuid\Converter\TimeConverterInterface;
use Ramsey\Uuid\FeatureSet;
use Ramsey\Uuid\Generator\DceSecurityGeneratorInterface;
use Ramsey\Uuid\Generator\DefaultNameGenerator;
use Ramsey\Uuid\Generator\NameGeneratorInterface;
use Ramsey\Uuid\Generator\RandomGeneratorInterface;
use Ramsey\Uuid\Generator\TimeGeneratorInterface;
use Ramsey\Uuid\Provider\NodeProviderInterface;
@@ -66,6 +68,7 @@ class UuidFactoryTest extends TestCase
$randomGenerator = Mockery::mock(RandomGeneratorInterface::class);
$timeConverter = Mockery::mock(TimeConverterInterface::class);
$timeGenerator = Mockery::mock(TimeGeneratorInterface::class);
$nameGenerator = Mockery::mock(NameGeneratorInterface::class);
$dceSecurityGenerator = Mockery::mock(DceSecurityGeneratorInterface::class);
$numberConverter = Mockery::mock(NumberConverterInterface::class);
$builder = Mockery::mock(UuidBuilderInterface::class);
@@ -77,6 +80,7 @@ class UuidFactoryTest extends TestCase
'getRandomGenerator' => $randomGenerator,
'getTimeConverter' => $timeConverter,
'getTimeGenerator' => $timeGenerator,
'getNameGenerator' => $nameGenerator,
'getDceSecurityGenerator' => $dceSecurityGenerator,
'getNumberConverter' => $numberConverter,
'getBuilder' => $builder,
@@ -204,4 +208,23 @@ class UuidFactoryTest extends TestCase
],
];
}
public function testFactoryReturnsDefaultNameGenerator()
{
$factory = new UuidFactory();
$this->assertInstanceOf(DefaultNameGenerator::class, $factory->getNameGenerator());
}
public function testFactoryReturnsSetNameGenerator()
{
$factory = new UuidFactory();
$this->assertInstanceOf(DefaultNameGenerator::class, $factory->getNameGenerator());
$nameGenerator = Mockery::mock(NameGeneratorInterface::class);
$factory->setNameGenerator($nameGenerator);
$this->assertSame($nameGenerator, $factory->getNameGenerator());
}
}
+1 -1
View File
@@ -651,7 +651,7 @@ class UuidTest extends TestCase
{
$factory = new UuidFactory();
$generator = new CombGenerator(
RandomGeneratorFactory::getGenerator(),
(new RandomGeneratorFactory)->getGenerator(),
$factory->getNumberConverter()
);
+32 -2
View File
@@ -13,7 +13,13 @@ if (!function_exists('uuid_create')) {
*/
function uuid_create($type = 0)
{
return '';
switch ($type) {
case 1:
return \Ramsey\Uuid\v1();
case 4:
default:
return \Ramsey\Uuid\v4();
}
}
}
@@ -24,7 +30,31 @@ if (!function_exists('uuid_parse')) {
*/
function uuid_parse($uuid)
{
return '';
return \Ramsey\Uuid\Uuid::fromString($uuid)->getBytes();
}
}
if (!function_exists('uuid_generate_md5')) {
/**
* @param string $ns
* @param string $name
* @return string
*/
function uuid_generate_md5($ns, $name)
{
return \Ramsey\Uuid\v3($ns, $name);
}
}
if (!function_exists('uuid_generate_sha1')) {
/**
* @param string $ns
* @param string $name
* @return string
*/
function uuid_generate_sha1($ns, $name)
{
return \Ramsey\Uuid\v5($ns, $name);
}
}