From b7f9dc50106b569abe1ce4711288bf22f81a2cef Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Fri, 27 Dec 2019 12:11:42 -0500 Subject: [PATCH] Check for RFC 4122, version 1 UUIDs with OrderedTimeCodec --- CHANGELOG.md | 9 +- src/Codec/OrderedTimeCodec.php | 26 ++++- tests/Codec/OrderedTimeCodecTest.php | 146 +++++++++++++++++++++++---- 3 files changed, 156 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index efa05a8..fda91bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,8 +26,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. `\RuntimeException`. `Uuid::getDateTime()` and `DegradedUuid::getDateTime()` may throw this exception if `\DateTimeImmutable` throws an error or exception. * Add `RandomSourceException` that descends from the built-in PHP - `\RuntimeException`. DefaultTimeGenerator, RandomBytesGenerator, and - RandomNodeProvider may throw this exception if `random_bytes()` or + `\RuntimeException`. `DefaultTimeGenerator`, `RandomBytesGenerator`, and + `RandomNodeProvider` may throw this exception if `random_bytes()` or `random_int()` throw an error or exception. ### Changed @@ -50,6 +50,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. * `TimeConverterInterface::convertTime(string $timestamp): string` * `UnsatisfiedDependencyException` and `UnsupportedOperationException` are now descended from `\LogicException`. Previously, they descended from `\RuntimeException`. +* When encoding to bytes or decoding from bytes, `OrderedTimeCodec` now checks + whether the UUID is an RFC 4122 variant, version 1 UUID. If not, it will throw + an exception—`InvalidArgumentException` when using + `OrderedTimeCodec::encodeBinary()` and `UnsupportedOperationException` when + using `OrderedTimeCodec::decodeBytes()`. ### Deprecated diff --git a/src/Codec/OrderedTimeCodec.php b/src/Codec/OrderedTimeCodec.php index bc2af0c..6878a58 100644 --- a/src/Codec/OrderedTimeCodec.php +++ b/src/Codec/OrderedTimeCodec.php @@ -15,6 +15,8 @@ declare(strict_types=1); namespace Ramsey\Uuid\Codec; use Ramsey\Uuid\Exception\InvalidArgumentException; +use Ramsey\Uuid\Exception\UnsupportedOperationException; +use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidInterface; /** @@ -45,6 +47,16 @@ class OrderedTimeCodec extends StringCodec */ public function encodeBinary(UuidInterface $uuid): string { + if ( + $uuid->getVariant() !== Uuid::RFC_4122 + || $uuid->getVersion() !== Uuid::UUID_TYPE_TIME + ) { + throw new InvalidArgumentException( + 'Expected version 1 (time-based) UUID; received ' + . var_export($uuid->toString(), true) + ); + } + $fields = $uuid->getFieldsHex(); $optimized = [ @@ -88,6 +100,18 @@ class OrderedTimeCodec extends StringCodec . substr($hex, 0, 4) . substr($hex, 16); - return $this->decode($hex); + $uuid = $this->decode($hex); + + if ( + $uuid->getVariant() !== Uuid::RFC_4122 + || $uuid->getVersion() !== Uuid::UUID_TYPE_TIME + ) { + throw new UnsupportedOperationException( + 'Attempting to decode a non-time-based UUID using ' + . 'OrderedTimeCodec' + ); + } + + return $uuid; } } diff --git a/tests/Codec/OrderedTimeCodecTest.php b/tests/Codec/OrderedTimeCodecTest.php index 1041dd7..d595d48 100644 --- a/tests/Codec/OrderedTimeCodecTest.php +++ b/tests/Codec/OrderedTimeCodecTest.php @@ -4,10 +4,17 @@ declare(strict_types=1); namespace Ramsey\Uuid\Test\Codec; +use Mockery; use PHPUnit\Framework\MockObject\MockObject; +use Ramsey\Uuid\Builder\DefaultUuidBuilder; use Ramsey\Uuid\Builder\UuidBuilderInterface; use Ramsey\Uuid\Codec\OrderedTimeCodec; +use Ramsey\Uuid\Converter\NumberConverterInterface; +use Ramsey\Uuid\Converter\TimeConverterInterface; +use Ramsey\Uuid\Exception\InvalidArgumentException; +use Ramsey\Uuid\Exception\UnsupportedOperationException; use Ramsey\Uuid\Test\TestCase; +use Ramsey\Uuid\UuidFactory; use Ramsey\Uuid\UuidInterface; class OrderedTimeCodecTest extends TestCase @@ -42,7 +49,8 @@ class OrderedTimeCodecTest extends TestCase parent::setUp(); $this->builder = $this->getMockBuilder(UuidBuilderInterface::class)->getMock(); $this->uuid = $this->getMockBuilder(UuidInterface::class)->getMock(); - $this->fields = ['time_low' => '58e0a7d7', + $this->fields = [ + 'time_low' => '58e0a7d7', 'time_mid' => 'eebc', 'time_hi_and_version' => '11d8', 'clock_seq_hi_and_reserved' => '96', @@ -75,23 +83,21 @@ class OrderedTimeCodecTest extends TestCase $this->assertEquals($this->uuidString, $result); } - public function testEncodeBinaryUsesFieldsHex(): void - { - $this->uuid->expects($this->once()) - ->method('getFieldsHex') - ->willReturn($this->fields); - $codec = new OrderedTimeCodec($this->builder); - $codec->encodeBinary($this->uuid); - } - - public function testEncodeBinaryReturnsBinaryString(): void + public function testEncodeBinary(): void { $expected = hex2bin($this->optimizedHex); - $this->uuid->method('getFieldsHex') - ->willReturn($this->fields); - $codec = new OrderedTimeCodec($this->builder); - $result = $codec->encodeBinary($this->uuid); - $this->assertEquals($expected, $result); + + $numberConverter = Mockery::mock(NumberConverterInterface::class); + $timeConverter = Mockery::mock(TimeConverterInterface::class); + $builder = new DefaultUuidBuilder($numberConverter, $timeConverter); + $codec = new OrderedTimeCodec($builder); + + $factory = new UuidFactory(); + $factory->setCodec($codec); + + $uuid = $factory->fromString($this->uuidString); + + $this->assertSame($expected, $codec->encodeBinary($uuid)); } public function testDecodeBytesThrowsExceptionWhenBytesStringNotSixteenCharacters(): void @@ -117,11 +123,107 @@ class OrderedTimeCodecTest extends TestCase public function testDecodeBytesRearrangesFields(): void { - $bytes = pack('H*', $this->optimizedHex); - $codec = new OrderedTimeCodec($this->builder); - $this->builder->method('build')->with($this->anything(), $this->equalTo($this->fields)) - ->willReturn($this->uuid); - $result = $codec->decodeBytes($bytes); - $this->assertEquals($this->uuid, $result); + $bytes = (string) hex2bin($this->optimizedHex); + + $numberConverter = Mockery::mock(NumberConverterInterface::class); + $timeConverter = Mockery::mock(TimeConverterInterface::class); + $builder = new DefaultUuidBuilder($numberConverter, $timeConverter); + $codec = new OrderedTimeCodec($builder); + + $factory = new UuidFactory(); + $factory->setCodec($codec); + + $expectedUuid = $factory->fromString($this->uuidString); + $uuidReturned = $codec->decodeBytes($bytes); + + $this->assertTrue($uuidReturned->equals($expectedUuid)); + } + + public function testEncodeBinaryThrowsExceptionForNonRfc4122Uuid(): void + { + $nonRfc4122Uuid = '58e0a7d7-eebc-11d8-d669-0800200c9a66'; + + $numberConverter = Mockery::mock(NumberConverterInterface::class); + $timeConverter = Mockery::mock(TimeConverterInterface::class); + $builder = new DefaultUuidBuilder($numberConverter, $timeConverter); + $codec = new OrderedTimeCodec($builder); + + $factory = new UuidFactory(); + $factory->setCodec($codec); + + $uuid = $factory->fromString($nonRfc4122Uuid); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'Expected version 1 (time-based) UUID; received ' + . "'{$nonRfc4122Uuid}'" + ); + + $codec->encodeBinary($uuid); + } + + public function testEncodeBinaryThrowsExceptionForNonTimeBasedUuid(): void + { + $nonTimeBasedUuid = '58e0a7d7-eebc-41d8-9669-0800200c9a66'; + + $numberConverter = Mockery::mock(NumberConverterInterface::class); + $timeConverter = Mockery::mock(TimeConverterInterface::class); + $builder = new DefaultUuidBuilder($numberConverter, $timeConverter); + $codec = new OrderedTimeCodec($builder); + + $factory = new UuidFactory(); + $factory->setCodec($codec); + + $uuid = $factory->fromString($nonTimeBasedUuid); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'Expected version 1 (time-based) UUID; received ' + . "'{$nonTimeBasedUuid}'" + ); + + $codec->encodeBinary($uuid); + } + + public function testDecodeBytesThrowsExceptionsForNonRfc4122Uuid(): void + { + $nonRfc4122OptimizedHex = '11d8eebc58e0a7d7d6690800200c9a66'; + $bytes = (string) hex2bin($nonRfc4122OptimizedHex); + + $numberConverter = Mockery::mock(NumberConverterInterface::class); + $timeConverter = Mockery::mock(TimeConverterInterface::class); + $builder = new DefaultUuidBuilder($numberConverter, $timeConverter); + $codec = new OrderedTimeCodec($builder); + + $factory = new UuidFactory(); + $factory->setCodec($codec); + + $this->expectException(UnsupportedOperationException::class); + $this->expectExceptionMessage( + 'Attempting to decode a non-time-based UUID using OrderedTimeCodec' + ); + + $codec->decodeBytes($bytes); + } + + public function testDecodeBytesThrowsExceptionsForNonTimeBasedUuid(): void + { + $nonTimeBasedOptimizedHex = '41d8eebc58e0a7d796690800200c9a66'; + $bytes = (string) hex2bin($nonTimeBasedOptimizedHex); + + $numberConverter = Mockery::mock(NumberConverterInterface::class); + $timeConverter = Mockery::mock(TimeConverterInterface::class); + $builder = new DefaultUuidBuilder($numberConverter, $timeConverter); + $codec = new OrderedTimeCodec($builder); + + $factory = new UuidFactory(); + $factory->setCodec($codec); + + $this->expectException(UnsupportedOperationException::class); + $this->expectExceptionMessage( + 'Attempting to decode a non-time-based UUID using OrderedTimeCodec' + ); + + $codec->decodeBytes($bytes); } }