diff --git a/benchmarks/Base58PHPEvent.php b/benchmarks/Base58PHPEvent.php new file mode 100644 index 0000000..af0666b --- /dev/null +++ b/benchmarks/Base58PHPEvent.php @@ -0,0 +1,33 @@ +base58 = new Base58(null, new PHPService()); + } + + /** + * @iterations 10000 + */ + public function encodeBase58() + { + $this->base58->encode('Hello World'); + } + + /** + * @iterations 10000 + */ + public function decodeBase58() + { + $this->base58->decode('JxF12TrwUP45BMd'); + } +} diff --git a/composer.json b/composer.json index 560701f..d497903 100644 --- a/composer.json +++ b/composer.json @@ -2,8 +2,8 @@ "name": "stephenhill/base58", "description": "Base58 Encoding and Decoding Library for PHP", "require-dev": { - "phpunit/phpunit": "4.*", - "athletic/athletic": "~0.1" + "athletic/athletic": "~0.1", + "phpunit/phpunit": "^7.4" }, "license": "MIT", "authors": [ @@ -21,4 +21,4 @@ "StephenHill\\Benchmarks\\": "benchmarks/" } } -} \ No newline at end of file +} diff --git a/phpunit.xml b/phpunit.xml index 0636241..cde5fdf 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -9,7 +9,6 @@ convertWarningsToExceptions="true" processIsolation="true" stopOnFailure="false" - syntaxCheck="true" beStrictAboutTestsThatDoNotTestAnything="true" beStrictAboutOutputDuringTests="true" verbose="true" diff --git a/src/BCMathService.php b/src/BCMathService.php index 9e1032a..6a7a1a4 100644 --- a/src/BCMathService.php +++ b/src/BCMathService.php @@ -81,7 +81,7 @@ public function encode($string) $output = ''; while ($decimal >= $this->base) { $div = bcdiv($decimal, $this->base, 0); - $mod = bcmod($decimal, $this->base); + $mod = (int) bcmod($decimal, $this->base); $output .= $this->alphabet[$mod]; $decimal = $div; } diff --git a/src/Base58.php b/src/Base58.php index 75a2e0d..91b5abf 100644 --- a/src/Base58.php +++ b/src/Base58.php @@ -57,7 +57,7 @@ public function __construct( $service = new BCMathService($alphabet); } else { - throw new \Exception('Please install the BC Math or GMP extension.'); + $service = new PHPService($alphabet); } } diff --git a/src/PHPService.php b/src/PHPService.php new file mode 100644 index 0000000..9cfcd34 --- /dev/null +++ b/src/PHPService.php @@ -0,0 +1,190 @@ +alphabet = $alphabet; + $this->base = \strlen($alphabet); + } + /** + * Encode a string into base58. + * + * @param string $string The string you wish to encode. + * @since Release v1.1.0 + * @return string The Base58 encoded string. + */ + public function encode($string): string + { + // Type validation + if (\is_string($string) === false) { + throw new InvalidArgumentException('Argument $string must be a string.'); + } + + // If the string is empty, then the encoded string is obviously empty + if ($string === '') { + return ''; + } + + // Strings in PHP are essentially 8-bit byte arrays + // so lets convert the string into a PHP array + $bytes = array_values(unpack('C*', $string)); + + $leadingZerosNeeded = 0; + foreach ($bytes as $byte) { + if ($byte !== 0) { + break; + } + + $leadingZerosNeeded++; + } + + $source = \array_slice($bytes, $leadingZerosNeeded); + $result = $this->convertBase($source, 256, 58); + + // Count existing leading zeros + $leadingZeroCount = 0; + foreach ($result as $digit) { + if ($digit !== 0) { + break; + } + + $leadingZeroCount++; + } + + // Now we need to add any missing leading zeros + for ($i = $leadingZeroCount; $i < $leadingZerosNeeded; $i++) { + array_unshift($result, 0); + } + + // Encode to a string + return implode('', array_map(function ($ord) { return $this->alphabet[$ord]; }, $result)); + } + + /** + * Decode base58 into a PHP string. + * + * @param string $base58 The base58 encoded string. + * @since Release v1.1.0 + * @return string Returns the decoded string. + */ + public function decode($base58): string + { + // Type Validation + if (\is_string($base58) === false) { + throw new InvalidArgumentException('Argument $base58 must be a string.'); + } + + // If the string is empty, then the decoded string is obviously empty + if ($base58 === '') { + return ''; + } + + $indexes = array_flip(str_split($this->alphabet)); + $chars = str_split($base58); + $digits = []; + + // Check for invalid characters in the supplied base58 string + foreach ($chars as $char) { + if (isset($indexes[$char]) === false) { + throw new InvalidArgumentException('Argument $base58 contains invalid characters. ($char: "'.$char.'" | $base58: "'.$base58.'") '); + } + + $digits[] = $indexes[$char]; + } + + $leadingZerosNeeded = 0; + foreach ($digits as $digit) { + if ($digit !== 0) { + break; + } + + $leadingZerosNeeded++; + } + + $source = \array_slice($digits, $leadingZerosNeeded); + $result = $this->convertBase($source, 58, 256); + + // Count existing leading zeros + $leadingZeroCount = 0; + foreach ($result as $digit) { + if ($digit !== 0) { + break; + } + + $leadingZeroCount++; + } + + // Now we need to add any missing leading zeros + for ($i = $leadingZeroCount; $i < $leadingZerosNeeded; $i++) { + array_unshift($result, 0); + } + + // Encode to a string + return implode('', array_map('\chr', $result)); + } + + /** + * Basic manual base conversion algorithm, + * @see https://en.wikipedia.org/wiki/Positional_notation#Base_conversion + **/ + private function convertBase(array $digits, int $base1, int $base2): array + { + $result = []; + + do { + $digitCount = \count($digits); + $quotient = []; + $remainder = 0; + + for ($i = 0; $i < $digitCount; $i++) { + $dividend = $remainder * $base1 + $digits[$i]; + $quotient[] = intdiv($dividend, $base2); + $remainder = $dividend % $base2; + } + + $result[] = $remainder; + $digits = $quotient; + } while (array_sum($quotient) > 0); + + // Now we need to reverse the results + return array_reverse($result); + } +} diff --git a/tests/Base58Test.php b/tests/Base58Test.php index bf135be..eca4e02 100644 --- a/tests/Base58Test.php +++ b/tests/Base58Test.php @@ -1,10 +1,12 @@