├── UPGRADING.md ├── .github └── workflows │ └── tests.yml ├── LICENSE ├── composer.json ├── src ├── Base62Proxy.php ├── Base62 │ ├── PhpEncoder.php │ ├── BcmathEncoder.php │ ├── BaseEncoder.php │ └── GmpEncoder.php └── Base62.php └── CHANGELOG.md /UPGRADING.md: -------------------------------------------------------------------------------- 1 | # Updgrading from 1.x to 2.x 2 | 3 | The `2.x` branch targets PHP 7.1 and up. All methods have their parameters and return values typehinted. Type juggling has been removed from everywhere. The `encode()` and `decode()` methods now assume string input and output. This means the following is not possible anymore. 4 | 5 | ```php 6 | $encoded = $base62->encode(987654321); 7 | $decoded = $base62->decode($encoded, true); 8 | ``` 9 | 10 | ```php 11 | $encoded = $base62->encode("987654321", true); 12 | $decoded = $base62->decode($encoded, true); 13 | ``` 14 | 15 | When working with integers you should now use the implicit `encodeInteger()` and `decodeInteger()` methods instead. 16 | 17 | ```php 18 | $encoded = $base62->encodeInteger(98765432); 19 | $decoded = $base62->decodeInteger($encoded); 20 | ``` 21 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | strategy: 6 | matrix: 7 | operating-system: [ubuntu-latest] 8 | php-versions: ["7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4", "8.5"] 9 | runs-on: ${{ matrix.operating-system }} 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v2 13 | - name: Setup PHP and extensions 14 | uses: shivammathur/setup-php@v2 15 | with: 16 | php-version: ${{ matrix.php-versions }} 17 | extensions: gmp 18 | coverage: xdebug 19 | #coverage: pcov 20 | - name: Install and cache Composer dependencies 21 | uses: ramsey/composer-install@v2 22 | - name: Run linter 23 | run: make lint 24 | - name: Run unit tests 25 | run: make unit 26 | - name: Run static analysis 27 | run: make static 28 | - name: Upload coverage 29 | uses: codecov/codecov-action@v1 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2021 Mika Tuupola 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tuupola/base62", 3 | "description": "Base62 encoder and decoder for arbitrary data", 4 | "keywords": [ 5 | "base62" 6 | ], 7 | "homepage": "https://github.com/tuupola/base62", 8 | "license": "MIT", 9 | "authors": [ 10 | { 11 | "name": "Mika Tuupola", 12 | "email": "tuupola@appelsiini.net", 13 | "homepage": "https://appelsiini.net/", 14 | "role": "Developer" 15 | } 16 | ], 17 | "require": { 18 | "php" : "^7.1|^8.0" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "Tuupola\\": "src" 23 | } 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { 27 | "Tuupola\\": "tests" 28 | } 29 | }, 30 | "require-dev": { 31 | "squizlabs/php_codesniffer": "^3.0", 32 | "phpunit/phpunit": "^7.0|^8.0|^9.0", 33 | "phpbench/phpbench": "^0.15.0|1.0.0-alpha3", 34 | "overtrue/phplint": "^2.3", 35 | "phpstan/phpstan": "^0.12.38" 36 | }, 37 | "suggest": { 38 | "ext-gmp": "GMP extension provides the fastest encoding and decoding." 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Base62Proxy.php: -------------------------------------------------------------------------------- 1 | Base62::GMP, 45 | ]; 46 | 47 | /** 48 | * Encode given data to a base62 string 49 | */ 50 | public static function encode(string $data): string 51 | { 52 | return (new Base62(self::$options))->encode($data); 53 | } 54 | 55 | /** 56 | * Decode given a base62 string back to data 57 | */ 58 | public static function decode(string $data): string 59 | { 60 | return (new Base62(self::$options))->decode($data); 61 | } 62 | 63 | /** 64 | * Encode given integer to a base62 string 65 | */ 66 | public static function encodeInteger(int $data): string 67 | { 68 | return (new Base62(self::$options))->encodeInteger($data); 69 | } 70 | 71 | /** 72 | * Decode given base62 string back to an integer 73 | */ 74 | public static function decodeInteger(string $data): int 75 | { 76 | return (new Base62(self::$options))->decodeInteger($data); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Base62/PhpEncoder.php: -------------------------------------------------------------------------------- 1 | options = array_merge($this->options, $options); 54 | if (function_exists("gmp_init")) { 55 | $this->encoder = new Base62\GmpEncoder($this->options); 56 | } else { 57 | $this->encoder = new Base62\PhpEncoder($this->options); 58 | } 59 | } 60 | 61 | /** 62 | * Encode given data to a base62 string 63 | */ 64 | public function encode(string $data): string 65 | { 66 | return $this->encoder->encode($data); 67 | } 68 | 69 | /** 70 | * Decode given a base62 string back to data 71 | */ 72 | public function decode(string $data): string 73 | { 74 | return $this->encoder->decode($data); 75 | } 76 | 77 | /** 78 | * Encode given integer to a base62 string 79 | */ 80 | public function encodeInteger(int $data): string 81 | { 82 | return $this->encoder->encodeInteger($data); 83 | } 84 | 85 | /** 86 | * Decode given base62 string back to an integer 87 | */ 88 | public function decodeInteger(string $data): int 89 | { 90 | return $this->encoder->decodeInteger($data); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file, in reverse chronological order by release. 4 | 5 | ## [2.1.1](https://github.com/tuupola/base62/compare/2.1.0...2.1.1) - 2025-12-08 6 | 7 | ### Fixed 8 | - Replace integer cast with int for PHP 8.5 ([#33](https://github.com/tuupola/base62/pull/33)). 9 | 10 | ## [2.1.0](https://github.com/tuupola/base62/compare/2.0.0...2.1.0) - 2020-09-09 11 | 12 | ### Added 13 | - Allow installing with PHP 8 ([#20](https://github.com/tuupola/base62/pull/20)). 14 | 15 | ## [2.0.0](https://github.com/tuupola/base62/compare/1.0.1...2.0.0) - 2018-12-30 16 | 17 | ### Changed 18 | - PHP 7.1 is now minimum requirement 19 | - All methods have return types 20 | - All methods are typehinted 21 | - All type juggling is removed 22 | 23 | ## [1.0.1](https://github.com/tuupola/base62/compare/1.0.0...1.0.1) - 2018-12-27 24 | ### Removed 25 | - The unused and undocumented second parameter from static proxy methods. Parameter was accidentally forgotten into the code. This could be considered a BC break. However since there have been practically no installs for 1.0.0 yet I doubt this will break anyones code. 26 | 27 | ## [1.0.0](https://github.com/tuupola/base62/compare/0.11.1...1.0.0) - 2018-12-20 28 | 29 | ### Changed 30 | - Tests are now run with all character sets ([#14](https://github.com/tuupola/base62/pull/14)). 31 | 32 | ## [0.11.1](https://github.com/tuupola/base62/compare/0.11.0...0.11.1) - 2018-09-11 33 | 34 | ### Fixed 35 | - GMP driver output was not matching others when binary data had leading 0x00 ([#13](https://github.com/tuupola/base62/pull/13)). 36 | 37 | ## [0.11.0](https://github.com/tuupola/base62/compare/0.10.0...0.11.0) - 2018-09-05 38 | 39 | ### Fixed 40 | - Leading 0x00 was stripped from binary data ([#4](https://github.com/tuupola/base62/issues/4), [#12](https://github.com/tuupola/base62/pull/12)) 41 | 42 | 43 | ## [0.10.0](https://github.com/tuupola/base62/compare/0.9.0...0.10.0) - 2018-03-28 44 | 45 | ### Changed 46 | - The `decode()` and `decodeInteger()` methods now throw `InvalidArgumentException` if the input string contains invalid characters ([#6](https://github.com/tuupola/base62/pull/6)). 47 | - Constructor now throws `InvalidArgumentException` if given character set is invalid ([#7](https://github.com/tuupola/base62/pull/7)). 48 | 49 | ## [0.9.0](https://github.com/tuupola/base62/compare/0.8.0...0.9.0) - 2017-10-09 50 | 51 | ### Added 52 | - Implicit `decodeInteger()` and `encodeInteger()` methods. 53 | 54 | ### Fixed 55 | - PHP encoder was returning returning wrong output when encoding integers and the float representation of the integer was wider than 53 bits. If your application uses big integers and PHP encoder only might be BC issues with `0.8.0`. GMP and BCMath encoders were not affected. 56 | 57 | ## [0.8.0](https://github.com/tuupola/base62/compare/0.7.0...0.8.0) - 2017-03-12 58 | 59 | This release is not compatible with `0.7.0`. Object syntax is now default. A quick way to upgrade is to add the following to your code: 60 | 61 | ```php 62 | use Tuupola\Base62Proxy as Base62; 63 | ``` 64 | 65 | ### Added 66 | - Possibility to use custom character sets. 67 | - Static proxy for those who want to use static syntax 68 | ```php 69 | use Tuupola\Base62Proxy as Base62; 70 | 71 | Base62::decode("foo"); 72 | ``` 73 | 74 | ## [0.7.0](https://github.com/tuupola/base62/compare/0.6.0...0.7.0) - 2016-10-09 75 | ### Added 76 | 77 | - Allow using object syntax, for example `$base62->decode("foo")`. 78 | - Optional BCMath encoder. Mostly a curiosity since it is slower than pure PHP encoder. 79 | 80 | ## [0.6.0](https://github.com/tuupola/base62/compare/0.5.0...0.6.0) - 2016-05-06 81 | ### Fixed 82 | 83 | - Encode integers as integers. Before they were cast as strings before encoding. 84 | 85 | ## 0.5.0 - 2016-05-05 86 | 87 | Initial realese. 88 | -------------------------------------------------------------------------------- /src/Base62/BaseEncoder.php: -------------------------------------------------------------------------------- 1 | Base62::GMP, 46 | ]; 47 | 48 | public function __construct(array $options = []) 49 | { 50 | $this->options = array_merge($this->options, $options); 51 | 52 | $uniques = count_chars($this->options["characters"], 3); 53 | /** @phpstan-ignore-next-line */ 54 | if (62 !== strlen($uniques) || 62 !== strlen($this->options["characters"])) { 55 | throw new InvalidArgumentException("Character set must contain 62 unique characters"); 56 | } 57 | } 58 | 59 | /** 60 | * Encode given data to a base62 string 61 | */ 62 | public function encode(string $data): string 63 | { 64 | $data = str_split($data); 65 | $data = array_map("ord", $data); 66 | 67 | $leadingZeroes = 0; 68 | while (!empty($data) && 0 === $data[0]) { 69 | $leadingZeroes++; 70 | array_shift($data); 71 | } 72 | $converted = $this->baseConvert($data, 256, 62); 73 | if (0 < $leadingZeroes) { 74 | $converted = array_merge( 75 | array_fill(0, $leadingZeroes, 0), 76 | $converted 77 | ); 78 | } 79 | return implode("", array_map(function ($index) { 80 | return $this->options["characters"][$index]; 81 | }, $converted)); 82 | } 83 | 84 | /** 85 | * Decode given a base62 string back to data 86 | */ 87 | public function decode(string $data): string 88 | { 89 | $this->validateInput($data); 90 | 91 | $data = str_split($data); 92 | $data = array_map(function ($character) { 93 | return strpos($this->options["characters"], $character); 94 | }, $data); 95 | 96 | $leadingZeroes = 0; 97 | while (!empty($data) && 0 === $data[0]) { 98 | $leadingZeroes++; 99 | array_shift($data); 100 | } 101 | 102 | $converted = $this->baseConvert($data, 62, 256); 103 | 104 | if (0 < $leadingZeroes) { 105 | $converted = array_merge( 106 | array_fill(0, $leadingZeroes, 0), 107 | $converted 108 | ); 109 | } 110 | 111 | return implode("", array_map("chr", $converted)); 112 | } 113 | 114 | private function validateInput(string $data): void 115 | { 116 | /* If the data contains characters that aren't in the character set. */ 117 | if (strlen($data) !== strspn($data, $this->options["characters"])) { 118 | $valid = str_split($this->options["characters"]); 119 | $invalid = str_replace($valid, "", $data); 120 | $invalid = count_chars($invalid, 3); 121 | 122 | throw new InvalidArgumentException( 123 | /** @phpstan-ignore-next-line */ 124 | "Data contains invalid characters \"{$invalid}\"" 125 | ); 126 | } 127 | } 128 | 129 | /** 130 | * Encode given integer to a base62 string 131 | */ 132 | public function encodeInteger(int $data): string 133 | { 134 | $data = [$data]; 135 | 136 | $converted = $this->baseConvert($data, 256, 62); 137 | 138 | return implode("", array_map(function ($index) { 139 | return $this->options["characters"][$index]; 140 | }, $converted)); 141 | } 142 | 143 | /** 144 | * Decode given base62 string back to an integer 145 | */ 146 | public function decodeInteger(string $data): int 147 | { 148 | $this->validateInput($data); 149 | 150 | $data = str_split($data); 151 | $data = array_map(function ($character) { 152 | return strpos($this->options["characters"], $character); 153 | }, $data); 154 | 155 | $converted = $this->baseConvert($data, 62, 10); 156 | return (int) implode("", $converted); 157 | } 158 | 159 | /** 160 | * Convert an integer between artbitrary bases 161 | */ 162 | abstract public function baseConvert(array $source, int $sourceBase, int $targetBase): array; 163 | } 164 | -------------------------------------------------------------------------------- /src/Base62/GmpEncoder.php: -------------------------------------------------------------------------------- 1 | Base62::GMP, 46 | ]; 47 | 48 | public function __construct(array $options = []) 49 | { 50 | $this->options = array_merge($this->options, $options); 51 | 52 | $uniques = count_chars($this->options["characters"], 3); 53 | /** @phpstan-ignore-next-line */ 54 | if (62 !== strlen($uniques) || 62 !== strlen($this->options["characters"])) { 55 | throw new InvalidArgumentException( 56 | "Character set must contain 62 unique characters" 57 | ); 58 | } 59 | } 60 | 61 | /** 62 | * Encode given data to a base62 string 63 | */ 64 | public function encode(string $data): string 65 | { 66 | $hex = bin2hex($data); 67 | 68 | $leadZeroBytes = 0; 69 | while ("" !== $hex && 0 === strpos($hex, "00")) { 70 | $leadZeroBytes++; 71 | $hex = substr($hex, 2); 72 | } 73 | 74 | /* gmp_init() cannot cope with a zero-length string. */ 75 | if ("" === $hex) { 76 | $base62 = str_repeat(Base62::GMP[0], $leadZeroBytes); 77 | } else { 78 | $base62 = str_repeat(Base62::GMP[0], $leadZeroBytes) . gmp_strval(gmp_init($hex, 16), 62); 79 | } 80 | 81 | if (Base62::GMP === $this->options["characters"]) { 82 | return $base62; 83 | } 84 | 85 | return strtr($base62, Base62::GMP, $this->options["characters"]); 86 | } 87 | 88 | /** 89 | * Decode given a base62 string back to data 90 | */ 91 | public function decode(string $data): string 92 | { 93 | $this->validateInput($data); 94 | 95 | if (Base62::GMP !== $this->options["characters"]) { 96 | $data = strtr($data, $this->options["characters"], Base62::GMP); 97 | } 98 | 99 | $leadZeroBytes = 0; 100 | while ("" !== $data && 0 === strpos($data, Base62::GMP[0])) { 101 | $leadZeroBytes++; 102 | $data = substr($data, 1); 103 | } 104 | 105 | /* gmp_init() cannot cope with a zero-length string. */ 106 | if ("" === $data) { 107 | return str_repeat("\x00", $leadZeroBytes); 108 | } 109 | 110 | $hex = gmp_strval(gmp_init($data, 62), 16); 111 | if (strlen($hex) % 2) { 112 | $hex = "0" . $hex; 113 | } 114 | 115 | return (string) hex2bin(str_repeat("00", $leadZeroBytes) . $hex); 116 | } 117 | 118 | /** 119 | * Encode given integer to a base62 string 120 | */ 121 | public function encodeInteger(int $data): string 122 | { 123 | $base62 = gmp_strval(gmp_init($data, 10), 62); 124 | 125 | if (Base62::GMP === $this->options["characters"]) { 126 | return $base62; 127 | } 128 | 129 | return strtr($base62, Base62::GMP, $this->options["characters"]); 130 | } 131 | 132 | /** 133 | * Decode given base62 string back to an integer 134 | */ 135 | public function decodeInteger(string $data): int 136 | { 137 | $this->validateInput($data); 138 | 139 | if (Base62::GMP !== $this->options["characters"]) { 140 | $data = strtr($data, $this->options["characters"], Base62::GMP); 141 | } 142 | 143 | $hex = gmp_strval(gmp_init($data, 62), 16); 144 | if (strlen($hex) % 2) { 145 | $hex = "0" . $hex; 146 | } 147 | 148 | return (int) hexdec($hex); 149 | } 150 | 151 | private function validateInput(string $data): void 152 | { 153 | /* If the data contains characters that aren't in the character set. */ 154 | if (strlen($data) !== strspn($data, $this->options["characters"])) { 155 | $valid = str_split($this->options["characters"]); 156 | $invalid = str_replace($valid, "", $data); 157 | $invalid = count_chars($invalid, 3); 158 | 159 | throw new InvalidArgumentException( 160 | /** @phpstan-ignore-next-line */ 161 | "Data contains invalid characters \"{$invalid}\"" 162 | ); 163 | } 164 | } 165 | } 166 | --------------------------------------------------------------------------------