From 4000e896f9f5787adf0ba9fa250c179af9f7257f Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Fri, 16 Sep 2022 09:43:44 -0500 Subject: [PATCH] refactor: remove dependency on ext-ctype --- .github/workflows/continuous-integration.yml | 6 +- composer.json | 2 - composer.lock | 3 +- docs/quickstart.rst | 3 - src/Generator/DefaultTimeGenerator.php | 4 +- src/Type/Hexadecimal.php | 39 +++++---- src/Type/Integer.php | 86 +++++++++++--------- tests/ExpectedBehaviorTest.php | 4 +- 8 files changed, 78 insertions(+), 69 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 2236a33..5cce18a 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -91,7 +91,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: php-version: "${{ matrix.php-version }}" - extensions: bcmath, ctype, gmp, sodium, uuid + extensions: bcmath, gmp, sodium, uuid coverage: "none" ini-values: "memory_limit=-1" @@ -118,7 +118,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: php-version: "latest" - extensions: bcmath, ctype, gmp, sodium, uuid + extensions: bcmath, gmp, sodium, uuid coverage: "pcov" ini-values: "memory_limit=-1" @@ -168,7 +168,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: php-version: "${{ matrix.php-version }}" - extensions: bcmath, ctype, gmp, sodium, uuid + extensions: bcmath, gmp, sodium, uuid coverage: "none" ini-values: "memory_limit=-1" diff --git a/composer.json b/composer.json index 9653cad..ec7ad3b 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,6 @@ ], "require": { "php": "^8.0", - "ext-ctype": "*", "ext-json": "*", "brick/math": "^0.8.8 || ^0.9 || ^0.10", "ramsey/collection": "^1.0" @@ -42,7 +41,6 @@ }, "suggest": { "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", - "ext-ctype": "Enables faster processing of character classification using ctype functions.", "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", diff --git a/composer.lock b/composer.lock index 634dda8..a353f90 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "93dcc08db8ab52aea6a540b1fa09dcfd", + "content-hash": "c8193a73ae72c856bbb82703b1788499", "packages": [ { "name": "brick/math", @@ -7361,7 +7361,6 @@ "prefer-lowest": false, "platform": { "php": "^8.0", - "ext-ctype": "*", "ext-json": "*" }, "platform-dev": [], diff --git a/docs/quickstart.rst b/docs/quickstart.rst index bdc310e..d1ee60d 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -11,9 +11,6 @@ Requirements ramsey/uuid |version| requires the following: * PHP 8.0+ -* `ext-ctype `_ or a polyfill that - provides ext-ctype, such as `symfony/polyfill-ctype - `_ * `ext-json `_ The JSON extension is normally enabled by default, but it is possible to disable diff --git a/src/Generator/DefaultTimeGenerator.php b/src/Generator/DefaultTimeGenerator.php index a1b39b0..ea1e2a6 100644 --- a/src/Generator/DefaultTimeGenerator.php +++ b/src/Generator/DefaultTimeGenerator.php @@ -23,11 +23,11 @@ use Ramsey\Uuid\Provider\TimeProviderInterface; use Ramsey\Uuid\Type\Hexadecimal; use Throwable; -use function ctype_xdigit; use function dechex; use function hex2bin; use function is_int; use function pack; +use function preg_match; use function sprintf; use function str_pad; use function strlen; @@ -120,7 +120,7 @@ class DefaultTimeGenerator implements TimeGeneratorInterface $node = dechex($node); } - if (!ctype_xdigit((string) $node) || strlen((string) $node) > 12) { + if (!preg_match('/^[A-Fa-f0-9]+$/', (string) $node) || strlen((string) $node) > 12) { throw new InvalidArgumentException('Invalid node value'); } diff --git a/src/Type/Hexadecimal.php b/src/Type/Hexadecimal.php index 3c8f30a..bf71ec4 100644 --- a/src/Type/Hexadecimal.php +++ b/src/Type/Hexadecimal.php @@ -17,10 +17,8 @@ namespace Ramsey\Uuid\Type; use Ramsey\Uuid\Exception\InvalidArgumentException; use ValueError; -use function ctype_xdigit; +use function preg_match; use function sprintf; -use function str_starts_with; -use function strtolower; use function substr; /** @@ -37,23 +35,11 @@ final class Hexadecimal implements TypeInterface private string $value; /** - * @param string $value The hexadecimal value to store + * @param self|string $value The hexadecimal value to store */ - public function __construct(string $value) + public function __construct(self | string $value) { - $value = strtolower($value); - - if (str_starts_with($value, '0x')) { - $value = substr($value, 2); - } - - if (!ctype_xdigit($value)) { - throw new InvalidArgumentException( - 'Value must be a hexadecimal number' - ); - } - - $this->value = $value; + $this->value = $value instanceof self ? (string) $value : $this->prepareValue($value); } public function toString(): string @@ -109,4 +95,21 @@ final class Hexadecimal implements TypeInterface $this->unserialize($data['string']); } + + private function prepareValue(string $value): string + { + $value = strtolower($value); + + if (str_starts_with($value, '0x')) { + $value = substr($value, 2); + } + + if (!preg_match('/^[A-Fa-f0-9]+$/', $value)) { + throw new InvalidArgumentException( + 'Value must be a hexadecimal number' + ); + } + + return $value; + } } diff --git a/src/Type/Integer.php b/src/Type/Integer.php index e41b3ca..50dac99 100644 --- a/src/Type/Integer.php +++ b/src/Type/Integer.php @@ -17,10 +17,10 @@ namespace Ramsey\Uuid\Type; use Ramsey\Uuid\Exception\InvalidArgumentException; use ValueError; -use function ctype_digit; -use function ltrim; +use function assert; +use function is_numeric; +use function preg_match; use function sprintf; -use function str_starts_with; use function substr; /** @@ -46,40 +46,7 @@ final class Integer implements NumberInterface public function __construct(float | int | string | self $value) { - $value = (string) $value; - $sign = '+'; - - // If the value contains a sign, remove it for ctype_digit() check. - if (str_starts_with($value, '-') || str_starts_with($value, '+')) { - $sign = substr($value, 0, 1); - $value = substr($value, 1); - } - - if (!ctype_digit($value)) { - throw new InvalidArgumentException( - 'Value must be a signed integer or a string containing only ' - . 'digits 0-9 and, optionally, a sign (+ or -)' - ); - } - - // Trim any leading zeros. - $value = ltrim($value, '0'); - - // Set to zero if the string is empty after trimming zeros. - if ($value === '') { - $value = '0'; - } - - // Add the negative sign back to the value. - if ($sign === '-' && $value !== '0') { - $value = $sign . $value; - $this->isNegative = true; - } - - /** @psalm-var numeric-string $numericValue */ - $numericValue = $value; - - $this->value = $numericValue; + $this->value = $value instanceof self ? (string) $value : $this->prepareValue($value); } public function isNegative(): bool @@ -95,6 +62,9 @@ final class Integer implements NumberInterface return $this->value; } + /** + * @psalm-return numeric-string + */ public function __toString(): string { return $this->toString(); @@ -143,4 +113,46 @@ final class Integer implements NumberInterface $this->unserialize($data['string']); } + + /** + * @return numeric-string + */ + private function prepareValue(float | int | string $value): string + { + $value = (string) $value; + $sign = '+'; + + // If the value contains a sign, remove it for digit pattern check. + if (str_starts_with($value, '-') || str_starts_with($value, '+')) { + $sign = substr($value, 0, 1); + $value = substr($value, 1); + } + + if (!preg_match('/^\d+$/', $value)) { + throw new InvalidArgumentException( + 'Value must be a signed integer or a string containing only ' + . 'digits 0-9 and, optionally, a sign (+ or -)' + ); + } + + // Trim any leading zeros. + $value = ltrim($value, '0'); + + // Set to zero if the string is empty after trimming zeros. + if ($value === '') { + $value = '0'; + } + + // Add the negative sign back to the value. + if ($sign === '-' && $value !== '0') { + $value = $sign . $value; + + /** @psalm-suppress InaccessibleProperty */ + $this->isNegative = true; + } + + assert(is_numeric($value)); + + return $value; + } } diff --git a/tests/ExpectedBehaviorTest.php b/tests/ExpectedBehaviorTest.php index 52a6d1e..289a5ad 100644 --- a/tests/ExpectedBehaviorTest.php +++ b/tests/ExpectedBehaviorTest.php @@ -119,7 +119,7 @@ class ExpectedBehaviorTest extends TestCase $this->assertSame(2, $uuid->getVariant()); $this->assertSame((int) substr($method, -1), $uuid->getVersion()); - $this->assertTrue(ctype_digit((string) $uuid->getInteger())); + $this->assertSame(1, preg_match('/^\d+$/', (string) $uuid->getInteger())); } public function provideStaticCreationMethods() @@ -158,7 +158,7 @@ class ExpectedBehaviorTest extends TestCase $this->assertSame('281474976710655', (string) $uuid->getNode()); $this->assertSame('3fff', $uuid->getClockSequenceHex()); $this->assertSame('16383', (string) $uuid->getClockSequence()); - $this->assertTrue(ctype_digit((string) $uuid->getTimestamp())); + $this->assertSame(1, preg_match('/^\d+$/', (string) $uuid->getTimestamp())); } /**