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']]; + } } }