From 3b5ed95b1b870fed6e373c9fa9bd61ce3be19ded Mon Sep 17 00:00:00 2001 From: Gustavo Freze Date: Sat, 3 Jan 2026 11:13:32 -0300 Subject: [PATCH 1/2] refactor: simplify Base62 decoding and internal conversions. Improve readability and robustness of Base62 decode and internal Decimal/Hexadecimal conversions while preserving current behavior. --- .gitignore | 6 +++--- composer.json | 6 +++--- infection.json.dist | 9 ++------- phpmd.xml | 6 +++++- src/Base62.php | 19 ++++++++++--------- src/Internal/Decimal.php | 9 ++++----- src/Internal/Hexadecimal.php | 24 +++++++++++++----------- tests/Base62Test.php | 2 ++ 8 files changed, 42 insertions(+), 39 deletions(-) diff --git a/.gitignore b/.gitignore index 1cb9b22..42b841a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ .idea + vendor report -.phpunit.cache +.phpunit.* -composer.lock -.phpunit.result.cache \ No newline at end of file +*.lock diff --git a/composer.json b/composer.json index af8bf0f..d3d71df 100644 --- a/composer.json +++ b/composer.json @@ -45,9 +45,9 @@ }, "require-dev": { "phpmd/phpmd": "^2.15", - "phpunit/phpunit": "^11", - "phpstan/phpstan": "^1", - "infection/infection": "^0", + "phpunit/phpunit": "^11.5", + "phpstan/phpstan": "^1.12", + "infection/infection": "^0.29", "squizlabs/php_codesniffer": "^3.11" }, "suggest": { diff --git a/infection.json.dist b/infection.json.dist index db91d0f..8bf7f17 100644 --- a/infection.json.dist +++ b/infection.json.dist @@ -17,13 +17,8 @@ }, "mutators": { "@default": true, - "BCMath": false, - "CastInt": false, - "Increment": false, - "GreaterThan": false, - "UnwrapSubstr": false, - "LogicalAndNegation": false, - "LogicalAndAllSubExprNegation": false + "UnwrapLtrim": false, + "DecrementInteger": false }, "minCoveredMsi": 100, "testFramework": "phpunit" diff --git a/phpmd.xml b/phpmd.xml index cd9072e..4d81dcd 100644 --- a/phpmd.xml +++ b/phpmd.xml @@ -35,7 +35,11 @@ - + + + + + diff --git a/src/Base62.php b/src/Base62.php index f21f157..5268802 100644 --- a/src/Base62.php +++ b/src/Base62.php @@ -10,8 +10,6 @@ final readonly class Base62 implements Encoder { - private const int BASE62_CHARACTER_LENGTH = 1; - public const string BASE62_RADIX = '62'; private const string BASE62_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; @@ -46,19 +44,22 @@ public function decode(): string throw new InvalidDecoding(value: $this->value); } - $bytes = 0; $value = $this->value; - while (!empty($value) && str_starts_with($value, self::BASE62_ALPHABET[0])) { - $bytes++; - $value = substr($value, self::BASE62_CHARACTER_LENGTH); + if ($value === '') { + return ''; } - if (empty($value)) { - return str_repeat("\x00", $bytes); + $leadingZeroCharacters = strspn($value, self::BASE62_ALPHABET[0]); + + if ($leadingZeroCharacters === strlen($value)) { + return str_repeat("\x00", max(0, $leadingZeroCharacters - 1)); } - $decimal = Decimal::from(number: $value, alphabet: self::BASE62_ALPHABET, baseRadix: self::BASE62_RADIX); + $bytes = $leadingZeroCharacters; + $number = ltrim($value, self::BASE62_ALPHABET[0]); + + $decimal = Decimal::from(number: $number, alphabet: self::BASE62_ALPHABET, baseRadix: self::BASE62_RADIX); $hexadecimal = Hexadecimal::from(value: $decimal->toHexadecimal()) ->fillWithZeroIfNecessary() ->toString(); diff --git a/src/Internal/Decimal.php b/src/Internal/Decimal.php index 3e2ad4c..bd21c76 100644 --- a/src/Internal/Decimal.php +++ b/src/Internal/Decimal.php @@ -13,10 +13,9 @@ private function __construct(private string $value) public static function from(string $number, string $alphabet, string $baseRadix): Decimal { $value = '0'; - $length = strlen($number); - for ($index = 0; $index < $length; $index++) { - $digit = (string)strpos($alphabet, $number[$index]); + foreach (str_split($number) as $character) { + $digit = (string)strpos($alphabet, $character); $value = bcmul($value, $baseRadix); $value = bcadd($value, $digit); } @@ -29,8 +28,8 @@ public function toHexadecimal(): string $value = $this->value; $hexadecimalValue = ''; - while (bccomp($value, '0') > 0) { - $remainder = (int)bcmod($value, Hexadecimal::HEXADECIMAL_RADIX); + while ($value !== '0') { + $remainder = intval(bcmod($value, Hexadecimal::HEXADECIMAL_RADIX)); $hexadecimalValue = sprintf('%s%s', Hexadecimal::HEXADECIMAL_ALPHABET[$remainder], $hexadecimalValue); $value = bcdiv($value, Hexadecimal::HEXADECIMAL_RADIX); } diff --git a/src/Internal/Hexadecimal.php b/src/Internal/Hexadecimal.php index 7b96c36..8e33696 100644 --- a/src/Internal/Hexadecimal.php +++ b/src/Internal/Hexadecimal.php @@ -31,15 +31,18 @@ public static function fromBinary(string $binary, string $alphabet): Hexadecimal public function removeLeadingZeroBytes(): Hexadecimal { - $bytes = 0; - $newValue = $this->value; + $value = $this->value; - while (str_starts_with($newValue, '00')) { - $bytes++; - $newValue = substr($newValue, self::HEXADECIMAL_BYTE_LENGTH); + $leadingZeroCharacters = strspn($value, '0'); + $offset = $leadingZeroCharacters - ($leadingZeroCharacters % self::HEXADECIMAL_BYTE_LENGTH); + + $bytes = intdiv($offset, self::HEXADECIMAL_BYTE_LENGTH); + + if ($offset === strlen($value)) { + $value = ''; } - return new Hexadecimal(value: $newValue, alphabet: $this->alphabet, bytes: $bytes); + return new Hexadecimal(value: $value, alphabet: $this->alphabet, bytes: $bytes); } public function fillWithZeroIfNecessary(): Hexadecimal @@ -61,11 +64,10 @@ public function isEmpty(): bool public function toBase(string $base): string { - $length = strlen($this->value); $decimalValue = '0'; - for ($index = 0; $index < $length; $index++) { - $digit = (string)strpos(self::HEXADECIMAL_ALPHABET, $this->value[$index]); + foreach (str_split($this->value) as $character) { + $digit = (string)strpos(self::HEXADECIMAL_ALPHABET, $character); $decimalValue = bcmul($decimalValue, self::HEXADECIMAL_RADIX); $decimalValue = bcadd($decimalValue, $digit); } @@ -73,8 +75,8 @@ public function toBase(string $base): string $digits = $this->alphabet; $result = ''; - while (bccomp($decimalValue, '0') > 0) { - $remainder = (int)bcmod($decimalValue, $base); + while ($decimalValue !== '0') { + $remainder = intval(bcmod($decimalValue, $base)); $result = sprintf('%s%s', $digits[$remainder], $result); $decimalValue = bcdiv($decimalValue, $base); } diff --git a/tests/Base62Test.php b/tests/Base62Test.php index 4ead139..b56d51f 100644 --- a/tests/Base62Test.php +++ b/tests/Base62Test.php @@ -88,6 +88,8 @@ public static function providerForTestDecode(): array { return [ 'Zero value' => ['value' => '0', 'expected' => ''], + 'Single zero byte' => ['value' => '00', 'expected' => "\x00"], + 'Two zero bytes' => ['value' => '000', 'expected' => "\x00\x00"], 'Empty string' => ['value' => '', 'expected' => ''], 'Hello world' => ['value' => 'T8dgcjRGuYUueWht', 'expected' => 'Hello world!'], 'Leading zeros' => ['value' => '000001', 'expected' => hex2bin('000000000001')], From 2951d27c5042f7a8f732dc57484039721d47c64b Mon Sep 17 00:00:00 2001 From: Gustavo Freze Date: Sat, 3 Jan 2026 11:43:36 -0300 Subject: [PATCH 2/2] refactor: simplify Base62 decoding and internal conversions. Improve readability and robustness of Base62 decode and internal Decimal/Hexadecimal conversions while preserving current behavior. --- src/Base62.php | 11 ++++++++--- tests/Base62Test.php | 29 +++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/Base62.php b/src/Base62.php index 5268802..0038d4d 100644 --- a/src/Base62.php +++ b/src/Base62.php @@ -27,17 +27,22 @@ public function encode(): string $hexadecimal = Hexadecimal::fromBinary(binary: $this->value, alphabet: self::BASE62_ALPHABET); $hexadecimal = $hexadecimal->removeLeadingZeroBytes(); - $base62 = str_repeat(self::BASE62_ALPHABET[0], $hexadecimal->getBytes()); + $prefix = str_repeat(self::BASE62_ALPHABET[0], $hexadecimal->getBytes()); if ($hexadecimal->isEmpty()) { - return $base62; + if ($hexadecimal->getBytes() === 0) { + return ''; + } + + return sprintf('%s%s', $prefix, self::BASE62_ALPHABET[0]); } $base62Value = $hexadecimal->toBase(base: self::BASE62_RADIX); - return sprintf('%s%s', $base62, $base62Value); + return sprintf('%s%s', $prefix, $base62Value); } + public function decode(): string { if (strlen($this->value) !== strspn($this->value, self::BASE62_ALPHABET)) { diff --git a/tests/Base62Test.php b/tests/Base62Test.php index b56d51f..44606fd 100644 --- a/tests/Base62Test.php +++ b/tests/Base62Test.php @@ -47,6 +47,22 @@ public function testWhenInvalidDecodingBase62(): void Base62::from(value: $value)->decode(); } + #[DataProvider('providerForTestEncodeAndDecodeWithAllZeroBytes')] + public function testEncodeAndDecodeWithAllZeroBytes(string $value): void + { + /** @Given a binary value containing only zero bytes */ + $encoder = Base62::from(value: $value); + + /** @When encoding the binary value */ + $encoded = $encoder->encode(); + + /** @When decoding the encoded value */ + $decoded = Base62::from(value: $encoded)->decode(); + + /** @Then the decoded value should match the original binary value */ + self::assertEquals($value, $decoded); + } + public function testWhenInvalidDecodingBase62WhenHex2BinFails(): void { $value = '\\A'; @@ -88,17 +104,26 @@ public static function providerForTestDecode(): array { return [ 'Zero value' => ['value' => '0', 'expected' => ''], - 'Single zero byte' => ['value' => '00', 'expected' => "\x00"], - 'Two zero bytes' => ['value' => '000', 'expected' => "\x00\x00"], 'Empty string' => ['value' => '', 'expected' => ''], 'Hello world' => ['value' => 'T8dgcjRGuYUueWht', 'expected' => 'Hello world!'], 'Leading zeros' => ['value' => '000001', 'expected' => hex2bin('000000000001')], + 'Two zero bytes' => ['value' => '000', 'expected' => "\x00\x00"], 'Numeric string' => ['value' => '1A0afZkibIAR2O', 'expected' => '1234567890'], + 'Single zero byte' => ['value' => '00', 'expected' => "\x00"], 'Single character' => ['value' => '1', 'expected' => "\001"], 'Special characters' => ['value' => 'MjehbVgJedVR', 'expected' => '@#$%^&*()'] ]; } + public static function providerForTestEncodeAndDecodeWithAllZeroBytes(): array + { + return [ + 'Single zero byte' => ['value' => "\x00"], + 'Two zero bytes' => ['value' => "\x00\x00"], + 'Eight zero bytes' => ['value' => str_repeat("\x00", 8)] + ]; + } + public static function providerForTestEncodeAndDecodeWithLeadingZeroBytes(): array { return [