├── LICENSE.md ├── README.md ├── composer.json └── src ├── Bencode.php ├── Decoder.php ├── Encoder.php ├── Exception.php └── Exception └── RuntimeException.php /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Ryan Chouinard 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 13 | > all 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 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bencode serialization for PHP 2 | 3 | [![Latest Version on Packagist][ico-version]][link-packagist] 4 | [![Software License][ico-license]](LICENSE.md) 5 | [![Build Status][ico-travis]][link-travis] 6 | [![Quality Score][ico-code-quality]][link-code-quality] 7 | [![Total Downloads][ico-downloads]][link-downloads] 8 | 9 | This library allows developers to encode or decode bencoded data strings in 10 | PHP 5.3+. More information about bencode can be found at [Wikipedia](http://en.wikipedia.org/wiki/Bencode). 11 | The format is primarily used in the .torrent file specification. 12 | 13 | 14 | ## Install 15 | 16 | Via Composer 17 | 18 | ``` bash 19 | $ composer require rych/bencode 20 | ``` 21 | 22 | 23 | ## Usage 24 | 25 | ### Encoding an array 26 | 27 | ```php 28 | "bar", 34 | "integer" => 42, 35 | "array" => array( 36 | "one", 37 | "two", 38 | "three", 39 | ), 40 | ); 41 | 42 | echo Bencode::encode($data); 43 | ``` 44 | 45 | The above produces the string `d5:arrayl3:one3:two5:threee7:integeri42e6:string3:bare`. 46 | 47 | ### Decoding a string 48 | 49 | ```php 50 | Array 64 | ( 65 | [0] => one 66 | [1] => two 67 | [2] => three 68 | ) 69 | 70 | [integer] => 42 71 | [string] => bar 72 | ) 73 | ``` 74 | 75 | 76 | ## Testing 77 | 78 | ``` bash 79 | $ vendor/bin/phpunit -c phpunit.dist.xml 80 | ``` 81 | 82 | 83 | ## License 84 | 85 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 86 | 87 | 88 | [ico-version]: https://img.shields.io/packagist/v/rych/bencode.svg?style=flat-square 89 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square 90 | [ico-travis]: https://img.shields.io/travis/rchouinard/bencode.svg?style=flat-square 91 | [ico-coveralls]: https://img.shields.io/coveralls/rchouinard/bencode.svg?style=flat-square 92 | [ico-code-quality]: https://img.shields.io/sensiolabs/i/c444c99a-2870-459b-9268-13c96166e8f7.svg?style=flat-square 93 | [ico-downloads]: https://img.shields.io/packagist/dt/rych/bencode.svg?style=flat-square 94 | 95 | [link-packagist]: https://packagist.org/packages/rych/bencode 96 | [link-travis]: https://travis-ci.org/rchouinard/bencode 97 | [link-coveralls]: https://coveralls.io/r/rchouinard/bencode 98 | [link-code-quality]: https://insight.sensiolabs.com/projects/c444c99a-2870-459b-9268-13c96166e8f7 99 | [link-downloads]: https://packagist.org/packages/rych/bencode 100 | [link-author]: https://github.com/rchouinard 101 | [link-contributors]: https://github.com/rchouinard/bencode/graphs/contributors 102 | 103 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rych/bencode", 3 | "type": "library", 4 | "description": "Bencode serializer for PHP 5.3+", 5 | "keywords": ["bencode", "serialize"], 6 | "homepage": "https://github.com/rchouinard/bencode", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Ryan Chouinard", 11 | "email": "rchouinard@gmail.com", 12 | "homepage": "http://ryanchouinard.com" 13 | } 14 | ], 15 | "require": { 16 | "php": ">=5.3.0" 17 | }, 18 | "require-dev": { 19 | "phpunit/phpunit": "~4.7" 20 | }, 21 | "autoload": { 22 | "psr-4": { 23 | "Rych\\Bencode\\": "src/" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Bencode.php: -------------------------------------------------------------------------------- 1 | 10 | * @license MIT License - http://www.opensource.org/licenses/mit-license.php 11 | */ 12 | 13 | namespace Rych\Bencode; 14 | 15 | /** 16 | * Bencode class 17 | * 18 | * Provides static convenience methods for encoding and decoding bencode 19 | * encoded strings. 20 | */ 21 | class Bencode 22 | { 23 | 24 | const TYPE_ARRAY = "array"; 25 | const TYPE_OBJECT = "object"; // NOT IMPLEMENTED 26 | 27 | /** 28 | * Decode a bencode encoded string 29 | * 30 | * @param string $string The string to decode. 31 | * @param string $decodeType Flag used to indicate whether the decoded 32 | * value should be returned as an object or an array. 33 | * @return mixed Returns the appropriate data type for the decoded data. 34 | */ 35 | public static function decode($string, $decodeType = self::TYPE_ARRAY) 36 | { 37 | return Decoder::decode($string, $decodeType); 38 | } 39 | 40 | /** 41 | * Encode a value into a bencode encoded string 42 | * 43 | * @param mixed $value The value to encode. 44 | * @return string Returns a bencode encoded string. 45 | */ 46 | public static function encode($value) 47 | { 48 | return Encoder::encode($value); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/Decoder.php: -------------------------------------------------------------------------------- 1 | 10 | * @license MIT License - http://www.opensource.org/licenses/mit-license.php 11 | */ 12 | 13 | namespace Rych\Bencode; 14 | 15 | use Rych\Bencode\Exception\RuntimeException; 16 | 17 | /** 18 | * Bencode decoder class 19 | * 20 | * Decodes bencode encoded strings. 21 | */ 22 | class Decoder 23 | { 24 | 25 | /** 26 | * The encoded source string 27 | * 28 | * @var string 29 | */ 30 | private $source; 31 | 32 | /** 33 | * The length of the encoded source string 34 | * 35 | * @var integer 36 | */ 37 | private $sourceLength; 38 | 39 | /** 40 | * The return type for the decoded value 41 | * 42 | * @var Bencode::TYPE_ARRAY|Bencode::TYPE_OBJECT 43 | */ 44 | private $decodeType; 45 | 46 | /** 47 | * The current offset of the parser. 48 | * 49 | * @var integer 50 | */ 51 | private $offset = 0; 52 | 53 | /** 54 | * Decoder constructor 55 | * 56 | * @param string $source The bencode encoded source. 57 | * @param string $decodeType Flag used to indicate whether the decoded 58 | * value should be returned as an object or an array. 59 | * @return void 60 | */ 61 | private function __construct($source, $decodeType) 62 | { 63 | $this->source = $source; 64 | $this->sourceLength = strlen($this->source); 65 | $this->decodeType = in_array($decodeType, array(Bencode::TYPE_ARRAY, Bencode::TYPE_OBJECT)) 66 | ? $decodeType 67 | : Bencode::TYPE_ARRAY; 68 | } 69 | 70 | /** 71 | * Decode a bencode encoded string 72 | * 73 | * @param string $source The string to decode. 74 | * @param string $decodeType Flag used to indicate whether the decoded 75 | * value should be returned as an object or an array. 76 | * @return mixed Returns the appropriate data type for the decoded data. 77 | * @throws RuntimeException 78 | */ 79 | public static function decode($source, $decodeType = Bencode::TYPE_ARRAY) 80 | { 81 | if (!is_string($source)) { 82 | throw new RuntimeException("Argument expected to be a string; Got " . gettype($source)); 83 | } 84 | 85 | $decoder = new self($source, $decodeType); 86 | $decoded = $decoder->doDecode(); 87 | 88 | if ($decoder->offset != $decoder->sourceLength) { 89 | throw new RuntimeException("Found multiple entities outside list or dict definitions"); 90 | } 91 | 92 | return $decoded; 93 | } 94 | 95 | /** 96 | * Iterate over encoded entities in the source string and decode them 97 | * 98 | * @return mixed Returns the decoded value. 99 | * @throws RuntimeException 100 | */ 101 | private function doDecode() 102 | { 103 | switch ($this->getChar()) { 104 | 105 | case "i": 106 | ++$this->offset; 107 | return $this->decodeInteger(); 108 | 109 | case "l": 110 | ++$this->offset; 111 | return $this->decodeList(); 112 | 113 | case "d": 114 | ++$this->offset; 115 | return $this->decodeDict(); 116 | 117 | default: 118 | if (ctype_digit($this->getChar())) { 119 | return $this->decodeString(); 120 | } 121 | 122 | } 123 | 124 | throw new RuntimeException("Unknown entity found at offset $this->offset"); 125 | } 126 | 127 | /** 128 | * Decode a bencode encoded integer 129 | * 130 | * @return integer Returns the decoded integer. 131 | * @throws RuntimeException 132 | */ 133 | private function decodeInteger() 134 | { 135 | $offsetOfE = strpos($this->source, "e", $this->offset); 136 | if (false === $offsetOfE) { 137 | throw new RuntimeException("Unterminated integer entity at offset $this->offset"); 138 | } 139 | 140 | $currentOffset = $this->offset; 141 | if ("-" == $this->getChar($currentOffset)) { 142 | ++$currentOffset; 143 | } 144 | 145 | if ($offsetOfE === $currentOffset) { 146 | throw new RuntimeException("Empty integer entity at offset $this->offset"); 147 | } 148 | 149 | while ($currentOffset < $offsetOfE) { 150 | if (!ctype_digit($this->getChar($currentOffset))) { 151 | throw new RuntimeException("Non-numeric character found in integer entity at offset $this->offset"); 152 | } 153 | ++$currentOffset; 154 | } 155 | 156 | $value = substr($this->source, $this->offset, $offsetOfE - $this->offset); 157 | 158 | // One last check to make sure zero-padded integers don't slip by, as 159 | // they're not allowed per bencode specification. 160 | $absoluteValue = (string) abs($value); 161 | if (1 < strlen($absoluteValue) && "0" == $value[0]) { 162 | throw new RuntimeException("Illegal zero-padding found in integer entity at offset $this->offset"); 163 | } 164 | 165 | $this->offset = $offsetOfE + 1; 166 | 167 | // The +0 auto-casts the chunk to either an integer or a float(in cases 168 | // where an integer would overrun the max limits of integer types) 169 | return $value + 0; 170 | } 171 | 172 | /** 173 | * Decode a bencode encoded string 174 | * 175 | * @return string Returns the decoded string. 176 | * @throws RuntimeException 177 | */ 178 | private function decodeString() 179 | { 180 | if ("0" === $this->getChar() && ":" != $this->getChar($this->offset + 1)) { 181 | throw new RuntimeException("Illegal zero-padding in string entity length declaration at offset $this->offset"); 182 | } 183 | 184 | $offsetOfColon = strpos($this->source, ":", $this->offset); 185 | if (false === $offsetOfColon) { 186 | throw new RuntimeException("Unterminated string entity at offset $this->offset"); 187 | } 188 | 189 | $contentLength = (int) substr($this->source, $this->offset, $offsetOfColon); 190 | if (($contentLength + $offsetOfColon + 1) > $this->sourceLength) { 191 | throw new RuntimeException("Unexpected end of string entity at offset $this->offset"); 192 | } 193 | 194 | $value = substr($this->source, $offsetOfColon + 1, $contentLength); 195 | $this->offset = $offsetOfColon + $contentLength + 1; 196 | 197 | return $value; 198 | } 199 | 200 | /** 201 | * Decode a bencode encoded list 202 | * 203 | * @return array Returns the decoded array. 204 | * @throws RuntimeException 205 | */ 206 | private function decodeList() 207 | { 208 | $list = array(); 209 | $terminated = false; 210 | $listOffset = $this->offset; 211 | 212 | while (false !== $this->getChar()) { 213 | if ("e" == $this->getChar()) { 214 | $terminated = true; 215 | break; 216 | } 217 | 218 | $list[] = $this->doDecode(); 219 | } 220 | 221 | if (!$terminated && false === $this->getChar()) { 222 | throw new RuntimeException("Unterminated list definition at offset $listOffset"); 223 | } 224 | 225 | $this->offset++; 226 | 227 | return $list; 228 | } 229 | 230 | /** 231 | * Decode a bencode encoded dictionary 232 | * 233 | * @return array Returns the decoded array. 234 | * @throws RuntimeException 235 | */ 236 | private function decodeDict() 237 | { 238 | $dict = array(); 239 | $terminated = false; 240 | $dictOffset = $this->offset; 241 | 242 | while (false !== $this->getChar()) { 243 | if ("e" == $this->getChar()) { 244 | $terminated = true; 245 | break; 246 | } 247 | 248 | $keyOffset = $this->offset; 249 | if (!ctype_digit($this->getChar())) { 250 | throw new RuntimeException("Invalid dictionary key at offset $keyOffset"); 251 | } 252 | 253 | $key = $this->decodeString(); 254 | if (isset ($dict[$key])) { 255 | throw new RuntimeException("Duplicate dictionary key at offset $keyOffset"); 256 | } 257 | 258 | $dict[$key] = $this->doDecode(); 259 | } 260 | 261 | if (!$terminated && false === $this->getChar()) { 262 | throw new RuntimeException("Unterminated dictionary definition at offset $dictOffset"); 263 | } 264 | 265 | $this->offset++; 266 | 267 | return $dict; 268 | } 269 | 270 | /** 271 | * Fetch the character at the specified source offset 272 | * 273 | * If offset is not provided, the current offset is used. 274 | * 275 | * @param integer $offset The offset to retrieve from the source string. 276 | * @return string|false Returns the character found at the specified 277 | * offset. If the specified offset is out of range, FALSE is returned. 278 | */ 279 | private function getChar($offset = null) 280 | { 281 | if (null === $offset) { 282 | $offset = $this->offset; 283 | } 284 | 285 | if (empty ($this->source) || $this->offset >= $this->sourceLength) { 286 | return false; 287 | } 288 | 289 | return $this->source[$offset]; 290 | } 291 | 292 | } 293 | -------------------------------------------------------------------------------- /src/Encoder.php: -------------------------------------------------------------------------------- 1 | 10 | * @license MIT License - http://www.opensource.org/licenses/mit-license.php 11 | */ 12 | 13 | namespace Rych\Bencode; 14 | 15 | /** 16 | * Bencode encoder class 17 | * 18 | * Encode values into bencode encoded strings. 19 | */ 20 | class Encoder 21 | { 22 | 23 | /** 24 | * Value to encode 25 | * 26 | * @var mixed 27 | */ 28 | private $data; 29 | 30 | /** 31 | * Encoder constructor 32 | * 33 | * @param mixed $data The value to encode. 34 | * @return void 35 | */ 36 | private function __construct($data) 37 | { 38 | $this->data = $data; 39 | } 40 | 41 | /** 42 | * Encode a value into a bencode encoded string 43 | * 44 | * @param mixed $data The value to encode. 45 | * @return string Returns the bencode encoded string. 46 | */ 47 | public static function encode($data) 48 | { 49 | if (is_object($data)) { 50 | if (method_exists($data, "toArray")) { 51 | $data = $data->toArray(); 52 | } else { 53 | $data = (array) $data; 54 | } 55 | } 56 | 57 | $encoder = new self($data); 58 | $encoded = $encoder->doEncode(); 59 | 60 | return $encoded; 61 | } 62 | 63 | /** 64 | * Iterate over values and encode them 65 | * 66 | * @param mixed $data The value to encode. 67 | * @return string Returns the bencode encoded string. 68 | */ 69 | private function doEncode($data = null) 70 | { 71 | $data = is_null($data) ? $this->data : $data; 72 | 73 | if (is_array($data) && (isset ($data[0]) || empty ($data))) { 74 | return $this->encodeList($data); 75 | } elseif (is_array($data)) { 76 | return $this->encodeDict($data); 77 | } elseif (is_integer($data) || is_float($data)) { 78 | $data = sprintf("%.0f", round($data, 0)); 79 | return $this->encodeInteger($data); 80 | } else { 81 | return $this->encodeString($data); 82 | } 83 | } 84 | 85 | /** 86 | * Encode an integer 87 | * 88 | * @param integer $data The integer to be encoded. 89 | * @return string Returns the bencode encoded integer. 90 | */ 91 | private function encodeInteger($data = null) 92 | { 93 | $data = is_null($data) ? $this->data : $data; 94 | return sprintf("i%.0fe", $data); 95 | } 96 | 97 | /** 98 | * Encode a string 99 | * 100 | * @param string $data The string to be encoded. 101 | * @return string Returns the bencode encoded string. 102 | */ 103 | private function encodeString($data = null) 104 | { 105 | $data = is_null($data) ? $this->data : $data; 106 | return sprintf("%d:%s", strlen($data), $data); 107 | } 108 | 109 | /** 110 | * Encode a list 111 | * 112 | * @param array $data The list to be encoded. 113 | * @return string Returns the bencode encoded list. 114 | */ 115 | private function encodeList(array $data = null) 116 | { 117 | $data = is_null($data) ? $this->data : $data; 118 | 119 | $list = ""; 120 | foreach ($data as $value) { 121 | $list .= $this->doEncode($value); 122 | } 123 | 124 | return "l{$list}e"; 125 | } 126 | 127 | /** 128 | * Encode a dictionary 129 | * 130 | * @param array $data The dictionary to be encoded. 131 | * @return string Returns the bencode encoded dictionary. 132 | */ 133 | private function encodeDict(array $data = null) 134 | { 135 | $data = is_null($data) ? $this->data : $data; 136 | ksort($data); // bencode spec requires dicts to be sorted alphabetically 137 | 138 | $dict = ""; 139 | foreach ($data as $key => $value) { 140 | $dict .= $this->encodeString($key) . $this->doEncode($value); 141 | } 142 | 143 | return "d{$dict}e"; 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /src/Exception.php: -------------------------------------------------------------------------------- 1 | 10 | * @license MIT License - http://www.opensource.org/licenses/mit-license.php 11 | */ 12 | 13 | namespace Rych\Bencode; 14 | 15 | /** 16 | * Exception marker interface 17 | */ 18 | interface Exception 19 | { 20 | } 21 | -------------------------------------------------------------------------------- /src/Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | 10 | * @license MIT License - http://www.opensource.org/licenses/mit-license.php 11 | */ 12 | 13 | namespace Rych\Bencode\Exception; 14 | 15 | use Rych\Bencode\Exception; 16 | 17 | /** 18 | * Runtime exception 19 | */ 20 | class RuntimeException extends \RuntimeException implements Exception 21 | { 22 | } 23 | --------------------------------------------------------------------------------