From 007c0fe9f3c04205b6e1c73d010a35ab98552d20 Mon Sep 17 00:00:00 2001 From: itismadness Date: Sun, 1 Jul 2018 22:55:02 -0800 Subject: [PATCH] Improve handling of error conditions in BT spec --- src/Bencode.php | 175 +++++++++++++++++++++++++++++++++++ src/BencodeTorrent.php | 133 ++------------------------ tests/BencodeTest.php | 67 ++++++++++++++ tests/BencodeTorrentTest.php | 9 +- 4 files changed, 254 insertions(+), 130 deletions(-) create mode 100644 src/Bencode.php create mode 100644 tests/BencodeTest.php diff --git a/src/Bencode.php b/src/Bencode.php new file mode 100644 index 0000000..68ce2dc --- /dev/null +++ b/src/Bencode.php @@ -0,0 +1,175 @@ +data = $data; + } + + /** + * Given a BEncoded string and decode it + * @param string $data + * @throws \RuntimeException + */ + public function decodeString(string $data) { + $this->data = $this->decode($data); + } + + /** + * Given a path to a file, decode the contents of it + * + * @param string $path + * @throws \RuntimeException + */ + public function decodeFile(string $path) { + $this->data = $this->decode(file_get_contents($path, FILE_BINARY)); + } + + /** + * Decodes a BEncoded string to the following values: + * - Dictionary (starts with d, ends with e) + * - List (starts with l, ends with e + * - Integer (starts with i, ends with e + * - String (starts with number denoting number of characters followed by : and then the string) + * + * @see https://wiki.theory.org/index.php/BitTorrentSpecification + * + * @param string $data + * @param int $pos + * @return mixed + */ + protected function decode(string $data, int &$pos = 0) { + $start_decode = $pos === 0; + if ($data[$pos] === 'd') { + $pos++; + $return = []; + while ($data[$pos] !== 'e') { + $key = $this->decode($data, $pos); + $value = $this->decode($data, $pos); + if ($key === null || $value === null) { + break; + } + if (!is_string($key)) { + throw new \RuntimeException('Invalid key type, must be string: '.gettype($key)); + } + $return[$key] = $value; + } + ksort($return); + $pos++; + } + elseif ($data[$pos] === 'l') { + $pos++; + $return = []; + while ($data[$pos] !== 'e') { + $value = $this->decode($data, $pos); + $return[] = $value; + } + $pos++; + } + elseif ($data[$pos] === 'i') { + $pos++; + $digits = strpos($data, 'e', $pos) - $pos; + $return = substr($data, $pos, $digits); + if ($return === '-0') { + throw new \RuntimeException('Cannot have integer value -0'); + } + $multiplier = 1; + if ($return[0] === '-') { + $multiplier = -1; + $return = substr($return, 1); + } + if (!ctype_digit($return)) { + $msg = 'Cannot have non-digit values in integer number: '.$return; + throw new \RuntimeException($msg); + } + $return = $multiplier * ((int) $return); + $pos += $digits + 1; + } + else { + $digits = strpos($data, ':', $pos) - $pos; + $len = (int) substr($data, $pos, $digits); + $pos += ($digits + 1); + $return = substr($data, $pos, $len); + $pos += $len; + } + if ($start_decode) { + if ($pos !== strlen($data)) { + throw new \RuntimeException('Could not fully decode bencode string'); + } + } + return $return; + } + + /** + * Get the internal data array + * @return mixed + */ + public function getData() { + return $this->data; + } + + /** + * @throws \RuntimeException + */ + protected function hasData() { + if ($this->data === null) { + throw new \RuntimeException('Must decode proper bencode string first'); + } + } + + /** + * @return string + */ + public function getEncode() : string { + $this->hasData(); + return $this->encodeVal($this->data); + } + + /** + * @param mixed $data + * @return string + */ + protected function encodeVal($data) : string { + if (is_array($data)) { + $return = ''; + $check = -1; + $list = true; + foreach ($data as $key => $value) { + if ($key !== ++$check) { + $list = false; + break; + } + } + + if ($list) { + $return .= 'l'; + foreach ($data as $value) { + $return .= $this->encodeVal($value); + } + } + else { + $return .= 'd'; + foreach ($data as $key => $value) { + $return .= $this->encodeVal(strval($key)); + $return .= $this->encodeVal($value); + } + } + $return .= 'e'; + } + elseif (is_integer($data)) { + $return = 'i'.$data.'e'; + } + else { + $return = strlen($data) . ':' . $data; + } + return $return; + } +} diff --git a/src/BencodeTorrent.php b/src/BencodeTorrent.php index c6e9c81..95e19af 100644 --- a/src/BencodeTorrent.php +++ b/src/BencodeTorrent.php @@ -24,12 +24,10 @@ namespace ApolloRip\BencodeTorrent; * For Gazelle, this also acts as a unification of the two original BEncode implementations * which were both used in separate areas of the codebase. */ -class BencodeTorrent { +class BencodeTorrent extends Bencode { const FILELIST_DELIM = 0xF7; private static $utf8_filelist_delim = null; - private $data; - public function __construct() { $this->setDelim(); } @@ -50,8 +48,8 @@ class BencodeTorrent { * @param array $data * @throws \RuntimeException */ - public function setData(array $data) { - $this->data = $data; + public function setData($data) { + parent::setData($data); $this->validate(); } @@ -61,7 +59,7 @@ class BencodeTorrent { * @throws \RuntimeException */ public function decodeString(string $data) { - $this->data = $this->decode($data); + parent::decodeString($data); $this->validate(); } @@ -72,76 +70,18 @@ class BencodeTorrent { * @throws \RuntimeException */ public function decodeFile(string $path) { - $this->data = $this->decode(file_get_contents($path, FILE_BINARY)); + parent::decodeFile($path); $this->validate(); } - /** - * Decodes a BEncoded string to the following values: - * - Dictionary (starts with d, ends with e) - * - List (starts with l, ends with e - * - Integer (starts with i, ends with e - * - String (starts with number denoting number of characters followed by : and then the string) - * - * @see https://wiki.theory.org/index.php/BitTorrentSpecification - * - * @param string $data - * @param int $pos - * @return array|bool|float|string - */ - private function decode(string $data, int &$pos = 0) { - if ($data[$pos] === 'd') { - $pos++; - $return = []; - while ($data[$pos] !== 'e') { - $key = $this->decode($data, $pos); - $value = $this->decode($data, $pos); - if (empty($key) || ($value !== 0 and empty($value))) { - break; - } - $return[$key] = $value; - } - ksort($return); - $pos++; - } - elseif ($data[$pos] === 'l') { - $pos++; - $return = []; - while ($data[$pos] !== 'e') { - $value = $this->decode($data, $pos); - $return[] = $value; - } - $pos++; - } - elseif ($data[$pos] === 'i') { - $pos++; - $digits = strpos($data, 'e', $pos) - $pos; - $return = (int) substr($data, $pos, $digits); - $pos += $digits + 1; - } - else { - $digits = strpos($data, ':', $pos) - $pos; - $len = (int) substr($data, $pos, $digits); - $pos += ($digits + 1); - $return = substr($data, $pos, $len); - $pos += $len; - } - return $return; - } - - /** - * Get the internal data array - * @return array - */ - public function getData() : array { - return $this->data; - } - /** * Validates that the internal data array * @throws \RuntimeException */ public function validate() { + if (!is_array($this->data)) { + throw new \TypeError('Data must be an array'); + } if (empty($this->data['info'])) { throw new \RuntimeException("Torrent dictionary doesn't have info key"); } @@ -163,63 +103,6 @@ class BencodeTorrent { } } - /** - * @throws \RuntimeException - */ - private function hasData() { - if (empty($this->data) || !is_array($this->data)) { - throw new \RuntimeException('Must decode proper bencode string first'); - } - } - - /** - * @return string - */ - public function getEncode() : string { - $this->hasData(); - return $this->encodeVal($this->data); - } - - /** - * @param mixed $data - * @return string - */ - private function encodeVal($data) : string { - if (is_array($data)) { - $return = ''; - $check = -1; - $list = true; - foreach ($data as $key => $value) { - if ($key !== ++$check) { - $list = false; - break; - } - } - - if ($list) { - $return .= 'l'; - foreach ($data as $value) { - $return .= $this->encodeVal($value); - } - } - else { - $return .= 'd'; - foreach ($data as $key => $value) { - $return .= $this->encodeVal(strval($key)); - $return .= $this->encodeVal($value); - } - } - $return .= 'e'; - } - elseif (is_integer($data)) { - $return = 'i'.$data.'e'; - } - else { - $return = strlen($data) . ':' . $data; - } - return $return; - } - /** * Utility function to clean out keys in the data and info dictionaries that we don't need in * our torrent file when we go to store it in the DB or serve it up to the user (with the diff --git a/tests/BencodeTest.php b/tests/BencodeTest.php new file mode 100644 index 0000000..5cb1b8a --- /dev/null +++ b/tests/BencodeTest.php @@ -0,0 +1,67 @@ + 'value']], + ['0:', ''], + ['1:a', 'a'], + ['9:123456789', '123456789'] + ]; + } + + /** + * @dataProvider dataProvider + * @param string $bencoded_string + * @param mixed $expected + */ + public function testDecodeEncode($bencoded_string, $expected) { + $bencode = new Bencode(); + $bencode->decodeString($bencoded_string); + $this->assertEquals($expected, $bencode->getData()); + $this->assertEquals($bencoded_string, $bencode->getEncode()); + } + + public function testEmptyDict() { + $bencode = new Bencode(); + $bencode->decodeString('de'); + $this->assertEquals([], $bencode->getData()); + $this->assertEquals('le', $bencode->getEncode()); + } + + /** + * @expectedException \RuntimeException + */ + public function testInvalidDictionaryKey() { + $bencode = new Bencode(); + $bencode->decodeString('di1e5:value'); + } + + public function invalidIntegers() { + return [['-0'], ['a'], ['1.0']]; + } + + /** + * @param string $value + * @dataProvider invalidIntegers + * @expectedException \RuntimeException + */ + public function testInvalidInteger(string $value) { + $bencode = new Bencode(); + $bencode->decodeString("i{$value}e"); + } + + /** + * @expectedException \RuntimeException + */ + public function testInvalidString() { + $bencode = new Bencode(); + $bencode->decodeString('i1e0e'); + } +} diff --git a/tests/BencodeTorrentTest.php b/tests/BencodeTorrentTest.php index 8de575b..949035f 100644 --- a/tests/BencodeTorrentTest.php +++ b/tests/BencodeTorrentTest.php @@ -2,9 +2,7 @@ namespace ApolloRip\BencodeTorrent; -use PHPUnit\Framework\TestCase; - -class BencodeTorrentTest extends TestCase { +class BencodeTorrentTest extends \PHPUnit\Framework\TestCase { public function testLoadTorrent() { $bencode = new BencodeTorrent(); try { @@ -183,8 +181,9 @@ class BencodeTorrentTest extends TestCase { 'info' => [ 'name' => 'test', 'length' => 1213134, - 'pieces' => 'fake pieces string' - ] + 'pieces' => 'fake pieces string', + ], + 'test' => '' ]; $bencode = new BencodeTorrent(); $bencode->decodeString(