diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index c972722..6c6d9ba 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -7,4 +7,6 @@ on: jobs: check: uses: thesis-php/.github/.github/workflows/check.yml@main + with: + php_versions: '["8.4"]' secrets: inherit diff --git a/README.md b/README.md index 9c168f7..02240a3 100644 --- a/README.md +++ b/README.md @@ -46,10 +46,13 @@ echo Endian\Order::little->unpackFloat( ```php use Thesis\Endian; +use BcMath\Number; -echo Endian\Order::native->unpackInt64( - Endian\Order::native->packInt64(\PHP_INT_MAX), -); // 9223372036854775807 +echo Endian\Order::native() + ->unpackInt64( + Endian\Order::native()->packInt64(new Number('9223372036854775807')), + ) + ->value; // 9223372036854775807 ``` ### Supported types: diff --git a/composer.json b/composer.json index b59d8b4..0721cc7 100644 --- a/composer.json +++ b/composer.json @@ -18,21 +18,20 @@ } ], "require": { - "php": "^8.3" + "php": "^8.4", + "ext-bcmath": "*" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", "ergebnis/composer-normalize": "^2.48.2", - "phpunit/phpunit": "^10.5.58" + "phpunit/phpunit": "^10.5.58", + "symfony/var-dumper": "^7.3" }, "minimum-stability": "stable", "autoload": { "psr-4": { "Thesis\\Endian\\": "src/" - }, - "files": [ - "src/Internal/functions.php" - ] + } }, "autoload-dev": { "psr-4": { @@ -46,7 +45,7 @@ }, "bump-after-update": "dev", "platform": { - "php": "8.3" + "php": "8.4" }, "sort-packages": true }, diff --git a/composer.lock b/composer.lock index 3422758..8034afe 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ce56a36efa4a54122f45ef9b0874d79f", + "content-hash": "3d18cfc5fd57f9dd3cda286f7c6da5dd", "packages": [], "packages-dev": [ { @@ -2350,6 +2350,245 @@ ], "time": "2023-02-07T11:34:05+00:00" }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v7.3.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/476c4ae17f43a9a36650c69879dcf5b1e6ae724d", + "reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/uid": "^6.4|^7.0", + "twig/twig": "^3.12" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v7.3.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-27T09:00:46+00:00" + }, { "name": "theseer/tokenizer", "version": "1.3.1", @@ -2407,11 +2646,12 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^8.3" + "php": "^8.4", + "ext-bcmath": "*" }, "platform-dev": {}, "platform-overrides": { - "php": "8.3" + "php": "8.4" }, "plugin-api-version": "2.6.0" } diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile index 4fed743..1d26a95 100644 --- a/docker/php/Dockerfile +++ b/docker/php/Dockerfile @@ -1,6 +1,6 @@ FROM composer:2.8 AS composer FROM mlocati/php-extension-installer:2.7 AS php-extension-installer -FROM php:8.3-cli-bookworm AS php-dev +FROM php:8.4-cli-bookworm AS php-dev COPY --from=composer /usr/bin/composer /usr/bin/ COPY --from=php-extension-installer /usr/bin/install-php-extensions /usr/bin/ @@ -18,7 +18,7 @@ EOF RUN < + * @phpstan-type Uint8 = int<0, 255> + * @phpstan-type Int16 = int<-32768, 32767> + * @phpstan-type Uint16 = int<0, 65535> + * @phpstan-type Int32 = int<-2147483648, 2147483647> + * @phpstan-type Uint32 = int<0, 4294967295> + * @phpstan-type Int64 = int<-9223372036854775808, 9223372036854775807> + * @phpstan-type Uint64 = int<0, 18446744073709551615> */ enum Order { case big; case little; public const self network = self::big; - public const self native = Internal\native; + + public static function native(): self + { + /** @var ?self $order */ + static $order; + $order ??= isLittleEndianMachine() ? Order::little : Order::big; + + return $order; + } /** + * @param Int8 $num * @return non-empty-string */ - public function packInt8(int $v): string + public function packInt8(int $num): string { - return \chr($v); + if ($num < 0) { + $num += 256; + } + + return $this->packUint8($num); } /** * @param non-empty-string $v + * @return Int8 */ public function unpackInt8(string $v): int { - $n = $this->unpackUint8($v); - - if ($n >= 0x80) { - $n -= 0x100; + $num = $this->unpackUint8($v); + if ($num >= 128) { + $num -= 256; } - return $n; + return $num; } /** - * @param non-negative-int $v + * @param Uint8 $num * @return non-empty-string */ - public function packUint8(int $v): string + public function packUint8(int $num): string { - return \chr($v); + return \chr($num); } /** * @param non-empty-string $v - * @return int<0, 255> + * @return Uint8 */ public function unpackUint8(string $v): int { @@ -55,194 +78,169 @@ public function unpackUint8(string $v): int } /** + * @param Int16 $num * @return non-empty-string */ - public function packInt16(int $v): string + public function packInt16(int $num): string { - if ($v < 0) { - $v = (1 << 16) + $v; + if ($num < 0) { + $num += 65536; } - /** @phpstan-ignore argument.type */ - return $this->packUint16($v); + return $this->packUint16($num); } /** * @param non-empty-string $v + * @return Int16 */ public function unpackInt16(string $v): int { - $n = $this->unpackUint16($v); - - if ($n >= 0x8000) { - $n -= 0x10000; + $num = $this->unpackUint16($v); + if ($num >= 32768) { + $num -= 65536; } - return $n; + return $num; } /** - * @param non-negative-int $v + * @param Uint16 $num * @return non-empty-string */ - public function packUint16(int $v): string + public function packUint16(int $num): string { - return match ($this) { - self::big => \chr($v >> 8) . \chr($v), - self::little => \chr($v) . \chr($v >> 8), - }; + return packBytes($num, match ($this) { + self::big => 'n', + self::little => 'v', + }); } /** * @param non-empty-string $v - * @return non-negative-int + * @return Uint16 */ public function unpackUint16(string $v): int { - /** @var non-negative-int */ - return match ($this) { - self::big => \ord($v[1]) | \ord($v[0]) << 8, - self::little => \ord($v[0]) | \ord($v[1]) << 8, - }; + /** @var Uint16 */ + return unpackBytes($v, match ($this) { + self::big => 'n', + self::little => 'v', + }); } /** + * @param Int32 $num * @return non-empty-string */ - public function packInt32(int $v): string + public function packInt32(int $num): string { - if ($v < 0) { - $v = (1 << 32) + $v; + if ($num < 0) { + $num += 4294967296; } - /** @phpstan-ignore argument.type */ - return $this->packUint32($v); + return $this->packUint32($num); } /** * @param non-empty-string $v + * @return Int32 */ public function unpackInt32(string $v): int { - $n = $this->unpackUint32($v); - - if ($n >= 0x80000000) { - $n -= 0x100000000; + $num = $this->unpackUint32($v); + if ($num >= 2147483648) { + $num -= 4294967296; } - return $n; + return $num; } /** - * @param non-negative-int $v + * @param Uint32 $num * @return non-empty-string */ - public function packUint32(int $v): string + public function packUint32(int $num): string { - return match ($this) { - self::big => \chr($v >> 24) - . \chr($v >> 16) - . \chr($v >> 8) - . \chr($v), - self::little => \chr($v) - . \chr($v >> 8) - . \chr($v >> 16) - . \chr($v >> 24), - }; + return packBytes($num, match ($this) { + self::big => 'N', + self::little => 'V', + }); } /** * @param non-empty-string $v - * @return non-negative-int + * @return Uint32 */ public function unpackUint32(string $v): int { - /** @var non-negative-int */ - return match ($this) { - self::big => \ord($v[3]) - | \ord($v[2]) << 8 - | \ord($v[1]) << 16 - | \ord($v[0]) << 24, - self::little => \ord($v[0]) - | \ord($v[1]) << 8 - | \ord($v[2]) << 16 - | \ord($v[3]) << 24, - }; + /** @var Uint32 */ + return unpackBytes($v, match ($this) { + self::big => 'N', + self::little => 'V', + }); } /** * @return non-empty-string */ - public function packInt64(int $v): string + public function packInt64(Number $num): string { - return $this->packUint64($v); + if ($num->compare(0) < 0) { + $num += new Number(2)->pow(64); + } + + return $this->packUint64($num); } /** * @param non-empty-string $v */ - public function unpackInt64(string $v): int + public function unpackInt64(string $v): Number { - return $this->unpackUint64($v); + $num = $this->unpackUint64($v); + if ($num->compare(new Number(2)->pow(63)) >= 0) { + $num = $num->sub(new Number(2)->pow(64), scale: 0); + } + + return $num; } /** * @return non-empty-string */ - public function packUint64(int $v): string + public function packUint64(Number $num): string { + $bytes = ''; + + for ($i = 0; $i < 8; ++$i) { + $bytes .= \chr((int) $num->mod(256)->value); + $num = $num->div(256, scale: 0); + } + return match ($this) { - self::big => \chr($v >> 56) - . \chr($v >> 48) - . \chr($v >> 40) - . \chr($v >> 32) - . \chr($v >> 24) - . \chr($v >> 16) - . \chr($v >> 8) - . \chr($v), - self::little => \chr($v) - . \chr($v >> 8) - . \chr($v >> 16) - . \chr($v >> 24) - . \chr($v >> 32) - . \chr($v >> 40) - . \chr($v >> 48) - . \chr($v >> 56), + self::big => strrev($bytes), + self::little => $bytes, }; } /** * @param non-empty-string $v - * @return non-negative-int */ - public function unpackUint64(string $v): int + public function unpackUint64(string $v): Number { return match ($this) { - self::big => \ord($v[7]) - | \ord($v[6]) << 8 - | \ord($v[5]) << 16 - | \ord($v[4]) << 24 - | \ord($v[3]) << 32 - | \ord($v[2]) << 40 - | \ord($v[1]) << 48 - | \ord($v[0]) << 56, - self::little => \ord($v[0]) - | \ord($v[1]) << 8 - | \ord($v[2]) << 16 - | \ord($v[3]) << 24 - | \ord($v[4]) << 32 - | \ord($v[5]) << 40 - | \ord($v[6]) << 48 - | \ord($v[7]) << 56, + self::big => unpackUint64BE($v), + self::little => unpackUint64LE($v), }; } /** * @return non-empty-string */ - public function packFloat(float $v): string + public function packFloat(float $num): string { - return Internal\packBytes($v, match ($this) { + return packBytes($num, match ($this) { self::big => 'G', self::little => 'g', }); @@ -253,7 +251,7 @@ public function packFloat(float $v): string */ public function unpackFloat(string $v): float { - return (float) Internal\unpackBytes($v, match ($this) { + return (float) unpackBytes($v, match ($this) { self::big => 'G', self::little => 'g', }); @@ -262,9 +260,9 @@ public function unpackFloat(string $v): float /** * @return non-empty-string */ - public function packDouble(float $v): string + public function packDouble(float $num): string { - return Internal\packBytes($v, match ($this) { + return packBytes($num, match ($this) { self::big => 'E', self::little => 'e', }); @@ -275,9 +273,69 @@ public function packDouble(float $v): string */ public function unpackDouble(string $v): float { - return (float) Internal\unpackBytes($v, match ($this) { + return (float) unpackBytes($v, match ($this) { self::big => 'E', self::little => 'e', }); } } + +/** + * @internal + * @param non-empty-string $v + */ +function unpackUint64BE(string $v): Number +{ + $num = new Number(0); + + for ($i = 0; $i < 8; ++$i) { + $num += new Number(\ord($v[$i])) * new Number(256)->pow(7 - $i, scale: 0); + } + + return $num; +} + +/** + * @internal + * @param non-empty-string $v + */ +function unpackUint64LE(string $v): Number +{ + $num = new Number(0); + + for ($i = 0; $i < 8; ++$i) { + $num += new Number(\ord($v[$i])) * new Number(256)->pow($i, scale: 0); + } + + return $num; +} + +/** + * @internal + * @param non-empty-string $bytes + * @param non-empty-string $format + */ +function unpackBytes(string $bytes, string $format): string|int|float +{ + /** @var string|int|float */ + return unpack($format, $bytes)[1] ?? throw new \RuntimeException(\sprintf('Cannot unpack "%s" using "%s".', $bytes, $format)); +} + +/** + * @internal + * @param non-empty-string $format + * @return non-empty-string + */ +function packBytes(mixed $value, string $format): string +{ + /** @var non-empty-string */ + return pack($format, $value); +} + +/** + * @internal + */ +function isLittleEndianMachine(): bool +{ + return (int) unpackBytes("\x01\x00", 'S') === 1; +} diff --git a/tests/OrderTest.php b/tests/OrderTest.php index 223d281..5ace9f2 100644 --- a/tests/OrderTest.php +++ b/tests/OrderTest.php @@ -4,6 +4,7 @@ namespace Thesis\Endian; +use BcMath\Number; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; @@ -68,7 +69,13 @@ public function testInt64(): void { foreach ([Order::big, Order::little] as $endian) { foreach ($this->sequence([-32_768, 32_767], [PHP_INT_MIN], [PHP_INT_MAX]) as $i) { - self::assertSame($i, $endian->unpackInt64($endian->packInt64($i))); + $num = new Number($i); + + self::assertEquals($num, $endian->unpackInt64($endian->packInt64($num))); + } + + foreach ([new Number('9223372036854775807'), new Number('-9223372036854775808')] as $num) { + self::assertEquals($num, $endian->unpackInt64($endian->packInt64($num))); } } } @@ -77,7 +84,13 @@ public function testUint64(): void { foreach ([Order::big, Order::little] as $endian) { foreach ($this->sequence([0, 65_535], [PHP_INT_MAX]) as $i) { - self::assertSame($i, $endian->unpackUint64($endian->packUint64($i))); + $num = new Number($i); + + self::assertEquals($num, $endian->unpackUint64($endian->packUint64($num))); + } + + foreach ([new Number('18446744073709551615')] as $num) { + self::assertEquals($num, $endian->unpackUint64($endian->packUint64($num))); } } }