From 79b84a428e72bc9e0e15b6a3b1e05bdcc0e040a4 Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Thu, 31 Oct 2024 18:32:03 +0100 Subject: [PATCH] Add an encoder and bump to PHP8.2 (#77) * Replace string with int normalization and update dependencies Refactor tests and implementation to use integers for normalization instead of strings, including Unsigned and Signed Integer Tests and associated data providers. Migrate dependencies in composer.json and update various configurations for PHP 8.2 syntax support and PHPUnit 11.0. --- .gitattributes | 2 +- .github/workflows/dependency-review.yml | 14 ++ .github/workflows/infection.yml | 34 +++ .github/workflows/integrate.yml | 48 +--- .github/workflows/lock-closed-issues.yml | 23 ++ .github/workflows/merge-me.yml | 28 --- .../workflows/release-on-milestone-closed.yml | 2 - .github/workflows/scorecards.yml | 62 +++++ .gitignore | 1 + Makefile | 50 ---- castor.php | 222 ++++++++++++++++++ composer.json | 14 +- deptrac.yaml | 6 +- ecs.php | 7 +- phpstan-baseline.neon | 95 +++++--- rector.php | 16 +- src/AbstractCBORObject.php | 5 +- src/ByteStringObject.php | 8 +- src/CBORObject.php | 6 +- src/Decoder.php | 53 +++-- src/Encoder.php | 117 +++++++++ src/EncoderInterface.php | 10 + src/IndefiniteLengthByteStringObject.php | 2 +- src/ListObject.php | 4 +- src/MapItem.php | 4 +- src/MapObject.php | 20 +- src/NegativeIntegerObject.php | 18 +- .../DoublePrecisionFloatObject.php | 15 ++ src/OtherObject/OtherObjectManager.php | 24 +- .../SinglePrecisionFloatObject.php | 15 ++ src/StringStream.php | 1 + src/Tag/BigFloatTag.php | 2 +- src/Tag/DecimalFractionTag.php | 2 +- src/Tag/TagManager.php | 22 +- src/Tag/TimestampTag.php | 2 +- src/TextStringObject.php | 6 +- src/UnsignedIntegerObject.php | 19 +- tests/DoublePrecisionFloat.php | 21 -- tests/DoublePrecisionFloatTest.php | 30 +++ tests/FloatTest.php | 2 +- tests/ListObjectTest.php | 8 +- tests/MapObjectTest.php | 16 +- tests/SignedIntegerTest.php | 24 +- tests/Tag/DatetimeTagTest.php | 2 +- tests/UnsignedIntegerTest.php | 32 +-- tests/VectorTest.php | 9 +- 46 files changed, 794 insertions(+), 329 deletions(-) create mode 100644 .github/workflows/dependency-review.yml create mode 100644 .github/workflows/infection.yml create mode 100644 .github/workflows/lock-closed-issues.yml delete mode 100644 .github/workflows/merge-me.yml create mode 100644 .github/workflows/scorecards.yml delete mode 100644 Makefile create mode 100644 castor.php create mode 100644 src/Encoder.php create mode 100644 src/EncoderInterface.php delete mode 100644 tests/DoublePrecisionFloat.php create mode 100644 tests/DoublePrecisionFloatTest.php diff --git a/.gitattributes b/.gitattributes index 06f47c0..d47f0d5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,8 +8,8 @@ /CODE_OF_CONDUCT.md export-ignore /deptrac.yaml export-ignore /ecs.php export-ignore +/castor.php export-ignore /infection.json.dist export-ignore -/Makefile export-ignore /phpstan.neon export-ignore /phpstan-baseline.neon export-ignore /phpunit.xml.dist export-ignore diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..b9d6d20 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,14 @@ +name: 'Dependency Review' +on: [pull_request] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: 'Checkout Repository' + uses: actions/checkout@v4 + - name: 'Dependency Review' + uses: actions/dependency-review-action@v4 diff --git a/.github/workflows/infection.yml b/.github/workflows/infection.yml new file mode 100644 index 0000000..97bdab5 --- /dev/null +++ b/.github/workflows/infection.yml @@ -0,0 +1,34 @@ +name: "Integrate" + +on: + push: + branches: + - "*.*.x" + +jobs: + mutation_testing: + name: "5️⃣ Mutation Testing" + runs-on: "ubuntu-latest" + steps: + - name: "Set up PHP" + uses: "shivammathur/setup-php@v2" + with: + php-version: "8.3" + extensions: "ctype, curl, dom, json, libxml, mbstring, openssl, phar, simplexml, sodium, tokenizer, xml, xmlwriter, zlib" + tools: "castor" + coverage: "xdebug" + + - name: "Checkout code" + uses: "actions/checkout@v4" + + - name: "Fetch Git base reference" + run: "git fetch --depth=1 origin ${GITHUB_BASE_REF}" + + - name: "Install dependencies" + uses: "ramsey/composer-install@v3" + with: + dependency-versions: "highest" + composer-options: "--optimize-autoloader" + + - name: "Execute Infection" + run: "castor infect" diff --git a/.github/workflows/integrate.yml b/.github/workflows/integrate.yml index 98731bc..e2bb67d 100644 --- a/.github/workflows/integrate.yml +++ b/.github/workflows/integrate.yml @@ -32,6 +32,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: php-version: "8.3" + tools: "castor" coverage: "none" - name: "Checkout code" @@ -43,7 +44,7 @@ jobs: dependency-versions: "highest" - name: "Check source code for syntax errors" - run: "composer exec -- parallel-lint src/ tests/" + run: "castor lint" unit_tests: name: "2️⃣ Unit and functional tests" @@ -55,7 +56,6 @@ jobs: operating-system: - "ubuntu-latest" php-version: - - "8.1" - "8.2" - "8.3" dependencies: @@ -67,6 +67,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: php-version: "${{ matrix.php-version }}" + tools: "castor" extensions: "ctype, dom, json, libxml, mbstring, openssl, phar, simplexml, tokenizer, xml, xmlwriter" coverage: "xdebug" @@ -80,7 +81,7 @@ jobs: composer-options: "--optimize-autoloader" - name: "Execute tests (PHP)" - run: "make ci-cc" + run: "castor test" # - name: Send coverage to Coveralls # if: "matrix.php-version == '8.1' && matrix.dependencies == 'highest'" @@ -101,6 +102,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: php-version: "8.3" + tools: "castor" extensions: "ctype, dom, json, libxml, mbstring, openssl, phar, simplexml, tokenizer, xml, xmlwriter" coverage: "none" @@ -120,7 +122,7 @@ jobs: run: "composer dump-autoload --optimize --strict-psr" - name: "Execute static analysis" - run: "make st" + run: "castor stan" coding_standards: name: "4️⃣ Coding Standards" @@ -133,6 +135,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: php-version: "8.3" + tools: "castor" extensions: "ctype, dom, json, libxml, mbstring, openssl, phar, simplexml, tokenizer, xml, xmlwriter" coverage: "none" @@ -146,40 +149,10 @@ jobs: composer-options: "--optimize-autoloader" - name: "Check coding style" - run: "make ci-cs" + run: "castor cs" - name: "Deptrac" - run: | - vendor/bin/deptrac analyse --fail-on-uncovered --no-cache - - mutation_testing: - name: "5️⃣ Mutation Testing" - needs: - - "byte_level" - - "syntax_errors" - runs-on: "ubuntu-latest" - steps: - - name: "Set up PHP" - uses: "shivammathur/setup-php@v2" - with: - php-version: "8.3" - extensions: "ctype, dom, json, libxml, mbstring, openssl, phar, simplexml, tokenizer, xml, xmlwriter" - coverage: "xdebug" - - - name: "Checkout code" - uses: "actions/checkout@v3" - - - name: "Fetch Git base reference" - run: "git fetch --depth=1 origin ${GITHUB_BASE_REF}" - - - name: "Install dependencies" - uses: "ramsey/composer-install@v2" - with: - dependency-versions: "highest" - composer-options: "--optimize-autoloader" - - - name: "Execute Infection" - run: "make ci-mu" + run: "castor deptrac" rector_checkstyle: name: "6️⃣ Rector Checkstyle" @@ -192,6 +165,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: php-version: "8.3" + tools: "castor" extensions: "ctype, dom, json, libxml, mbstring, openssl, phar, simplexml, tokenizer, xml, xmlwriter" coverage: "xdebug" @@ -208,7 +182,7 @@ jobs: composer-options: "--optimize-autoloader" - name: "Execute Rector" - run: "make rector" + run: "castor rector" exported_files: name: "7️⃣ Exported files" diff --git a/.github/workflows/lock-closed-issues.yml b/.github/workflows/lock-closed-issues.yml new file mode 100644 index 0000000..fedb91d --- /dev/null +++ b/.github/workflows/lock-closed-issues.yml @@ -0,0 +1,23 @@ +name: 'Lock Issues' + +on: + schedule: + - cron: '12 6 * * *' + +jobs: + lock: + runs-on: ubuntu-latest + steps: + - uses: dessant/lock-threads@v5 + with: + github-token: ${{ github.token }} + issue-inactive-days: '31' + exclude-issue-created-before: '' + exclude-any-issue-labels: '' + add-issue-labels: '' + issue-comment: > + This thread has been automatically locked since there has not been + any recent activity after it was closed. Please open a new issue for + related bugs. + issue-lock-reason: 'resolved' + process-only: 'issues' diff --git a/.github/workflows/merge-me.yml b/.github/workflows/merge-me.yml deleted file mode 100644 index 1796365..0000000 --- a/.github/workflows/merge-me.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Merge me! - -on: - check_suite: - types: - - completed - -jobs: - merge-me: - name: Merge me! - runs-on: ubuntu-latest - steps: - - name: Merge me! - uses: ridedott/merge-me-action@v2.10.35 - with: - # Depending on branch protection rules, a manually populated - # `GITHUB_TOKEN_WORKAROUND` environment variable with permissions to - # push to a protected branch must be used. This variable can have an - # arbitrary name, as an example, this repository uses - # `GITHUB_TOKEN_DOTTBOTT`. - # - # When using a custom token, it is recommended to leave the following - # comment for other developers to be aware of the reasoning behind it: - # - # This must be used as GitHub Actions token does not support - # pushing to protected branches. - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - MERGE_METHOD: MERGE diff --git a/.github/workflows/release-on-milestone-closed.yml b/.github/workflows/release-on-milestone-closed.yml index b9986a8..00f7347 100644 --- a/.github/workflows/release-on-milestone-closed.yml +++ b/.github/workflows/release-on-milestone-closed.yml @@ -1,5 +1,3 @@ -# https://help.github.com/en/categories/automating-your-workflow-with-github-actions - name: "Automatic Releases" on: diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml new file mode 100644 index 0000000..a6379ab --- /dev/null +++ b/.github/workflows/scorecards.yml @@ -0,0 +1,62 @@ +name: Scorecards supply-chain security + +on: + schedule: + - cron: '34 4 * * 6' + push: + branches: + - "*.*.x" + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecards analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Used to receive a badge. (Upcoming feature) + id-token: write + # Needs for private repositories. + contents: read + actions: read + + steps: + - name: "Checkout code" + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@v2.3.3 + with: + results_file: results.sarif + results_format: sarif + # (Optional) Read-only PAT token. Uncomment the `repo_token` line below if: + # - you want to enable the Branch-Protection check on a *public* repository, or + # - you are installing Scorecards on a *private* repository + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. + # repo_token: ${{ secrets.SCORECARD_READ_TOKEN }} + + # Publish the results for public repositories to enable scorecard badges. For more details, see + # https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories, `publish_results` will automatically be set to `false`, regardless + # of the value entered here. + publish_results: true + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@v4.3.3 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard. + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: results.sarif diff --git a/.gitignore b/.gitignore index 8da2f5c..ec1249c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ yarn-error.log /composer.lock /vendor infection.txt +/.castor.stub.php diff --git a/Makefile b/Makefile deleted file mode 100644 index aa9acad..0000000 --- a/Makefile +++ /dev/null @@ -1,50 +0,0 @@ -mu: vendor ## Mutation tests - vendor/bin/infection -s --threads=$$(nproc) --min-msi=3 --min-covered-msi=58 - -tests: vendor ## Run all tests - vendor/bin/phpunit --color - -cc: vendor ## Show test coverage rates (HTML) - vendor/bin/phpunit --coverage-html ./build - -cs: vendor ## Fix all files using defined ECS rules - vendor/bin/ecs check --fix - -tu: vendor ## Run only unit tests - vendor/bin/phpunit --color --group Unit - -ti: vendor ## Run only integration tests - vendor/bin/phpunit --color --group Integration - -tf: vendor ## Run only functional tests - vendor/bin/phpunit --color --group Functional - -st: vendor ## Run static analyse - vendor/bin/phpstan analyse - - -################################################ - -ci-mu: vendor ## Mutation tests (for Github only) - vendor/bin/infection --logger-github -s --threads=$$(nproc) --min-msi=3 --min-covered-msi=58 - -ci-cc: vendor ## Show test coverage rates (console) - vendor/bin/phpunit --coverage-text - -ci-cs: vendor ## Check all files using defined ECS rules - vendor/bin/ecs check - -################################################ - - -vendor: composer.json composer.lock - composer validate - composer install - -rector: vendor ## Check all files using Rector - vendor/bin/rector process --ansi --dry-run --xdebug - -.DEFAULT_GOAL := help -help: - @grep -E '(^[a-zA-Z_-]+:.*?##.*$$)|(^##)' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[32m%-30s\033[0m %s\n", $$1, $$2}' | sed -e 's/\[32m##/[33m/' -.PHONY: help diff --git a/castor.php b/castor.php new file mode 100644 index 0000000..a55a3fb --- /dev/null +++ b/castor.php @@ -0,0 +1,222 @@ +title('Running infection'); + $nproc = run('nproc', quiet: true); + if (! $nproc->isSuccessful()) { + io()->error('Cannot determine the number of processors'); + return; + } + $threads = (int) $nproc->getOutput(); + $command = [ + 'php', + 'vendor/bin/infection', + sprintf('--min-msi=%s', $minMsi), + sprintf('--min-covered-msi=%s', $minCoveredMsi), + sprintf('--threads=%s', $threads), + ]; + if ($ci) { + $command[] = '--logger-github'; + $command[] = '-s'; + } + $context = context(); + $context = $context->withEnvironment([ + 'XDEBUG_MODE' => 'coverage', + ]); + run($command, context: $context); +} + +#[AsTask(description: 'Run tests')] +function test(bool $coverageHtml = false, bool $coverageText = false, null|string $group = null): void +{ + io()->title('Running tests'); + $command = ['php', 'vendor/bin/phpunit', '--color']; + $context = context(); + $context = $context->withEnvironment([ + 'XDEBUG_MODE' => 'off', + ]); + if ($coverageHtml) { + $command[] = '--coverage-html=build/coverage'; + $context = $context->withEnvironment([ + 'XDEBUG_MODE' => 'coverage', + ]); + } + if ($coverageText) { + $command[] = '--coverage-text'; + $context = $context->withEnvironment([ + 'XDEBUG_MODE' => 'coverage', + ]); + } + if ($group !== null) { + $command[] = sprintf('--group=%s', $group); + } + run($command, context: $context); +} + +#[AsTask(description: 'Coding standards check')] +function cs( + #[AsOption(description: 'Fix issues if possible')] + bool $fix = false, + #[AsOption(description: 'Clear cache')] + bool $clearCache = false +): void { + io()->title('Running coding standards check'); + $command = ['php', 'vendor/bin/ecs', 'check']; + $context = context(); + $context = $context->withEnvironment([ + 'XDEBUG_MODE' => 'off', + ]); + if ($fix) { + $command[] = '--fix'; + } + if ($clearCache) { + $command[] = '--clear-cache'; + } + run($command, context: $context); +} + +#[AsTask(description: 'Running PHPStan')] +function stan(#[AsOption(description: 'Generate baseline')] bool $baseline = false): void +{ + io()->title('Running PHPStan'); + $command = ['php', 'vendor/bin/phpstan', 'analyse']; + if ($baseline) { + $command[] = '--generate-baseline'; + } + $context = context(); + $context = $context->withEnvironment([ + 'XDEBUG_MODE' => 'off', + ]); + run($command, context: $context); +} + +#[AsTask(description: 'Validate Composer configuration')] +function validate(): void +{ + io()->title('Validating Composer configuration'); + $command = ['composer', 'validate', '--strict']; + $context = context(); + $context = $context->withEnvironment([ + 'XDEBUG_MODE' => 'off', + ]); + run($command, context: $context); + + $command = ['composer', 'dump-autoload', '--optimize', '--strict-psr']; + run($command, context: $context); +} + +/** + * @param array $allowedLicenses + */ +#[AsTask(description: 'Check licenses')] +function checkLicenses( + array $allowedLicenses = ['Apache-2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'ISC', 'MIT', 'MPL-2.0', 'OSL-3.0'] +): void { + io()->title('Checking licenses'); + $allowedExceptions = []; + $command = ['composer', 'licenses', '-f', 'json']; + $context = context(); + $context = $context->withEnvironment([ + 'XDEBUG_MODE' => 'off', + ]); + $context = $context->withQuiet(); + $result = run($command, context: $context); + if (! $result->isSuccessful()) { + io()->error('Cannot determine licenses'); + exit(1); + } + $licenses = json_decode((string) $result->getOutput(), true); + $disallowed = array_filter( + $licenses['dependencies'], + static fn (array $info, $name) => ! in_array($name, $allowedExceptions, true) + && count(array_diff($info['license'], $allowedLicenses)) === 1, + \ARRAY_FILTER_USE_BOTH + ); + $allowed = array_filter( + $licenses['dependencies'], + static fn (array $info, $name) => in_array($name, $allowedExceptions, true) + || count(array_diff($info['license'], $allowedLicenses)) === 0, + \ARRAY_FILTER_USE_BOTH + ); + if (count($disallowed) > 0) { + io() + ->table( + ['Package', 'License'], + array_map( + static fn ($name, $info) => [$name, implode(', ', $info['license'])], + array_keys($disallowed), + $disallowed + ) + ); + io() + ->error('Disallowed licenses found'); + exit(1); + } + io() + ->table( + ['Package', 'License'], + array_map( + static fn ($name, $info) => [$name, implode(', ', $info['license'])], + array_keys($allowed), + $allowed + ) + ); + io() + ->success('All licenses are allowed'); +} + +#[AsTask(description: 'Run Rector')] +function rector( + #[AsOption(description: 'Fix issues if possible')] + bool $fix = false, + #[AsOption(description: 'Clear cache')] + bool $clearCache = false +): void { + io()->title('Running Rector'); + $command = ['php', 'vendor/bin/rector', 'process', '--ansi']; + if (! $fix) { + $command[] = '--dry-run'; + } + if ($clearCache) { + $command[] = '--clear-cache'; + } + $context = context(); + $context = $context->withEnvironment([ + 'XDEBUG_MODE' => 'off', + ]); + run($command, context: $context); +} + +#[AsTask(description: 'Run Rector')] +function deptrac(): void +{ + io()->title('Running Rector'); + $command = ['php', 'vendor/bin/deptrac', 'analyse', '--fail-on-uncovered', '--no-cache']; + $context = context(); + $context = $context->withEnvironment([ + 'XDEBUG_MODE' => 'off', + ]); + run($command, context: $context); +} + +#[AsTask(description: 'Run Linter')] +function lint(): void +{ + io()->title('Running Linter'); + $command = ['composer', 'exec', '--', 'parallel-lint', __DIR__ . '/src/', __DIR__ . '/tests/']; + $context = context(); + $context = $context->withEnvironment([ + 'XDEBUG_MODE' => 'off', + ]); + run($command, context: $context); +} diff --git a/composer.json b/composer.json index 13925d3..fbeac99 100644 --- a/composer.json +++ b/composer.json @@ -24,27 +24,27 @@ } }, "require": { - "php": ">=8.0", + "php": ">=8.2", "ext-mbstring": "*", "brick/math": "^0.9|^0.10|^0.11|^0.12" }, "require-dev": { "ext-json": "*", - "ekino/phpstan-banned-code": "^1.0", - "infection/infection": "^0.27", + "ekino/phpstan-banned-code": "^2.0", + "infection/infection": "^0.29", "phpstan/extension-installer": "^1.1", "phpstan/phpstan": "^1.0", "phpstan/phpstan-beberlei-assert": "^1.0", "phpstan/phpstan-deprecation-rules": "^1.0", "phpstan/phpstan-phpunit": "^1.0", "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^10.1", - "rector/rector": "^0.19", + "phpunit/phpunit": "^10.1|^11.0", + "rector/rector": "^1.0", "roave/security-advisories": "dev-latest", - "symfony/var-dumper": "^6.0|^7.0", + "symfony/var-dumper": "^6.4|^7.1", "symplify/easy-coding-standard": "^12.0", "php-parallel-lint/php-parallel-lint": "^1.3", - "qossmic/deptrac-shim": "^1.0" + "qossmic/deptrac": "^2.0" }, "config": { "sort-packages": true, diff --git a/deptrac.yaml b/deptrac.yaml index 7573742..45f331c 100644 --- a/deptrac.yaml +++ b/deptrac.yaml @@ -4,11 +4,11 @@ parameters: layers: - name: 'CBOR' collectors: - - type: 'className' - regex: '^CBO\\' + - type: 'classLike' + value: '^CBO\\' - name: 'Vendors' collectors: - - { type: className, regex: '^Brick\\' } + - { type: 'classLike', value: '^Brick\\' } ruleset: CBOR: - Vendors diff --git a/ecs.php b/ecs.php index 2d4767b..3c327bf 100644 --- a/ecs.php +++ b/ecs.php @@ -19,7 +19,6 @@ use PhpCsFixer\Fixer\PhpTag\LinebreakAfterOpeningTagFixer; use PhpCsFixer\Fixer\PhpUnit\PhpUnitTestAnnotationFixer; use PhpCsFixer\Fixer\PhpUnit\PhpUnitTestCaseStaticMethodCallsFixer; -use PhpCsFixer\Fixer\PhpUnit\PhpUnitTestClassRequiresCoversFixer; use PhpCsFixer\Fixer\ReturnNotation\SimplifiedNullReturnFixer; use PhpCsFixer\Fixer\Strict\DeclareStrictTypesFixer; use PhpCsFixer\Fixer\Strict\StrictComparisonFixer; @@ -31,6 +30,7 @@ $header = ''; return static function (ECSConfig $config) use ($header): void { + $header = ''; $config->import(SetList::PSR_12); $config->import(SetList::CLEAN_CODE); $config->import(SetList::DOCTRINE_ANNOTATIONS); @@ -85,8 +85,5 @@ ]); $config->parallel(); - $config->paths([__DIR__]); - $config->skip( - [__DIR__ . '/.github', __DIR__ . '/build', __DIR__ . '/vendor', PhpUnitTestClassRequiresCoversFixer::class] - ); + $config->paths([__DIR__ . '/src', __DIR__ . '/tests']); }; diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 8de4af4..2c44fb3 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,36 +1,61 @@ parameters: - ignoreErrors: - - - message: "#^Instanceof between CBOR\\\\MapItem and CBOR\\\\MapItem will always evaluate to true\\.$#" - count: 1 - path: src/MapObject.php - - - - message: "#^Parameter \\#1 \\$value of static method CBOR\\\\Utils\\:\\:binToBigInteger\\(\\) expects string, string\\|null given\\.$#" - count: 3 - path: src/OtherObject/DoublePrecisionFloatObject.php - - - - message: "#^Parameter \\#1 \\$value of static method CBOR\\\\Utils\\:\\:binToBigInteger\\(\\) expects string, string\\|null given\\.$#" - count: 3 - path: src/OtherObject/HalfPrecisionFloatObject.php - - - - message: "#^PHPDoc tag @var with type CBOR\\\\OtherObject is not subtype of native type string\\.$#" - count: 1 - path: src/OtherObject/OtherObjectManager.php - - - - message: "#^Parameter \\#1 \\$value of static method CBOR\\\\Utils\\:\\:binToBigInteger\\(\\) expects string, string\\|null given\\.$#" - count: 3 - path: src/OtherObject/SinglePrecisionFloatObject.php - - - - message: "#^PHPDoc tag @var with type CBOR\\\\Tag is not subtype of native type string\\.$#" - count: 1 - path: src/Tag/TagManager.php - - - - message: "#^Parameter \\#1 \\$value of static method CBOR\\\\Utils\\:\\:binToInt\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/Tag/TagManager.php + ignoreErrors: + - + message: "#^Method CBOR\\\\NegativeIntegerObject\\:\\:getValue\\(\\) should return int\\|numeric\\-string but returns int\\|string\\.$#" + count: 1 + path: src/NegativeIntegerObject.php + + - + message: "#^Cannot access offset 1 on array\\|false\\.$#" + count: 1 + path: src/OtherObject/DoublePrecisionFloatObject.php + + - + message: "#^Parameter \\#1 \\$value of static method CBOR\\\\Utils\\:\\:binToBigInteger\\(\\) expects string, string\\|null given\\.$#" + count: 3 + path: src/OtherObject/DoublePrecisionFloatObject.php + + - + message: "#^Parameter \\#2 \\$data of class CBOR\\\\OtherObject\\\\DoublePrecisionFloatObject constructor expects string\\|null, string\\|false given\\.$#" + count: 1 + path: src/OtherObject/DoublePrecisionFloatObject.php + + - + message: "#^Parameter \\#1 \\$value of static method CBOR\\\\Utils\\:\\:binToBigInteger\\(\\) expects string, string\\|null given\\.$#" + count: 3 + path: src/OtherObject/HalfPrecisionFloatObject.php + + - + message: "#^Cannot access offset 1 on array\\|false\\.$#" + count: 1 + path: src/OtherObject/SinglePrecisionFloatObject.php + + - + message: "#^Parameter \\#1 \\$value of static method CBOR\\\\Utils\\:\\:binToBigInteger\\(\\) expects string, string\\|null given\\.$#" + count: 3 + path: src/OtherObject/SinglePrecisionFloatObject.php + + - + message: "#^Parameter \\#2 \\$data of class CBOR\\\\OtherObject\\\\SinglePrecisionFloatObject constructor expects string\\|null, string\\|false given\\.$#" + count: 1 + path: src/OtherObject/SinglePrecisionFloatObject.php + + - + message: "#^Parameter \\#1 \\$num1 of function bcmul expects numeric\\-string, string given\\.$#" + count: 1 + path: src/Tag/BigFloatTag.php + + - + message: "#^Parameter \\#1 \\$num1 of function bcmul expects numeric\\-string, string given\\.$#" + count: 1 + path: src/Tag/DecimalFractionTag.php + + - + message: "#^Parameter \\#1 \\$value of static method CBOR\\\\Utils\\:\\:binToInt\\(\\) expects string, string\\|null given\\.$#" + count: 1 + path: src/Tag/TagManager.php + + - + message: "#^Method CBOR\\\\UnsignedIntegerObject\\:\\:getValue\\(\\) should return int\\|numeric\\-string but returns int\\|string\\.$#" + count: 1 + path: src/UnsignedIntegerObject.php diff --git a/rector.php b/rector.php index 0e6ac9a..1ad732b 100644 --- a/rector.php +++ b/rector.php @@ -4,7 +4,6 @@ use Rector\Config\RectorConfig; use Rector\PHPUnit\CodeQuality\Rector\Class_\PreferPHPUnitThisCallRector; -use Rector\PHPUnit\Set\PHPUnitLevelSetList; use Rector\PHPUnit\Set\PHPUnitSetList; use Rector\Set\ValueObject\LevelSetList; use Rector\Set\ValueObject\SetList; @@ -13,20 +12,27 @@ return static function (RectorConfig $config): void { $config->import(SetList::DEAD_CODE); - $config->import(LevelSetList::UP_TO_PHP_80); + $config->import(LevelSetList::UP_TO_PHP_82); + $config->import(SymfonySetList::SYMFONY_64); + $config->import(SymfonySetList::SYMFONY_50_TYPES); + $config->import(SymfonySetList::SYMFONY_52_VALIDATOR_ATTRIBUTES); $config->import(SymfonySetList::SYMFONY_CODE_QUALITY); - $config->import(PHPUnitLevelSetList::UP_TO_PHPUNIT_100); + $config->import(SymfonySetList::SYMFONY_CONSTRUCTOR_INJECTION); + $config->import(SymfonySetList::ANNOTATIONS_TO_ATTRIBUTES); $config->import(PHPUnitSetList::PHPUNIT_CODE_QUALITY); + $config->import(PHPUnitSetList::ANNOTATIONS_TO_ATTRIBUTES); + $config->import(PHPUnitSetList::PHPUNIT_110); $config->parallel(); $config->paths([__DIR__ . '/src', __DIR__ . '/tests']); $config->skip( [ - __DIR__ . '/src/IndefiniteLengthMapObject.php', __DIR__ . '/src/MapObject.php', PreferPHPUnitThisCallRector::class, ] ); - $config->phpVersion(PhpVersion::PHP_80); + $config->phpVersion(PhpVersion::PHP_82); + $config->parallel(); $config->importNames(); $config->importShortClasses(); + $config->removeUnusedImports(); }; diff --git a/src/AbstractCBORObject.php b/src/AbstractCBORObject.php index 0e0351d..e04cf3e 100644 --- a/src/AbstractCBORObject.php +++ b/src/AbstractCBORObject.php @@ -4,13 +4,12 @@ namespace CBOR; -use Stringable; use function chr; -abstract class AbstractCBORObject implements CBORObject, Stringable +abstract class AbstractCBORObject implements CBORObject { public function __construct( - private int $majorType, + private readonly int $majorType, protected int $additionalInformation ) { } diff --git a/src/ByteStringObject.php b/src/ByteStringObject.php index b7bb01f..1a91f2f 100644 --- a/src/ByteStringObject.php +++ b/src/ByteStringObject.php @@ -11,9 +11,9 @@ final class ByteStringObject extends AbstractCBORObject implements Normalizable { private const MAJOR_TYPE = self::MAJOR_TYPE_BYTE_STRING; - private string $value; + private readonly string $value; - private ?string $length = null; + private readonly ?string $length; public function __construct(string $data) { @@ -27,9 +27,7 @@ public function __construct(string $data) public function __toString(): string { $result = parent::__toString(); - if ($this->length !== null) { - $result .= $this->length; - } + $result .= $this->length ?? ''; return $result . $this->value; } diff --git a/src/CBORObject.php b/src/CBORObject.php index 2da9f8a..816b86a 100644 --- a/src/CBORObject.php +++ b/src/CBORObject.php @@ -4,7 +4,9 @@ namespace CBOR; -interface CBORObject +use Stringable; + +interface CBORObject extends Stringable { public const MAJOR_TYPE_UNSIGNED_INTEGER = 0b000; @@ -86,8 +88,6 @@ interface CBORObject public const TAG_CBOR = 55799; - public function __toString(): string; - public function getMajorType(): int; public function getAdditionalInformation(): int; diff --git a/src/Decoder.php b/src/Decoder.php index 67e98f7..0c8b556 100644 --- a/src/Decoder.php +++ b/src/Decoder.php @@ -35,9 +35,10 @@ use InvalidArgumentException; use RuntimeException; use function ord; +use function sprintf; use const STR_PAD_LEFT; -final class Decoder implements DecoderInterface +final readonly class Decoder implements DecoderInterface { private TagManagerInterface $tagObjectManager; @@ -150,7 +151,7 @@ private function processInfinite(Stream $stream, int $mt, bool $breakable): CBOR } return $object; - case CBORObject::MAJOR_TYPE_TEXT_STRING : //3 + case CBORObject::MAJOR_TYPE_TEXT_STRING: //3 $object = IndefiniteLengthTextStringObject::create(); while (! ($it = $this->process($stream, true)) instanceof BreakObject) { if (! $it instanceof TextStringObject) { @@ -162,7 +163,7 @@ private function processInfinite(Stream $stream, int $mt, bool $breakable): CBOR } return $object; - case CBORObject::MAJOR_TYPE_LIST : //4 + case CBORObject::MAJOR_TYPE_LIST: //4 $object = IndefiniteLengthListObject::create(); $it = $this->process($stream, true); while (! $it instanceof BreakObject) { @@ -171,23 +172,23 @@ private function processInfinite(Stream $stream, int $mt, bool $breakable): CBOR } return $object; - case CBORObject::MAJOR_TYPE_MAP : //5 + case CBORObject::MAJOR_TYPE_MAP: //5 $object = IndefiniteLengthMapObject::create(); while (! ($it = $this->process($stream, true)) instanceof BreakObject) { $object->add($it, $this->process($stream, false)); } return $object; - case CBORObject::MAJOR_TYPE_OTHER_TYPE : //7 + case CBORObject::MAJOR_TYPE_OTHER_TYPE: //7 if (! $breakable) { throw new InvalidArgumentException('Cannot parse the data. No enclosing indefinite.'); } return BreakObject::create(); - case CBORObject::MAJOR_TYPE_UNSIGNED_INTEGER : //0 - case CBORObject::MAJOR_TYPE_NEGATIVE_INTEGER : //1 - case CBORObject::MAJOR_TYPE_TAG : //6 - default : + case CBORObject::MAJOR_TYPE_UNSIGNED_INTEGER: //0 + case CBORObject::MAJOR_TYPE_NEGATIVE_INTEGER: //1 + case CBORObject::MAJOR_TYPE_TAG: //6 + default: throw new InvalidArgumentException(sprintf( 'Cannot parse the data. Found infinite length for Major Type "%s" (%d).', str_pad(decbin($mt), 5, '0', STR_PAD_LEFT), @@ -198,28 +199,28 @@ private function processInfinite(Stream $stream, int $mt, bool $breakable): CBOR private function generateTagManager(): TagManagerInterface { - return TagManager::create() - ->add(DatetimeTag::class) - ->add(TimestampTag::class) + return TagManager::create([ + DatetimeTag::class, + TimestampTag::class, - ->add(UnsignedBigIntegerTag::class) - ->add(NegativeBigIntegerTag::class) + UnsignedBigIntegerTag::class, + NegativeBigIntegerTag::class, - ->add(DecimalFractionTag::class) - ->add(BigFloatTag::class) + DecimalFractionTag::class, + BigFloatTag::class, - ->add(Base64UrlEncodingTag::class) - ->add(Base64EncodingTag::class) - ->add(Base16EncodingTag::class) - ->add(CBOREncodingTag::class) + Base64UrlEncodingTag::class, + Base64EncodingTag::class, + Base16EncodingTag::class, + CBOREncodingTag::class, - ->add(UriTag::class) - ->add(Base64UrlTag::class) - ->add(Base64Tag::class) - ->add(MimeTag::class) + UriTag::class, + Base64UrlTag::class, + Base64Tag::class, + MimeTag::class, - ->add(CBORTag::class) - ; + CBORTag::class, + ]); } private function generateOtherObjectManager(): OtherObjectManagerInterface diff --git a/src/Encoder.php b/src/Encoder.php new file mode 100644 index 0000000..d8ed60e --- /dev/null +++ b/src/Encoder.php @@ -0,0 +1,117 @@ +processData($data, $options); + } + + private function processData(mixed $data, int $option): CBORObject + { + return match (true) { + $data instanceof CBORObject => $data, + is_string($data) => preg_match('//u', $data) === 1 ? $this->processTextString( + $data, + $option + ) : $this->processByteString($data, $option), + is_array($data) => array_is_list($data) ? $this->processList($data, $option) : $this->processMap( + $data, + $option + ), + is_int($data) => $data < 0 ? NegativeIntegerObject::create($data) : UnsignedIntegerObject::create($data), + is_float($data) => $this->processFloat($data, $option), + $data === null => NullObject::create(), + $data === false => FalseObject::create(), + $data === true => TrueObject::create(), + default => throw new InvalidArgumentException('Unsupported data type'), + }; + } + + /** + * @param array $data + */ + private function processList(array $data, int $option): ListObject|IndefiniteLengthListObject + { + $isIndefinite = 0 !== ($option & self::INDEFINITE_LIST_LENGTH); + $list = $isIndefinite ? IndefiniteLengthListObject::create() : ListObject::create(); + foreach ($data as $item) { + $list->add($this->processData($item, $option)); + } + + return $list; + } + + /** + * @param array $data + */ + private function processMap(array $data, int $option): MapObject|IndefiniteLengthMapObject + { + $isIndefinite = 0 !== ($option & self::INDEFINITE_MAP_LENGTH); + $map = $isIndefinite ? IndefiniteLengthMapObject::create() : MapObject::create(); + foreach ($data as $key => $value) { + $map->add($this->processData($key, $option), $this->processData($value, $option)); + } + + return $map; + } + + private function processFloat(float $data, int $option): SinglePrecisionFloatObject|DoublePrecisionFloatObject + { + $isSinglePrecisionFloat = 0 !== ($option & self::FLOAT_FORMAT_SINGLE_PRECISION); + + return match (true) { + $isSinglePrecisionFloat => SinglePrecisionFloatObject::createFromFloat($data), + default => DoublePrecisionFloatObject::createFromFloat($data), + }; + } + + private function processTextString(string $data, int $option): TextStringObject|IndefiniteLengthTextStringObject + { + $isIndefinite = 0 !== ($option & self::INDEFINITE_TEXT_STRING_LENGTH); + $cbor = TextStringObject::create($data); + + if (! $isIndefinite) { + return $cbor; + } + + return IndefiniteLengthTextStringObject::create()->add($cbor); + } + + private function processByteString(string $data, int $option): ByteStringObject|IndefiniteLengthByteStringObject + { + $isIndefinite = 0 !== ($option & self::INDEFINITE_BYTE_STRING_LENGTH); + $cbor = ByteStringObject::create($data); + + if (! $isIndefinite) { + return $cbor; + } + + return IndefiniteLengthByteStringObject::create()->add($cbor); + } +} diff --git a/src/EncoderInterface.php b/src/EncoderInterface.php new file mode 100644 index 0000000..f9c18ee --- /dev/null +++ b/src/EncoderInterface.php @@ -0,0 +1,10 @@ +chunks as $chunk) { - $result .= $chunk->__toString(); + $result .= (string) $chunk; } return $result . "\xFF"; diff --git a/src/ListObject.php b/src/ListObject.php index 4f8da72..06f7f26 100644 --- a/src/ListObject.php +++ b/src/ListObject.php @@ -46,9 +46,7 @@ public function __construct(array $data = []) public function __toString(): string { $result = parent::__toString(); - if ($this->length !== null) { - $result .= $this->length; - } + $result .= $this->length ?? ''; foreach ($this->data as $object) { $result .= (string) $object; } diff --git a/src/MapItem.php b/src/MapItem.php index 7cb4a25..3fea08c 100644 --- a/src/MapItem.php +++ b/src/MapItem.php @@ -7,8 +7,8 @@ class MapItem { public function __construct( - private CBORObject $key, - private CBORObject $value + private readonly CBORObject $key, + private readonly CBORObject $value ) { } diff --git a/src/MapObject.php b/src/MapObject.php index 72a7431..a7c1139 100644 --- a/src/MapObject.php +++ b/src/MapObject.php @@ -26,7 +26,7 @@ final class MapObject extends AbstractCBORObject implements Countable, IteratorA */ private array $data; - private ?string $length = null; + private ?string $length; /** * @param MapItem[] $data @@ -34,12 +34,6 @@ final class MapObject extends AbstractCBORObject implements Countable, IteratorA public function __construct(array $data = []) { [$additionalInformation, $length] = LengthCalculator::getLengthOfArray($data); - array_map(static function ($item): void { - if (! $item instanceof MapItem) { - throw new InvalidArgumentException('The list must contain only MapItem objects.'); - } - }, $data); - parent::__construct(self::MAJOR_TYPE, $additionalInformation); $this->data = $data; $this->length = $length; @@ -48,16 +42,10 @@ public function __construct(array $data = []) public function __toString(): string { $result = parent::__toString(); - if ($this->length !== null) { - $result .= $this->length; - } + $result .= $this->length ?? ''; foreach ($this->data as $object) { - $result .= $object->getKey() - ->__toString() - ; - $result .= $object->getValue() - ->__toString() - ; + $result .= (string) $object->getKey(); + $result .= (string) $object->getValue(); } return $result; diff --git a/src/NegativeIntegerObject.php b/src/NegativeIntegerObject.php index 93c0ee7..f9ed16f 100644 --- a/src/NegativeIntegerObject.php +++ b/src/NegativeIntegerObject.php @@ -14,7 +14,7 @@ final class NegativeIntegerObject extends AbstractCBORObject implements Normaliz public function __construct( int $additionalInformation, - private ?string $data + private readonly ?string $data ) { parent::__construct(self::MAJOR_TYPE, $additionalInformation); } @@ -46,21 +46,29 @@ public static function createFromString(string $value): self return self::createBigInteger($integer); } - public function getValue(): string + /** + * @return numeric-string|int + */ + public function getValue(): string|int { if ($this->data === null) { - return (string) (-1 - $this->additionalInformation); + return -1 - $this->additionalInformation; } $result = Utils::binToBigInteger($this->data); $minusOne = BigInteger::of(-1); - return $minusOne->minus($result) + $valueAsString = $minusOne->minus($result) ->toBase(10) ; + $valueAsInt = (int) $valueAsString; + return (string) $valueAsInt === $valueAsString ? $valueAsInt : $valueAsString; } - public function normalize(): string + /** + * @return numeric-string|int + */ + public function normalize(): string|int { return $this->getValue(); } diff --git a/src/OtherObject/DoublePrecisionFloatObject.php b/src/OtherObject/DoublePrecisionFloatObject.php index db3a1d2..7debc1e 100644 --- a/src/OtherObject/DoublePrecisionFloatObject.php +++ b/src/OtherObject/DoublePrecisionFloatObject.php @@ -19,6 +19,21 @@ public static function supportedAdditionalInformation(): array return [self::OBJECT_DOUBLE_PRECISION_FLOAT]; } + public static function createFromFloat(float $number): self + { + $value = match (true) { + is_nan($number) => hex2bin('7FF8000000000000'), + is_infinite($number) && $number > 0 => hex2bin('7FF0000000000000'), + is_infinite($number) && $number < 0 => hex2bin('FFF0000000000000'), + default => (fn (): string => unpack('S', "\x01\x00")[1] === 1 ? strrev(pack('d', $number)) : pack( + 'd', + $number + ))(), + }; + + return new self(self::OBJECT_DOUBLE_PRECISION_FLOAT, $value); + } + public static function createFromLoadedData(int $additionalInformation, ?string $data): Base { return new self($additionalInformation, $data); diff --git a/src/OtherObject/OtherObjectManager.php b/src/OtherObject/OtherObjectManager.php index e691c3d..ababcbb 100644 --- a/src/OtherObject/OtherObjectManager.php +++ b/src/OtherObject/OtherObjectManager.php @@ -4,22 +4,30 @@ namespace CBOR\OtherObject; -use CBOR\OtherObject; use InvalidArgumentException; use function array_key_exists; -class OtherObjectManager implements OtherObjectManagerInterface +final class OtherObjectManager implements OtherObjectManagerInterface { /** - * @var string[] + * @param class-string[] $classes */ - private array $classes = []; + public function __construct( + private array $classes = [], + ) { + } - public static function create(): self + /** + * @param class-string[] $classes + */ + public static function create(array $classes = []): self { - return new self(); + return new self($classes); } + /** + * @param class-string $class + */ public function add(string $class): self { foreach ($class::supportedAdditionalInformation() as $ai) { @@ -32,6 +40,9 @@ public function add(string $class): self return $this; } + /** + * @return class-string + */ public function getClassForValue(int $value): string { return array_key_exists($value, $this->classes) ? $this->classes[$value] : GenericObject::class; @@ -39,7 +50,6 @@ public function getClassForValue(int $value): string public function createObjectForValue(int $value, ?string $data): OtherObjectInterface { - /** @var OtherObject $class */ $class = $this->getClassForValue($value); return $class::createFromLoadedData($value, $data); diff --git a/src/OtherObject/SinglePrecisionFloatObject.php b/src/OtherObject/SinglePrecisionFloatObject.php index d47cd30..a5a44fb 100644 --- a/src/OtherObject/SinglePrecisionFloatObject.php +++ b/src/OtherObject/SinglePrecisionFloatObject.php @@ -18,6 +18,21 @@ public static function supportedAdditionalInformation(): array return [self::OBJECT_SINGLE_PRECISION_FLOAT]; } + public static function createFromFloat(float $number): self + { + $value = match (true) { + is_nan($number) => hex2bin('7FC00000'), + is_infinite($number) && $number > 0 => hex2bin('7F800000'), + is_infinite($number) && $number < 0 => hex2bin('FF800000'), + default => (fn (): string => unpack('S', "\x01\x00")[1] === 1 ? strrev(pack('f', $number)) : pack( + 'f', + $number + ))(), + }; + + return new self(self::OBJECT_DOUBLE_PRECISION_FLOAT, $value); + } + public static function createFromLoadedData(int $additionalInformation, ?string $data): Base { return new self($additionalInformation, $data); diff --git a/src/StringStream.php b/src/StringStream.php index d522813..3dbf1da 100644 --- a/src/StringStream.php +++ b/src/StringStream.php @@ -6,6 +6,7 @@ use InvalidArgumentException; use RuntimeException; +use function sprintf; final class StringStream implements Stream { diff --git a/src/Tag/BigFloatTag.php b/src/Tag/BigFloatTag.php index 1de2cbe..088fba6 100644 --- a/src/Tag/BigFloatTag.php +++ b/src/Tag/BigFloatTag.php @@ -78,6 +78,6 @@ public function normalize() /** @var UnsignedIntegerObject|NegativeIntegerObject|NegativeBigIntegerTag|UnsignedBigIntegerTag $m */ $m = $object->get(1); - return rtrim(bcmul($m->normalize(), bcpow('2', $e->normalize(), 100), 100), '0'); + return rtrim(bcmul((string) $m->normalize(), bcpow('2', (string) $e->normalize(), 100), 100), '0'); } } diff --git a/src/Tag/DecimalFractionTag.php b/src/Tag/DecimalFractionTag.php index 9eafd25..27980e9 100644 --- a/src/Tag/DecimalFractionTag.php +++ b/src/Tag/DecimalFractionTag.php @@ -77,6 +77,6 @@ public function normalize() /** @var UnsignedIntegerObject|NegativeIntegerObject|NegativeBigIntegerTag|UnsignedBigIntegerTag $m */ $m = $object->get(1); - return rtrim(bcmul($m->normalize(), bcpow('10', $e->normalize(), 100), 100), '0'); + return rtrim(bcmul((string) $m->normalize(), bcpow('10', (string) $e->normalize(), 100), 100), '0'); } } diff --git a/src/Tag/TagManager.php b/src/Tag/TagManager.php index 0d3eb6d..40c0a8c 100644 --- a/src/Tag/TagManager.php +++ b/src/Tag/TagManager.php @@ -5,7 +5,6 @@ namespace CBOR\Tag; use CBOR\CBORObject; -use CBOR\Tag; use CBOR\Utils; use InvalidArgumentException; use function array_key_exists; @@ -13,15 +12,24 @@ final class TagManager implements TagManagerInterface { /** - * @var string[] + * @param class-string[] $classes */ - private array $classes = []; + public function __construct( + private array $classes = [] + ) { + } - public static function create(): self + /** + * @param array> $classes + */ + public static function create(array $classes = []): self { - return new self(); + return new self($classes); } + /** + * @param class-string $class + */ public function add(string $class): self { if ($class::getTagId() < 0) { @@ -32,6 +40,9 @@ public function add(string $class): self return $this; } + /** + * @return class-string + */ public function getClassForValue(int $value): string { return array_key_exists($value, $this->classes) ? $this->classes[$value] : GenericTag::class; @@ -44,7 +55,6 @@ public function createObjectForValue(int $additionalInformation, ?string $data, Utils::assertString($data, 'Invalid data'); $value = Utils::binToInt($data); } - /** @var Tag $class */ $class = $this->getClassForValue($value); return $class::createFromLoadedData($additionalInformation, $data, $object); diff --git a/src/Tag/TimestampTag.php b/src/Tag/TimestampTag.php index afdec9f..4723f5a 100644 --- a/src/Tag/TimestampTag.php +++ b/src/Tag/TimestampTag.php @@ -51,7 +51,7 @@ public function normalize(): DateTimeInterface switch (true) { case $object instanceof UnsignedIntegerObject: case $object instanceof NegativeIntegerObject: - $formatted = DateTimeImmutable::createFromFormat('U', $object->normalize()); + $formatted = DateTimeImmutable::createFromFormat('U', (string) $object->normalize()); break; case $object instanceof HalfPrecisionFloatObject: diff --git a/src/TextStringObject.php b/src/TextStringObject.php index a0ca87a..dfe77e5 100644 --- a/src/TextStringObject.php +++ b/src/TextStringObject.php @@ -13,7 +13,7 @@ final class TextStringObject extends AbstractCBORObject implements Normalizable private ?string $length = null; - private string $data; + private readonly string $data; public function __construct(string $data) { @@ -27,9 +27,7 @@ public function __construct(string $data) public function __toString(): string { $result = parent::__toString(); - if ($this->length !== null) { - $result .= $this->length; - } + $result .= $this->length ?? ''; return $result . $this->data; } diff --git a/src/UnsignedIntegerObject.php b/src/UnsignedIntegerObject.php index 3483334..95c4159 100644 --- a/src/UnsignedIntegerObject.php +++ b/src/UnsignedIntegerObject.php @@ -14,7 +14,7 @@ final class UnsignedIntegerObject extends AbstractCBORObject implements Normaliz public function __construct( int $additionalInformation, - private ?string $data + private readonly ?string $data ) { parent::__construct(self::MAJOR_TYPE, $additionalInformation); } @@ -58,18 +58,25 @@ public function getMajorType(): int return self::MAJOR_TYPE; } - public function getValue(): string + /** + * @return numeric-string|int + */ + public function getValue(): string|int { if ($this->data === null) { - return (string) $this->additionalInformation; + return $this->additionalInformation; } $integer = BigInteger::fromBase(bin2hex($this->data), 16); - - return $integer->toBase(10); + $valueAsString = $integer->toBase(10); + $valueAsInt = (int) $valueAsString; + return (string) $valueAsInt === $valueAsString ? $valueAsInt : $valueAsString; } - public function normalize(): string + /** + * @return numeric-string|int + */ + public function normalize(): string|int { return $this->getValue(); } diff --git a/tests/DoublePrecisionFloat.php b/tests/DoublePrecisionFloat.php deleted file mode 100644 index 3dcf68f..0000000 --- a/tests/DoublePrecisionFloat.php +++ /dev/null @@ -1,21 +0,0 @@ -normalize()); - } -} diff --git a/tests/DoublePrecisionFloatTest.php b/tests/DoublePrecisionFloatTest.php new file mode 100644 index 0000000..0234eb9 --- /dev/null +++ b/tests/DoublePrecisionFloatTest.php @@ -0,0 +1,30 @@ +normalize()); + static::assertSame(hex2bin('fb3fd5555555555555'), $obj->__toString()); + } + + #[Test] + public function aDoublePrecisionObjectCanBeCreatedFromFloat(): void + { + $obj = DoublePrecisionFloatObject::createFromFloat(1 / 3); + static::assertSame(1 / 3, $obj->normalize()); + static::assertSame(hex2bin('fb3fd5555555555555'), $obj->__toString()); + } +} diff --git a/tests/FloatTest.php b/tests/FloatTest.php index fba3096..94f1d4f 100644 --- a/tests/FloatTest.php +++ b/tests/FloatTest.php @@ -16,7 +16,7 @@ final class FloatTest extends CBORTestCase { #[DataProvider('getDataSet')] #[Test] - public function aFloatCanBeParsed(string $data): void + public function aFloatCanBeParsed(int|string $data): void { $stream = StringStream::create(hex2bin($data)); $object = $this->getDecoder() diff --git a/tests/ListObjectTest.php b/tests/ListObjectTest.php index 1bdb7b1..9dadd40 100644 --- a/tests/ListObjectTest.php +++ b/tests/ListObjectTest.php @@ -37,7 +37,7 @@ public function aListActsAsAnArray(): void static::assertCount(3, $object1); static::assertCount(3, $object2); - static::assertSame(['Hello', 'World', '3'], $object2->normalize()); + static::assertSame(['Hello', 'World', 3], $object2->normalize()); static::assertSame($object1->normalize(), $object2->normalize()); static::assertSame((string) $object1, (string) $object2); static::assertArrayHasKey(0, $object2); @@ -45,7 +45,7 @@ public function aListActsAsAnArray(): void static::assertArrayHasKey(2, $object2); static::assertSame($object2[0]->normalize(), 'Hello'); static::assertSame($object2[1]->normalize(), 'World'); - static::assertSame($object2[2]->normalize(), '3'); + static::assertSame($object2[2]->normalize(), 3); } #[Test] @@ -70,7 +70,7 @@ public function anIndefiniteLengthListActsAsAnArray(): void static::assertCount(3, $object1); static::assertCount(3, $object2); - static::assertSame(['Hello', 'World', '3'], $object2->normalize()); + static::assertSame(['Hello', 'World', 3], $object2->normalize()); static::assertSame($object1->normalize(), $object2->normalize()); static::assertSame((string) $object1, (string) $object2); static::assertArrayHasKey(0, $object2); @@ -78,6 +78,6 @@ public function anIndefiniteLengthListActsAsAnArray(): void static::assertArrayHasKey(2, $object2); static::assertSame($object2[0]->normalize(), 'Hello'); static::assertSame($object2[1]->normalize(), 'World'); - static::assertSame($object2[2]->normalize(), '3'); + static::assertSame($object2[2]->normalize(), 3); } } diff --git a/tests/MapObjectTest.php b/tests/MapObjectTest.php index 527b694..0407e69 100644 --- a/tests/MapObjectTest.php +++ b/tests/MapObjectTest.php @@ -43,8 +43,8 @@ public function aMapActsAsAnArray(): void static::assertSame([ 10 => 'Hello', -150 => 'World', - 'AZERTY' => '1', - 'Test' => '3', + 'AZERTY' => 1, + 'Test' => 3, ], $object2->normalize()); static::assertSame($object1->normalize(), $object2->normalize()); static::assertSame((string) $object1, (string) $object2); @@ -54,8 +54,8 @@ public function aMapActsAsAnArray(): void static::assertArrayHasKey('Test', $object2); static::assertSame($object2[10]->normalize(), 'Hello'); static::assertSame($object2[-150]->normalize(), 'World'); - static::assertSame($object2['AZERTY']->normalize(), '1'); - static::assertSame($object2['Test']->normalize(), '3'); + static::assertSame($object2['AZERTY']->normalize(), 1); + static::assertSame($object2['Test']->normalize(), 3); } #[Test] @@ -83,8 +83,8 @@ public function anIndefiniteLengthMapActsAsAnArray(): void static::assertSame([ 10 => 'Hello', -150 => 'World', - 'AZERTY' => '1', - 'Test' => '3', + 'AZERTY' => 1, + 'Test' => 3, ], $object2->normalize()); static::assertSame($object1->normalize(), $object2->normalize()); static::assertSame((string) $object1, (string) $object2); @@ -94,7 +94,7 @@ public function anIndefiniteLengthMapActsAsAnArray(): void static::assertArrayHasKey('Test', $object2); static::assertSame($object2[10]->normalize(), 'Hello'); static::assertSame($object2[-150]->normalize(), 'World'); - static::assertSame($object2['AZERTY']->normalize(), '1'); - static::assertSame($object2['Test']->normalize(), '3'); + static::assertSame($object2['AZERTY']->normalize(), 1); + static::assertSame($object2['Test']->normalize(), 3); } } diff --git a/tests/SignedIntegerTest.php b/tests/SignedIntegerTest.php index 6e2b57e..0d765b7 100644 --- a/tests/SignedIntegerTest.php +++ b/tests/SignedIntegerTest.php @@ -20,7 +20,7 @@ final class SignedIntegerTest extends CBORTestCase #[Test] public function createOnValidValue( int $intValue, - string $expectedIntValue, + int $expectedIntValue, int $expectedMajorType, int $expectedAdditionalInformation ): void { @@ -32,15 +32,15 @@ public function createOnValidValue( public static function getValidValue(): Iterator { - yield [-12_345_678, '-12345678', 1, 26]; - yield [-255, '-255', 1, 24]; - yield [-254, '-254', 1, 24]; - yield [-65535, '-65535', 1, 25]; - yield [-18, '-18', 1, 17]; + yield [-12_345_678, -12345678, 1, 26]; + yield [-255, -255, 1, 24]; + yield [-254, -254, 1, 24]; + yield [-65535, -65535, 1, 25]; + yield [-18, -18, 1, 17]; } #[Test] - public function ceateOnNegativeValue(): void + public function createOnNegativeValue(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('The value must be a negative integer.'); @@ -59,7 +59,7 @@ public function createOnOutOfRangeValue(): void #[DataProvider('getDataSet')] #[Test] - public function anUnsignedIntegerCanBeEncodedAndDecoded(string $data, string $expectedNormalizedData): void + public function anUnsignedIntegerCanBeEncodedAndDecoded(string $data, int|string $expectedNormalizedData): void { $stream = StringStream::create(hex2bin($data)); $object = $this->getDecoder() @@ -72,10 +72,10 @@ public function anUnsignedIntegerCanBeEncodedAndDecoded(string $data, string $ex public static function getDataSet(): Iterator { - yield ['20', '-1']; - yield ['29', '-10']; - yield ['3863', '-100']; - yield ['3903e7', '-1000']; + yield ['20', -1]; + yield ['29', -10]; + yield ['3863', -100]; + yield ['3903e7', -1000]; yield ['c349010000000000000000', '-18446744073709551617']; yield ['3bffffffffffffffff', '-18446744073709551616']; } diff --git a/tests/Tag/DatetimeTagTest.php b/tests/Tag/DatetimeTagTest.php index 42d18dc..567bae7 100644 --- a/tests/Tag/DatetimeTagTest.php +++ b/tests/Tag/DatetimeTagTest.php @@ -53,7 +53,7 @@ public function createValidTimestampTagWithUnsignedInteger(): void public function createValidTimestampTagWithNegativeInteger(): void { $tag = TimestampTag::create(NegativeIntegerObject::create(-10)); - static::assertSame('-10.000000', $tag->normalize()->format('U.u')); + static::assertEqualsWithDelta(-10.0, $tag->normalize()->format('U.u'), 0.00001); } #[Test] diff --git a/tests/UnsignedIntegerTest.php b/tests/UnsignedIntegerTest.php index 75b65e9..5933238 100644 --- a/tests/UnsignedIntegerTest.php +++ b/tests/UnsignedIntegerTest.php @@ -20,7 +20,7 @@ final class UnsignedIntegerTest extends CBORTestCase #[Test] public function createOnValidValue( int $intValue, - string $expectedIntValue, + int $expectedIntValue, int $expectedMajorType, int $expectedAdditionalInformation ): void { @@ -32,10 +32,10 @@ public function createOnValidValue( public static function getValidValue(): Iterator { - yield [12_345_678, '12345678', 0, 26]; - yield [255, '255', 0, 25]; - yield [254, '254', 0, 24]; - yield [18, '18', 0, 18]; + yield [12_345_678, 12345678, 0, 26]; + yield [255, 255, 0, 25]; + yield [254, 254, 0, 24]; + yield [18, 18, 0, 18]; } #[Test] @@ -58,7 +58,7 @@ public function createOnOutOfRangeValue(): void #[DataProvider('getDataSet')] #[Test] - public function anUnsignedIntegerCanBeParsed(string $data, string $expectedNormalizedData): void + public function anUnsignedIntegerCanBeParsed(string $data, int|string $expectedNormalizedData): void { $stream = StringStream::create(hex2bin($data)); $object = $this->getDecoder() @@ -70,16 +70,16 @@ public function anUnsignedIntegerCanBeParsed(string $data, string $expectedNorma public static function getDataSet(): Iterator { - yield ['00', '0']; - yield ['01', '1']; - yield ['0a', '10']; - yield ['17', '23']; - yield ['1818', '24']; - yield ['1819', '25']; - yield ['1864', '100']; - yield ['1903e8', '1000']; - yield ['1a000f4240', '1000000']; - yield ['1b000000e8d4a51000', '1000000000000']; + yield ['00', 0]; + yield ['01', 1]; + yield ['0a', 10]; + yield ['17', 23]; + yield ['1818', 24]; + yield ['1819', 25]; + yield ['1864', 100]; + yield ['1903e8', 1000]; + yield ['1a000f4240', 1000000]; + yield ['1b000000e8d4a51000', 1000000000000]; yield ['1bffffffffffffffff', '18446744073709551615']; yield ['c249010000000000000000', '18446744073709551616']; } diff --git a/tests/VectorTest.php b/tests/VectorTest.php index e633375..18ab715 100644 --- a/tests/VectorTest.php +++ b/tests/VectorTest.php @@ -14,8 +14,8 @@ */ final class VectorTest extends CBORTestCase { - #[DataProvider('getVectors')] #[Test] + #[DataProvider('getVectors')] public function createOnValidValue(string $cbor, string $hex): void { $stream = StringStream::create(base64_decode($cbor, true)); @@ -26,8 +26,11 @@ public function createOnValidValue(string $cbor, string $hex): void static::assertSame(hex2bin($hex), (string) $result); } - public static function getVectors(): array + public static function getVectors(): iterable { - return json_decode(file_get_contents(__DIR__ . '/vectors.json'), true, 512, JSON_THROW_ON_ERROR); + $data = json_decode(file_get_contents(__DIR__ . '/vectors.json'), true, 512, JSON_THROW_ON_ERROR); + foreach ($data as $datum) { + yield [$datum['cbor'], $datum['hex']]; + } } }