8 Commits
7 changed files with 292 additions and 144 deletions

No files matched your search

+6
View File
@@ -6,8 +6,14 @@ php:
- '7.0'
- '7.1'
- '7.2'
- '7.3'
- nightly
matrix:
fast_finish: true
allow_failures:
- php: nightly
cache:
directories:
- $HOME/.composer/cache
+22 -5
View File
@@ -1,8 +1,25 @@
BEncode Torrent
===============
PHP library for encoding and decoding BitTorrent BEncode data, focused around
[Gazelle](https://github.com/ApolloRIP/Gazelle).
[[![Build Status](https://travis-ci.org/OPSnet/bencode-torrent.svg?branch=master)](https://travis-ci.org/OPSnet/bencode-torrent)
[![Packagist](https://img.shields.io/packagist/v/orpheusnet/bencode-torrent.svg)](https://packagist.org/packages/orpheusnet/bencode-torrent)
![PHP from Travis config](https://img.shields.io/travis/php-v/OPSnet/bencode-torrent.svg)
PHP library for encoding and decoding BitTorrent BEncode data, focused around usage within
[Gazelle](https://github.com/OPSnet/Gazelle).
Usage
-----
`composer require orpheusnet/bencode-torrent`
```php
use OrpheusNET\BencodeTorrent;
$bencode = new Bencode();
$bencode->decodeFile('path/to/file.torrent');
```
Description
-----------
BEncode is the encoding used by BitTorrent to store and transmitting loosely structured data. It supports
* byte strings
@@ -23,7 +40,7 @@ As such, this library will make some checks when loading data that these mandato
raised. More information on these fields can be found at
[BitTorrentSpecification#Metainfo_File_Structure](https://wiki.theory.org/index.php/BitTorrentSpecification#Metainfo_File_Structure).
Finally, this library is primarily aimed at being used within the [Gazelle](https://github.com/ApolloRIP/Gazelle) so
Finally, this library is primarily aimed at being used within the [Gazelle](https://github.com/OPSnet/Gazelle) so
we have some utility functions within the library that make sense there to accomplish the following things:
* Ensuring torrent files are marked as 'private'
* Setting a 'source' on torrents (to ensure unique info hash)
@@ -32,5 +49,5 @@ we have some utility functions within the library that make sense there to accom
This is based (loosely) off the code in the two separate BEncode libraries within WCD's Gazelle
([bencodetorrent.class.php](https://github.com/WhatCD/Gazelle/blob/master/classes/bencodetorrent.class.php) and
[torrent.class.php](https://github.com/WhatCD/Gazelle/blob/master/classes/torrent.class.php)), but without the
necessary 32bit shims as well as make it a unified library used for both uploading and downloading the torrent files.
[torrent.class.php](https://github.com/WhatCD/Gazelle/blob/master/classes/torrent.class.php)), but without the now
unnecessary 32bit shims as well as making it a unified library used for both uploading and downloading the torrent files.
+8 -7
View File
@@ -1,24 +1,25 @@
{
"type": "library",
"name": "apollorip/bencode-torrent",
"name": "orpheusnet/bencode-torrent",
"description": "PHP Library for decoding and encoding BitTorrent BEncoded data, built for Gazelle",
"license": "Unlicense",
"authors": [
{
"name": "itismadness",
"email": "[email protected]"
"name": "Orpheus",
"email": "[email protected]"
}
],
"autoload": {
"psr-4": {
"ApolloRIP\\BencodeTorrent\\": "src/"
"OrpheusNET\\BencodeTorrent\\": "src/"
}
},
"require": {},
"require": {
"php": "^7"
},
"require-dev": {
"phpunit/phpunit": "^6",
"phpunit/phpunit": "^6|^7",
"squizlabs/php_codesniffer": "3.*",
"php": "^7",
"ext-mbstring": "^7"
}
}
+175
View File
@@ -0,0 +1,175 @@
<?php
namespace OrpheusNET\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;
}
}
+9 -126
View File
@@ -1,6 +1,6 @@
<?php
namespace ApolloRip\BencodeTorrent;
namespace OrpheusNET\BencodeTorrent;
/**
* BEncode service that allows us to encode PHP objects into BEncode and decode
@@ -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 OrpheusNET\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');
}
}
+5 -6
View File
@@ -1,10 +1,8 @@
<?php
namespace ApolloRip\BencodeTorrent;
namespace OrpheusNET\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(