6 Commits
Author SHA1 Message Date
itismadness da95cbacce Update readme for 0.10.0 release 2018-07-02 01:41:16 -04:00
itismadness c0e4100135 Fix bencode string 2018-07-02 01:34:30 -04:00
itismadness d634179b9b allow PHP nightly to fail on travis 2018-07-02 01:31:27 -04:00
itismadness 007c0fe9f3 Improve handling of error conditions in BT spec 2018-07-01 22:55:34 -04:00
it is madness d95a0c1f8c Merge pull request #1 from ChrisOrlando/fix
Fix infinite loop when dict value is int 0
2018-07-01 16:38:07 -04:00
Chris Orlando 6b9d56bfc7 Fix infinite loop when dict value is int 0 2018-06-25 21:34:10 -04:00
6 changed files with 270 additions and 130 deletions

No files matched your search

+5
View File
@@ -8,6 +8,11 @@ php:
- '7.2'
- nightly
matrix:
fast_finish: true
allow_failures:
- php: nightly
cache:
directories:
- $HOME/.composer/cache
+11
View File
@@ -1,9 +1,20 @@
BEncode Torrent
===============
[![Build Status](https://travis-ci.org/ApolloRIP/bencode-torrent.svg?branch=master)](https://travis-ci.org/ApolloRIP/bencode-torrent)
[![Packagist](https://img.shields.io/packagist/v/apollorip/bencode-torrent.svg)](https://packagist.org/packages/apollorip/bencode-torrent)
![PHP from Travis config](https://img.shields.io/travis/php-v/ApolloRIP/bencode-torrent.svg)
PHP library for encoding and decoding BitTorrent BEncode data, focused around
[Gazelle](https://github.com/ApolloRIP/Gazelle).
Usage
-----
`composer require apollorip/bencode-torrent`
Description
-----------
BEncode is the encoding used by BitTorrent to store and transmitting loosely structured data. It supports
* byte strings
* integers
+175
View File
@@ -0,0 +1,175 @@
<?php
namespace ApolloRIP\BencodeTorrent;
class Bencode {
protected $data = null;
/**
* Sets the internal data array
* @param mixed $data
* @throws \RuntimeException
*/
public function setData($data) {
$this->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;
}
}
+8 -125
View File
@@ -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) || 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
+67
View File
@@ -0,0 +1,67 @@
<?php
namespace ApolloRip\BencodeTorrent;
class BencodeTest extends \PHPUnit\Framework\TestCase {
public function dataProvider() {
return [
['i0e', 0],
['i-1e', -1],
['i20e', 20],
['le', []],
['d4:item5:valuee', ['item' => '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:valuee');
}
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');
}
}
+4 -5
View File
@@ -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(