From 264d64d33d5c7c76e00e48fa35459455b28b6a31 Mon Sep 17 00:00:00 2001 From: Matias Navarro Carter Date: Fri, 3 Jan 2025 15:44:37 +0000 Subject: [PATCH] feat: performance improvements BREAKING-CHANGE: Removed Castor\Bytes as a dependency and replace it by Castor\Uuid\ByteArray BREAKING-CHANGE: Removed Brick\Math\BigNumber from Time classes --- .dev/.gitignore | 3 +- .dev/docker/php/Dockerfile | 2 +- composer.json | 13 +- composer.lock | 379 ++++++----------------- include/functions.php | 28 +- phpbench.json | 26 +- psalm-baseline.xml | 91 ++++++ psalm.xml | 1 + src/Uuid.php | 4 +- src/Uuid/Any.php | 55 ++-- src/Uuid/ByteArray.php | 85 +++++ src/Uuid/System/MacProvider.php | 4 +- src/Uuid/System/MacProvider/Fallback.php | 5 +- src/Uuid/System/MacProvider/FromOs.php | 26 +- src/Uuid/System/State.php | 5 +- src/Uuid/System/State/Fixed.php | 10 +- src/Uuid/System/State/Standard.php | 26 +- src/Uuid/System/Time.php | 7 +- src/Uuid/System/Time/Gregorian.php | 46 +-- src/Uuid/System/Time/Unix.php | 36 ++- src/Uuid/Version1.php | 49 +-- src/Uuid/Version3.php | 4 +- src/Uuid/Version4.php | 5 +- src/Uuid/Version5.php | 6 +- src/Uuid/Version6.php | 17 +- src/Uuid/Version7.php | 9 +- tests/Benchmark/CastorVsRamseyBench.php | 234 ++++++++++++++ tests/Benchmark/UuidGenerationBench.php | 72 ----- tests/Uuid/AnyTest.php | 10 +- tests/Uuid/ByteArrayTest.php | 32 ++ tests/Uuid/System/Time/GregorianTest.php | 3 +- tests/Uuid/Version1Test.php | 24 +- tests/Uuid/Version3Test.php | 10 +- tests/Uuid/Version4Test.php | 10 +- tests/Uuid/Version5Test.php | 10 +- tests/Uuid/Version6Test.php | 18 +- tests/Uuid/Version7Test.php | 10 +- 37 files changed, 818 insertions(+), 557 deletions(-) create mode 100644 psalm-baseline.xml create mode 100644 src/Uuid/ByteArray.php create mode 100644 tests/Benchmark/CastorVsRamseyBench.php delete mode 100644 tests/Benchmark/UuidGenerationBench.php create mode 100644 tests/Uuid/ByteArrayTest.php diff --git a/.dev/.gitignore b/.dev/.gitignore index 2fbf865..d2bb2d1 100644 --- a/.dev/.gitignore +++ b/.dev/.gitignore @@ -1,2 +1,3 @@ debug -coverage \ No newline at end of file +coverage +bench.html \ No newline at end of file diff --git a/.dev/docker/php/Dockerfile b/.dev/docker/php/Dockerfile index be79ff6..62b1287 100644 --- a/.dev/docker/php/Dockerfile +++ b/.dev/docker/php/Dockerfile @@ -22,7 +22,7 @@ RUN install-php-extensions bcmath FROM base AS dev -RUN install-php-extensions xdebug pcntl posix +RUN install-php-extensions xdebug pcntl posix opcache # Install and Configure XDebug COPY ./xdebug.ini /usr/local/etc/php/conf.d/60_xdebug.ini \ No newline at end of file diff --git a/composer.json b/composer.json index 54f448d..fbe9b86 100644 --- a/composer.json +++ b/composer.json @@ -41,11 +41,8 @@ "require": { "php": ">=8.3", "ext-bcmath": "*", - "castor/io": "^0.3.0", - "castor/bytes": "^0.2.0", - "castor/functions": "^0.2.0", - "brick/date-time": "^0.6", - "brick/math": "^0.10" + "castor/functions": "^1.0", + "brick/date-time": "^0.7" }, "require-dev": { "phpunit/phpunit": "^10.5", @@ -74,9 +71,11 @@ "test:unit": "phpunit --colors --exclude-group=integration --exclude-group=e2e", "test:e2e": "phpunit --colors --group=e2e", "test:integration": "phpunit --colors --group=integration", - "bench": "phpbench run tests/Benchmark --report=default", + "bench": "phpbench run --report=default --output=html tests/Benchmark", "profile": "phpbench xdebug:profile tests/Benchmark --progress=none", "psalm": "psalm --no-cache --threads=5", - "psalm:gh": "psalm --no-cache --threads=5 --long-progress --output-format=github" + "psalm:gh": "psalm --no-cache --threads=5 --long-progress --output-format=github", + "psalm:fix": "psalm --update-baseline", + "psalm:allow": "psalm --set-baseline=psalm-baseline.xml" } } diff --git a/composer.lock b/composer.lock index bece8ab..92a392d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d09848fbd39c7f6f7807f5121329aab8", + "content-hash": "b9bd81608627bc7bde1681442ce7d079", "packages": [ { "name": "brick/date-time", - "version": "0.6.5", + "version": "0.7.0", "source": { "type": "git", "url": "https://github.com/brick/date-time.git", - "reference": "db3b2508d7bb7c61deb7e7450a90bcc0439eb7e4" + "reference": "7b39737f630a0e5d2a2dc0834b0da77a065efaed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/date-time/zipball/db3b2508d7bb7c61deb7e7450a90bcc0439eb7e4", - "reference": "db3b2508d7bb7c61deb7e7450a90bcc0439eb7e4", + "url": "https://api.github.com/repos/brick/date-time/zipball/7b39737f630a0e5d2a2dc0834b0da77a065efaed", + "reference": "7b39737f630a0e5d2a2dc0834b0da77a065efaed", "shasum": "" }, "require": { @@ -27,7 +27,7 @@ "guzzlehttp/guzzle": "^7.0", "php-coveralls/php-coveralls": "^2.2", "phpunit/phpunit": "^10.5", - "vimeo/psalm": "5.17.0" + "vimeo/psalm": "5.25.0" }, "suggest": { "ext-timezonedb": "This PECL extension provides up-to-date timezone information" @@ -44,14 +44,32 @@ ], "description": "Date and time library", "keywords": [ + "DayOfWeek", + "LocalDateRange", + "LocalDateTime", + "LocalTime", + "Month", + "MonthDay", + "Year", + "YearMonth", + "YearMonthRange", + "YearWeek", + "ZonedDateTime", "brick", "date", "datetime", - "time" + "duration", + "instant", + "interval", + "localdate", + "period", + "stopwatch", + "time", + "timezone" ], "support": { "issues": "https://github.com/brick/date-time/issues", - "source": "https://github.com/brick/date-time/tree/0.6.5" + "source": "https://github.com/brick/date-time/tree/0.7.0" }, "funding": [ { @@ -59,180 +77,21 @@ "type": "github" } ], - "time": "2024-06-19T21:32:48+00:00" - }, - { - "name": "brick/math", - "version": "0.10.2", - "source": { - "type": "git", - "url": "https://github.com/brick/math.git", - "reference": "459f2781e1a08d52ee56b0b1444086e038561e3f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/459f2781e1a08d52ee56b0b1444086e038561e3f", - "reference": "459f2781e1a08d52ee56b0b1444086e038561e3f", - "shasum": "" - }, - "require": { - "ext-json": "*", - "php": "^7.4 || ^8.0" - }, - "require-dev": { - "php-coveralls/php-coveralls": "^2.2", - "phpunit/phpunit": "^9.0", - "vimeo/psalm": "4.25.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Brick\\Math\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Arbitrary-precision arithmetic library", - "keywords": [ - "Arbitrary-precision", - "BigInteger", - "BigRational", - "arithmetic", - "bigdecimal", - "bignum", - "brick", - "math" - ], - "support": { - "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.10.2" - }, - "funding": [ - { - "url": "https://github.com/BenMorel", - "type": "github" - } - ], - "time": "2022-08-10T22:54:19+00:00" - }, - { - "name": "castor/bytes", - "version": "0.2.0", - "source": { - "type": "git", - "url": "git@github.com:castor-labs/php-lib-bytes.git", - "reference": "510a569a62a4a0ea65a47921d86777e33024e54a" - }, - "dist": { - "type": "zip", - "url": "https://castor-labs.github.io/php-packages/archive/castor/bytes/castor-bytes-510a569a62a4a0ea65a47921d86777e33024e54a-zip-8cb644.zip", - "reference": "510a569a62a4a0ea65a47921d86777e33024e54a", - "shasum": "239e492cfd5d5c7dd37870b49301c1cb13d26c6d" - }, - "require": { - "castor/functions": "^0.2.0", - "castor/io": "^0.3.0", - "php": ">=8.3" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^3.49", - "phpunit/phpunit": "^10.5", - "vimeo/psalm": "^5.22" - }, - "type": "library", - "autoload": { - "psr-4": { - "Castor\\": "src" - } - }, - "autoload-dev": { - "psr-4": { - "Castor\\": "tests" - } - }, - "scripts": { - "pr": [ - "@fmt", - "@psalm", - "@test" - ], - "ci": [ - "@fmt:check", - "@psalm:gh", - "@test" - ], - "fmt": [ - "php-cs-fixer fix --diff --ansi" - ], - "fmt:check": [ - "php-cs-fixer fix --dry-run --diff --ansi" - ], - "test": [ - "phpunit --colors" - ], - "test:unit": [ - "phpunit --colors --exclude-group=integration --exclude-group=e2e" - ], - "test:e2e": [ - "phpunit --colors --group=e2e" - ], - "test:integration": [ - "phpunit --colors --group=integration" - ], - "psalm": [ - "psalm --no-cache --threads=5 --use-baseline" - ], - "psalm:gh": [ - "psalm --no-cache --threads=5 --long-progress --output-format=github --use-baseline" - ], - "psalm:fix": [ - "psalm --update-baseline" - ], - "psalm:allow": [ - "psalm --set-baseline=psalm-baseline.xml" - ] - }, - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Matias Navarro Carter", - "email": "mnavarrocarter@gmail.com", - "role": "Lead Maintainer" - } - ], - "description": "A useful value object to work with bytes as a slice of unsigned 8-bit integers", - "homepage": "https://castor-labs.github.io/php-lib-std/packages/bytes", - "keywords": [ - "bytes", - "castor", - "php", - "php82", - "stdlib", - "uint8" - ], - "support": { - "source": "https://github.com/castor-labs/php-lib-bytes/tree/0.2.0", - "issues": "https://github.com/castor-labs/php-lib-bytes/issues" - }, - "time": "2024-07-05T20:12:43+00:00" + "time": "2024-06-23T14:32:17+00:00" }, { "name": "castor/functions", - "version": "0.2.0", + "version": "1.0.0", "source": { "type": "git", "url": "git@github.com:castor-labs/php-lib-functions.git", - "reference": "ee64f957cacbae447f6ac0fd090a704b7a91e671" + "reference": "98f0e07500eaf452da603ec687cf2b8bc923601b" }, "dist": { "type": "zip", - "url": "https://castor-labs.github.io/php-packages/archive/castor/functions/castor-functions-ee64f957cacbae447f6ac0fd090a704b7a91e671-zip-03f9f1.zip", - "reference": "ee64f957cacbae447f6ac0fd090a704b7a91e671", - "shasum": "2a3be2f791a15386f65764505dc03415ac9fc936" + "url": "https://castor-labs.github.io/php-packages/archive/castor/functions/castor-functions-98f0e07500eaf452da603ec687cf2b8bc923601b-zip-2443fb.zip", + "reference": "98f0e07500eaf452da603ec687cf2b8bc923601b", + "shasum": "7f4b2aca3d970b4cdf5d87cfa833f11d650afc59" }, "require": { "ext-mbstring": "*", @@ -320,116 +179,10 @@ "stdlib" ], "support": { - "source": "https://github.com/castor-labs/php-lib-functions/tree/0.2.0", + "source": "https://github.com/castor-labs/php-lib-functions/tree/1.0.0", "issues": "https://github.com/castor-labs/php-lib-functions/issues" }, - "time": "2024-07-05T17:02:58+00:00" - }, - { - "name": "castor/io", - "version": "0.3.0", - "source": { - "type": "git", - "url": "git@github.com:castor-labs/php-lib-io.git", - "reference": "e669d4107862ae257cbcf414dbe7603219274f74" - }, - "dist": { - "type": "zip", - "url": "https://castor-labs.github.io/php-packages/archive/castor/io/castor-io-e669d4107862ae257cbcf414dbe7603219274f74-zip-f49fef.zip", - "reference": "e669d4107862ae257cbcf414dbe7603219274f74", - "shasum": "6b766037300732d4e0ccc4e43c9378908e7dd638" - }, - "require": { - "php": ">=8.3" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^3.49", - "phpunit/phpunit": "^10.5", - "vimeo/psalm": "^5.22" - }, - "type": "library", - "autoload": { - "psr-4": { - "Castor\\Io\\": "src" - }, - "files": [ - "include/functions.php" - ] - }, - "autoload-dev": { - "psr-4": { - "Castor\\Io\\": "tests" - } - }, - "scripts": { - "pr": [ - "@fmt", - "@psalm", - "@test" - ], - "ci": [ - "@fmt:check", - "@psalm:gh", - "@test" - ], - "fmt": [ - "php-cs-fixer fix --diff --ansi" - ], - "fmt:check": [ - "php-cs-fixer fix --dry-run --diff --ansi" - ], - "test": [ - "phpunit --colors" - ], - "test:unit": [ - "phpunit --colors --exclude-group=integration --exclude-group=e2e" - ], - "test:e2e": [ - "phpunit --colors --group=e2e" - ], - "test:integration": [ - "phpunit --colors --group=integration" - ], - "psalm": [ - "psalm --no-cache --threads=5 --use-baseline" - ], - "psalm:gh": [ - "psalm --no-cache --threads=5 --long-progress --output-format=github --use-baseline" - ], - "psalm:fix": [ - "psalm --update-baseline" - ], - "psalm:allow": [ - "psalm --set-baseline=psalm-baseline.xml" - ] - }, - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Matias Navarro Carter", - "email": "mnavarrocarter@gmail.com", - "role": "Lead Maintainer" - } - ], - "description": "Input and output abstractions for PHP applications", - "homepage": "https://castor-labs.github.io/php-lib-std/packages/io", - "keywords": [ - "castor", - "input", - "io", - "output", - "php", - "php82", - "stdlib", - "streams" - ], - "support": { - "source": "https://github.com/castor-labs/php-lib-io/tree/0.3.0", - "issues": "https://github.com/castor-labs/php-lib-io/issues" - }, - "time": "2024-07-05T19:09:24+00:00" + "time": "2024-12-31T15:30:07+00:00" } ], "packages-dev": [ @@ -593,6 +346,66 @@ ], "time": "2024-04-13T18:00:56+00:00" }, + { + "name": "brick/math", + "version": "0.12.1", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "f510c0a40911935b77b86859eb5223d58d660df1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/f510c0a40911935b77b86859eb5223d58d660df1", + "reference": "f510c0a40911935b77b86859eb5223d58d660df1", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^10.1", + "vimeo/psalm": "5.16.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "bignumber", + "brick", + "decimal", + "integer", + "math", + "mathematics", + "rational" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.12.1" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2023-11-29T23:19:16+00:00" + }, { "name": "clue/ndjson-react", "version": "v1.3.0", @@ -6140,13 +5953,13 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { "php": ">=8.3", "ext-bcmath": "*" }, - "platform-dev": {}, + "platform-dev": [], "plugin-api-version": "2.6.0" } diff --git a/include/functions.php b/include/functions.php index b9b0ae9..d32e5cb 100644 --- a/include/functions.php +++ b/include/functions.php @@ -32,7 +32,7 @@ function parse(string $uuid, bool $lazy = true): Uuid /** * Creates a UUID from the raw bytes. */ -function fromBytes(Bytes|string $bytes): Uuid +function fromBytes(ByteArray|string $bytes): Uuid { return Any::fromBytes($bytes); } @@ -59,7 +59,7 @@ function max(): Uuid /** @var null|Uuid $uuid */ static $uuid = null; if (null === $uuid) { - $uuid = Any::fromBytes(Bytes::fromUint8( + $uuid = Any::fromBytes(ByteArray::create([ 0xFF, 0xFF, 0xFF, @@ -76,7 +76,7 @@ function max(): Uuid 0xFF, 0xFF, 0xFF, - )); + ])); } return $uuid; @@ -90,7 +90,7 @@ function nil(): Uuid /** @var null|Uuid $uuid */ static $uuid = null; if (null === $uuid) { - $uuid = Any::fromBytes(Bytes::fromUint8( + $uuid = Any::fromBytes(ByteArray::create([ 0x00, 0x00, 0x00, @@ -107,7 +107,7 @@ function nil(): Uuid 0x00, 0x00, 0x00, - )); + ])); } return $uuid; @@ -115,9 +115,9 @@ function nil(): Uuid namespace Castor\Uuid\Ns; -use Castor\Bytes; use Castor\Uuid; use Castor\Uuid\Any; +use Castor\Uuid\ByteArray; /** * Returns the UUID namespace for Domain Name System (DNS). @@ -127,7 +127,7 @@ function dns(): Uuid /** @var null|Uuid $uuid */ static $uuid = null; if (null === $uuid) { - $uuid = Any::fromBytes(Bytes::fromUint8( + $uuid = Any::fromBytes(ByteArray::create([ 0x6B, 0xA7, 0xB8, @@ -144,7 +144,7 @@ function dns(): Uuid 0xD4, 0x30, 0xC8, - )); + ])); } return $uuid; @@ -158,7 +158,7 @@ function oid(): Uuid /** @var null|Uuid $uuid */ static $uuid = null; if (null === $uuid) { - $uuid = Any::fromBytes(Bytes::fromUint8( + $uuid = Any::fromBytes(ByteArray::create([ 0x6B, 0xA7, 0xB8, @@ -175,7 +175,7 @@ function oid(): Uuid 0xD4, 0x30, 0xC8, - )); + ])); } return $uuid; @@ -189,7 +189,7 @@ function url(): Uuid /** @var null|Uuid $uuid */ static $uuid = null; if (null === $uuid) { - $uuid = Any::fromBytes(Bytes::fromUint8( + $uuid = Any::fromBytes(ByteArray::create([ 0x6B, 0xA7, 0xB8, @@ -206,7 +206,7 @@ function url(): Uuid 0xD4, 0x30, 0xC8, - )); + ])); } return $uuid; @@ -220,7 +220,7 @@ function x500(): Uuid /** @var null|Uuid $uuid */ static $uuid = null; if (null === $uuid) { - $uuid = Any::fromBytes(Bytes::fromUint8( + $uuid = Any::fromBytes(ByteArray::create([ 0x6B, 0xA7, 0xB8, @@ -237,7 +237,7 @@ function x500(): Uuid 0xD4, 0x30, 0xC8, - )); + ])); } return $uuid; diff --git a/phpbench.json b/phpbench.json index 4e6e1ea..d7df3e6 100644 --- a/phpbench.json +++ b/phpbench.json @@ -1,5 +1,29 @@ { "$schema":"./vendor/phpbench/phpbench/phpbench.schema.json", "runner.bootstrap": "vendor/autoload.php", - "xdebug.command_handler_output_dir": ".dev/debug" + "runner.output_mode": "time", + "runner.retry_threshold": 5, + "runner.php_config": { + "opcache.enable": true, + "opcache.enable_cli": true, + "opcache.jit": 1235, + "apc.enable": true, + "apc.enable_cli": true + }, + "xdebug.command_handler_output_dir": ".dev/debug", + "report.generators": { + "default": { + "extends": "expression", + "title": "Castor UUID Benchmarks", + "break": [ "benchmark" ], + "cols": [ "benchmark", "subject", "set", "mem_peak", "mode", "best", "mean", "worst", "stdev", "rstdev" ] + } + }, + "report.outputs": { + "html": { + "renderer": "html", + "path": ".dev/bench.html", + "title": "Castor UUID Benchmarks" + } + } } \ No newline at end of file diff --git a/psalm-baseline.xml b/psalm-baseline.xml new file mode 100644 index 0000000..1004d33 --- /dev/null +++ b/psalm-baseline.xml @@ -0,0 +1,91 @@ + + + + + bytes->toArray(), $this->toString()]]]> + + + , 1: string}]]> + + + + + + + toArray(), $offset, $length)]]> + + + + + + + + + + bytes->toHex(), 16, 10)]]> + + + + + + + + bytes->toHex(), 16, 10)]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + >= 4]]> + + + + > 4) | (($bytes[0] & 0x0F) << 4)]]> + > 4) | (($bytes[1] & 0x0F) << 4)]]> + > 4) | (($bytes[2] & 0x0F) << 4)]]> + > 4) | (($bytes[3] & 0x0F) << 4)]]> + > 4) | (($bytes[4] & 0x0F) << 4)]]> + + > 4)]]> + > 4)]]> + > 4)]]> + > 4)]]> + > 4)]]> + > 4)]]> + + + + + + + + + diff --git a/psalm.xml b/psalm.xml index 735479d..a25bd83 100644 --- a/psalm.xml +++ b/psalm.xml @@ -6,6 +6,7 @@ xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd" findUnusedBaselineEntry="true" findUnusedCode="false" + errorBaseline="psalm-baseline.xml" > diff --git a/src/Uuid.php b/src/Uuid.php index 5e2196f..f1ca782 100644 --- a/src/Uuid.php +++ b/src/Uuid.php @@ -16,6 +16,8 @@ namespace Castor; +use Castor\Uuid\ByteArray; + /** * This is the base contract for a UUID. * @@ -31,7 +33,7 @@ interface Uuid * * Implementors MUST NOT return the original reference stored inside the UUID. */ - public function getBytes(): Bytes; + public function getBytes(): ByteArray; /** * Returns the standard segmented hexadecimal representation of the UUID. diff --git a/src/Uuid/Any.php b/src/Uuid/Any.php index d0b03dc..943ca38 100644 --- a/src/Uuid/Any.php +++ b/src/Uuid/Any.php @@ -16,8 +16,7 @@ namespace Castor\Uuid; -use Castor\Bytes; -use Castor\Encoding\Error; +use Castor\Encoding\Failure; use Castor\RegExp; use Castor\Str; use Castor\Uuid; @@ -35,7 +34,7 @@ */ class Any implements Uuid, \Stringable, \JsonSerializable { - protected const string PATTERN = '/^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/'; + protected const string PATTERN = '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/'; protected const int STR_VERSION_OFFSET = 14; @@ -51,7 +50,7 @@ class Any implements Uuid, \Stringable, \JsonSerializable protected const int VAB = 8; protected function __construct( - private Bytes $bytes, + private ByteArray $bytes, private string $string = '', ) {} @@ -61,20 +60,20 @@ public function __toString(): string } /** - * @return array{0: string} + * @return array{0: array<0,255>, 1: string} */ public function __serialize(): array { - return [$this->toString()]; + return [$this->bytes->toArray(), $this->toString()]; } /** - * @param array{0: string} $data + * @param array{0: array<0,255>, 1: string} $data */ public function __unserialize(array $data): void { - $this->bytes = new Bytes(''); - $this->string = $data[0]; + $this->bytes = ByteArray::create($data[0]); + $this->string = $data[1]; } public function toString(): string @@ -86,10 +85,14 @@ public function toString(): string return $this->string; } - public function getBytes(): Bytes + public function getBytes(): ByteArray { - if ($this->bytes->len() === 0) { - $this->bytes = self::parse($this->string, false)->getBytes(); + if ($this->bytes->count() === 0) { + try { + $this->bytes = ByteArray::fromHex(Str\replace($this->string, '-', '')); + } catch (Failure $e) { + throw new \LogicException('Impossible error', previous : $e); + } } return clone $this->bytes; @@ -123,13 +126,13 @@ public function toUrn(): string * * @throws ParsingError if the bytes are invalid */ - public static function fromBytes(Bytes|string $bytes): Uuid + public static function fromBytes(ByteArray|string $bytes): Uuid { if (\is_string($bytes)) { - $bytes = new Bytes($bytes); + $bytes = ByteArray::fromRaw($bytes); } - if (self::LEN !== $bytes->len()) { + if (self::LEN !== $bytes->count()) { throw new ParsingError('UUID must have 16 bytes.'); } @@ -156,15 +159,17 @@ public static function fromBytes(Bytes|string $bytes): Uuid */ public static function parse(string $uuid, bool $lazy = true): Uuid { + $uuid = Str\toLower($uuid); + if ($lazy) { return self::lazy($uuid); } - $uuid = Str\toLower(Str\replace($uuid, '-', '')); + $uuid = Str\replace($uuid, '-', ''); try { - $bytes = Bytes::fromHex($uuid); - } catch (Error $e) { + $bytes = ByteArray::fromHex($uuid); + } catch (Failure $e) { throw new ParsingError('Invalid hexadecimal in UUID.', previous: $e); } @@ -186,13 +191,13 @@ protected static function lazy(string $uuid): Uuid } return match ($uuid[self::STR_VERSION_OFFSET]) { - '1' => new Version1(new Bytes(''), $uuid), - '3' => new Version3(new Bytes(''), $uuid), - '4' => new Version4(new Bytes(''), $uuid), - '5' => new Version5(new Bytes(''), $uuid), - '6' => new Version6(new Bytes(''), $uuid), - '7' => new Version7(new Bytes(''), $uuid), - default => new Any(new Bytes(''), $uuid) + '1' => new Version1(new ByteArray(0), $uuid), + '3' => new Version3(new ByteArray(0), $uuid), + '4' => new Version4(new ByteArray(0), $uuid), + '5' => new Version5(new ByteArray(0), $uuid), + '6' => new Version6(new ByteArray(0), $uuid), + '7' => new Version7(new ByteArray(0), $uuid), + default => new Any(new ByteArray(0), $uuid) }; } } diff --git a/src/Uuid/ByteArray.php b/src/Uuid/ByteArray.php new file mode 100644 index 0000000..23340e1 --- /dev/null +++ b/src/Uuid/ByteArray.php @@ -0,0 +1,85 @@ +> + */ +final class ByteArray extends \SplFixedArray +{ + /** + * @throws Failure + */ + public static function fromHex(string $hex): self + { + return self::fromRaw(Hex\decode($hex)); + } + + public static function fromRaw(string $raw): self + { + $bytes = Bytes\unpack($raw); + + return self::create($bytes); + } + + /** + * @param array> $array + */ + public static function create(array $array): self + { + $self = new self(\count($array)); + $self->allocate(...$array); + + return $self; + } + + /** + * @param int<0,255> ...$bytes + */ + public function allocate(int ...$bytes): void + { + // @var array $bytes + foreach ($bytes as $i => $byte) { + $this[$i] = $byte; + } + } + + public function toRaw(): string + { + return Bytes\pack(...$this->toArray()); + } + + public function slice(int $offset, ?int $length = null): ByteArray + { + return self::create(Arr\slice($this->toArray(), $offset, $length)); + } + + public function toHex(): string + { + return Hex\encode($this->toRaw()); + } + + public function equals(ByteArray $bytes): bool + { + return $this == $bytes; + } +} diff --git a/src/Uuid/System/MacProvider.php b/src/Uuid/System/MacProvider.php index 53ec596..94aaa09 100644 --- a/src/Uuid/System/MacProvider.php +++ b/src/Uuid/System/MacProvider.php @@ -16,7 +16,7 @@ namespace Castor\Uuid\System; -use Castor\Bytes; +use Castor\Uuid\ByteArray; interface MacProvider { @@ -28,7 +28,7 @@ interface MacProvider * In case is possible for the implementation not to be able to find any MAC, you must compose a * MacProvider\RandUniMultiCast as a fallback. * - * @return Bytes[] + * @return ByteArray[] */ public function getMacAddresses(): array; } diff --git a/src/Uuid/System/MacProvider/Fallback.php b/src/Uuid/System/MacProvider/Fallback.php index befa98d..50520ad 100644 --- a/src/Uuid/System/MacProvider/Fallback.php +++ b/src/Uuid/System/MacProvider/Fallback.php @@ -17,6 +17,7 @@ namespace Castor\Uuid\System\MacProvider; use Castor\Bytes; +use Castor\Uuid\ByteArray; use Castor\Uuid\System\MacProvider; use Random\Randomizer; @@ -34,11 +35,11 @@ public function __construct( ) {} /** - * @return Bytes[] + * @return ByteArray[] */ public function getMacAddresses(): array { - $b = new Bytes($this->random->getBytes(6)); + $b = ByteArray::fromRaw($this->random->getBytes(6)); $b[0] = $b[0] & 0xFE | 0x01; return [$b]; diff --git a/src/Uuid/System/MacProvider/FromOs.php b/src/Uuid/System/MacProvider/FromOs.php index 7427ae8..a36a81e 100644 --- a/src/Uuid/System/MacProvider/FromOs.php +++ b/src/Uuid/System/MacProvider/FromOs.php @@ -17,8 +17,8 @@ namespace Castor\Uuid\System\MacProvider; use Castor\Arr; -use Castor\Bytes; -use Castor\Encoding\Error; +use Castor\Encoding\Failure; +use Castor\Uuid\ByteArray; use Castor\Uuid\System\MacProvider; final class FromOs implements MacProvider @@ -26,15 +26,15 @@ final class FromOs implements MacProvider /** * Pattern to match nodes in ifconfig and ipconfig output. */ - private const IFCONFIG_PATTERN = '/[^:]([0-9a-f]{2}([:-])[0-9a-f]{2}(\2[0-9a-f]{2}){4})[^:]/i'; + private const string IFCONFIG_PATTERN = '/[^:]([0-9a-f]{2}([:-])[0-9a-f]{2}(\2[0-9a-f]{2}){4})[^:]/i'; /** * Pattern to match nodes in sysfs stream output. */ - private const SYSFS_PATTERN = '/^([0-9a-f]{2}:){5}[0-9a-f]{2}$/i'; + private const string SYSFS_PATTERN = '/^([0-9a-f]{2}:){5}[0-9a-f]{2}$/i'; /** - * @param null|Bytes[] $cached + * @param null|ByteArray[] $cached */ public function __construct( private readonly MacProvider $next, @@ -42,7 +42,7 @@ public function __construct( ) {} /** - * @return Bytes[] + * @return ByteArray[] */ public function getMacAddresses(): array { @@ -55,7 +55,7 @@ public function getMacAddresses(): array } /** - * @return Bytes[] + * @return ByteArray[] * * TODO: Adjust to work within docker containers */ @@ -84,7 +84,7 @@ private function tryGetMacAddresses(): array /** * Returns MAC address from the first system interface via the sysfs interface. * - * @return Bytes[] + * @return ByteArray[] */ private function getFromSysFile(): array { @@ -112,7 +112,7 @@ private function getFromSysFile(): array } // Map any macs we have - return [...Arr\map($macs, fn (string $mac): Bytes => Bytes::fromHex($this->cleanMac($mac)))]; + return [...Arr\map($macs, fn (string $mac): ByteArray => ByteArray::fromHex($this->cleanMac($mac)))]; } /** @@ -124,11 +124,11 @@ private function getFromSysFile(): array * * @codeCoverageIgnore * - * @return Bytes[] + * @return ByteArray[] */ private function getFromConsoleCommand(): array { - /** @var Bytes[] $macs */ + /** @var ByteArray[] $macs */ $macs = []; $disabledFunctions = \strtolower((string) \ini_get('disable_functions')); @@ -173,8 +173,8 @@ private function getFromConsoleCommand(): array foreach ($matches[1] as $iface) { if ('00:00:00:00:00:00' !== $iface && '00-00-00-00-00-00' !== $iface) { try { - $macs[] = Bytes::fromHex($this->cleanMac($iface)); - } catch (Error) { + $macs[] = ByteArray::fromHex($this->cleanMac($iface)); + } catch (Failure) { continue; // Ignore invalid macs } } diff --git a/src/Uuid/System/State.php b/src/Uuid/System/State.php index c214a7e..b069f3e 100644 --- a/src/Uuid/System/State.php +++ b/src/Uuid/System/State.php @@ -17,6 +17,7 @@ namespace Castor\Uuid\System; use Castor\Bytes; +use Castor\Uuid\ByteArray; use Castor\Uuid\System\Time\Gregorian; /** @@ -27,7 +28,7 @@ interface State /** * Returns the system's clock sequence as bytes. */ - public function getClockSequence(): Bytes; + public function getClockSequence(): ByteArray; /** * Returns the system time. @@ -37,5 +38,5 @@ public function getTime(): Gregorian; /** * Returns the system's node. */ - public function getNode(): Bytes; + public function getNode(): ByteArray; } diff --git a/src/Uuid/System/State/Fixed.php b/src/Uuid/System/State/Fixed.php index 415663a..3de73a8 100644 --- a/src/Uuid/System/State/Fixed.php +++ b/src/Uuid/System/State/Fixed.php @@ -16,7 +16,7 @@ namespace Castor\Uuid\System\State; -use Castor\Bytes; +use Castor\Uuid\ByteArray; use Castor\Uuid\System\State; use Castor\Uuid\System\Time\Gregorian; @@ -24,11 +24,11 @@ { public function __construct( private Gregorian $time, - private Bytes $clockSeq, - private Bytes $node, + private ByteArray $clockSeq, + private ByteArray $node, ) {} - public function getClockSequence(): Bytes + public function getClockSequence(): ByteArray { return $this->clockSeq; } @@ -38,7 +38,7 @@ public function getTime(): Gregorian return $this->time; } - public function getNode(): Bytes + public function getNode(): ByteArray { return $this->node; } diff --git a/src/Uuid/System/State/Standard.php b/src/Uuid/System/State/Standard.php index dd0bf24..9b9db17 100644 --- a/src/Uuid/System/State/Standard.php +++ b/src/Uuid/System/State/Standard.php @@ -17,7 +17,7 @@ namespace Castor\Uuid\System\State; use Brick\DateTime\Clock; -use Castor\Bytes; +use Castor\Uuid\ByteArray; use Castor\Uuid\System\MacProvider; use Castor\Uuid\System\MacProvider\Fallback; use Castor\Uuid\System\MacProvider\FromOs; @@ -30,18 +30,18 @@ final class Standard implements State { private static ?Standard $global = null; - private Bytes $lastTimestamp; - private Bytes $clockSequence; - private Bytes $macAddress; + private ByteArray $lastTimestamp; + private ByteArray $clockSequence; + private ByteArray $macAddress; public function __construct( private readonly Clock $clock, private readonly Randomizer $random, private readonly MacProvider $macProvider, ) { - $this->macAddress = new Bytes(''); - $this->clockSequence = new Bytes(''); - $this->lastTimestamp = new Bytes(''); + $this->macAddress = new ByteArray(0); + $this->clockSequence = new ByteArray(0); + $this->lastTimestamp = new ByteArray(0); } public static function global(): Standard @@ -58,9 +58,9 @@ public static function global(): Standard return self::$global; } - public function getClockSequence(): Bytes + public function getClockSequence(): ByteArray { - if (0 === $this->clockSequence->len()) { + if (0 === $this->clockSequence->count()) { $this->clockSequence = $this->generateClockSequence(); } @@ -80,9 +80,9 @@ public function getTime(): Gregorian return $gregorianTime; } - public function getNode(): Bytes + public function getNode(): ByteArray { - if (0 === $this->macAddress->len()) { + if (0 === $this->macAddress->count()) { $macs = $this->macProvider->getMacAddresses(); $this->macAddress = $macs[0]; } @@ -90,8 +90,8 @@ public function getNode(): Bytes return $this->macAddress; } - private function generateClockSequence(): Bytes + private function generateClockSequence(): ByteArray { - return new Bytes(\pack('n*', $this->random->getBytes(2))); + return ByteArray::fromRaw($this->random->getBytes(2)); } } diff --git a/src/Uuid/System/Time.php b/src/Uuid/System/Time.php index 2a16cb5..588fbd9 100644 --- a/src/Uuid/System/Time.php +++ b/src/Uuid/System/Time.php @@ -17,7 +17,6 @@ namespace Castor\Uuid\System; use Brick\DateTime\Instant; -use Brick\Math\BigInteger; interface Time { @@ -27,7 +26,9 @@ interface Time public function getInstant(): Instant; /** - * Returns the timestamp as an integer from this time. + * Returns the timestamp as a numeric integer from this time. + * + * @return numeric-string */ - public function getTimestamp(): BigInteger; + public function getTimestamp(): string; } diff --git a/src/Uuid/System/Time/Gregorian.php b/src/Uuid/System/Time/Gregorian.php index b15fc06..6afd252 100644 --- a/src/Uuid/System/Time/Gregorian.php +++ b/src/Uuid/System/Time/Gregorian.php @@ -18,10 +18,8 @@ use Brick\DateTime\Clock; use Brick\DateTime\Instant; -use Brick\Math\BigInteger; -use Brick\Math\RoundingMode; -use Castor\Bytes; -use Castor\Encoding\Error; +use Castor\Encoding\Failure; +use Castor\Uuid\ByteArray; use Castor\Uuid\System\Time; /** @@ -41,34 +39,38 @@ private const string SECOND_INTERVALS = '10000000'; public function __construct( - public Bytes $bytes, + public ByteArray $bytes, ) { - if ($this->bytes->len() !== 8) { + if ($this->bytes->count() !== 8) { throw new \InvalidArgumentException('Gregorian time must be 64 bits long'); } } - public static function fromTimestamp(BigInteger $timestamp): self + public static function fromTimestamp(string $timestamp): self { - $hex = \str_pad($timestamp->toBase(16), 16, '0', STR_PAD_LEFT); + if (!\is_numeric($timestamp)) { + throw new \InvalidArgumentException('Timestamp must be a valid numeric string'); + } + + $hex = \str_pad(\base_convert($timestamp, 10, 16), 16, '0', STR_PAD_LEFT); try { - return new self(Bytes::fromHex($hex)); - } catch (Error $e) { + return new self(ByteArray::fromHex($hex)); + } catch (Failure $e) { throw new \RuntimeException('Impossible error', previous: $e); } } public static function fromInstant(Instant $instant): self { - $epochSeconds = BigInteger::of($instant->getEpochSecond()); - $nanoSeconds = BigInteger::of($instant->getNano()); + $epochSeconds = (string) $instant->getEpochSecond(); + $nanoSeconds = (string) $instant->getNano(); - $secondsTicks = $epochSeconds->multipliedBy(self::SECOND_INTERVALS); - $nanoTicks = $nanoSeconds->dividedBy(100, RoundingMode::DOWN); - $ticksSinceEpoch = $secondsTicks->plus($nanoTicks); + $secondsTicks = \bcmul($epochSeconds, self::SECOND_INTERVALS); + $nanoTicks = \bcdiv($nanoSeconds, '100'); + $ticksSinceEpoch = \bcadd($secondsTicks, $nanoTicks); - return self::fromTimestamp($ticksSinceEpoch->plus(self::GREGORIAN_TO_UNIX_OFFSET)); + return self::fromTimestamp(\bcadd($ticksSinceEpoch, self::GREGORIAN_TO_UNIX_OFFSET, 0)); } public static function now(Clock $clock): self @@ -78,19 +80,19 @@ public static function now(Clock $clock): self public function getInstant(): Instant { - $ticksSinceEpoch = $this->getTimestamp()->minus(self::GREGORIAN_TO_UNIX_OFFSET); // Subtract gregorian offset + $ticksSinceEpoch = \bcsub($this->getTimestamp(), self::GREGORIAN_TO_UNIX_OFFSET); // Subtract gregorian offset - $epochSeconds = $ticksSinceEpoch->dividedBy(self::SECOND_INTERVALS, RoundingMode::DOWN); - $nanoSeconds = $ticksSinceEpoch->remainder(self::SECOND_INTERVALS)->multipliedBy(100); + $epochSeconds = \bcdiv($ticksSinceEpoch, self::SECOND_INTERVALS); + $nanoSeconds = \bcmul(\bcmod($ticksSinceEpoch, self::SECOND_INTERVALS), '100'); - return Instant::of($epochSeconds->toInt(), $nanoSeconds->toInt()); + return Instant::of((int) $epochSeconds, (int) $nanoSeconds); } /** * Returns the number of 100 nanosecond intervals since 1582-10-15 00:00:00 UTC as a numeric string. */ - public function getTimestamp(): BigInteger + public function getTimestamp(): string { - return BigInteger::fromBase($this->bytes->toHex(), 16); + return \base_convert($this->bytes->toHex(), 16, 10); } } diff --git a/src/Uuid/System/Time/Unix.php b/src/Uuid/System/Time/Unix.php index 003a3ad..be40887 100644 --- a/src/Uuid/System/Time/Unix.php +++ b/src/Uuid/System/Time/Unix.php @@ -18,37 +18,39 @@ use Brick\DateTime\Clock; use Brick\DateTime\Instant; -use Brick\Math\BigInteger; -use Brick\Math\RoundingMode; -use Castor\Bytes; -use Castor\Encoding\Error; +use Castor\Encoding\Failure; +use Castor\Uuid\ByteArray; use Castor\Uuid\System\Time; readonly class Unix implements Time { public function __construct( - public Bytes $bytes + public ByteArray $bytes ) { - if ($this->bytes->len() !== 6) { + if ($this->bytes->count() !== 6) { throw new \InvalidArgumentException('Unix time must be 48 bits long'); } } public static function fromInstant(Instant $instant): self { - $secondsInMilliseconds = BigInteger::of($instant->getEpochSecond())->multipliedBy(1000); - $nanosInMilliseconds = BigInteger::of($instant->getNano())->dividedBy(1e+6, RoundingMode::DOWN); + $secondsInMilliseconds = \bcmul((string) $instant->getEpochSecond(), '1000'); + $nanosInMilliseconds = \bcdiv((string) $instant->getNano(), (string) 1e+6); - return self::fromTimestamp($secondsInMilliseconds->plus($nanosInMilliseconds)); + return self::fromTimestamp(\bcadd($secondsInMilliseconds, $nanosInMilliseconds, 0)); } - public static function fromTimestamp(BigInteger $timestamp): self + public static function fromTimestamp(string $timestamp): self { - $hex = \str_pad($timestamp->toBase(16), 12, '0', STR_PAD_LEFT); + if (!\is_numeric($timestamp)) { + throw new \InvalidArgumentException('Timestamp must be a valid numeric string'); + } + + $hex = \str_pad(\base_convert($timestamp, 10, 16), 12, '0', STR_PAD_LEFT); try { - return new self(Bytes::fromHex($hex)); - } catch (Error $e) { + return new self(ByteArray::fromHex($hex)); + } catch (Failure $e) { throw new \RuntimeException('Impossible error', previous: $e); } } @@ -61,16 +63,16 @@ public static function now(Clock $clock): self public function getInstant(): Instant { $timestamp = $this->getTimestamp(); - $nanoSeconds = $timestamp->multipliedBy(1e+6); + $nanoSeconds = \bcmul($timestamp, (string) 1e+6); - return Instant::of(0, $nanoSeconds->toInt()); + return Instant::of(0, (int) $nanoSeconds); } /** * Returns the number of millisecond elapsed since 1970-01-01 00:00:00 UTC. */ - public function getTimestamp(): BigInteger + public function getTimestamp(): string { - return BigInteger::fromBase($this->bytes->toHex(), 16); + return \base_convert($this->bytes->toHex(), 16, 10); } } diff --git a/src/Uuid/Version1.php b/src/Uuid/Version1.php index c54fe1c..8e8a675 100644 --- a/src/Uuid/Version1.php +++ b/src/Uuid/Version1.php @@ -48,7 +48,7 @@ public static function parse(string $uuid, bool $lazy = true): self /** * Creates a UUID Version 1 from the raw bytes. */ - public static function fromBytes(Bytes|string $bytes): self + public static function fromBytes(ByteArray|string $bytes): self { $uuid = parent::fromBytes($bytes); if (!$uuid instanceof self) { @@ -61,21 +61,22 @@ public static function fromBytes(Bytes|string $bytes): self public static function generate(?State $state = null): self { $state = $state ?? Standard::global(); - $ts = $state->getTime()->bytes->asString(); - $node = $state->getNode()->asString(); - $seq = $state->getClockSequence()->asString(); - - $bytes = new Bytes( - $ts[4]. - $ts[5]. - $ts[6]. - $ts[7]. - $ts[2]. - $ts[3]. - $ts[0]. - $ts[1]. - $seq. - $node + $ts = $state->getTime()->bytes; + $node = $state->getNode(); + $seq = $state->getClockSequence(); + + $bytes = new ByteArray(self::LEN); + $bytes->allocate( + $ts[4], + $ts[5], + $ts[6], + $ts[7], + $ts[2], + $ts[3], + $ts[0], + $ts[1], + ...$seq, + ...$node ); // We set the 7th octet to 0001 XXXX (version 1) @@ -92,18 +93,18 @@ public static function generate(?State $state = null): self */ public function getTime(): Gregorian { - $bytes = $this->getBytes(); - $bytes[6] = $bytes[6] & 0x0F; // Unset the version bits - $b = $bytes->asString(); - $bytes = new Bytes($b[6].$b[7].$b[4].$b[5].$b[0].$b[1].$b[2].$b[3]); + $b = $this->getBytes(); + $b[6] &= 0x0F; // Unset the version bits + $b->setSize(8); + $b->allocate($b[6], $b[7], $b[4], $b[5], $b[0], $b[1], $b[2], $b[3]); - return new Gregorian($bytes); + return new Gregorian($b); } /** * Returns the node of this UUID. */ - public function getNode(): Bytes + public function getNode(): ByteArray { return $this->getBytes()->slice(10); } @@ -111,10 +112,10 @@ public function getNode(): Bytes /** * Returns the clock sequence of this UUID. */ - public function getClockSeq(): Bytes + public function getClockSeq(): ByteArray { $bytes = $this->getBytes(); - $bytes[8] = $bytes[8] & 0x3F; // Unset the variant bits + $bytes[8] &= 0x3F; // Unset the variant bits return $bytes->slice(8, 2); } diff --git a/src/Uuid/Version3.php b/src/Uuid/Version3.php index f6dbc34..3b472b6 100644 --- a/src/Uuid/Version3.php +++ b/src/Uuid/Version3.php @@ -48,7 +48,7 @@ public static function parse(string $uuid, bool $lazy = true): self /** * Creates a UUID Version 3 from the raw bytes. */ - public static function fromBytes(Bytes|string $bytes): self + public static function fromBytes(ByteArray|string $bytes): self { $uuid = parent::fromBytes($bytes); if (!$uuid instanceof self) { @@ -60,7 +60,7 @@ public static function fromBytes(Bytes|string $bytes): self public static function create(Uuid $namespace, string $name): self { - $bytes = new Bytes(@\hash(self::HASHING_ALGO, $namespace->getBytes()->asString().$name, true)); + $bytes = ByteArray::fromRaw(@\hash(self::HASHING_ALGO, $namespace->getBytes()->toRaw().$name, true)); // We set the 7th octet to 0011 XXXX (version 3) $bytes[self::VEB] = $bytes[self::VEB] & 0x0F | 0x30; // AND 0000 1111 OR 0011 0000 diff --git a/src/Uuid/Version4.php b/src/Uuid/Version4.php index 99abe54..8f3f72a 100644 --- a/src/Uuid/Version4.php +++ b/src/Uuid/Version4.php @@ -16,7 +16,6 @@ namespace Castor\Uuid; -use Castor\Bytes; use Castor\Uuid\System\Random; use Random\Randomizer; @@ -43,7 +42,7 @@ public static function parse(string $uuid, bool $lazy = true): self return $v4; } - public static function fromBytes(Bytes|string $bytes): self + public static function fromBytes(ByteArray|string $bytes): self { $uuid = parent::fromBytes($bytes); if (!$uuid instanceof self) { @@ -57,7 +56,7 @@ public static function generate(?Randomizer $randomizer = null): self { $randomizer = $randomizer ?? Random::global(); - $bytes = new Bytes($randomizer->getBytes(self::LEN)); + $bytes = ByteArray::fromRaw($randomizer->getBytes(self::LEN)); // We set the 7th octet to 0100 XXXX (version 4) $bytes[self::VEB] = $bytes[self::VEB] & 0x0F | 0x40; // AND 0000 1111 OR 0100 0000 diff --git a/src/Uuid/Version5.php b/src/Uuid/Version5.php index b72f695..befbfa3 100644 --- a/src/Uuid/Version5.php +++ b/src/Uuid/Version5.php @@ -43,7 +43,7 @@ public static function parse(string $uuid, bool $lazy = true): self return $v5; } - public static function fromBytes(Bytes|string $bytes): self + public static function fromBytes(ByteArray|string $bytes): self { $uuid = parent::fromBytes($bytes); if (!$uuid instanceof self) { @@ -55,8 +55,8 @@ public static function fromBytes(Bytes|string $bytes): self public static function create(Uuid $namespace, string $name): self { - $bytes = @\hash(self::HASHING_ALGO, $namespace->getBytes()->asString().$name, true); - $bytes = new Bytes(\substr($bytes, 0, self::LEN)); + $bytes = @\hash(self::HASHING_ALGO, $namespace->getBytes()->toRaw().$name, true); + $bytes = ByteArray::fromRaw(\substr($bytes, 0, self::LEN)); // We set the 7th octet to 0101 XXXX (version 5) $bytes[self::VEB] = $bytes[self::VEB] & 0x0F | 0x50; // // AND 0000 1111 OR 0101 0000 diff --git a/src/Uuid/Version6.php b/src/Uuid/Version6.php index ec42752..0cd459b 100644 --- a/src/Uuid/Version6.php +++ b/src/Uuid/Version6.php @@ -16,7 +16,6 @@ namespace Castor\Uuid; -use Castor\Bytes; use Castor\Uuid\System\State; use Castor\Uuid\System\State\Standard; use Castor\Uuid\System\Time\Gregorian; @@ -48,7 +47,7 @@ public static function parse(string $uuid, bool $lazy = true): self /** * Creates a UUID Version 6 from the raw bytes. */ - public static function fromBytes(Bytes|string $bytes): self + public static function fromBytes(ByteArray|string $bytes): self { $uuid = parent::fromBytes($bytes); if (!$uuid instanceof self) { @@ -67,10 +66,12 @@ public static function generate(?State $state = null): self { $state = $state ?? Standard::global(); $time = $state->getTime()->bytes; - $node = $state->getNode()->asString(); - $seq = $state->getClockSequence()->asString(); + $seq = $state->getClockSequence(); + $node = $state->getNode(); - $bytes = new Bytes($time->asString().$seq.$node); + // $bytes = new Bytes($time->asString().$seq.$node); + $bytes = new ByteArray(self::LEN); + $bytes->allocate(...$time, ...$seq, ...$node); // We need to shift 4 bits to constraint this to a 60 bit number, discarding the first 4 bits $bytes[0] = (($bytes[0] & 0x0F) << 4) | ($bytes[1] >> 4); @@ -113,7 +114,7 @@ public function getTime(): Gregorian /** * Returns the node of this UUID. */ - public function getNode(): Bytes + public function getNode(): ByteArray { return $this->getBytes()->slice(10); } @@ -121,10 +122,10 @@ public function getNode(): Bytes /** * Returns the clock sequence of this UUID. */ - public function getClockSeq(): Bytes + public function getClockSeq(): ByteArray { $bytes = $this->getBytes(); - $bytes[8] = $bytes[8] & 0x3F; // Unset the variant bits + $bytes[8] &= 0x3F; // Unset the variant bits return $bytes->slice(8, 2); } diff --git a/src/Uuid/Version7.php b/src/Uuid/Version7.php index d2d2beb..c33e75b 100644 --- a/src/Uuid/Version7.php +++ b/src/Uuid/Version7.php @@ -52,7 +52,7 @@ public static function parse(string $uuid, bool $lazy = true): self /** * Creates a UUID Version 7 from the raw bytes. */ - public static function fromBytes(Bytes|string $bytes): self + public static function fromBytes(ByteArray|string $bytes): self { $uuid = parent::fromBytes($bytes); if (!$uuid instanceof self) { @@ -70,9 +70,10 @@ public static function generate(?Unix $timestamp = null, ?Randomizer $randomizer $timestamp = $timestamp ?? Unix::fromInstant(Instant::now()); $randomizer = $randomizer ?? Random::global(); - $bytes = new Bytes( - $timestamp->bytes->asString(). - $randomizer->getBytes(10), + $bytes = new ByteArray(16); + $bytes->allocate( + ...$timestamp->bytes, + ...Bytes\unpack($randomizer->getBytes(10)), ); // We set the 7th octet to 0111 XXXX (version 7) diff --git a/tests/Benchmark/CastorVsRamseyBench.php b/tests/Benchmark/CastorVsRamseyBench.php new file mode 100644 index 0000000..718ce79 --- /dev/null +++ b/tests/Benchmark/CastorVsRamseyBench.php @@ -0,0 +1,234 @@ +toString(); + } + + #[Subject] + public function v1_generate_to_string_ramsey(): void + { + Ramsey::uuid1()->toString(); + } + + #[Subject] + public function v1_parse_to_bytes_castor(): void + { + Uuid\Version1::parse(self::V1_UUID)->getBytes()->toRaw(); + } + + #[Subject] + public function v1_parse_to_bytes_ramsey(): void + { + Ramsey::fromString(self::V1_UUID)->getBytes(); + } + + #[Subject] + public function v1_from_bytes_to_string_castor(): void + { + Uuid\Version1::fromBytes(self::V1_UUID_BIN)->toString(); + } + + #[Subject] + public function v1_from_bytes_to_string_ramsey(): void + { + Ramsey::fromBytes(self::V1_UUID_BIN)->toString(); + } + + #[Subject] + public function v4_generate_castor(): void + { + Uuid\Version4::generate(); + } + + #[Subject] + public function v4_generate_ramsey(): void + { + Ramsey::uuid4(); + } + + #[Subject] + public function v4_generate_to_string_castor(): void + { + Uuid\Version4::generate()->toString(); + } + + #[Subject] + public function v4_generate_to_string_ramsey(): void + { + Ramsey::uuid1()->toString(); + } + + #[Subject] + public function v4_parse_to_bytes_castor(): void + { + Uuid\Version4::parse(self::V4_UUID)->getBytes()->toRaw(); + } + + #[Subject] + public function v4_parse_to_bytes_ramsey(): void + { + Ramsey::fromString(self::V4_UUID)->getBytes(); + } + + #[Subject] + public function v4_from_bytes_to_string_castor(): void + { + Uuid\Version4::fromBytes(self::V4_UUID_BIN)->toString(); + } + + #[Subject] + public function v4_from_bytes_to_string_ramsey(): void + { + Ramsey::fromBytes(self::V4_UUID_BIN)->toString(); + } + + #[Subject] + public function v6_generate_castor(): void + { + Uuid\Version6::generate(); + } + + #[Subject] + public function v6_generate_ramsey(): void + { + Ramsey::uuid6(); + } + + #[Subject] + public function v6_generate_to_string_castor(): void + { + Uuid\Version4::generate()->toString(); + } + + #[Subject] + public function v6_generate_to_string_ramsey(): void + { + Ramsey::uuid1()->toString(); + } + + #[Subject] + public function v6_parse_to_bytes_castor(): void + { + Uuid\Version6::parse(self::V6_UUID)->getBytes()->toRaw(); + } + + #[Subject] + public function v6_parse_to_bytes_ramsey(): void + { + Ramsey::fromString(self::V6_UUID)->getBytes(); + } + + #[Subject] + public function v6_from_bytes_to_string_castor(): void + { + Uuid\Version6::fromBytes(self::V6_UUID_BIN)->toString(); + } + + #[Subject] + public function v6_from_bytes_to_string_ramsey(): void + { + Ramsey::fromBytes(self::V6_UUID_BIN)->toString(); + } + + #[Subject] + public function v7_generate_castor(): void + { + Uuid\Version7::generate(); + } + + #[Subject] + public function v7_generate_ramsey(): void + { + Ramsey::uuid7(); + } + + #[Subject] + public function v7_generate_to_string_castor(): void + { + Uuid\Version7::generate()->toString(); + } + + #[Subject] + public function v7_generate_to_string_ramsey(): void + { + Ramsey::uuid1()->toString(); + } + + #[Subject] + public function v7_parse_to_bytes_castor(): void + { + Uuid\Version7::parse(self::V7_UUID)->getBytes()->toRaw(); + } + + #[Subject] + public function v7_parse_to_bytes_ramsey(): void + { + Ramsey::fromString(self::V7_UUID)->getBytes(); + } + + #[Subject] + public function v7_from_bytes_to_string_castor(): void + { + Uuid\Version7::fromBytes(self::V7_UUID_BIN)->toString(); + } + + #[Subject] + public function v7_from_bytes_to_string_ramsey(): void + { + Ramsey::fromBytes(self::V7_UUID_BIN)->toString(); + } +} diff --git a/tests/Benchmark/UuidGenerationBench.php b/tests/Benchmark/UuidGenerationBench.php deleted file mode 100644 index 8a76880..0000000 --- a/tests/Benchmark/UuidGenerationBench.php +++ /dev/null @@ -1,72 +0,0 @@ - $unknown], JSON_THROW_ON_ERROR); - $this->assertSame('O:15:"Castor\Uuid\Any":1:{i:0;s:36:"99cf973d-3fe7-8ee4-88bd-a0991a048794";}', $serialized); + $hash = \md5($serialized); + + $this->assertSame( + 'e0d569ae9961295361a930432d035100', + $hash, + 'Does not match hash of serialized data: '.$serialized + ); $this->assertTrue($unknown->equals(\unserialize($serialized))); $this->assertSame('{"uuid":"99cf973d-3fe7-8ee4-88bd-a0991a048794"}', $json); $this->assertSame('99cf973d-3fe7-8ee4-88bd-a0991a048794', (string) $unknown); diff --git a/tests/Uuid/ByteArrayTest.php b/tests/Uuid/ByteArrayTest.php new file mode 100644 index 0000000..1634a80 --- /dev/null +++ b/tests/Uuid/ByteArrayTest.php @@ -0,0 +1,32 @@ +assertTrue($a->equals($b)); + } +} diff --git a/tests/Uuid/System/Time/GregorianTest.php b/tests/Uuid/System/Time/GregorianTest.php index 711b0d5..4b0c543 100644 --- a/tests/Uuid/System/Time/GregorianTest.php +++ b/tests/Uuid/System/Time/GregorianTest.php @@ -18,7 +18,6 @@ use Brick\DateTime\Clock\FixedClock; use Brick\DateTime\Instant; -use Brick\Math\BigInteger; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; @@ -34,7 +33,7 @@ public function it_creates_gregorian_time(): void #[Test] public function it_creates_from_timestamp(): void { - $instant = Gregorian::fromTimestamp(BigInteger::of('139127190010002012'))->getInstant(); + $instant = Gregorian::fromTimestamp('139127190010002012')->getInstant(); $this->assertSame(1693426201, $instant->getEpochSecond()); $this->assertSame(201200, $instant->getNano()); // Nano precision is lost because of 100 nano intervals } diff --git a/tests/Uuid/Version1Test.php b/tests/Uuid/Version1Test.php index da3ed69..63911d4 100644 --- a/tests/Uuid/Version1Test.php +++ b/tests/Uuid/Version1Test.php @@ -16,8 +16,6 @@ namespace Castor\Uuid; -use Brick\Math\BigInteger; -use Castor\Bytes; use Castor\Uuid\System\Time\Gregorian; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\TestWith; @@ -29,9 +27,9 @@ class Version1Test extends TestCase public function it_generates(): void { $state = new System\State\Fixed( - Gregorian::fromTimestamp(BigInteger::of('139127190012012330')), - Bytes::fromHex('0001'), - Bytes::fromHex('00b0d063c226') + Gregorian::fromTimestamp('139127190012012330'), + ByteArray::fromHex('0001'), + ByteArray::fromHex('00b0d063c226') ); $v1 = Version1::generate($state); @@ -48,9 +46,9 @@ public function it_generates(): void public function its_compatible_with_ramsey(): void { $state = new System\State\Fixed( - new Gregorian(Bytes::fromHex('01ee4782395c14c4')), - Bytes::fromHex('17ae'), - Bytes::fromHex('0242ac1b0004') + new Gregorian(ByteArray::fromHex('01ee4782395c14c4')), + ByteArray::fromHex('17ae'), + ByteArray::fromHex('0242ac1b0004') ); $v1 = Version1::generate($state); @@ -86,12 +84,18 @@ public function it_parses_with_error(string $in, bool $lazy, string $expectedErr #[Test] public function it_serializes(): void { - $v1 = Version1::parse('5102999c-4771-11ee-be56-0242ac120002'); + $v1 = Version1::parse('5102999c-4771-11ee-be56-0242ac120002', false); $serialized = \serialize($v1); $json = \json_encode(['uuid' => $v1], JSON_THROW_ON_ERROR); - $this->assertSame('O:20:"Castor\Uuid\Version1":1:{i:0;s:36:"5102999c-4771-11ee-be56-0242ac120002";}', $serialized); + $hash = \md5($serialized); + + $this->assertSame( + 'caba92f9403e49b428d6d7eec655bfbf', + $hash, + 'Does not match hash of serialized data: '.$serialized + ); $this->assertTrue($v1->equals(\unserialize($serialized))); $this->assertSame('{"uuid":"5102999c-4771-11ee-be56-0242ac120002"}', $json); $this->assertSame('5102999c-4771-11ee-be56-0242ac120002', (string) $v1); diff --git a/tests/Uuid/Version3Test.php b/tests/Uuid/Version3Test.php index 769a241..ec9dc41 100644 --- a/tests/Uuid/Version3Test.php +++ b/tests/Uuid/Version3Test.php @@ -47,12 +47,18 @@ public function it_parses_with_error(string $in, bool $lazy, string $expectedErr #[Test] public function it_serializes(): void { - $v3 = Version3::parse('a0f6aad0-cdf5-3ddc-a2ac-0bddb3249309'); + $v3 = Version3::parse('a0f6aad0-cdf5-3ddc-a2ac-0bddb3249309', false); $serialized = \serialize($v3); $json = \json_encode(['uuid' => $v3], JSON_THROW_ON_ERROR); - $this->assertSame('O:20:"Castor\Uuid\Version3":1:{i:0;s:36:"a0f6aad0-cdf5-3ddc-a2ac-0bddb3249309";}', $serialized); + $hash = \md5($serialized); + + $this->assertSame( + 'e7c111909fae531b94cd9a64ab8c6355', + $hash, + 'Does not match hash of serialized data: '.$serialized + ); $this->assertTrue($v3->equals(\unserialize($serialized))); $this->assertSame('{"uuid":"a0f6aad0-cdf5-3ddc-a2ac-0bddb3249309"}', $json); $this->assertSame('a0f6aad0-cdf5-3ddc-a2ac-0bddb3249309', (string) $v3); diff --git a/tests/Uuid/Version4Test.php b/tests/Uuid/Version4Test.php index 7eebafc..8486b88 100644 --- a/tests/Uuid/Version4Test.php +++ b/tests/Uuid/Version4Test.php @@ -68,12 +68,18 @@ public function it_generates_unique_v4(): void #[Test] public function it_serializes(): void { - $v4 = Version4::parse('fa06067f-602d-404a-a34c-45c6a7744011'); + $v4 = Version4::parse('fa06067f-602d-404a-a34c-45c6a7744011', false); $serialized = \serialize($v4); $json = \json_encode(['uuid' => $v4], JSON_THROW_ON_ERROR); - $this->assertSame('O:20:"Castor\Uuid\Version4":1:{i:0;s:36:"fa06067f-602d-404a-a34c-45c6a7744011";}', $serialized); + $hash = \md5($serialized); + + $this->assertSame( + '491e89b4ce01f6dbed1e5ea44cb565f2', + $hash, + 'Does not match hash of serialized data: '.$serialized + ); $this->assertTrue($v4->equals(\unserialize($serialized))); $this->assertSame('{"uuid":"fa06067f-602d-404a-a34c-45c6a7744011"}', $json); $this->assertSame('fa06067f-602d-404a-a34c-45c6a7744011', (string) $v4); diff --git a/tests/Uuid/Version5Test.php b/tests/Uuid/Version5Test.php index bdbd0cd..5f47e0d 100644 --- a/tests/Uuid/Version5Test.php +++ b/tests/Uuid/Version5Test.php @@ -50,12 +50,18 @@ public function it_parses_error(string $in, bool $lazy, string $expectedError): #[Test] public function it_serializes(): void { - $v5 = Version5::parse('5fe80e27-269a-5cce-98c3-989ddd181b71'); + $v5 = Version5::parse('5fe80e27-269a-5cce-98c3-989ddd181b71', false); $serialized = \serialize($v5); $json = \json_encode(['uuid' => $v5], JSON_THROW_ON_ERROR); - $this->assertSame('O:20:"Castor\Uuid\Version5":1:{i:0;s:36:"5fe80e27-269a-5cce-98c3-989ddd181b71";}', $serialized); + $hash = \md5($serialized); + + $this->assertSame( + '47c422afb014e163834dc042a101dc2c', + $hash, + 'Does not match hash of serialized data: '.$serialized + ); $this->assertTrue($v5->equals(\unserialize($serialized))); $this->assertSame('{"uuid":"5fe80e27-269a-5cce-98c3-989ddd181b71"}', $json); $this->assertSame('5fe80e27-269a-5cce-98c3-989ddd181b71', (string) $v5); diff --git a/tests/Uuid/Version6Test.php b/tests/Uuid/Version6Test.php index 7ff80dc..d2bd353 100644 --- a/tests/Uuid/Version6Test.php +++ b/tests/Uuid/Version6Test.php @@ -16,8 +16,6 @@ namespace Castor\Uuid; -use Brick\Math\BigInteger; -use Castor\Bytes; use Castor\Uuid\System\Time\Gregorian; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\TestWith; @@ -29,9 +27,9 @@ class Version6Test extends TestCase public function it_generates(): void { $state = new System\State\Fixed( - Gregorian::fromTimestamp(BigInteger::of('139127190012012330')), - Bytes::fromHex('0001'), - Bytes::fromHex('00b0d063c226') + Gregorian::fromTimestamp('139127190012012330'), + ByteArray::fromHex('0001'), + ByteArray::fromHex('00b0d063c226') ); $v6 = Version6::generate($state); @@ -72,12 +70,18 @@ public function it_parses_with_error(string $in, bool $lazy, string $expectedErr #[Test] public function it_serializes(): void { - $v6 = Version6::parse('1ee47713-343a-672a-8001-00b0d063c226'); + $v6 = Version6::parse('1ee47713-343a-672a-8001-00b0d063c226', false); $serialized = \serialize($v6); $json = \json_encode(['uuid' => $v6], JSON_THROW_ON_ERROR); - $this->assertSame('O:20:"Castor\Uuid\Version6":1:{i:0;s:36:"1ee47713-343a-672a-8001-00b0d063c226";}', $serialized); + $hash = \md5($serialized); + + $this->assertSame( + '077ffc6a8dc1d17a78309ce9d3c8473c', + $hash, + 'Does not match hash of serialized data: '.$serialized + ); $this->assertTrue($v6->equals(\unserialize($serialized))); $this->assertSame('{"uuid":"1ee47713-343a-672a-8001-00b0d063c226"}', $json); $this->assertSame('1ee47713-343a-672a-8001-00b0d063c226', (string) $v6); diff --git a/tests/Uuid/Version7Test.php b/tests/Uuid/Version7Test.php index 7e7ac95..b24d9be 100644 --- a/tests/Uuid/Version7Test.php +++ b/tests/Uuid/Version7Test.php @@ -67,12 +67,18 @@ public function it_parses_with_error(string $in, bool $lazy, string $expectedErr #[Test] public function it_serializes(): void { - $v7 = Version7::parse('0190bddb-5bb6-7896-9c8b-18dbd0ab4337'); + $v7 = Version7::parse('0190bddb-5bb6-7896-9c8b-18dbd0ab4337', false); $serialized = \serialize($v7); $json = \json_encode(['uuid' => $v7], JSON_THROW_ON_ERROR); - $this->assertSame('O:20:"Castor\Uuid\Version7":1:{i:0;s:36:"0190bddb-5bb6-7896-9c8b-18dbd0ab4337";}', $serialized); + $hash = \md5($serialized); + + $this->assertSame( + '7c63c60f49f177088cffdf53000b9b68', + $hash, + 'Does not match hash of serialized data: '.$serialized + ); $this->assertTrue($v7->equals(\unserialize($serialized))); $this->assertSame('{"uuid":"0190bddb-5bb6-7896-9c8b-18dbd0ab4337"}', $json); $this->assertSame('0190bddb-5bb6-7896-9c8b-18dbd0ab4337', (string) $v7);