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
6 changes: 3 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.idea

vendor
report
.phpunit.cache
.phpunit.*

composer.lock
.phpunit.result.cache
*.lock
6 changes: 3 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
9 changes: 2 additions & 7 deletions infection.json.dist
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 5 additions & 1 deletion phpmd.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@
<rule ref="rulesets/design.xml/CouplingBetweenObjects"/>
<rule ref="rulesets/design.xml/DevelopmentCodeFragment"/>

<rule ref="rulesets/naming.xml/LongVariable"/>
<rule ref="rulesets/naming.xml/LongVariable">
<properties>
<property name="maximum" value="25"/>
</properties>
</rule>
<rule ref="rulesets/naming.xml/ShortVariable">
<properties>
<property name="minimum" value="2"/>
Expand Down
30 changes: 18 additions & 12 deletions src/Base62.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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();
Expand Down
9 changes: 4 additions & 5 deletions src/Internal/Decimal.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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);
}
Expand Down
24 changes: 13 additions & 11 deletions src/Internal/Hexadecimal.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -61,20 +64,19 @@ 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);
}

$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);
}
Expand Down
27 changes: 27 additions & 0 deletions tests/Base62Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 [
Expand Down