diff --git a/src/Builder/DegradedUuidBuilder.php b/src/Builder/DegradedUuidBuilder.php index d577f2d..d5f839b 100644 --- a/src/Builder/DegradedUuidBuilder.php +++ b/src/Builder/DegradedUuidBuilder.php @@ -38,8 +38,6 @@ class DegradedUuidBuilder implements UuidBuilderInterface private $timeConverter; /** - * Constructs the DegradedUuidBuilder - * * @param NumberConverterInterface $numberConverter The number converter to * use when constructing the DegradedUuid * @param TimeConverterInterface $timeConverter The time converter to use diff --git a/src/Codec/GuidStringCodec.php b/src/Codec/GuidStringCodec.php index 24e8689..dce190c 100644 --- a/src/Codec/GuidStringCodec.php +++ b/src/Codec/GuidStringCodec.php @@ -25,33 +25,6 @@ use Ramsey\Uuid\UuidInterface; */ class GuidStringCodec extends StringCodec { - /** - * @psalm-pure - */ - public function encode(UuidInterface $uuid): string - { - /** @var string[] $components */ - $components = array_values($uuid->getFieldsHex()); - - // Swap byte-order on the first three fields. - $components = $this->swapFields($components); - - return vsprintf( - '%08s-%04s-%04s-%02s%02s-%012s', - $components - ); - } - - /** - * @psalm-pure - */ - public function encodeBinary(UuidInterface $uuid): string - { - $components = array_values($uuid->getFieldsHex()); - - return (string) hex2bin(implode('', $components)); - } - /** * @throws InvalidUuidStringException * @@ -61,10 +34,7 @@ class GuidStringCodec extends StringCodec */ public function decode(string $encodedUuid): UuidInterface { - $components = $this->extractComponents($encodedUuid); - - /** @var string[] $components */ - $components = $this->swapFields($components); + $components = $this->swapBytes($this->extractComponents($encodedUuid)); return $this->getBuilder()->build($this, $this->getFields($components)); } @@ -83,28 +53,22 @@ class GuidStringCodec extends StringCodec } /** - * Swap fields to support GUID byte order - * - * @param string[] $components An array of UUID components (the UUID exploded on its dashes) + * @param string[] $fields The fields that comprise this UUID * * @return string[] * * @psalm-pure */ - private function swapFields(array $components): array + private function swapBytes(array $fields): array { - $hex = unpack('H*', pack('L', hexdec($components[0]))); - assert(is_string($hex[1])); - $components[0] = $hex[1]; + $fields = array_values($fields); - $hex = unpack('H*', pack('S', hexdec($components[1]))); - assert(is_string($hex[1])); - $components[1] = $hex[1]; + // Swap bytes to support GUID byte order. + $bytes = (string) hex2bin(implode('', $fields)); + $fields[0] = bin2hex($bytes[3] . $bytes[2] . $bytes[1] . $bytes[0]); + $fields[1] = bin2hex($bytes[5] . $bytes[4]); + $fields[2] = bin2hex($bytes[7] . $bytes[6]); - $hex = unpack('H*', pack('S', hexdec($components[2]))); - assert(is_string($hex[1])); - $components[2] = $hex[1]; - - return $components; + return $fields; } } diff --git a/src/Codec/StringCodec.php b/src/Codec/StringCodec.php index be63516..0a07d17 100644 --- a/src/Codec/StringCodec.php +++ b/src/Codec/StringCodec.php @@ -60,7 +60,7 @@ class StringCodec implements CodecInterface */ public function encodeBinary(UuidInterface $uuid): string { - return (string) hex2bin($uuid->getHex()); + return $uuid->getBytes(); } /** diff --git a/src/FeatureSet.php b/src/FeatureSet.php index 641589d..393e00a 100644 --- a/src/FeatureSet.php +++ b/src/FeatureSet.php @@ -34,6 +34,8 @@ use Ramsey\Uuid\Generator\RandomGeneratorFactory; use Ramsey\Uuid\Generator\RandomGeneratorInterface; use Ramsey\Uuid\Generator\TimeGeneratorFactory; use Ramsey\Uuid\Generator\TimeGeneratorInterface; +use Ramsey\Uuid\Guid\DegradedGuidBuilder; +use Ramsey\Uuid\Guid\GuidBuilder; use Ramsey\Uuid\Provider\Node\FallbackNodeProvider; use Ramsey\Uuid\Provider\Node\RandomNodeProvider; use Ramsey\Uuid\Provider\Node\SystemNodeProvider; @@ -145,7 +147,7 @@ class FeatureSet $this->numberConverter = $this->buildNumberConverter(); $this->timeConverter = $this->buildTimeConverter(); - $this->builder = $this->buildUuidBuilder(); + $this->builder = $this->buildUuidBuilder($useGuids); $this->codec = $this->buildCodec($useGuids); $this->nodeProvider = $this->buildNodeProvider(); $this->randomGenerator = $this->buildRandomGenerator(); @@ -319,13 +321,23 @@ class FeatureSet /** * Returns a UUID builder configured for this environment + * + * @param bool $useGuids Whether to build UUIDs using the GuidStringCodec */ - private function buildUuidBuilder(): UuidBuilderInterface + private function buildUuidBuilder(bool $useGuids = false): UuidBuilderInterface { + if ($this->is64BitSystem() && $useGuids) { + return new GuidBuilder($this->numberConverter, $this->timeConverter); + } + if ($this->is64BitSystem()) { return new DefaultUuidBuilder($this->numberConverter, $this->timeConverter); } + if ($useGuids) { + return new DegradedGuidBuilder($this->numberConverter, $this->timeConverter); + } + return new DegradedUuidBuilder($this->numberConverter, $this->timeConverter); } diff --git a/src/Guid/DegradedGuid.php b/src/Guid/DegradedGuid.php new file mode 100644 index 0000000..c0f39f4 --- /dev/null +++ b/src/Guid/DegradedGuid.php @@ -0,0 +1,48 @@ + + * @license http://opensource.org/licenses/MIT MIT + */ + +declare(strict_types=1); + +namespace Ramsey\Uuid\Guid; + +use Ramsey\Uuid\Codec\CodecInterface; +use Ramsey\Uuid\Converter\NumberConverterInterface; +use Ramsey\Uuid\Converter\TimeConverterInterface; +use Ramsey\Uuid\DegradedUuid; +use Ramsey\Uuid\UuidInterface; + +/** + * DegradedGuid represents a GUID on 32-bit systems + * + * Some of the functionality of a DegradedGuid is not present or degraded, since + * 32-bit systems are unable to perform the necessary mathematical operations or + * represent the integers appropriately. + * + * @psalm-immutable + */ +class DegradedGuid extends DegradedUuid implements UuidInterface +{ + /** + * @param string[] $fields + */ + public function __construct( + array $fields, + NumberConverterInterface $numberConverter, + CodecInterface $codec, + TimeConverterInterface $timeConverter + ) { + $this->fields = new GuidFields((string) hex2bin(implode('', $fields))); + $this->codec = $codec; + $this->numberConverter = $numberConverter; + $this->timeConverter = $timeConverter; + } +} diff --git a/src/Guid/DegradedGuidBuilder.php b/src/Guid/DegradedGuidBuilder.php new file mode 100644 index 0000000..7c4775e --- /dev/null +++ b/src/Guid/DegradedGuidBuilder.php @@ -0,0 +1,72 @@ + + * @license http://opensource.org/licenses/MIT MIT + */ + +declare(strict_types=1); + +namespace Ramsey\Uuid\Guid; + +use Ramsey\Uuid\Builder\UuidBuilderInterface; +use Ramsey\Uuid\Codec\CodecInterface; +use Ramsey\Uuid\Converter\NumberConverterInterface; +use Ramsey\Uuid\Converter\TimeConverterInterface; +use Ramsey\Uuid\UuidInterface; + +/** + * DegradedGuidBuilder builds instances of DegradedGuid + * + * @psalm-immutable + */ +class DegradedGuidBuilder implements UuidBuilderInterface +{ + /** + * @var NumberConverterInterface + */ + private $numberConverter; + + /** + * @var TimeConverterInterface + */ + private $timeConverter; + + /** + * @param NumberConverterInterface $numberConverter The number converter to + * use when constructing the DegradedGuid + * @param TimeConverterInterface $timeConverter The time converter to use + * for converting timestamps extracted from a UUID to Unix timestamps + */ + public function __construct( + NumberConverterInterface $numberConverter, + TimeConverterInterface $timeConverter + ) { + $this->numberConverter = $numberConverter; + $this->timeConverter = $timeConverter; + } + + /** + * Builds and returns a DegradedGuid + * + * @param CodecInterface $codec The codec to use for building this DegradedGuid instance + * @param string[] $fields An array of fields from which to construct a DegradedGuid instance; + * see {@see \Ramsey\Uuid\UuidInterface::getFieldsHex()} for array structure. + * + * @return DegradedGuid The DegradedGuidBuilder returns an instance of Ramsey\Uuid\Guid\DegradedGuid + */ + public function build(CodecInterface $codec, array $fields): UuidInterface + { + return new DegradedGuid( + $fields, + $this->numberConverter, + $codec, + $this->timeConverter + ); + } +} diff --git a/src/Guid/Guid.php b/src/Guid/Guid.php new file mode 100644 index 0000000..f746277 --- /dev/null +++ b/src/Guid/Guid.php @@ -0,0 +1,53 @@ + + * @license http://opensource.org/licenses/MIT MIT + */ + +declare(strict_types=1); + +namespace Ramsey\Uuid\Guid; + +use Ramsey\Uuid\Codec\CodecInterface; +use Ramsey\Uuid\Converter\NumberConverterInterface; +use Ramsey\Uuid\Converter\TimeConverterInterface; +use Ramsey\Uuid\Uuid; +use Ramsey\Uuid\UuidInterface; + +/** + * Guid represents a UUID with "native" (little-endian) byte order + * + * From Wikipedia: + * + * > The first three fields are unsigned 32- and 16-bit integers and are subject + * > to swapping, while the last two fields consist of uninterpreted bytes, not + * > subject to swapping. This byte swapping applies even for versions 3, 4, and + * > 5, where the canonical fields do not correspond to the content of the UUID. + * + * @link https://en.wikipedia.org/wiki/Universally_unique_identifier#Variants UUID Variants on Wikipedia + * + * @psalm-immutable + */ +class Guid extends Uuid implements UuidInterface +{ + /** + * @param string[] $fields + */ + public function __construct( + array $fields, + NumberConverterInterface $numberConverter, + CodecInterface $codec, + TimeConverterInterface $timeConverter + ) { + $this->fields = new GuidFields((string) hex2bin(implode('', $fields))); + $this->codec = $codec; + $this->numberConverter = $numberConverter; + $this->timeConverter = $timeConverter; + } +} diff --git a/src/Guid/GuidBuilder.php b/src/Guid/GuidBuilder.php new file mode 100644 index 0000000..5550836 --- /dev/null +++ b/src/Guid/GuidBuilder.php @@ -0,0 +1,72 @@ + + * @license http://opensource.org/licenses/MIT MIT + */ + +declare(strict_types=1); + +namespace Ramsey\Uuid\Guid; + +use Ramsey\Uuid\Builder\UuidBuilderInterface; +use Ramsey\Uuid\Codec\CodecInterface; +use Ramsey\Uuid\Converter\NumberConverterInterface; +use Ramsey\Uuid\Converter\TimeConverterInterface; +use Ramsey\Uuid\UuidInterface; + +/** + * GuidBuilder builds instances of Guid + * + * @psalm-immutable + */ +class GuidBuilder implements UuidBuilderInterface +{ + /** + * @var NumberConverterInterface + */ + private $numberConverter; + + /** + * @var TimeConverterInterface + */ + private $timeConverter; + + /** + * @param NumberConverterInterface $numberConverter The number converter to + * use when constructing the Guid + * @param TimeConverterInterface $timeConverter The time converter to use + * for converting timestamps extracted from a UUID to Unix timestamps + */ + public function __construct( + NumberConverterInterface $numberConverter, + TimeConverterInterface $timeConverter + ) { + $this->numberConverter = $numberConverter; + $this->timeConverter = $timeConverter; + } + + /** + * Builds and returns a Guid + * + * @param CodecInterface $codec The codec to use for building this Guid instance + * @param string[] $fields An array of fields from which to construct a Guid instance; + * see {@see \Ramsey\Uuid\UuidInterface::getFieldsHex()} for array structure. + * + * @return Guid The GuidBuilder returns an instance of Ramsey\Uuid\Guid\Guid + */ + public function build(CodecInterface $codec, array $fields): UuidInterface + { + return new Guid( + $fields, + $this->numberConverter, + $codec, + $this->timeConverter + ); + } +} diff --git a/src/Guid/GuidFields.php b/src/Guid/GuidFields.php new file mode 100644 index 0000000..375a0d7 --- /dev/null +++ b/src/Guid/GuidFields.php @@ -0,0 +1,154 @@ + + * @license http://opensource.org/licenses/MIT MIT + */ + +declare(strict_types=1); + +namespace Ramsey\Uuid\Guid; + +use Ramsey\Uuid\Exception\InvalidArgumentException; +use Ramsey\Uuid\Fields\SerializableFieldsTrait; +use Ramsey\Uuid\Rfc4122\NilTrait; +use Ramsey\Uuid\Rfc4122\Rfc4122FieldsInterface; +use Ramsey\Uuid\Rfc4122\VariantTrait; +use Ramsey\Uuid\Rfc4122\VersionTrait; +use Ramsey\Uuid\Uuid; + +/** + * GUIDs are comprised of a set of named fields, according to RFC 4122 + * + * @psalm-immutable + */ +final class GuidFields implements Rfc4122FieldsInterface +{ + use NilTrait; + use SerializableFieldsTrait; + use VariantTrait; + use VersionTrait; + + /** + * @var string + */ + private $bytes; + + /** + * @param string $bytes A 16-byte binary string representation of a UUID + * + * @throws InvalidArgumentException if the byte string is not exactly 16 bytes + * @throws InvalidArgumentException if the byte string does not represent a GUID + * @throws InvalidArgumentException if the byte string does not contain a valid version + */ + public function __construct(string $bytes) + { + if (strlen($bytes) !== 16) { + throw new InvalidArgumentException( + 'The byte string must be 16 bytes long; ' + . 'received ' . strlen($bytes) . ' bytes' + ); + } + + $this->bytes = $bytes; + + if (!$this->isCorrectVariant()) { + throw new InvalidArgumentException( + 'The byte string received does not conform to the RFC ' + . '4122 or Microsoft Corporation variants' + ); + } + + if (!$this->isCorrectVersion()) { + throw new InvalidArgumentException( + 'The byte string received does not contain a valid version' + ); + } + } + + public function getBytes(): string + { + return $this->bytes; + } + + public function getTimeLow(): string + { + // Swap the bytes from little endian to network byte order. + $hex = unpack( + 'H*', + pack( + 'v*', + hexdec(bin2hex(substr($this->bytes, 2, 2))), + hexdec(bin2hex(substr($this->bytes, 0, 2))) + ) + ); + + return (string) ($hex[1] ?? ''); + } + + public function getTimeMid(): string + { + // Swap the bytes from little endian to network byte order. + $hex = unpack( + 'H*', + pack( + 'v', + hexdec(bin2hex(substr($this->bytes, 4, 2))) + ) + ); + + return (string) ($hex[1] ?? ''); + } + + public function getTimeHiAndVersion(): string + { + // Swap the bytes from little endian to network byte order. + $hex = unpack( + 'H*', + pack( + 'v', + hexdec(bin2hex(substr($this->bytes, 6, 2))) + ) + ); + + return (string) ($hex[1] ?? ''); + } + + public function getClockSeqHiAndReserved(): string + { + return bin2hex(substr($this->bytes, 8, 1)); + } + + public function getClockSeqLow(): string + { + return bin2hex(substr($this->bytes, 9, 1)); + } + + public function getNode(): string + { + return bin2hex(substr($this->bytes, 10)); + } + + public function getVersion(): ?int + { + $parts = unpack('n*', $this->bytes); + + return ((int) $parts[4] >> 4) & 0x00f; + } + + private function isCorrectVariant(): bool + { + if ($this->isNil()) { + return true; + } + + $variant = $this->getVariant(); + + return $variant === Uuid::RFC_4122 || $variant === Uuid::RESERVED_MICROSOFT; + } +} diff --git a/tests/Codec/GuidStringCodecTest.php b/tests/Codec/GuidStringCodecTest.php index 179099e..bd6b2de 100644 --- a/tests/Codec/GuidStringCodecTest.php +++ b/tests/Codec/GuidStringCodecTest.php @@ -4,9 +4,14 @@ declare(strict_types=1); namespace Ramsey\Uuid\Test\Codec; +use Mockery; use PHPUnit\Framework\MockObject\MockObject; use Ramsey\Uuid\Builder\UuidBuilderInterface; use Ramsey\Uuid\Codec\GuidStringCodec; +use Ramsey\Uuid\Converter\NumberConverterInterface; +use Ramsey\Uuid\Converter\TimeConverterInterface; +use Ramsey\Uuid\Guid\Guid; +use Ramsey\Uuid\Guid\GuidBuilder; use Ramsey\Uuid\Test\TestCase; use Ramsey\Uuid\UuidInterface; @@ -32,9 +37,10 @@ class GuidStringCodecTest extends TestCase parent::setUp(); $this->builder = $this->getMockBuilder(UuidBuilderInterface::class)->getMock(); $this->uuid = $this->getMockBuilder(UuidInterface::class)->getMock(); - $this->fields = ['time_low' => '12345678', + $this->fields = [ + 'time_low' => '12345678', 'time_mid' => '1234', - 'time_hi_and_version' => 'abcd', + 'time_hi_and_version' => '4bcd', 'clock_seq_hi_and_reserved' => 'ab', 'clock_seq_low' => 'ef', 'node' => '1234abcd4321', @@ -58,68 +64,54 @@ class GuidStringCodecTest extends TestCase public function testEncodeReturnsFormattedString(): void { - $this->skipIfBigEndianHost(); $this->uuid->method('getFieldsHex') ->willReturn($this->fields); $codec = new GuidStringCodec($this->builder); $result = $codec->encode($this->uuid); - $this->assertEquals('78563412-3412-cdab-abef-1234abcd4321', $result); + $this->assertSame('12345678-1234-4bcd-abef-1234abcd4321', $result); } - public function testEncodeReturnsFormattedStringOnBigEndian(): void + public function testEncodeBinary(): void { - $this->skipIfLittleEndianHost(); - $this->uuid->method('getFieldsHex') - ->willReturn($this->fields); + $expectedBytes = hex2bin('785634123412cd4babef1234abcd4321'); + + $fields = [ + 'time_low' => '78563412', + 'time_mid' => '3412', + 'time_hi_and_version' => 'cd4b', + 'clock_seq_hi_and_reserved' => 'ab', + 'clock_seq_low' => 'ef', + 'node' => '1234abcd4321', + ]; + $codec = new GuidStringCodec($this->builder); - $result = $codec->encode($this->uuid); - $this->assertEquals('12345678-1234-abcd-abef-1234abcd4321', $result); + $numberConverter = Mockery::mock(NumberConverterInterface::class); + $timeConverter = Mockery::mock(TimeConverterInterface::class); + + $uuid = new Guid($fields, $numberConverter, $codec, $timeConverter); + + $bytes = $codec->encodeBinary($uuid); + + $this->assertSame($expectedBytes, $bytes); } - public function testEncodeBinaryUsesFieldsArray(): void + public function testDecodeReturnsGuid(): void { - $this->uuid->expects($this->once()) - ->method('getFieldsHex') - ->willReturn($this->fields); - $codec = new GuidStringCodec($this->builder); - $codec->encodeBinary($this->uuid); - } + $string = 'uuid:12345678-1234-4bcd-abef-1234abcd4321'; - public function testEncodeBinaryReturnsBinaryString(): void - { - $expected = hex2bin('123456781234abcdabef1234abcd4321'); - $this->uuid->method('getFieldsHex') - ->willReturn($this->fields); - $codec = new GuidStringCodec($this->builder); - $result = $codec->encodeBinary($this->uuid); - $this->assertEquals($expected, $result); - } + $numberConverter = Mockery::mock(NumberConverterInterface::class); + $timeConverter = Mockery::mock(TimeConverterInterface::class); + $builder = new GuidBuilder($numberConverter, $timeConverter); + $codec = new GuidStringCodec($builder); + $guid = $codec->decode($string); - public function testDecodeUsesBuilderOnFields(): void - { - $this->skipIfBigEndianHost(); - $string = 'uuid:78563412-3412-cdab-abef-1234abcd4321'; - $this->builder->expects($this->once()) - ->method('build') - ->with($this->isInstanceOf(GuidStringCodec::class), $this->fields); - $codec = new GuidStringCodec($this->builder); - $codec->decode($string); - } - - public function testDecodeUsesBuilderOnFieldsOnBigEndian(): void - { - $this->skipIfLittleEndianHost(); - $string = 'uuid:12345678-1234-abcd-abef-1234abcd4321'; - $this->builder->expects($this->once()) - ->method('build') - ->with($this->isInstanceOf(GuidStringCodec::class), $this->fields); - $codec = new GuidStringCodec($this->builder); - $codec->decode($string); + $this->assertInstanceOf(Guid::class, $guid); + $this->assertSame('12345678-1234-4bcd-abef-1234abcd4321', $guid->toString()); } public function testDecodeReturnsUuidFromBuilder(): void { - $string = 'uuid:78563412-3412-cdab-abef-1234abcd4321'; + $string = 'uuid:78563412-3412-cd4b-abef-1234abcd4321'; $this->builder->method('build') ->willReturn($this->uuid); @@ -130,7 +122,7 @@ class GuidStringCodecTest extends TestCase public function testDecodeBytesReturnsUuid(): void { - $string = '123456781234abcdabef1234abcd4321'; + $string = '1234567812344bcd4bef1234abcd4321'; $bytes = pack('H*', $string); $codec = new GuidStringCodec($this->builder); $this->builder->method('build') diff --git a/tests/Codec/StringCodecTest.php b/tests/Codec/StringCodecTest.php index 8536990..0bff301 100644 --- a/tests/Codec/StringCodecTest.php +++ b/tests/Codec/StringCodecTest.php @@ -70,23 +70,14 @@ class StringCodecTest extends TestCase $this->assertEquals($this->uuidString, $result); } - public function testEncodeBinaryUsesHexadecimalValue(): void - { - $this->uuid->expects($this->once()) - ->method('getHex') - ->willReturn('123456781234abcdabef1234abcd4321'); - $codec = new StringCodec($this->builder); - $codec->encodeBinary($this->uuid); - } - public function testEncodeBinaryReturnsBinaryString(): void { $expected = hex2bin('123456781234abcdabef1234abcd4321'); - $this->uuid->method('getHex') - ->willReturn('123456781234abcdabef1234abcd4321'); + $this->uuid->method('getBytes') + ->willReturn(hex2bin('123456781234abcdabef1234abcd4321')); $codec = new StringCodec($this->builder); $result = $codec->encodeBinary($this->uuid); - $this->assertEquals($expected, $result); + $this->assertSame($expected, $result); } public function testDecodeUsesBuilderOnFields(): void diff --git a/tests/Encoder/TimestampLastCombCodecTest.php b/tests/Encoder/TimestampLastCombCodecTest.php index 1b4fb08..c045dc8 100644 --- a/tests/Encoder/TimestampLastCombCodecTest.php +++ b/tests/Encoder/TimestampLastCombCodecTest.php @@ -46,8 +46,8 @@ class TimestampLastCombCodecTest extends TestCase /** @var MockObject & UuidInterface $uuidMock */ $uuidMock = $this->getMockBuilder(UuidInterface::class)->getMock(); $uuidMock->expects($this->any()) - ->method('getHex') - ->willReturn('0800200c9a6611e19b21ff6f8cb0c57d'); + ->method('getBytes') + ->willReturn(hex2bin('0800200c9a6611e19b21ff6f8cb0c57d')); $encodedUuid = $this->codec->encodeBinary($uuidMock); $this->assertSame(hex2bin('0800200c9a6611e19b21ff6f8cb0c57d'), $encodedUuid); diff --git a/tests/Guid/GuidFieldsTest.php b/tests/Guid/GuidFieldsTest.php new file mode 100644 index 0000000..59f707c --- /dev/null +++ b/tests/Guid/GuidFieldsTest.php @@ -0,0 +1,169 @@ +expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'The byte string must be 16 bytes long; received 6 bytes' + ); + + new GuidFields('foobar'); + } + + /** + * @dataProvider nonRfc4122GuidVariantProvider + */ + public function testConstructorThrowsExceptionIfNotRfc4122Variant(string $guid): void + { + $bytes = (string) hex2bin($guid); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'The byte string received does not conform to the RFC 4122 or ' + . 'Microsoft Corporation variants' + ); + + new GuidFields($bytes); + } + + /** + * These values are already in GUID byte order, for easy testing. + * + * @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingTraversableTypeHintSpecification + */ + public function nonRfc4122GuidVariantProvider(): array + { + // In string representation, the following IDs would begin as: + // ff6f8cb0-c57d-11e1-... + return [ + ['b08c6fff7dc5e1110b210800200c9a66'], + ['b08c6fff7dc5e1111b210800200c9a66'], + ['b08c6fff7dc5e1112b210800200c9a66'], + ['b08c6fff7dc5e1113b210800200c9a66'], + ['b08c6fff7dc5e1114b210800200c9a66'], + ['b08c6fff7dc5e1115b210800200c9a66'], + ['b08c6fff7dc5e1116b210800200c9a66'], + ['b08c6fff7dc5e1117b210800200c9a66'], + ['b08c6fff7dc5e111eb210800200c9a66'], + ['b08c6fff7dc5e111fb210800200c9a66'], + ]; + } + + /** + * @dataProvider invalidVersionProvider + */ + public function testConstructorThrowsExceptionIfInvalidVersion(string $guid): void + { + $bytes = (string) hex2bin($guid); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'The byte string received does not contain a valid version' + ); + + new GuidFields($bytes); + } + + /** + * @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingTraversableTypeHintSpecification + */ + public function invalidVersionProvider(): array + { + // The following UUIDs are in GUID byte order. Dashes have + // been removed in the tests to distinguish these from string + // representations, which are never in GUID byte order. + return [ + ['b08c6fff7dc5e1018b210800200c9a66'], + ['b08c6fff7dc5e1618b210800200c9a66'], + ['b08c6fff7dc5e1719b210800200c9a66'], + ['b08c6fff7dc5e181ab210800200c9a66'], + ['b08c6fff7dc5e191bb210800200c9a66'], + ]; + } + + /** + * @param string|int $expectedValue + * + * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingAnyTypeHint + * @dataProvider fieldGetterMethodProvider + */ + public function testFieldGetterMethods(string $bytes, string $methodName, $expectedValue): void + { + $bytes = (string) hex2bin($bytes); + $fields = new GuidFields($bytes); + + $this->assertSame($expectedValue, $fields->$methodName()); + } + + /** + * @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingTraversableTypeHintSpecification + */ + public function fieldGetterMethodProvider(): array + { + // The following UUIDs are in GUID byte order. Dashes have + // been removed in the tests to distinguish these from string + // representations, which are never in GUID byte order. + return [ + // For ff6f8cb0-c57d-11e1-cb21-0800200c9a66 + ['b08c6fff7dc5e111cb210800200c9a66', 'getClockSeqHiAndReserved', 'cb'], + ['b08c6fff7dc5e111cb210800200c9a66', 'getClockSeqLow', '21'], + ['b08c6fff7dc5e111cb210800200c9a66', 'getNode', '0800200c9a66'], + ['b08c6fff7dc5e111cb210800200c9a66', 'getTimeHiAndVersion', '11e1'], + ['b08c6fff7dc5e111cb210800200c9a66', 'getTimeLow', 'ff6f8cb0'], + ['b08c6fff7dc5e111cb210800200c9a66', 'getTimeMid', 'c57d'], + ['b08c6fff7dc5e111cb210800200c9a66', 'getVariant', 6], + ['b08c6fff7dc5e111cb210800200c9a66', 'getVersion', 1], + + // For ff6f8cb0-c57d-41e1-db21-0800200c9a66 + ['b08c6fff7dc5e141db210800200c9a66', 'getClockSeqHiAndReserved', 'db'], + ['b08c6fff7dc5e141db210800200c9a66', 'getClockSeqLow', '21'], + ['b08c6fff7dc5e141db210800200c9a66', 'getNode', '0800200c9a66'], + ['b08c6fff7dc5e141db210800200c9a66', 'getTimeHiAndVersion', '41e1'], + ['b08c6fff7dc5e141db210800200c9a66', 'getTimeLow', 'ff6f8cb0'], + ['b08c6fff7dc5e141db210800200c9a66', 'getTimeMid', 'c57d'], + ['b08c6fff7dc5e141db210800200c9a66', 'getVariant', 6], + ['b08c6fff7dc5e141db210800200c9a66', 'getVersion', 4], + + // For ff6f8cb0-c57d-31e1-8b21-0800200c9a66 + ['b08c6fff7dc5e1318b210800200c9a66', 'getClockSeqHiAndReserved', '8b'], + ['b08c6fff7dc5e1318b210800200c9a66', 'getClockSeqLow', '21'], + ['b08c6fff7dc5e1318b210800200c9a66', 'getNode', '0800200c9a66'], + ['b08c6fff7dc5e1318b210800200c9a66', 'getTimeHiAndVersion', '31e1'], + ['b08c6fff7dc5e1318b210800200c9a66', 'getTimeLow', 'ff6f8cb0'], + ['b08c6fff7dc5e1318b210800200c9a66', 'getTimeMid', 'c57d'], + ['b08c6fff7dc5e1318b210800200c9a66', 'getVariant', 2], + ['b08c6fff7dc5e1318b210800200c9a66', 'getVersion', 3], + + // For ff6f8cb0-c57d-51e1-9b21-0800200c9a66 + ['b08c6fff7dc5e1519b210800200c9a66', 'getClockSeqHiAndReserved', '9b'], + ['b08c6fff7dc5e1519b210800200c9a66', 'getClockSeqLow', '21'], + ['b08c6fff7dc5e1519b210800200c9a66', 'getNode', '0800200c9a66'], + ['b08c6fff7dc5e1519b210800200c9a66', 'getTimeHiAndVersion', '51e1'], + ['b08c6fff7dc5e1519b210800200c9a66', 'getTimeLow', 'ff6f8cb0'], + ['b08c6fff7dc5e1519b210800200c9a66', 'getTimeMid', 'c57d'], + ['b08c6fff7dc5e1519b210800200c9a66', 'getVariant', 2], + ['b08c6fff7dc5e1519b210800200c9a66', 'getVersion', 5], + ]; + } + + public function testSerializingFields(): void + { + $bytes = (string) hex2bin('b08c6fff7dc5e111cb210800200c9a66'); + $fields = new GuidFields($bytes); + + $serializedFields = serialize($fields); + $unserializedFields = unserialize($serializedFields); + + $this->assertEquals($fields, $unserializedFields); + } +} diff --git a/tests/UuidFactoryTest.php b/tests/UuidFactoryTest.php index daf818c..26b8f00 100644 --- a/tests/UuidFactoryTest.php +++ b/tests/UuidFactoryTest.php @@ -32,7 +32,8 @@ class UuidFactoryTest extends TestCase $uuid = $factory->fromString('ff6f8cb0-c57d-11e1-9b21-0800200c9a66'); - $this->assertEquals('ff6f8cb0-c57d-11e1-9b21-0800200c9a66', $uuid->toString()); + $this->assertSame('ff6f8cb0-c57d-11e1-9b21-0800200c9a66', $uuid->toString()); + $this->assertSame(hex2bin('b08c6fff7dc5e1119b210800200c9a66'), $uuid->getBytes()); } public function testFromStringParsesUuidInLowercase(): void diff --git a/tests/UuidTest.php b/tests/UuidTest.php index ca19f2c..75bde29 100644 --- a/tests/UuidTest.php +++ b/tests/UuidTest.php @@ -27,7 +27,6 @@ use Ramsey\Uuid\Generator\RandomGeneratorInterface; use Ramsey\Uuid\Provider\Time\FixedTimeProvider; use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidFactory; -use Ramsey\Uuid\UuidInterface; use Ramsey\Uuid\Validator\Validator; use Ramsey\Uuid\Validator\ValidatorInterface; use stdClass; @@ -54,46 +53,19 @@ class UuidTest extends TestCase */ public function testFromGuidStringOnLittleEndianHost(): void { - $this->skipIfBigEndianHost(); - - $uuid = Uuid::fromString('b08c6fff-7dc5-e111-9b21-0800200c9a66'); + $uuid = Uuid::fromString('ff6f8cb0-c57d-11e1-9b21-0800200c9a66'); Uuid::setFactory(new UuidFactory(new FeatureSet(true))); - $guid = Uuid::fromString('b08c6fff-7dc5-e111-9b21-0800200c9a66'); + $guid = Uuid::fromString('ff6f8cb0-c57d-11e1-9b21-0800200c9a66'); $this->assertInstanceOf(Uuid::class, $guid); // UUID's and GUID's share the same textual representation. - $this->assertEquals($uuid->toString(), $guid->toString()); + $this->assertSame($uuid->toString(), $guid->toString()); - // But not the same binary representation (this assertion is valid on - // little endian hosts only). - $this->assertNotEquals(bin2hex($uuid->getBytes()), bin2hex($guid->getBytes())); - } - - /** - * Tests that UUID and GUID's have the same textual representation and the - * same binary representation. - * - * This test is only valid on big endian hosts. - */ - public function testFromGuidStringOnBigEndianHost(): void - { - $this->skipIfLittleEndianHost(); - - $uuid = Uuid::fromString('b08c6fff-7dc5-e111-9b21-0800200c9a66'); - - Uuid::setFactory(new UuidFactory(new FeatureSet(true))); - - $guid = Uuid::fromString('b08c6fff-7dc5-e111-9b21-0800200c9a66'); - - $this->assertInstanceOf(Uuid::class, $guid); - // UUID's and GUID's share the same textual representation - $this->assertEquals($uuid->toString(), $guid->toString()); - // But not the same binary representation (this assertion is valid on little endian hosts - // only) - $this->assertEquals(bin2hex($uuid->getBytes()), bin2hex($guid->getBytes())); + // But not the same binary representation. + $this->assertNotSame($uuid->getBytes(), $guid->getBytes()); } public function testFromStringWithCurlyBraces(): void @@ -1563,52 +1535,37 @@ class UuidTest extends TestCase $this->assertTrue($uuid->equals($fromBytesUuid)); } - public function testFromGuidBytesOnLittleEndianHost(): void + public function testGuidBytesMatchesUuidWithSameString(): void { - $this->skipIfBigEndianHost(); - $uuidFactory = new UuidFactory(new FeatureSet(false)); $guidFactory = new UuidFactory(new FeatureSet(true)); - // Check that parsing BE bytes as LE reverses fields $uuid = $uuidFactory->fromString('ff6f8cb0-c57d-11e1-9b21-0800200c9a66'); $bytes = $uuid->getBytes(); - $guid = $guidFactory->fromBytes($bytes); + // Swap the order of the bytes for a GUID. + $guidBytes = $bytes[3] . $bytes[2] . $bytes[1] . $bytes[0]; + $guidBytes .= $bytes[5] . $bytes[4]; + $guidBytes .= $bytes[7] . $bytes[6]; + $guidBytes .= substr($bytes, 8); - // First three fields should be reversed - $this->assertEquals('b08c6fff-7dc5-e111-9b21-0800200c9a66', $guid->toString()); + $guid = $guidFactory->fromBytes($guidBytes); - // Check that parsing LE bytes as LE preserves fields - $guid = $guidFactory->fromString('ff6f8cb0-c57d-11e1-9b21-0800200c9a66'); - $bytes = $guid->getBytes(); - - $parsedGuid = $guidFactory->fromBytes($bytes); - - $this->assertEquals($guid->toString(), $parsedGuid->toString()); + $this->assertSame($uuid->toString(), $guid->toString()); + $this->assertTrue($uuid->equals($guid)); } - public function testFromGuidBytesOnBigEndianHost(): void + public function testGuidBytesProducesSameGuidString(): void { - $this->skipIfLittleEndianHost(); - - $uuidFactory = new UuidFactory(new FeatureSet(false)); $guidFactory = new UuidFactory(new FeatureSet(true)); - $uuid = $uuidFactory->fromString('ff6f8cb0-c57d-11e1-9b21-0800200c9a66'); - $bytes = $uuid->getBytes(); - - $guid = $guidFactory->fromBytes($bytes); - - // UUIDs and GUIDs should have the same binary representation on BE hosts - $this->assertEquals($uuid->toString(), $guid->toString()); - $guid = $guidFactory->fromString('ff6f8cb0-c57d-11e1-9b21-0800200c9a66'); $bytes = $guid->getBytes(); $parsedGuid = $guidFactory->fromBytes($bytes); - $this->assertEquals($guid->toString(), $parsedGuid->toString()); + $this->assertSame($guid->toString(), $parsedGuid->toString()); + $this->assertTrue($guid->equals($parsedGuid)); } public function testFromBytesArgumentTooShort(): void