mirror of
https://github.com/OPSnet/bencode-torrent.git
synced 2026-11-08 17:35:34 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da95cbacce | ||
|
|
c0e4100135 | ||
|
|
d634179b9b | ||
|
|
007c0fe9f3 | ||
|
|
d95a0c1f8c | ||
|
|
6b9d56bfc7 |
No files matched your search
@@ -8,6 +8,11 @@ php:
|
||||
- '7.2'
|
||||
- nightly
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
allow_failures:
|
||||
- php: nightly
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.composer/cache
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
BEncode Torrent
|
||||
===============
|
||||
|
||||
[](https://travis-ci.org/ApolloRIP/bencode-torrent)
|
||||
[](https://packagist.org/packages/apollorip/bencode-torrent)
|
||||

|
||||
|
||||
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
@@ -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
@@ -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
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user