diff --git a/src/Rfc4122/Fields.php b/src/Rfc4122/Fields.php index d313eb0..0989d84 100644 --- a/src/Rfc4122/Fields.php +++ b/src/Rfc4122/Fields.php @@ -123,20 +123,49 @@ final class Fields implements FieldsInterface return new Hexadecimal(bin2hex(substr($this->bytes, 4, 2))); } + /** + * Returns the full 60-bit timestamp, without the version + * + * For version 2 UUIDs, the time_low field is the local identifier and + * should not be returned as part of the time. For this reason, we set the + * bottom 32 bits of the timestamp to 0's. As a result, there is some loss + * of fidelity of the timestamp, for version 2 UUIDs. The timestamp can be + * off by a range of 0 to 429.4967295 seconds (or 7 minutes, 9 seconds, and + * 496730 microseconds). + * + * For version 6 UUIDs, the timestamp order is reversed from the typical RFC + * 4122 order (the time bits are in the correct bit order, so that it is + * monotonically increasing). In returning the timestamp value, we put the + * bits in the order: time_low + time_mid + time_hi. + */ public function getTimestamp(): Hexadecimal { - $timestamp = sprintf( - '%03x%04s%08s', - hexdec($this->getTimeHiAndVersion()->toString()) & 0x0fff, - $this->getTimeMid()->toString(), - $this->getTimeLow()->toString() - ); + switch ($this->getVersion()) { + case Uuid::UUID_TYPE_DCE_SECURITY: + $timestamp = sprintf( + '%03x%04s%08s', + hexdec($this->getTimeHiAndVersion()->toString()) & 0x0fff, + $this->getTimeMid()->toString(), + '' + ); - // Put the timestamp into the correct order, if this is a v6 UUID. - if ($this->getVersion() === Uuid::UUID_TYPE_PEABODY) { - $timestamp = substr($timestamp, 7) - . substr($timestamp, 3, 4) - . substr($timestamp, 0, 3); + break; + case Uuid::UUID_TYPE_PEABODY: + $timestamp = sprintf( + '%08s%04s%03x', + $this->getTimeLow()->toString(), + $this->getTimeMid()->toString(), + hexdec($this->getTimeHiAndVersion()->toString()) & 0x0fff + ); + + break; + default: + $timestamp = sprintf( + '%03x%04s%08s', + hexdec($this->getTimeHiAndVersion()->toString()) & 0x0fff, + $this->getTimeMid()->toString(), + $this->getTimeLow()->toString() + ); } return new Hexadecimal($timestamp); diff --git a/src/Rfc4122/UuidV2.php b/src/Rfc4122/UuidV2.php index fdbc1ec..ae6c442 100644 --- a/src/Rfc4122/UuidV2.php +++ b/src/Rfc4122/UuidV2.php @@ -14,15 +14,22 @@ declare(strict_types=1); namespace Ramsey\Uuid\Rfc4122; +use DateTimeImmutable; +use DateTimeInterface; use Ramsey\Uuid\Codec\CodecInterface; use Ramsey\Uuid\Converter\NumberConverterInterface; use Ramsey\Uuid\Converter\TimeConverterInterface; +use Ramsey\Uuid\Exception\DateTimeException; use Ramsey\Uuid\Exception\InvalidArgumentException; use Ramsey\Uuid\Rfc4122\FieldsInterface as Rfc4122FieldsInterface; use Ramsey\Uuid\Type\Integer as IntegerObject; use Ramsey\Uuid\Uuid; +use Throwable; use function hexdec; +use function str_pad; + +use const STR_PAD_LEFT; /** * DCE Security version, or version 2, UUIDs include local domain identifier, @@ -65,6 +72,41 @@ final class UuidV2 extends Uuid implements UuidInterface parent::__construct($fields, $numberConverter, $codec, $timeConverter); } + /** + * Returns a DateTimeInterface object representing the timestamp associated + * with the UUID + * + * It is important to note that a version 2 UUID suffers from some loss of + * fidelity of the timestamp, due to replacing the time_low field with the + * local identifier. When constructing the timestamp value for date + * purposes, we replace the local identifier bits with zeros. As a result, + * the timestamp can be off by a range of 0 to 429.4967295 seconds (or 7 + * minutes, 9 seconds, and 496730 microseconds). + * + * Astute observers might note this value directly corresponds to 2^32, or + * 0xffffffff. The local identifier is 32-bits, and we have set each of + * these bits to 0, so the maximum range of timestamp drift is 0x00000000 + * to 0xffffffff (counted in 100-nanosecond intervals). + * + * @return DateTimeImmutable A PHP DateTimeImmutable instance representing + * the timestamp of a version 2 UUID + */ + public function getDateTime(): DateTimeInterface + { + $time = $this->timeConverter->convertTime($this->fields->getTimestamp()); + + try { + return new DateTimeImmutable( + '@' + . $time->getSeconds()->toString() + . '.' + . str_pad($time->getMicroSeconds()->toString(), 6, '0', STR_PAD_LEFT) + ); + } catch (Throwable $e) { + throw new DateTimeException($e->getMessage(), (int) $e->getCode(), $e); + } + } + /** * Returns the local domain used to create this version 2 UUID */ diff --git a/src/UuidFactory.php b/src/UuidFactory.php index afa17d4..0486d38 100644 --- a/src/UuidFactory.php +++ b/src/UuidFactory.php @@ -185,6 +185,25 @@ class UuidFactory implements UuidFactoryInterface $this->timeGenerator = $generator; } + /** + * Returns the DCE Security generator used by this factory + */ + public function getDceSecurityGenerator(): DceSecurityGeneratorInterface + { + return $this->dceSecurityGenerator; + } + + /** + * Sets the DCE Security generator to use for this factory + * + * @param DceSecurityGeneratorInterface $generator A generator to generate + * binary data, based on a local domain and local identifier + */ + public function setDceSecurityGenerator(DceSecurityGeneratorInterface $generator): void + { + $this->dceSecurityGenerator = $generator; + } + /** * Returns the number converter used by this factory */ diff --git a/tests/Rfc4122/FieldsTest.php b/tests/Rfc4122/FieldsTest.php index c1ae3e9..40282a6 100644 --- a/tests/Rfc4122/FieldsTest.php +++ b/tests/Rfc4122/FieldsTest.php @@ -187,6 +187,18 @@ class FieldsTest extends TestCase ['00000000-0000-0000-0000-000000000000', 'getVariant', 0], ['00000000-0000-0000-0000-000000000000', 'getVersion', null], ['00000000-0000-0000-0000-000000000000', 'isNil', true], + + ['000001f5-5cde-21ea-8400-0242ac130003', 'getClockSeq', '0400'], + ['000001f5-5cde-21ea-8400-0242ac130003', 'getClockSeqHiAndReserved', '84'], + ['000001f5-5cde-21ea-8400-0242ac130003', 'getClockSeqLow', '00'], + ['000001f5-5cde-21ea-8400-0242ac130003', 'getNode', '0242ac130003'], + ['000001f5-5cde-21ea-8400-0242ac130003', 'getTimeHiAndVersion', '21ea'], + ['000001f5-5cde-21ea-8400-0242ac130003', 'getTimeLow', '000001f5'], + ['000001f5-5cde-21ea-8400-0242ac130003', 'getTimeMid', '5cde'], + ['000001f5-5cde-21ea-8400-0242ac130003', 'getTimestamp', '1ea5cde00000000'], + ['000001f5-5cde-21ea-8400-0242ac130003', 'getVariant', 2], + ['000001f5-5cde-21ea-8400-0242ac130003', 'getVersion', 2], + ['000001f5-5cde-21ea-8400-0242ac130003', 'isNil', false], ]; } diff --git a/tests/Rfc4122/UuidV2Test.php b/tests/Rfc4122/UuidV2Test.php index 5b9ca46..f8bd02d 100644 --- a/tests/Rfc4122/UuidV2Test.php +++ b/tests/Rfc4122/UuidV2Test.php @@ -4,16 +4,28 @@ declare(strict_types=1); namespace Ramsey\Uuid\Test\Rfc4122; +use DateTimeInterface; use Mockery; use Ramsey\Uuid\Codec\CodecInterface; +use Ramsey\Uuid\Converter\Number\GenericNumberConverter; use Ramsey\Uuid\Converter\NumberConverterInterface; +use Ramsey\Uuid\Converter\Time\GenericTimeConverter; use Ramsey\Uuid\Converter\TimeConverterInterface; use Ramsey\Uuid\Exception\InvalidArgumentException; +use Ramsey\Uuid\Generator\DceSecurityGenerator; +use Ramsey\Uuid\Generator\DefaultTimeGenerator; +use Ramsey\Uuid\Math\BrickMathCalculator; +use Ramsey\Uuid\Provider\Dce\SystemDceSecurityProvider; +use Ramsey\Uuid\Provider\Node\StaticNodeProvider; +use Ramsey\Uuid\Provider\Time\FixedTimeProvider; use Ramsey\Uuid\Rfc4122\FieldsInterface; use Ramsey\Uuid\Rfc4122\UuidV2; use Ramsey\Uuid\Test\TestCase; +use Ramsey\Uuid\Type\Hexadecimal; use Ramsey\Uuid\Type\Integer; +use Ramsey\Uuid\Type\Time; use Ramsey\Uuid\Uuid; +use Ramsey\Uuid\UuidFactory; class UuidV2Test extends TestCase { @@ -63,17 +75,40 @@ class UuidV2Test extends TestCase public function testGetLocalDomainAndIdentifier( int $domain, Integer $identifier, + Time $time, int $expectedDomain, string $expectedDomainName, - string $expectedIdentifier + string $expectedIdentifier, + string $expectedTimestamp, + string $expectedTime ): void { + $calculator = new BrickMathCalculator(); + $genericConverter = new GenericTimeConverter($calculator); + $numberConverter = new GenericNumberConverter($calculator); + $nodeProvider = new StaticNodeProvider(new Hexadecimal('1234567890ab')); + $timeProvider = new FixedTimeProvider($time); + $timeGenerator = new DefaultTimeGenerator($nodeProvider, $genericConverter, $timeProvider); + $dceProvider = new SystemDceSecurityProvider(); + $dceGenerator = new DceSecurityGenerator($numberConverter, $timeGenerator, $dceProvider); + + $factory = new UuidFactory(); + $factory->setTimeGenerator($timeGenerator); + $factory->setDceSecurityGenerator($dceGenerator); + /** @var UuidV2 $uuid */ - $uuid = Uuid::uuid2($domain, $identifier); + $uuid = $factory->uuid2($domain, $identifier); + + /** @var FieldsInterface $fields */ + $fields = $uuid->getFields(); $this->assertSame($expectedDomain, $uuid->getLocalDomain()); $this->assertSame($expectedDomainName, $uuid->getLocalDomainName()); $this->assertInstanceOf(Integer::class, $uuid->getLocalIdentifier()); $this->assertSame($expectedIdentifier, $uuid->getLocalIdentifier()->toString()); + $this->assertSame($expectedTimestamp, $fields->getTimestamp()->toString()); + $this->assertInstanceOf(DateTimeInterface::class, $uuid->getDateTime()); + $this->assertSame($expectedTime, $uuid->getDateTime()->format('U.u')); + $this->assertSame('1334567890ab', $fields->getNode()->toString()); } /** @@ -85,37 +120,88 @@ class UuidV2Test extends TestCase [ 'domain' => Uuid::DCE_DOMAIN_PERSON, 'identifier' => new Integer('12345678'), + 'time' => new Time(0, 0), 'expectedDomain' => 0, 'expectedDomainName' => 'person', 'expectedIdentifier' => '12345678', + 'expectedTimestamp' => '1b21dd200000000', + 'expectedTime' => '-32.723763', ], [ 'domain' => Uuid::DCE_DOMAIN_GROUP, 'identifier' => new Integer('87654321'), + 'time' => new Time(0, 0), 'expectedDomain' => 1, 'expectedDomainName' => 'group', 'expectedIdentifier' => '87654321', + 'expectedTimestamp' => '1b21dd200000000', + 'expectedTime' => '-32.723763', ], [ 'domain' => Uuid::DCE_DOMAIN_ORG, 'identifier' => new Integer('1'), + 'time' => new Time(0, 0), 'expectedDomain' => 2, 'expectedDomainName' => 'org', 'expectedIdentifier' => '1', + 'expectedTimestamp' => '1b21dd200000000', + 'expectedTime' => '-32.723763', ], [ 'domain' => Uuid::DCE_DOMAIN_PERSON, 'identifier' => new Integer('0'), + 'time' => new Time(1583208664, 444109), 'expectedDomain' => 0, 'expectedDomainName' => 'person', 'expectedIdentifier' => '0', + 'expectedTimestamp' => '1ea5d0500000000', + 'expectedTime' => '1583208664.444109', + ], + [ + 'domain' => Uuid::DCE_DOMAIN_PERSON, + 'identifier' => new Integer('2147483647'), + 'time' => new Time(1583208879, 500000), + 'expectedDomain' => 0, + 'expectedDomainName' => 'person', + 'expectedIdentifier' => '2147483647', + // This time is the same as in the previous test because of the + // loss of precision by setting the lowest 32 bits to zeros. + 'expectedTimestamp' => '1ea5d0500000000', + 'expectedTime' => '1583208664.444109', ], [ 'domain' => Uuid::DCE_DOMAIN_PERSON, 'identifier' => new Integer('4294967295'), + 'time' => new Time(1583208879, 500000), 'expectedDomain' => 0, 'expectedDomainName' => 'person', 'expectedIdentifier' => '4294967295', + // This time is the same as in the previous test because of the + // loss of precision by setting the lowest 32 bits to zeros. + 'expectedTimestamp' => '1ea5d0500000000', + 'expectedTime' => '1583208664.444109', + ], + [ + 'domain' => Uuid::DCE_DOMAIN_PERSON, + 'identifier' => new Integer('4294967295'), + 'time' => new Time(1583209093, 940838), + 'expectedDomain' => 0, + 'expectedDomainName' => 'person', + 'expectedIdentifier' => '4294967295', + // This time is the same as in the previous test because of the + // loss of precision by setting the lowest 32 bits to zeros. + 'expectedTimestamp' => '1ea5d0500000000', + 'expectedTime' => '1583208664.444109', + ], + [ + 'domain' => Uuid::DCE_DOMAIN_PERSON, + 'identifier' => new Integer('4294967295'), + 'time' => new Time(1583209093, 940839), + 'expectedDomain' => 0, + 'expectedDomainName' => 'person', + 'expectedIdentifier' => '4294967295', + 'expectedTimestamp' => '1ea5d0600000000', + 'expectedTime' => '1583209093.940838', ], ]; }