switch from mmccooks php-json-canonicalization library to root23s which, according to mike, better deals with floating point values

This commit is contained in:
Mario Vavti
2025-10-30 21:30:18 +01:00
parent d11b05de71
commit 3cf7e09609
23 changed files with 343 additions and 424 deletions

View File

@@ -601,10 +601,6 @@ return array(
'Michelf\\Markdown' => $vendorDir . '/michelf/php-markdown/Michelf/Markdown.php',
'Michelf\\MarkdownExtra' => $vendorDir . '/michelf/php-markdown/Michelf/MarkdownExtra.php',
'Michelf\\MarkdownInterface' => $vendorDir . '/michelf/php-markdown/Michelf/MarkdownInterface.php',
'Mmccook\\JsonCanonicalizator\\JsonCanonicalizator' => $vendorDir . '/mmccook/php-json-canonicalization-scheme/src/JsonCanonicalizator.php',
'Mmccook\\JsonCanonicalizator\\JsonCanonicalizatorFactory' => $vendorDir . '/mmccook/php-json-canonicalization-scheme/src/JsonCanonicalizatorFactory.php',
'Mmccook\\JsonCanonicalizator\\JsonCanonicalizatorInterface' => $vendorDir . '/mmccook/php-json-canonicalization-scheme/src/JsonCanonicalizatorInterface.php',
'Mmccook\\JsonCanonicalizator\\Utils' => $vendorDir . '/mmccook/php-json-canonicalization-scheme/src/Utils.php',
'OAuth2\\Autoloader' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/Autoloader.php',
'OAuth2\\ClientAssertionType\\ClientAssertionTypeInterface' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/ClientAssertionType/ClientAssertionTypeInterface.php',
'OAuth2\\ClientAssertionType\\HttpBasic' => $vendorDir . '/bshaffer/oauth2-server-php/src/OAuth2/ClientAssertionType/HttpBasic.php',
@@ -903,6 +899,10 @@ return array(
'Ramsey\\Uuid\\UuidInterface' => $vendorDir . '/ramsey/uuid/src/UuidInterface.php',
'Ramsey\\Uuid\\Validator\\GenericValidator' => $vendorDir . '/ramsey/uuid/src/Validator/GenericValidator.php',
'Ramsey\\Uuid\\Validator\\ValidatorInterface' => $vendorDir . '/ramsey/uuid/src/Validator/ValidatorInterface.php',
'Root23\\JsonCanonicalizer\\ArrayHelperTrait' => $vendorDir . '/root23/php-json-canonicalization/src/ArrayHelperTrait.php',
'Root23\\JsonCanonicalizer\\Converter' => $vendorDir . '/root23/php-json-canonicalization/src/Converter.php',
'Root23\\JsonCanonicalizer\\JsonCanonicalizer' => $vendorDir . '/root23/php-json-canonicalization/src/JsonCanonicalizer.php',
'Root23\\JsonCanonicalizer\\JsonCanonicalizerInterface' => $vendorDir . '/root23/php-json-canonicalization/src/JsonCanonicalizerInterface.php',
'Sabre\\CalDAV\\Backend\\AbstractBackend' => $vendorDir . '/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php',
'Sabre\\CalDAV\\Backend\\BackendInterface' => $vendorDir . '/sabre/dav/lib/CalDAV/Backend/BackendInterface.php',
'Sabre\\CalDAV\\Backend\\NotificationSupport' => $vendorDir . '/sabre/dav/lib/CalDAV/Backend/NotificationSupport.php',

View File

@@ -27,6 +27,7 @@ return array(
'Sabre\\HTTP\\' => array($vendorDir . '/sabre/http/lib'),
'Sabre\\Event\\' => array($vendorDir . '/sabre/event/lib'),
'Sabre\\' => array($vendorDir . '/sabre/dav/lib'),
'Root23\\JsonCanonicalizer\\' => array($vendorDir . '/root23/php-json-canonicalization/src'),
'Ramsey\\Uuid\\' => array($vendorDir . '/ramsey/uuid/src'),
'Ramsey\\Collection\\' => array($vendorDir . '/ramsey/collection/src'),
'Psr\\Log\\' => array($vendorDir . '/psr/log/src'),
@@ -36,7 +37,6 @@ return array(
'ParagonIE\\EasyECC\\' => array($vendorDir . '/paragonie/easy-ecc/src'),
'ParagonIE\\ConstantTime\\' => array($vendorDir . '/paragonie/constant_time_encoding/src'),
'OTPHP\\' => array($vendorDir . '/spomky-labs/otphp/src'),
'Mmccook\\JsonCanonicalizator\\' => array($vendorDir . '/mmccook/php-json-canonicalization-scheme/src'),
'Michelf\\' => array($vendorDir . '/michelf/php-markdown/Michelf'),
'Mdanter\\Ecc\\' => array($vendorDir . '/paragonie/ecc/src'),
'League\\Uri\\' => array($vendorDir . '/league/uri', $vendorDir . '/league/uri-interfaces'),

View File

@@ -65,6 +65,7 @@ class ComposerStaticInit7b34d7e50a62201ec5d5e526a5b8b35d
),
'R' =>
array (
'Root23\\JsonCanonicalizer\\' => 25,
'Ramsey\\Uuid\\' => 12,
'Ramsey\\Collection\\' => 18,
),
@@ -83,7 +84,6 @@ class ComposerStaticInit7b34d7e50a62201ec5d5e526a5b8b35d
),
'M' =>
array (
'Mmccook\\JsonCanonicalizator\\' => 28,
'Michelf\\' => 8,
'Mdanter\\Ecc\\' => 12,
),
@@ -211,6 +211,10 @@ class ComposerStaticInit7b34d7e50a62201ec5d5e526a5b8b35d
array (
0 => __DIR__ . '/..' . '/sabre/dav/lib',
),
'Root23\\JsonCanonicalizer\\' =>
array (
0 => __DIR__ . '/..' . '/root23/php-json-canonicalization/src',
),
'Ramsey\\Uuid\\' =>
array (
0 => __DIR__ . '/..' . '/ramsey/uuid/src',
@@ -248,10 +252,6 @@ class ComposerStaticInit7b34d7e50a62201ec5d5e526a5b8b35d
array (
0 => __DIR__ . '/..' . '/spomky-labs/otphp/src',
),
'Mmccook\\JsonCanonicalizator\\' =>
array (
0 => __DIR__ . '/..' . '/mmccook/php-json-canonicalization-scheme/src',
),
'Michelf\\' =>
array (
0 => __DIR__ . '/..' . '/michelf/php-markdown/Michelf',
@@ -945,10 +945,6 @@ class ComposerStaticInit7b34d7e50a62201ec5d5e526a5b8b35d
'Michelf\\Markdown' => __DIR__ . '/..' . '/michelf/php-markdown/Michelf/Markdown.php',
'Michelf\\MarkdownExtra' => __DIR__ . '/..' . '/michelf/php-markdown/Michelf/MarkdownExtra.php',
'Michelf\\MarkdownInterface' => __DIR__ . '/..' . '/michelf/php-markdown/Michelf/MarkdownInterface.php',
'Mmccook\\JsonCanonicalizator\\JsonCanonicalizator' => __DIR__ . '/..' . '/mmccook/php-json-canonicalization-scheme/src/JsonCanonicalizator.php',
'Mmccook\\JsonCanonicalizator\\JsonCanonicalizatorFactory' => __DIR__ . '/..' . '/mmccook/php-json-canonicalization-scheme/src/JsonCanonicalizatorFactory.php',
'Mmccook\\JsonCanonicalizator\\JsonCanonicalizatorInterface' => __DIR__ . '/..' . '/mmccook/php-json-canonicalization-scheme/src/JsonCanonicalizatorInterface.php',
'Mmccook\\JsonCanonicalizator\\Utils' => __DIR__ . '/..' . '/mmccook/php-json-canonicalization-scheme/src/Utils.php',
'OAuth2\\Autoloader' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/Autoloader.php',
'OAuth2\\ClientAssertionType\\ClientAssertionTypeInterface' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/ClientAssertionType/ClientAssertionTypeInterface.php',
'OAuth2\\ClientAssertionType\\HttpBasic' => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src/OAuth2/ClientAssertionType/HttpBasic.php',
@@ -1247,6 +1243,10 @@ class ComposerStaticInit7b34d7e50a62201ec5d5e526a5b8b35d
'Ramsey\\Uuid\\UuidInterface' => __DIR__ . '/..' . '/ramsey/uuid/src/UuidInterface.php',
'Ramsey\\Uuid\\Validator\\GenericValidator' => __DIR__ . '/..' . '/ramsey/uuid/src/Validator/GenericValidator.php',
'Ramsey\\Uuid\\Validator\\ValidatorInterface' => __DIR__ . '/..' . '/ramsey/uuid/src/Validator/ValidatorInterface.php',
'Root23\\JsonCanonicalizer\\ArrayHelperTrait' => __DIR__ . '/..' . '/root23/php-json-canonicalization/src/ArrayHelperTrait.php',
'Root23\\JsonCanonicalizer\\Converter' => __DIR__ . '/..' . '/root23/php-json-canonicalization/src/Converter.php',
'Root23\\JsonCanonicalizer\\JsonCanonicalizer' => __DIR__ . '/..' . '/root23/php-json-canonicalization/src/JsonCanonicalizer.php',
'Root23\\JsonCanonicalizer\\JsonCanonicalizerInterface' => __DIR__ . '/..' . '/root23/php-json-canonicalization/src/JsonCanonicalizerInterface.php',
'Sabre\\CalDAV\\Backend\\AbstractBackend' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php',
'Sabre\\CalDAV\\Backend\\BackendInterface' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Backend/BackendInterface.php',
'Sabre\\CalDAV\\Backend\\NotificationSupport' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Backend/NotificationSupport.php',

View File

@@ -1369,67 +1369,6 @@
},
"install-path": "../michelf/php-markdown"
},
{
"name": "mmccook/php-json-canonicalization-scheme",
"version": "1.0.0",
"version_normalized": "1.0.0.0",
"source": {
"type": "git",
"url": "https://github.com/mmccook/php-json-canonicalization-scheme.git",
"reference": "cd6d3e7645a2c1e62574a9a2437d68e9e74e799f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/mmccook/php-json-canonicalization-scheme/zipball/cd6d3e7645a2c1e62574a9a2437d68e9e74e799f",
"reference": "cd6d3e7645a2c1e62574a9a2437d68e9e74e799f",
"shasum": ""
},
"require": {
"php": "^8.1"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.13",
"pestphp/pest": "^1.20",
"phpstan/phpstan": "^1.10",
"spatie/ray": "^1.28"
},
"time": "2023-08-07T18:12:27+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Mmccook\\JsonCanonicalizator\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark M. McCook",
"email": "mark.mccook@gmail.com",
"role": "Developer"
}
],
"description": "This is my package php-json-canonicalization-scheme",
"homepage": "https://github.com/mmccook/php-json-canonicalization-scheme",
"keywords": [
"mmccook",
"php-json-canonicalization-scheme"
],
"support": {
"issues": "https://github.com/mmccook/php-json-canonicalization-scheme/issues",
"source": "https://github.com/mmccook/php-json-canonicalization-scheme/tree/1.0.0"
},
"funding": [
{
"url": "https://github.com/mmccook",
"type": "github"
}
],
"install-path": "../mmccook/php-json-canonicalization-scheme"
},
{
"name": "paragonie/constant_time_encoding",
"version": "v3.1.3",
@@ -2499,6 +2438,45 @@
},
"install-path": "../ramsey/uuid"
},
{
"name": "root23/php-json-canonicalization",
"version": "1.0.1",
"version_normalized": "1.0.1.0",
"source": {
"type": "git",
"url": "https://github.com/root23/php-json-canonicalization.git",
"reference": "be888e03a171c2b9667265d03924bd6bfc3fe85a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/root23/php-json-canonicalization/zipball/be888e03a171c2b9667265d03924bd6bfc3fe85a",
"reference": "be888e03a171c2b9667265d03924bd6bfc3fe85a",
"shasum": ""
},
"require": {
"php": ">=8.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^v3.27.0",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^9"
},
"time": "2023-09-27T08:27:25+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Root23\\JsonCanonicalizer\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"description": "Serialize data into canonical way, based on RFC-8785.",
"support": {
"issues": "https://github.com/root23/php-json-canonicalization/issues",
"source": "https://github.com/root23/php-json-canonicalization/tree/1.0.1"
},
"install-path": "../root23/php-json-canonicalization"
},
{
"name": "sabre/dav",
"version": "4.7.0",

View File

@@ -3,7 +3,7 @@
'name' => 'zotlabs/hubzilla',
'pretty_version' => 'dev-10.6RC',
'version' => 'dev-10.6RC',
'reference' => '7cf7aa397e26ebad0262b6ce5e4c46895de16d78',
'reference' => 'd11b05de71d84416aa085abd8570c038b1dc618a',
'type' => 'application',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -181,15 +181,6 @@
'aliases' => array(),
'dev_requirement' => false,
),
'mmccook/php-json-canonicalization-scheme' => array(
'pretty_version' => '1.0.0',
'version' => '1.0.0.0',
'reference' => 'cd6d3e7645a2c1e62574a9a2437d68e9e74e799f',
'type' => 'library',
'install_path' => __DIR__ . '/../mmccook/php-json-canonicalization-scheme',
'aliases' => array(),
'dev_requirement' => false,
),
'paragonie/constant_time_encoding' => array(
'pretty_version' => 'v3.1.3',
'version' => '3.1.3.0',
@@ -355,6 +346,15 @@
0 => '4.9.1',
),
),
'root23/php-json-canonicalization' => array(
'pretty_version' => '1.0.1',
'version' => '1.0.1.0',
'reference' => 'be888e03a171c2b9667265d03924bd6bfc3fe85a',
'type' => 'library',
'install_path' => __DIR__ . '/../root23/php-json-canonicalization',
'aliases' => array(),
'dev_requirement' => false,
),
'sabre/dav' => array(
'pretty_version' => '4.7.0',
'version' => '4.7.0.0',
@@ -544,7 +544,7 @@
'zotlabs/hubzilla' => array(
'pretty_version' => 'dev-10.6RC',
'version' => 'dev-10.6RC',
'reference' => '7cf7aa397e26ebad0262b6ce5e4c46895de16d78',
'reference' => 'd11b05de71d84416aa085abd8570c038b1dc618a',
'type' => 'application',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),

View File

@@ -1,4 +0,0 @@
# Changelog
All notable changes to `php-json-canonicalization-scheme` will be documented in this file.

View File

@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) mmccook <mark.mccook@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -1,43 +0,0 @@
# JSON Canonicalization for PHP 8.1+
[![Latest Version on Packagist](https://img.shields.io/packagist/v/mmccook/php-json-canonicalization-scheme.svg?style=flat-square)](https://packagist.org/packages/mmccook/php-json-canonicalization-scheme)
[![Tests](https://img.shields.io/github/actions/workflow/status/mmccook/php-json-canonicalization-scheme/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/mmccook/php-json-canonicalization-scheme/actions/workflows/run-tests.yml)
[![Total Downloads](https://img.shields.io/packagist/dt/mmccook/php-json-canonicalization-scheme.svg?style=flat-square)](https://packagist.org/packages/mmccook/php-json-canonicalization-scheme)
Needed a way to canonicalize JSON to validate webhooks from [The Campaign Registry](https://csp-api.campaignregistry.com/v2/restAPI)
couldn't find an actively maintained package that all passed the JCS tests, so I used the one listed on the JCS Github and updated/refactored it.
## Installation
You can install the package via composer:
```bash
composer require mmccook/php-json-canonicalization-scheme
```
## Usage
```php
$canonicalization = JsonCanonicalizatorFactory::getInstance();
$canonicalizedJsonString = $canonicalization->canonicalize($input);
```
## Testing
```bash
composer test
```
## Changelog
Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.
## Contributing
Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details.
## Credits
- [Mark M. McCook](https://github.com/mmccook)
## License
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

View File

@@ -1,50 +0,0 @@
{
"name": "mmccook/php-json-canonicalization-scheme",
"description": "This is my package php-json-canonicalization-scheme",
"keywords": [
"mmccook",
"php-json-canonicalization-scheme"
],
"homepage": "https://github.com/mmccook/php-json-canonicalization-scheme",
"license": "MIT",
"authors": [
{
"name": "Mark M. McCook",
"email": "mark.mccook@gmail.com",
"role": "Developer"
}
],
"require": {
"php": "^8.1"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.13",
"pestphp/pest": "^1.20",
"phpstan/phpstan": "^1.10",
"spatie/ray": "^1.28"
},
"autoload": {
"psr-4": {
"Mmccook\\JsonCanonicalizator\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Mmccook\\JsonCanonicalizator\\Tests\\": "tests"
}
},
"scripts": {
"test": "vendor/bin/pest",
"test-coverage": "vendor/bin/pest --coverage",
"format": "vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php --allow-risky=yes"
},
"config": {
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true,
"phpstan/extension-installer": true
}
},
"minimum-stability": "stable",
"prefer-stable": true
}

View File

@@ -1,79 +0,0 @@
<?php
namespace Mmccook\JsonCanonicalizator;
class JsonCanonicalizator implements JsonCanonicalizatorInterface
{
public const JSON_FLAGS = \JSON_UNESCAPED_UNICODE | \JSON_UNESCAPED_SLASHES;
/**
* @param $data
* @param bool $asHex
* @return string
*/
public function canonicalize($data, bool $asHex = false): string
{
ob_start();
$this->serialize($data);
$result = ob_get_clean();
return $asHex ? Utils::asHex($result) : $result;
}
private function serialize($item)
{
if (is_float($item)) {
echo Utils::es6NumberFormat($item);
return;
}
if (null === $item || is_scalar($item)) {
echo json_encode($item, self::JSON_FLAGS);
return;
}
if (is_array($item) && ! Utils::isAssoc($item)) {
echo '[';
$next = false;
foreach ($item as $element) {
if ($next) {
echo ',';
}
$next = true;
$this->serialize($element);
}
echo ']';
return;
}
if (is_object($item)) {
$item = (array)$item;
}
uksort($item, function (string $a, string $b) {
$a = mb_convert_encoding($a, 'UTF-16BE');
$b = mb_convert_encoding($b, 'UTF-16BE');
return strcmp($a, $b);
});
echo '{';
$next = false;
foreach ($item as $key => $value) {
//var_dump($key, $value);
if ($next) {
echo ',';
}
$next = true;
$outKey = json_encode((string)$key, self::JSON_FLAGS);
echo $outKey, ':', $this->serialize($value);
}
echo '}';
}
}

View File

@@ -1,13 +0,0 @@
<?php
declare(strict_types=1);
namespace Mmccook\JsonCanonicalizator;
class JsonCanonicalizatorFactory
{
public static function getInstance(): JsonCanonicalizator
{
return new JsonCanonicalizator();
}
}

View File

@@ -1,10 +0,0 @@
<?php
declare(strict_types=1);
namespace Mmccook\JsonCanonicalizator;
interface JsonCanonicalizatorInterface
{
public function canonicalize($data, bool $asHex): string;
}

View File

@@ -1,54 +0,0 @@
<?php
namespace Mmccook\JsonCanonicalizator;
class Utils
{
/**
* @param array $array
* @return bool
*/
public static function isAssoc(array $array): bool
{
$keys = array_keys($array);
return array_keys($keys) !== $keys;
}
public static function asHex(string $data): string
{
return rtrim(chunk_split(bin2hex($data), 2, ' '));
}
public static function es6NumberFormat(float $number): string
{
if (is_nan($number) || is_infinite($number)) {
throw new \RuntimeException("can't use Nan or Infinity in json");
}
if (0.0 === $number) {
return '0';
}
$sign = '';
if ($number < 0) {
$sign = '-';
$number = -$number;
}
if ($number < 1e+21 && $number >= 1e-6) {
$formatted = sprintf('%F', $number);
$formatted = rtrim($formatted, '0'); // first remove all zeros at the end
$formatted = rtrim($formatted, '.'); // If the string now ends with a decimal point, then remove it, too.
} else {
$formatted = sprintf('%e', $number);
$parts = explode('e', $formatted);
$parts[0] = rtrim($parts[0], '0');
$parts[0] = rtrim($parts[0], '.');
$formatted = implode('e', $parts);
}
return $sign . $formatted;
}
}

View File

@@ -0,0 +1,12 @@
.idea
.php_cs
.php_cs.cache
.phpunit.result.cache
build
composer.lock
coverage
docs
phpunit.xml
psalm.xml
vendor
.php-cs-fixer.cache

View File

@@ -0,0 +1,41 @@
# php-json-canonicalistaion
Serialize data into canonical way, based on RFC-8785.
RFC-8785 - https://tools.ietf.org/html/rfc8785
Inspired by https://github.com/aywan/php-json-canonicalization & https://github.com/cyberphone
## Installation
```code
composer require root23/php-json-canonicalizator
```
## Usage
```code
$canonicalizator = JsonCanonicalizatorFactory::getInstance();
$canonicalizedJsonString = $canonicalizator->canonicalize($input, false);
$canonicalizedJsonString = $canonicalizator->canonicalize($input, true); // hex
```
## Example input:
```code
{
"numbers": [333333333.33333329, 1E30, 4.50, 2e-3, 0.000000000000000000000000001],
"string": "\u20ac$\u000F\u000aA'\u0042\u0022\u005c\\\"\/",
"literals": [null, true, false]
}
```
## Output:
```code
{"literals":[null,true,false],"numbers":[333333333.3333333,1e+30,4.5,0.002,1e-27],"string":"€$\u000f\nA'B\"\\\\\"/"}
```
## Hexademical Output:
```code
7b 22 6c 69 74 65 72 61 6c 73 22 3a 5b 6e 75 6c 6c 2c 74 72 75 65 2c 66 61 6c 73 65 5d 2c 22 6e
75 6d 62 65 72 73 22 3a 5b 33 33 33 33 33 33 33 33 33 2e 33 33 33 33 33 33 33 2c 31 65 2b 33 30
2c 34 2e 35 2c 30 2e 30 30 32 2c 31 65 2d 32 37 5d 2c 22 73 74 72 69 6e 67 22 3a 22 e2 82 ac 24
5c 75 30 30 30 66 5c 6e 41 27 42 5c 22 5c 5c 5c 5c 5c 22 2f 22 7d
```

View File

@@ -0,0 +1,32 @@
{
"name": "root23/php-json-canonicalization",
"description": "Serialize data into canonical way, based on RFC-8785.",
"require": {
"php": ">=8.0"
},
"require-dev": {
"phpunit/phpunit": "^9",
"phpstan/phpstan": "^1.10",
"friendsofphp/php-cs-fixer": "^v3.27.0"
},
"config": {
"allow-plugins": {
"pestphp/pest-plugin": true
}
},
"autoload": {
"psr-4": {
"Root23\\JsonCanonicalizer\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Root23\\JsonCanonicalizer\\Tests\\": "tests"
}
},
"scripts": {
"test": "vendor/bin/phpunit tests"
},
"minimum-stability": "stable",
"prefer-stable": true
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Root23\JsonCanonicalizer;
trait ArrayHelperTrait
{
public static function isArrayAssoc(array $array): bool
{
return [] !== $array && !array_is_list($array);
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Root23\JsonCanonicalizer;
class Converter
{
public static function toHex(string $data): string
{
return rtrim(chunk_split(bin2hex($data), 2, ' '));
}
public static function toEs6NumberFormat(float $number): string
{
if (is_nan($number) || is_infinite($number)) {
throw new \LogicException("Infinity or NaN can't use in json");
}
if ($number === 0.0) {
return '0';
}
$sign = '';
if ($number < 0) {
$sign = '-';
$number = -$number;
}
if ($number < 1e+21 && $number >= 1e-6) {
$formatted = number_format($number, 7, '.', '');
$formatted = rtrim($formatted, '.0');
} else {
$formatted = sprintf('%e', $number);
$parts = explode('e', $formatted);
$parts[0] = rtrim($parts[0], '.0');
$formatted = implode('e', $parts);
}
return $sign . $formatted;
}
}

View File

@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace Root23\JsonCanonicalizer;
final class JsonCanonicalizer implements JsonCanonicalizerInterface
{
use ArrayHelperTrait;
public const JSON_FLAGS = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
public const ENCODING = 'UTF-16BE';
/**
* @throws \JsonException
*/
public function canonicalize($data): string
{
ob_start();
$this->encode($data);
return ob_get_clean();
}
/**
* @throws \JsonException
*/
public function canonicalizeAsHex($data): string
{
ob_start();
$this->encode($data);
return Converter::toHex(ob_get_clean());
}
/**
* @throws \JsonException
*/
private function encode($item): void
{
if (is_float($item)) {
echo Converter::toEs6NumberFormat($item);
return;
}
if (is_null($item) || is_scalar($item)) {
echo json_encode($item, JSON_THROW_ON_ERROR | self::JSON_FLAGS);
return;
}
if (is_array($item) && !self::isArrayAssoc($item)) {
echo '[';
$next = false;
foreach ($item as $element) {
if ($next) {
echo ',';
}
$next = true;
$this->encode($element);
}
echo ']';
return;
}
if (is_object($item)) {
$item = (array)$item;
}
uksort($item, static function (string $a, string $b) {
$a = mb_convert_encoding($a, self::ENCODING);
$b = mb_convert_encoding($b, self::ENCODING);
return strcmp($a, $b);
});
echo '{';
$next = false;
foreach ($item as $key => $value) {
if ($next) {
echo ',';
}
$next = true;
$outKey = json_encode((string)$key, JSON_THROW_ON_ERROR | self::JSON_FLAGS);
echo $outKey, ':', $this->encode($value);
}
echo '}';
}
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Root23\JsonCanonicalizer;
interface JsonCanonicalizerInterface
{
public function canonicalize($data): string;
public function canonicalizeAsHex($data): string;
}