diff --git a/src/Generator/DceSecurityGenerator.php b/src/Generator/DceSecurityGenerator.php index 5f12a19..a3f07f2 100644 --- a/src/Generator/DceSecurityGenerator.php +++ b/src/Generator/DceSecurityGenerator.php @@ -15,7 +15,7 @@ declare(strict_types=1); namespace Ramsey\Uuid\Generator; use Ramsey\Uuid\Converter\NumberConverterInterface; -use Ramsey\Uuid\Exception\InvalidArgumentException; +use Ramsey\Uuid\Exception\DceSecurityException; use Ramsey\Uuid\Provider\DceSecurityProviderInterface; use Ramsey\Uuid\Type\Hexadecimal; use Ramsey\Uuid\Type\Integer as IntegerObject; @@ -25,6 +25,7 @@ use function hex2bin; use function in_array; use function pack; use function str_pad; +use function strlen; use function substr_replace; use const STR_PAD_LEFT; @@ -41,6 +42,16 @@ class DceSecurityGenerator implements DceSecurityGeneratorInterface Uuid::DCE_DOMAIN_ORG, ]; + /** + * Upper bounds for the clock sequence in DCE Security UUIDs. + */ + private const CLOCK_SEQ_HIGH = 63; + + /** + * Lower bounds for the clock sequence in DCE Security UUIDs. + */ + private const CLOCK_SEQ_LOW = 0; + /** * @var NumberConverterInterface */ @@ -73,15 +84,27 @@ class DceSecurityGenerator implements DceSecurityGeneratorInterface ?int $clockSeq = null ): string { if (!in_array($localDomain, self::DOMAINS)) { - throw new InvalidArgumentException( + throw new DceSecurityException( 'Local domain must be a valid DCE Security domain' ); } + if ($localIdentifier && $localIdentifier->isNegative()) { + throw new DceSecurityException( + 'Local identifier out of bounds; it must be a value between 0 and 4294967295' + ); + } + + if ($clockSeq > self::CLOCK_SEQ_HIGH || $clockSeq < self::CLOCK_SEQ_LOW) { + throw new DceSecurityException( + 'Clock sequence out of bounds; it must be a value between 0 and 63' + ); + } + switch ($localDomain) { case Uuid::DCE_DOMAIN_ORG: if ($localIdentifier === null) { - throw new InvalidArgumentException( + throw new DceSecurityException( 'A local identifier must be provided for the org domain' ); } @@ -102,18 +125,30 @@ class DceSecurityGenerator implements DceSecurityGeneratorInterface break; } + $identifierHex = $this->numberConverter->toHex($localIdentifier->toString()); + + // The maximum value for the local identifier is 0xffffffff, or + // 4294967295. This is 8 hexadecimal digits, so if the length of + // hexadecimal digits is greater than 8, we know the value is greater + // than 0xffffffff. + if (strlen($identifierHex) > 8) { + throw new DceSecurityException( + 'Local identifier out of bounds; it must be a value between 0 and 4294967295' + ); + } + $domainByte = pack('n', $localDomain)[1]; - $identifierBytes = hex2bin(str_pad( - $this->numberConverter->toHex($localIdentifier->toString()), - 8, - '0', - STR_PAD_LEFT - )); + $identifierBytes = hex2bin(str_pad($identifierHex, 8, '0', STR_PAD_LEFT)); if ($node instanceof Hexadecimal) { $node = $node->toString(); } + // Shift the clock sequence 8 bits to the left, so it matches 0x3f00. + if ($clockSeq !== null) { + $clockSeq = $clockSeq << 8; + } + /** @var string $bytes */ $bytes = $this->timeGenerator->generate($node, $clockSeq); diff --git a/tests/FunctionsTest.php b/tests/FunctionsTest.php index 399d533..357bd4f 100644 --- a/tests/FunctionsTest.php +++ b/tests/FunctionsTest.php @@ -32,7 +32,7 @@ class FunctionsTest extends TestCase Uuid::DCE_DOMAIN_PERSON, new IntegerObject('1004'), new Hexadecimal('aabbccdd0011'), - 1234 + 63 ); /** @var FieldsInterface $fields */ diff --git a/tests/Generator/DceSecurityGeneratorTest.php b/tests/Generator/DceSecurityGeneratorTest.php index 24aad62..378a04c 100644 --- a/tests/Generator/DceSecurityGeneratorTest.php +++ b/tests/Generator/DceSecurityGeneratorTest.php @@ -8,7 +8,7 @@ use Mockery; use Ramsey\Uuid\Converter\Number\GenericNumberConverter; use Ramsey\Uuid\Converter\NumberConverterInterface; use Ramsey\Uuid\Converter\Time\GenericTimeConverter; -use Ramsey\Uuid\Exception\InvalidArgumentException; +use Ramsey\Uuid\Exception\DceSecurityException; use Ramsey\Uuid\Generator\DceSecurityGenerator; use Ramsey\Uuid\Generator\DefaultTimeGenerator; use Ramsey\Uuid\Generator\TimeGeneratorInterface; @@ -44,7 +44,6 @@ class DceSecurityGeneratorTest extends TestCase int $providedDomain, ?IntegerObject $providedId, ?Hexadecimal $providedNode, - ?int $providedClockSeq, string $expectedId, string $expectedDomain, string $expectedNode, @@ -68,7 +67,7 @@ class DceSecurityGeneratorTest extends TestCase $dceSecurityGenerator = new DceSecurityGenerator($numberConverter, $timeGenerator, $dceSecurityProvider); - $bytes = $dceSecurityGenerator->generate($providedDomain, $providedId, $providedNode, $providedClockSeq); + $bytes = $dceSecurityGenerator->generate($providedDomain, $providedId, $providedNode); $this->assertSame($expectedId, bin2hex(substr($bytes, 0, 4))); $this->assertSame($expectedDomain, bin2hex(substr($bytes, 9, 1))); @@ -91,7 +90,6 @@ class DceSecurityGeneratorTest extends TestCase 'providedDomain' => Uuid::DCE_DOMAIN_PERSON, 'providedId' => null, 'providedNode' => null, - 'providedClockSeq' => null, 'expectedId' => '000003e9', 'expectedDomain' => '00', 'expectedNode' => '001122334455', @@ -106,7 +104,6 @@ class DceSecurityGeneratorTest extends TestCase 'providedDomain' => Uuid::DCE_DOMAIN_GROUP, 'providedId' => null, 'providedNode' => null, - 'providedClockSeq' => null, 'expectedId' => '000007d1', 'expectedDomain' => '01', 'expectedNode' => '001122334455', @@ -121,7 +118,6 @@ class DceSecurityGeneratorTest extends TestCase 'providedDomain' => Uuid::DCE_DOMAIN_ORG, 'providedId' => new IntegerObject('4294967295'), 'providedNode' => null, - 'providedClockSeq' => null, 'expectedId' => 'ffffffff', 'expectedDomain' => '02', 'expectedNode' => '001122334455', @@ -138,7 +134,7 @@ class DceSecurityGeneratorTest extends TestCase $generator = new DceSecurityGenerator($numberConverter, $timeGenerator, $dceSecurityProvider); - $this->expectException(InvalidArgumentException::class); + $this->expectException(DceSecurityException::class); $this->expectExceptionMessage('Local domain must be a valid DCE Security domain'); $generator->generate(42); @@ -152,9 +148,133 @@ class DceSecurityGeneratorTest extends TestCase $generator = new DceSecurityGenerator($numberConverter, $timeGenerator, $dceSecurityProvider); - $this->expectException(InvalidArgumentException::class); + $this->expectException(DceSecurityException::class); $this->expectExceptionMessage('A local identifier must be provided for the org domain'); $generator->generate(Uuid::DCE_DOMAIN_ORG); } + + public function testClockSequenceLowerBounds(): void + { + $dceSecurityProvider = Mockery::mock(DceSecurityProviderInterface::class); + $nodeProvider = Mockery::mock(NodeProviderInterface::class); + $timeProvider = new FixedTimeProvider(new Time(1583527677, 111984)); + + $calculator = new BrickMathCalculator(); + $numberConverter = new GenericNumberConverter($calculator); + $timeConverter = new GenericTimeConverter($calculator); + $timeGenerator = new DefaultTimeGenerator($nodeProvider, $timeConverter, $timeProvider); + + $dceSecurityGenerator = new DceSecurityGenerator($numberConverter, $timeGenerator, $dceSecurityProvider); + + $bytes = $dceSecurityGenerator->generate( + Uuid::DCE_DOMAIN_ORG, + new IntegerObject(1001), + new Hexadecimal('0123456789ab'), + 0 + ); + + $hex = bin2hex($bytes); + + $this->assertSame('000003e9', substr($hex, 0, 8)); + $this->assertSame('5feb01ea', substr($hex, 8, 8)); + $this->assertSame('00', substr($hex, 16, 2)); + $this->assertSame('02', substr($hex, 18, 2)); + $this->assertSame('0123456789ab', substr($hex, 20)); + } + + public function testClockSequenceUpperBounds(): void + { + $dceSecurityProvider = Mockery::mock(DceSecurityProviderInterface::class); + $nodeProvider = Mockery::mock(NodeProviderInterface::class); + $timeProvider = new FixedTimeProvider(new Time(1583527677, 111984)); + + $calculator = new BrickMathCalculator(); + $numberConverter = new GenericNumberConverter($calculator); + $timeConverter = new GenericTimeConverter($calculator); + $timeGenerator = new DefaultTimeGenerator($nodeProvider, $timeConverter, $timeProvider); + + $dceSecurityGenerator = new DceSecurityGenerator($numberConverter, $timeGenerator, $dceSecurityProvider); + + $bytes = $dceSecurityGenerator->generate( + Uuid::DCE_DOMAIN_ORG, + new IntegerObject(1001), + new Hexadecimal('0123456789ab'), + 63 + ); + + $hex = bin2hex($bytes); + + $this->assertSame('000003e9', substr($hex, 0, 8)); + $this->assertSame('5feb01ea', substr($hex, 8, 8)); + $this->assertSame('3f', substr($hex, 16, 2)); + $this->assertSame('02', substr($hex, 18, 2)); + $this->assertSame('0123456789ab', substr($hex, 20)); + } + + public function testExceptionThrownWhenClockSequenceTooLow(): void + { + $dceSecurityProvider = Mockery::mock(DceSecurityProviderInterface::class); + $timeGenerator = Mockery::mock(TimeGeneratorInterface::class); + $numberConverter = Mockery::mock(NumberConverterInterface::class); + + $dceSecurityGenerator = new DceSecurityGenerator($numberConverter, $timeGenerator, $dceSecurityProvider); + + $this->expectException(DceSecurityException::class); + $this->expectExceptionMessage( + 'Clock sequence out of bounds; it must be a value between 0 and 63' + ); + + $dceSecurityGenerator->generate(Uuid::DCE_DOMAIN_ORG, null, null, -1); + } + + public function testExceptionThrownWhenClockSequenceTooHigh(): void + { + $dceSecurityProvider = Mockery::mock(DceSecurityProviderInterface::class); + $timeGenerator = Mockery::mock(TimeGeneratorInterface::class); + $numberConverter = Mockery::mock(NumberConverterInterface::class); + + $dceSecurityGenerator = new DceSecurityGenerator($numberConverter, $timeGenerator, $dceSecurityProvider); + + $this->expectException(DceSecurityException::class); + $this->expectExceptionMessage( + 'Clock sequence out of bounds; it must be a value between 0 and 63' + ); + + $dceSecurityGenerator->generate(Uuid::DCE_DOMAIN_ORG, null, null, 64); + } + + public function testExceptionThrownWhenLocalIdTooLow(): void + { + $dceSecurityProvider = Mockery::mock(DceSecurityProviderInterface::class); + $timeGenerator = Mockery::mock(TimeGeneratorInterface::class); + $numberConverter = Mockery::mock(NumberConverterInterface::class); + + $dceSecurityGenerator = new DceSecurityGenerator($numberConverter, $timeGenerator, $dceSecurityProvider); + + $this->expectException(DceSecurityException::class); + $this->expectExceptionMessage( + 'Local identifier out of bounds; it must be a value between 0 and 4294967295' + ); + + $dceSecurityGenerator->generate(Uuid::DCE_DOMAIN_ORG, new IntegerObject(-1)); + } + + public function testExceptionThrownWhenLocalIdTooHigh(): void + { + $dceSecurityProvider = Mockery::mock(DceSecurityProviderInterface::class); + $timeGenerator = Mockery::mock(TimeGeneratorInterface::class); + + $calculator = new BrickMathCalculator(); + $numberConverter = new GenericNumberConverter($calculator); + + $dceSecurityGenerator = new DceSecurityGenerator($numberConverter, $timeGenerator, $dceSecurityProvider); + + $this->expectException(DceSecurityException::class); + $this->expectExceptionMessage( + 'Local identifier out of bounds; it must be a value between 0 and 4294967295' + ); + + $dceSecurityGenerator->generate(Uuid::DCE_DOMAIN_ORG, new IntegerObject('4294967296')); + } }