Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 13 additions & 18 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,49 +7,44 @@ on:
permissions:
contents: read

env:
PHP_VERSION: '8.3'

jobs:
auto-review:
name: Auto review
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: Use PHP 8.2
- name: Configure PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
php-version: ${{ env.PHP_VERSION }}

- name: Install dependencies
run: composer update --no-progress --optimize-autoloader

- name: Run phpcs
run: composer phpcs

- name: Run phpmd
run: composer phpmd
- name: Run review
run: composer review

tests:
name: Tests
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: Use PHP 8.2
- name: Use PHP ${{ env.PHP_VERSION }}
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
php-version: ${{ env.PHP_VERSION }}

- name: Install dependencies
run: composer update --no-progress --optimize-autoloader

- name: Run unit tests
env:
XDEBUG_MODE: coverage
run: composer test

- name: Run mutation tests
run: composer test-mutation
- name: Run tests
run: composer tests
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
DOCKER_RUN = docker run --rm -it --net=host -v ${PWD}:/app -w /app gustavofreze/php:8.2
DOCKER_RUN = docker run --rm -it --net=host -v ${PWD}:/app -w /app gustavofreze/php:8.3

.PHONY: configure test test-file test-no-coverage review show-reports clean

Expand Down
12 changes: 5 additions & 7 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,18 @@
}
},
"require": {
"php": "^8.2",
"ext-gmp": "*"
"php": "^8.3",
"ext-bcmath": "*"
},
"require-dev": {
"phpmd/phpmd": "^2.15",
"phpunit/phpunit": "^11",
"phpstan/phpstan": "^1",
"infection/infection": "^0.29",
"squizlabs/php_codesniffer": "^3.10"
"squizlabs/php_codesniffer": "^3.11"
},
"suggest": {
"ext-gmp": "Enables faster math with arbitrary-precision integers using GMP."
"ext-bcmath": "Enables the extension which is an interface to the GNU implementation as a Basic Calculator utility library."
},
"scripts": {
"phpcs": "phpcs --standard=PSR12 --extensions=php ./src",
Expand All @@ -60,7 +60,6 @@
"test": "phpunit --log-junit=report/coverage/junit.xml --coverage-xml=report/coverage/coverage-xml --coverage-html=report/coverage/coverage-html tests",
"test-mutation": "infection --only-covered --logger-html=report/coverage/mutation-report.html --coverage=report/coverage --min-msi=100 --min-covered-msi=100 --threads=4",
"test-no-coverage": "phpunit --no-coverage",
"test-mutation-no-coverage": "infection --only-covered --min-msi=100 --threads=4",
"review": [
"@phpcs",
"@phpmd",
Expand All @@ -71,8 +70,7 @@
"@test-mutation"
],
"tests-no-coverage": [
"@test-no-coverage",
"@test-mutation-no-coverage"
"@test-no-coverage"
]
}
}
7 changes: 6 additions & 1 deletion infection.json.dist
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"timeout": 10,
"timeout": 30,
"testFramework": "phpunit",
"tmpDir": "report/infection/",
"source": {
Expand All @@ -13,7 +13,12 @@
},
"mutators": {
"@default": true,
"BCMath": false,
"CastInt": false,
"Increment": false,
"GreaterThan": false,
"UnwrapSubstr": false,
"UnwrapStrToLower": false,
"LogicalAndNegation": false,
"LogicalAndAllSubExprNegation": false
},
Expand Down
34 changes: 16 additions & 18 deletions src/Base62.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@

namespace TinyBlocks\Encoder;

use TinyBlocks\Encoder\Internal\Decimal;
use TinyBlocks\Encoder\Internal\Exceptions\InvalidDecoding;
use TinyBlocks\Encoder\Internal\Hexadecimal;

final readonly class Base62 implements Encoder
{
private const BASE62_RADIX = 62;
private const BASE62_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
private const BASE62_CHARACTER_LENGTH = 1;
private const BASE62_HEXADECIMAL_RADIX = 16;
public const int BASE62_RADIX = 62;
private const int BASE62_CHARACTER_LENGTH = 1;

private const string BASE62_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';

private function __construct(private string $value)
{
Expand All @@ -25,18 +26,18 @@ public static function from(string $value): Encoder

public function encode(): string
{
$hexadecimal = Hexadecimal::fromBinary(binary: $this->value);
$bytes = $hexadecimal->removeLeadingZeroBytes();
$hexadecimal = Hexadecimal::fromBinary(binary: $this->value, alphabet: self::BASE62_ALPHABET);
$hexadecimal = $hexadecimal->removeLeadingZeroBytes();

$base62 = str_repeat(self::BASE62_ALPHABET[0], $bytes);
$base62 = str_repeat(self::BASE62_ALPHABET[0], $hexadecimal->getBytes());

if ($hexadecimal->isEmpty()) {
return $base62;
}

$number = $hexadecimal->toGmpInit(base: self::BASE62_HEXADECIMAL_RADIX);
$base62Value = $hexadecimal->toBase(base: self::BASE62_RADIX);

return sprintf('%s%s', $base62, gmp_strval($number, self::BASE62_RADIX));
return sprintf('%s%s', $base62, $base62Value);
}

public function decode(): string
Expand All @@ -57,16 +58,13 @@ public function decode(): string
return str_repeat("\x00", $bytes);
}

$number = gmp_init($value, self::BASE62_RADIX);
$hexadecimal = Hexadecimal::fromGmp(number: $number, base: self::BASE62_HEXADECIMAL_RADIX);
$hexadecimal->padLeft();

$binary = hex2bin(sprintf('%s%s', str_repeat('00', $bytes), $hexadecimal->toString()));
$decimal = Decimal::fromBase62(number: $value, alphabet: self::BASE62_ALPHABET);
$hexadecimal = Hexadecimal::from(value: $decimal->toHexadecimal())
->fillWithZeroIfNecessary()
->toString();

if (!is_string($binary)) {
throw new InvalidDecoding(value: $this->value);
}
$binary = hex2bin($hexadecimal);

return $binary;
return sprintf('%s%s', str_repeat("\x00", $bytes), $binary);
}
}
42 changes: 42 additions & 0 deletions src/Internal/Decimal.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace TinyBlocks\Encoder\Internal;

