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..0038d4d 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';
@@ -29,36 +27,44 @@ 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)) {
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..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';
@@ -91,12 +107,23 @@ public static function providerForTestDecode(): array
'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 [