From 0bff9e8660e1e9c4c1958af1873467e732505a9d Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Sat, 8 Feb 2020 13:12:20 -0600 Subject: [PATCH] Add NameGeneratorInterface and generators for v3 and v5 UUIDs --- CHANGELOG.md | 7 ++ src/Exception/NameException.php | 25 +++++ src/FeatureSet.php | 34 +++++++ src/Generator/DefaultNameGenerator.php | 42 ++++++++ src/Generator/NameGeneratorFactory.php | 30 ++++++ src/Generator/NameGeneratorInterface.php | 36 +++++++ src/Generator/PeclUuidNameGenerator.php | 53 ++++++++++ src/Generator/RandomGeneratorFactory.php | 2 +- src/UuidFactory.php | 31 +++++- tests/FeatureSetTest.php | 16 +++ tests/Generator/DefaultNameGeneratorTest.php | 80 +++++++++++++++ tests/Generator/NameGeneratorFactoryTest.php | 19 ++++ tests/Generator/PeclUuidNameGeneratorTest.php | 97 +++++++++++++++++++ .../Generator/RandomGeneratorFactoryTest.php | 2 +- tests/UuidFactoryTest.php | 23 +++++ tests/UuidTest.php | 2 +- tests/phpstan-bootstrap.php | 34 ++++++- 17 files changed, 525 insertions(+), 8 deletions(-) create mode 100644 src/Exception/NameException.php create mode 100644 src/Generator/DefaultNameGenerator.php create mode 100644 src/Generator/NameGeneratorFactory.php create mode 100644 src/Generator/NameGeneratorInterface.php create mode 100644 src/Generator/PeclUuidNameGenerator.php create mode 100644 tests/Generator/DefaultNameGeneratorTest.php create mode 100644 tests/Generator/NameGeneratorFactoryTest.php create mode 100644 tests/Generator/PeclUuidNameGeneratorTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 5043c2e..f243825 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/Exception/NameException.php b/src/Exception/NameException.php new file mode 100644 index 0000000..54d32ec --- /dev/null +++ b/src/Exception/NameException.php @@ -0,0 +1,25 @@ + + * @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 +{ +} diff --git a/src/FeatureSet.php b/src/FeatureSet.php index 4f23171..c3c887c 100644 --- a/src/FeatureSet.php +++ b/src/FeatureSet.php @@ -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 */ diff --git a/src/Generator/DefaultNameGenerator.php b/src/Generator/DefaultNameGenerator.php new file mode 100644 index 0000000..0f8c43c --- /dev/null +++ b/src/Generator/DefaultNameGenerator.php @@ -0,0 +1,42 @@ + + * @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; + } +} diff --git a/src/Generator/NameGeneratorFactory.php b/src/Generator/NameGeneratorFactory.php new file mode 100644 index 0000000..6f08e29 --- /dev/null +++ b/src/Generator/NameGeneratorFactory.php @@ -0,0 +1,30 @@ + + * @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(); + } +} diff --git a/src/Generator/NameGeneratorInterface.php b/src/Generator/NameGeneratorInterface.php new file mode 100644 index 0000000..e064952 --- /dev/null +++ b/src/Generator/NameGeneratorInterface.php @@ -0,0 +1,36 @@ + + * @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; +} diff --git a/src/Generator/PeclUuidNameGenerator.php b/src/Generator/PeclUuidNameGenerator.php new file mode 100644 index 0000000..55740ae --- /dev/null +++ b/src/Generator/PeclUuidNameGenerator.php @@ -0,0 +1,53 @@ + + * @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); + } +} diff --git a/src/Generator/RandomGeneratorFactory.php b/src/Generator/RandomGeneratorFactory.php index 4f2cdb5..b723ac2 100644 --- a/src/Generator/RandomGeneratorFactory.php +++ b/src/Generator/RandomGeneratorFactory.php @@ -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(); } diff --git a/src/UuidFactory.php b/src/UuidFactory.php index 93fcb94..585b4c5 100644 --- a/src/UuidFactory.php +++ b/src/UuidFactory.php @@ -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); } diff --git a/tests/FeatureSetTest.php b/tests/FeatureSetTest.php index 394da44..4948b27 100644 --- a/tests/FeatureSetTest.php +++ b/tests/FeatureSetTest.php @@ -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()); + } } diff --git a/tests/Generator/DefaultNameGeneratorTest.php b/tests/Generator/DefaultNameGeneratorTest.php new file mode 100644 index 0000000..8c3ee19 --- /dev/null +++ b/tests/Generator/DefaultNameGeneratorTest.php @@ -0,0 +1,80 @@ +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'); + } +} diff --git a/tests/Generator/NameGeneratorFactoryTest.php b/tests/Generator/NameGeneratorFactoryTest.php new file mode 100644 index 0000000..f21d4b5 --- /dev/null +++ b/tests/Generator/NameGeneratorFactoryTest.php @@ -0,0 +1,19 @@ +assertInstanceOf(DefaultNameGenerator::class, $factory->getGenerator()); + } +} diff --git a/tests/Generator/PeclUuidNameGeneratorTest.php b/tests/Generator/PeclUuidNameGeneratorTest.php new file mode 100644 index 0000000..65b9cb3 --- /dev/null +++ b/tests/Generator/PeclUuidNameGeneratorTest.php @@ -0,0 +1,97 @@ +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'); + } +} diff --git a/tests/Generator/RandomGeneratorFactoryTest.php b/tests/Generator/RandomGeneratorFactoryTest.php index 20ea82a..88cfc3e 100644 --- a/tests/Generator/RandomGeneratorFactoryTest.php +++ b/tests/Generator/RandomGeneratorFactoryTest.php @@ -12,7 +12,7 @@ class RandomGeneratorFactoryTest extends TestCase { public function testFactoryReturnsRandomBytesGenerator(): void { - $generator = RandomGeneratorFactory::getGenerator(); + $generator = (new RandomGeneratorFactory)->getGenerator(); $this->assertInstanceOf(RandomBytesGenerator::class, $generator); } diff --git a/tests/UuidFactoryTest.php b/tests/UuidFactoryTest.php index edf2c92..5c0427b 100644 --- a/tests/UuidFactoryTest.php +++ b/tests/UuidFactoryTest.php @@ -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()); + } } diff --git a/tests/UuidTest.php b/tests/UuidTest.php index 3502813..add5537 100644 --- a/tests/UuidTest.php +++ b/tests/UuidTest.php @@ -651,7 +651,7 @@ class UuidTest extends TestCase { $factory = new UuidFactory(); $generator = new CombGenerator( - RandomGeneratorFactory::getGenerator(), + (new RandomGeneratorFactory)->getGenerator(), $factory->getNumberConverter() ); diff --git a/tests/phpstan-bootstrap.php b/tests/phpstan-bootstrap.php index de35e38..f7aaf53 100644 --- a/tests/phpstan-bootstrap.php +++ b/tests/phpstan-bootstrap.php @@ -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); } }