use TinyBlocks\Encoder\Base62;

final readonly class Decimal
{
private function __construct(private string $value)
{
}

public static function fromBase62(string $number, string $alphabet): Decimal
{
$value = '0';
$length = strlen($number);

for ($index = 0; $index < $length; $index++) {
$digit = strpos($alphabet, $number[$index]);
$value = bcmul($value, (string)Base62::BASE62_RADIX);
$value = bcadd($value, (string)$digit);
}

return new Decimal(value: $value);
}

public function toHexadecimal(): string
{
$value = $this->value;
$hexadecimalValue = '';

while (bccomp($value, '0') > 0) {
$remainder = bcmod($value, Hexadecimal::HEXADECIMAL_RADIX);
$hexadecimalValue = sprintf('%s%s', Hexadecimal::HEXADECIMAL_ALPHABET[(int)$remainder], $hexadecimalValue);
$value = bcdiv($value, Hexadecimal::HEXADECIMAL_RADIX);
}

return $hexadecimalValue;
}
}
1 change: 1 addition & 0 deletions src/Internal/Exceptions/InvalidDecoding.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ final class InvalidDecoding extends RuntimeException
public function __construct(private readonly string $value)
{
$template = 'The value <%s> could not be decoded.';

parent::__construct(message: sprintf($template, $this->value));
}
}
82 changes: 54 additions & 28 deletions src/Internal/Hexadecimal.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,60 +4,86 @@

namespace TinyBlocks\Encoder\Internal;

use GMP;

final class Hexadecimal
final readonly class Hexadecimal
{
private const HEXADECIMAL_BYTE_LENGTH = 2;
private const int DEFAULT_BYTE_COUNT = 0;
private const int HEXADECIMAL_BYTE_LENGTH = 2;

private string $value;
public const string HEXADECIMAL_RADIX = '16';
public const string HEXADECIMAL_ALPHABET = '0123456789abcdef';

private function __construct(string $value)
{
$this->value = $value;
private function __construct(
private string $value,
private string $alphabet,
private int $bytes = self::DEFAULT_BYTE_COUNT
) {
}

public static function fromGmp(GMP $number, int $base): Hexadecimal
public static function from(string $value): Hexadecimal
{
return new Hexadecimal(value: gmp_strval($number, $base));
return new Hexadecimal(value: $value, alphabet: self::HEXADECIMAL_ALPHABET);
}

public static function fromBinary(string $binary): Hexadecimal
public static function fromBinary(string $binary, string $alphabet): Hexadecimal
{
return new Hexadecimal(value: bin2hex($binary));
return new Hexadecimal(value: bin2hex($binary), alphabet: $alphabet);
}

public function isEmpty(): bool
public function removeLeadingZeroBytes(): Hexadecimal
{
return empty($this->value);
$bytes = 0;
$newValue = $this->value;

while (str_starts_with($newValue, '00')) {
$bytes++;
$newValue = substr($newValue, self::HEXADECIMAL_BYTE_LENGTH);
}

return new Hexadecimal(value: $newValue, alphabet: $this->alphabet, bytes: $bytes);
}

public function padLeft(): void
public function fillWithZeroIfNecessary(): Hexadecimal
{
if (strlen($this->value) % 2 !== 0) {
$this->value = sprintf('0%s', $this->value);
}
$newValue = strlen($this->value) % 2 !== 0 ? sprintf('0%s', $this->value) : $this->value;

return new Hexadecimal(value: $newValue, alphabet: $this->alphabet, bytes: $this->bytes);
}

public function toString(): string
public function getBytes(): int
{
return $this->value;
return $this->bytes;
}

public function toGmpInit(int $base): GMP
public function isEmpty(): bool
{
return gmp_init($this->value, $base);
return empty($this->value);
}

public function removeLeadingZeroBytes(): int
public function toBase(int $base): string
{
$bytes = 0;
$length = strlen($this->value);
$decimalValue = '0';

while (str_starts_with($this->value, '00')) {
$bytes++;
$this->value = substr($this->value, self::HEXADECIMAL_BYTE_LENGTH);
for ($index = 0; $index < $length; $index++) {
$digit = strpos(self::HEXADECIMAL_ALPHABET, strtolower($this->value[$index]));
$decimalValue = bcmul($decimalValue, self::HEXADECIMAL_RADIX);
$decimalValue = bcadd($decimalValue, (string)$digit);
}

$digits = $this->alphabet;
$result = '';

while (bccomp($decimalValue, '0') > 0) {
$remainder = bcmod($decimalValue, (string)$base);
$result = sprintf('%s%s', $digits[(int)$remainder], $result);
$decimalValue = bcdiv($decimalValue, (string)$base);
}

return $bytes;
return $result ?: '0';
}

public function toString(): string
{
return $this->value;
}
}
Loading
Loading