31 Commits
Author SHA1 Message Date
itismadness fe026fb3a3 Fix path to create BencodeTorrent 2023-10-19 05:25:23 +00:00
itismadness 922a60224d Update README usage info 2023-10-19 05:20:30 +00:00
itismadness a56d7d85c2 Update test README badge 2023-10-13 00:12:17 +00:00
itismadness d2dacb03ec Add empty data validate test 2023-10-13 00:09:21 +00:00
itismadness 9a030b6823 Add phpunit cache to gitignore 2023-10-13 00:01:20 +00:00
itismadness 2addd8d588 Update minimum PHP to 8.1 2023-10-13 00:00:15 +00:00
itismadness ea558fc679 Replace deprecated utf8_encode function 2023-10-12 23:59:53 +00:00
itismadness 42f5170482 Mark as php 8.0 compatible 2021-08-22 14:43:26 +00:00
itismadness 917cf865a9 allow empty components on file path so long as not totally empty 2020-10-12 00:27:44 +00:00
itismadness ca9617422a add getCreationDate method 2020-10-03 20:31:03 +00:00
itismadness 5ce1e6c6cc remove unnecessary devDependency 2020-10-03 19:17:16 +00:00
itismadness 6778e228fb update README 2020-10-03 19:16:39 +00:00
itismadness 9748eef34d move to github actions for testing 2020-10-03 19:07:52 +00:00
itismadness 3efb191299 add phpstan for testing 2020-10-03 19:01:33 +00:00
itismadness bdbc17e173 enable strict types 2020-10-03 19:01:22 +00:00
itismadness a9229a067b move to PSR12 coding standard 2020-10-03 18:47:30 +00:00
itismadness 93707559cc update to phpunit 8 2020-10-03 18:42:13 +00:00
itismadness 1b414569bd add getSource function 2020-10-03 18:35:07 +00:00
itismadness e07f029d12 Add back in PHP 7.2 to test Travis 2018-11-10 12:30:07 -05:00
itismadness 95374ae069 Test TravisCI not building commits 2018-11-10 12:19:25 -05:00
it is madness 11c2e4519c Update README.md 2018-11-10 12:15:38 -05:00
itismadness 911e2899f4 Update readme and test against PHP 7.3 2018-11-10 11:44:14 -05:00
itismadness dd0243afeb Update namespace. Apollo is dead, long live Orpheus. 2018-11-10 10:34:53 -05:00
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
itismadness 5b94cf1056 Add another function for Gazelle (0.9.2) 2018-04-12 16:59:30 -04:00
itismadness 74929fdac8 Add 'hasFiles()' method for Gazelle upload check 2018-04-11 08:41:00 -04:00
12 changed files with 658 additions and 320 deletions

No files matched your search

+46
View File
@@ -0,0 +1,46 @@
name: Test
on:
push:
branches: [ '*' ]
pull_request:
branches: [ '*' ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
php-version: ['8.1', '8.2']
steps:
- uses: actions/checkout@v2
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
tools: composer, pecl
coverage: pcov
- name: Validate composer.json and composer.lock
run: composer validate
- name: Cache Composer packages
id: composer-cache
uses: actions/cache@v2
with:
path: vendor
key: ${{ runner.os }}-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-${{ matrix.php-version }}-composer-
${{ runner.os }}-composer-
- name: Install dependencies
if: steps.composer-cache.outputs.cache-hit != 'true'
run: composer install --prefer-dist --no-progress --no-suggest
- run: composer lint
- run: composer stan
- run: composer test
+2
View File
@@ -3,3 +3,5 @@ report/
vendor/
.DS_Store
composer.lock
.phpunit.result.cache
.phpunit.cache
-21
View File
@@ -1,21 +0,0 @@
language: php
sudo: true
dist: trusty
php:
- '7.0'
- '7.1'
- '7.2'
- nightly
cache:
directories:
- $HOME/.composer/cache
install:
- composer install
script:
- vendor/bin/phpunit
- vendor/bin/phpcs src
- vendor/bin/phpcs tests
+33 -7
View File
@@ -1,16 +1,41 @@
BEncode Torrent
===============
PHP library for encoding and decoding BitTorrent BEncode data, focused around
[Gazelle](https://github.com/ApolloRIP/Gazelle).
[![Test](https://github.com/OPSnet/bencode-torrent/actions/workflows/test.yml/badge.svg?branch=master&event=push)](https://github.com/OPSnet/bencode-torrent/actions/workflows/test.yml)
[![Packagist](https://img.shields.io/packagist/v/orpheusnet/bencode-torrent.svg)](https://packagist.org/packages/orpheusnet/bencode-torrent)
![Packagist PHP Version Support](https://img.shields.io/packagist/php-v/orpheusnet/bencode-torrent)
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
require('vendor/autoload.php');
$bencode = new \OrpheusNET\BencodeTorrent\BencodeTorrent();
$bencode->decodeFile('path/to/file.torrent');
var_dump($bencode->getName());
var_dump($bencode->getHexInfoHash());
var_dump($bencode->getFileList());
```
See the [`BencodeTorrent`](src/BencodeTorrent.php) class for more information on the available methods.
Additionally, see [`Bencode`](src/Bencode.php) for the underlying library to encode/decode BEncode data.
Description
-----------
BEncode is the encoding used by BitTorrent to store and transmitting loosely structured data. It supports:
BEncode is the encoding used by BitTorrent to store and transmitting loosely structured data. It supports
* byte strings
* integers
* lists
* dictionaries (associative arrays, where keys are sorted alphabetically)
You can see more information about how these types are supported at
You can see more information about how these types are supported at
[BitTorrentSpecification#Bencoding](https://wiki.theory.org/index.php/BitTorrentSpecification#Bencoding).
In addition to the above, torrent files are expected to be BEncoded dictionaries that contain minimally the keys
@@ -23,8 +48,9 @@ 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)
* Cleaning out unnecessary fields that also reveal stuff about a user (like __announce list__ and __created by__)
@@ -32,5 +58,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.
+17 -9
View File
@@ -1,24 +1,32 @@
{
"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": ">=8.1",
"ext-mbstring": "*"
},
"require-dev": {
"phpunit/phpunit": "^6",
"squizlabs/php_codesniffer": "3.*",
"php": "^7",
"ext-mbstring": "^7"
"phpunit/phpunit": "^10.4.1",
"squizlabs/php_codesniffer": "^3.7.2",
"phpstan/phpstan": "^1.10.38"
},
"scripts": {
"test": "phpunit",
"lint": "phpcs",
"lint:fix": "phpcbf",
"stan": "phpstan analyse"
}
}
+5 -36
View File
@@ -1,38 +1,7 @@
<?xml version="1.0"?>
<ruleset name="Gazelle Standard">
<description>The coding standard for the Gazelle project</description>
<ruleset name="PSR12 Standard">
<file>src</file>
<file>tests</file>
<!-- Include the whole PSR-1 standard -->
<rule ref="PSR1"/>
<!-- Include the whole PSR-2 standard -->
<rule ref="PSR2">
<exclude name="PSR2.Classes.ClassDeclaration.OpenBraceNewLine"/>
</rule>
<!-- Do not allow trailing whitespace at the end of lines -->
<rule ref="Squiz.WhiteSpace.SuperfluousWhitespace" />
<rule ref="Squiz.Functions.MultiLineFunctionDeclaration.BraceOnSameLine">
<severity>0</severity>
</rule>
<rule ref="Squiz.ControlStructures.ControlSignature.SpaceAfterCloseBrace">
<severity>0</severity>
</rule>
<rule ref="Generic.Classes.OpeningBraceSameLine"/>
<rule ref="Generic.Functions.OpeningFunctionBraceKernighanRitchie"/>
<!-- Ensure that proper line endings are used -->
<rule ref="Generic.Files.LineEndings">
<properties>
<property name="eolChar" value="\n"/>
</properties>
</rule>
<!-- Increase PHPCS/PEAR limit of 85 chars per line to 100 -->
<rule ref="Generic.Files.LineLength">
<properties>
<property name="lineLimit" value="100"/>
<property name="absoluteLineLimit" value="0"/>
</properties>
</rule>
</ruleset>
<rule ref="PSR12" />
</ruleset>
+5
View File
@@ -0,0 +1,5 @@
parameters:
level: 5
paths:
- src
- tests
+20 -19
View File
@@ -1,19 +1,20 @@
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/6.5/phpunit.xsd"
colors="true"
bootstrap="vendor/autoload.php">
<testsuites>
<testsuite name="unit">
<directory>tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">src</directory>
<exclude />
</whitelist>
</filter>
<logging>
<log type="coverage-html" target="report" />
</logging>
</phpunit>
<?xml version="1.0"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.4/phpunit.xsd" colors="true" bootstrap="vendor/autoload.php" cacheDirectory=".phpunit.cache">
<coverage>
<report>
<html outputDirectory="report"/>
</report>
</coverage>
<testsuites>
<testsuite name="unit">
<directory>tests</directory>
</testsuite>
</testsuites>
<logging/>
<source>
<include>
<directory suffix=".php">src</directory>
</include>
<exclude/>
</source>
</phpunit>
+180
View File
@@ -0,0 +1,180 @@
<?php
declare(strict_types=1);
namespace OrpheusNET\BencodeTorrent;
class Bencode
{
/** @var array */
protected $data;
/**
* Sets the internal data array
* @param mixed $data
*/
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));
}
/**
* 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;
}
}
+104 -185
View File
@@ -1,6 +1,8 @@
<?php
namespace ApolloRip\BencodeTorrent;
declare(strict_types=1);
namespace OrpheusNET\BencodeTorrent;
/**
* BEncode service that allows us to encode PHP objects into BEncode and decode
@@ -24,13 +26,14 @@ 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 {
const FILELIST_DELIM = 0xF7;
class BencodeTorrent extends Bencode
{
public const FILELIST_DELIM = 0xF7;
/** @var string */
private static $utf8_filelist_delim = null;
private $data;
public function __construct() {
public function __construct()
{
$this->setDelim();
}
@@ -39,9 +42,10 @@ class BencodeTorrent {
* and char to set a class constant or variable, so we wait till the class is initialized
* for the first time to set it.
*/
private function setDelim() {
private function setDelim(): void
{
if (self::$utf8_filelist_delim === null) {
self::$utf8_filelist_delim = utf8_encode(chr(self::FILELIST_DELIM));
self::$utf8_filelist_delim = mb_convert_encoding(chr(self::FILELIST_DELIM), 'UTF-8', 'ISO-8859-1');
}
}
@@ -50,8 +54,9 @@ class BencodeTorrent {
* @param array $data
* @throws \RuntimeException
*/
public function setData(array $data) {
$this->data = $data;
public function setData($data): void
{
parent::setData($data);
$this->validate();
}
@@ -60,8 +65,9 @@ class BencodeTorrent {
* @param string $data
* @throws \RuntimeException
*/
public function decodeString(string $data) {
$this->data = $this->decode($data);
public function decodeString(string $data): void
{
parent::decodeString($data);
$this->validate();
}
@@ -71,77 +77,21 @@ class BencodeTorrent {
* @param string $path
* @throws \RuntimeException
*/
public function decodeFile(string $path) {
$this->data = $this->decode(file_get_contents($path, FILE_BINARY));
public function decodeFile(string $path): void
{
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
* @throws \TypeError|\RuntimeException
*/
public function validate() {
public function validate(): void
{
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");
}
@@ -155,71 +105,14 @@ class BencodeTorrent {
return strlen($element) === 0;
}
);
if (count($filter) > 0) {
throw new \RuntimeException('Cannot have empty path for a file');
if (count($filter) === count($file[$path_key]) && $file['length'] !== 0) {
throw new \RuntimeException('Cannot have a file with no path');
}
}
}
}
}
/**
* @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
@@ -228,7 +121,8 @@ class BencodeTorrent {
*
* @return bool flag to indicate if we altered the info dictionary
*/
public function clean() : bool {
public function clean(): bool
{
$this->cleanDataDictionary();
return $this->cleanInfoDictionary();
}
@@ -238,7 +132,8 @@ class BencodeTorrent {
* overwritten dynamically on any downloaded torrent (like announce or comment), so that we
* store the smallest encoded string within the database and cuts down on potential waste.
*/
public function cleanDataDictionary() {
public function cleanDataDictionary(): void
{
$allowed_keys = array('encoding', 'info');
foreach ($this->data as $key => $value) {
if (!in_array($key, $allowed_keys)) {
@@ -261,7 +156,8 @@ class BencodeTorrent {
*
* @return bool
*/
public function cleanInfoDictionary() : bool {
public function cleanInfoDictionary(): bool
{
$cleaned = false;
$allowed_keys = array('files', 'name', 'piece length', 'pieces', 'private', 'length',
'name.utf8', 'name.utf-8', 'md5sum', 'sha1', 'source',
@@ -281,7 +177,8 @@ class BencodeTorrent {
*
* @return bool
*/
public function isPrivate() : bool {
public function isPrivate(): bool
{
$this->hasData();
return isset($this->data['info']['private']) && $this->data['info']['private'] === 1;
}
@@ -295,7 +192,8 @@ class BencodeTorrent {
*
* @return bool
*/
public function makePrivate() : bool {
public function makePrivate(): bool
{
$this->hasData();
if ($this->isPrivate()) {
return false;
@@ -311,12 +209,9 @@ class BencodeTorrent {
* 'official' flag (no accepted BEP on it), but it has become the defacto standard with more
* clients supporting it natively. Returns a boolean on whether or not the source was changed
* so that an appropriate screen can be shown to the user.
*
* @param string $source
*
* @return bool true if the source was set/changed, false if no change
*/
public function setSource(string $source) : bool {
public function setSource(string $source): bool
{
$this->hasData();
if (isset($this->data['info']['source']) && $this->data['info']['source'] === $source) {
return false;
@@ -329,15 +224,32 @@ class BencodeTorrent {
return true;
}
/**
* Get the source flag if one has been set
*/
public function getSource(): ?string
{
$this->hasData();
return $this->data['info']['source'] ?? null;
}
/**
* Get the creation date for torrent if one has been set
*/
public function getCreationDate(): ?int
{
$this->hasData();
return $this->data['creation date'] ?? null;
}
/**
* Function to allow you set any number of keys and values in the data dictionary. You can
* set the value in a dictionary by concatenating the keys into a string with a period
* separator (ex: info.name will set name field in the info dictionary) so that the rest
* of the dictionary is unaffected.
*
* @param array $array
*/
public function setValue(array $array) {
public function setValue(array $array): void
{
foreach ($array as $key => $value) {
if (is_array($value)) {
ksort($value);
@@ -370,19 +282,20 @@ class BencodeTorrent {
*
* @return string
*/
public function getInfoHash() : string {
public function getInfoHash(): string
{
$this->hasData();
return sha1($this->encodeVal($this->data['info']));
}
public function getHexInfoHash(): string {
public function getHexInfoHash(): string
{
return pack('H*', $this->getInfoHash());
}
/**
* @return string
*/
public function getName() {
public function getName(): string
{
$this->hasData();
if (isset($this->data['info']['name.utf-8'])) {
return $this->data['info']['name.utf-8'];
}
@@ -393,15 +306,14 @@ class BencodeTorrent {
* Get the total size in bytes of the files in the torrent. For a single file torrent, it'll
* just be the 'length' key in the 'info' dictionary, else we iterate through the 'files' list
* and add up the 'length' of each element.
*
* @return int
*/
public function getSize() : int {
public function getSize(): int
{
$this->hasData();
$cur_size = 0;
if (!isset($this->data['info']['files'])) {
$cur_size = $this->data['info']['length'];
}
else {
} else {
foreach ($this->data['info']['files'] as $file) {
$cur_size += $file['length'];
}
@@ -418,7 +330,8 @@ class BencodeTorrent {
*
* @return array
*/
public function getFileList() : array {
public function getFileList(): array
{
$files = [];
if (!isset($this->data['info']['files'])) {
// Single-file torrent
@@ -426,27 +339,32 @@ class BencodeTorrent {
$this->data['info']['name.utf-8'] :
$this->data['info']['name']);
$size = $this->data['info']['length'];
$files[] = array('name' => $name, 'size' => $size);
}
else {
$path_key = isset($this->data['info']['files'][0]['path.utf-8']) ?
'path.utf-8' :
'path';
$files[] = ['path' => $name, 'size' => $size];
} else {
$size = 0;
foreach ($this->data['info']['files'] as $file) {
$tmp_path = array();
foreach ($file[$path_key] as $sub_path) {
$tmp_path[] = $sub_path;
}
$files[] = array('name' => implode('/', $tmp_path), 'size' => $file['length']);
$size += $file['length'];
$path_key = isset($file['path.utf-8']) ? 'path.utf-8' : 'path';
$files[] = ['path' => implode('/', $file[$path_key]), 'size' => $file['length']];
}
uasort(
usort(
$files,
function ($a, $b) {
return strnatcasecmp($a['name'], $b['name']);
return strnatcasecmp($a['path'], $b['path']);
}
);
}
return $files;
return array('total_size' => $size, 'files' => $files);
}
public function hasFiles(): bool
{
return isset($this->data['info']['files']);
}
public function hasEncryptedFiles(): bool
{
return isset($this->data['encrypted_files']);
}
/**
@@ -459,17 +377,18 @@ class BencodeTorrent {
*
* @return array
*/
public function getGazelleFileList() : array {
public function getGazelleFileList(): array
{
$files = [];
foreach ($this->getFileList() as $file) {
$name = $file['name'];
foreach ($this->getFileList()['files'] as $file) {
$path = $file['path'];
$size = $file['size'];
$name = $this->makeUTF8(strtr($name, "\n\r\t", ' '));
$ext_pos = strrpos($name, '.');
$path = $this->makeUTF8(strtr($path, "\n\r\t", ' '));
$ext_pos = strrpos($path, '.');
// Should not be $ExtPos !== false. Extension-less files that start with a .
// should not get extensions
$ext = ($ext_pos ? trim(substr($name, $ext_pos + 1)) : '');
$files[] = sprintf("%s s%ds %s %s", ".$ext", $size, $name, self::$utf8_filelist_delim);
$ext = ($ext_pos ? trim(substr($path, $ext_pos + 1)) : '');
$files[] = sprintf("%s s%ds %s %s", ".$ext", $size, $path, self::$utf8_filelist_delim);
}
return $files;
}
@@ -481,7 +400,8 @@ class BencodeTorrent {
*
* @return string
*/
private function makeUTF8(string $str) : string {
private function makeUTF8(string $str): string
{
if (preg_match('//u', $str)) {
$encoding = 'UTF-8';
}
@@ -496,8 +416,7 @@ class BencodeTorrent {
// @codeCoverageIgnoreEnd
if ($encoding === 'UTF-8') {
return $str;
}
else {
} else {
return @mb_convert_encoding($str, 'UTF-8', $encoding);
}
}
+72
View File
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace OrpheusNET\BencodeTorrent;
class BencodeTest extends \PHPUnit\Framework\TestCase
{
public static function dataProvider(): array
{
return [
['i0e', 0],
['i-1e', -1],
['i20e', 20],
['le', []],
['d4:item5:valuee', ['item' => 'value']],
['0:', ''],
['1:a', 'a'],
['9:123456789', '123456789']
];
}
/**
* @dataProvider dataProvider
* @param mixed $expected
*/
public function testDecodeEncode(string $bencoded_string, $expected): void
{
$bencode = new Bencode();
$bencode->decodeString($bencoded_string);
$this->assertEquals($expected, $bencode->getData());
$this->assertEquals($bencoded_string, $bencode->getEncode());
}
public function testEmptyDict(): void
{
$bencode = new Bencode();
$bencode->decodeString('de');
$this->assertEquals([], $bencode->getData());
$this->assertEquals('le', $bencode->getEncode());
}
public function testInvalidDictionaryKey(): void
{
$bencode = new Bencode();
$this->expectException(\RuntimeException::class);
$bencode->decodeString('di1e5:valuee');
}
public static function invalidIntegers(): array
{
return [['-0'], ['a'], ['1.0']];
}
/**
* @param string $value
* @dataProvider invalidIntegers
*/
public function testInvalidInteger(string $value): void
{
$bencode = new Bencode();
$this->expectException(\RuntimeException::class);
$bencode->decodeString("i{$value}e");
}
public function testInvalidString(): void
{
$bencode = new Bencode();
$this->expectException(\RuntimeException::class);
$bencode->decodeString('i1e0e');
}
}
+174 -43
View File
@@ -1,16 +1,17 @@
<?php
namespace ApolloRip\BencodeTorrent;
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
namespace OrpheusNET\BencodeTorrent;
class BencodeTorrentTest extends TestCase {
public function testLoadTorrent() {
class BencodeTorrentTest extends \PHPUnit\Framework\TestCase
{
public function testLoadTorrent(): void
{
$bencode = new BencodeTorrent();
try {
$bencode->decodeFile(__DIR__.'/data/test_1.torrent');
}
catch (\Exception $exc) {
$bencode->decodeFile(__DIR__ . '/data/test_1.torrent');
} catch (\Exception $exc) {
$this->fail('Decode should not have thrown exception');
}
$data = $bencode->getData();
@@ -85,16 +86,69 @@ class BencodeTorrentTest extends TestCase {
$this->assertEquals(16020, strlen($data['info']['pieces']));
$this->assertEquals(1, $data['info']['private']);
$this->assertEquals('APL', $data['info']['source']);
$this->assertStringEqualsFile(__DIR__.'/data/test_1.torrent', $bencode->getEncode());
$this->assertStringEqualsFile(__DIR__ . '/data/test_1.torrent', $bencode->getEncode());
$this->assertEquals(104916260, $bencode->getSize());
$this->assertTrue($bencode->isPrivate());
$bencode->decodeString($bencode->getEncode());
$this->assertStringEqualsFile(__DIR__.'/data/test_1.torrent', $bencode->getEncode());
$this->assertStringEqualsFile(__DIR__ . '/data/test_1.torrent', $bencode->getEncode());
$this->assertEquals('1f830103427029a88dd5fde85be74e622ee07951', $bencode->getInfoHash());
$this->assertEquals($bencode->getInfoHash(), unpack('H*', $bencode->getHexInfoHash())[1]);
$this->assertFalse($bencode->hasEncryptedFiles());
$file_list = [
'total_size' => 104916260,
'files' => [
[
'size' => 9584196,
'path' => '01 Death with Dignity.mp3'
],
[
'size' => 12310347,
'path' => '02 Should have known better.mp3'
],
[
'size' => 8871591,
'path' => '03 All of me wants all of you.mp3'
],
[
'size' => 7942661,
'path' => '04 Drawn to the Blood.mp3'
],
[
'size' => 5878964,
'path' => '05 Eugene.mp3'
],
[
'size' => 11175567,
'path' => '06 Fourth of July.mp3'
],
[
'size' => 11367829,
'path' => '07 The Only Thing.mp3'
],
[
'size' => 7789055,
'path' => '08 Carrie & Lowell.mp3'
],
[
'size' => 12197480,
'path' => '09 John My Beloved.mp3'
],
[
'size' => 6438044,
'path' => '10 No shade in the shadow of the cross.mp3'
],
[
'size' => 11360526,
'path' => '11 Blue Bucket of Gold.mp3'
]
]
];
$this->assertEquals($file_list, $bencode->getFileList());
$this->assertSame(1425699508, $bencode->getCreationDate());
}
public function testSetData() {
public function testSetData(): void
{
$data = [
'encoding' => 'UTF8',
'info' => [
@@ -112,27 +166,41 @@ class BencodeTorrentTest extends TestCase {
$this->assertEquals('test', $bencode->getName());
$this->assertFalse($bencode->isPrivate());
$this->assertEquals(1213134, $bencode->getSize());
$this->assertEquals([['name' => 'test', 'size' => 1213134]], $bencode->getFileList());
$this->assertEquals(
[
'total_size' => 1213134,
'files' => [
[
'path' => 'test',
'size' => 1213134
]
]
],
$bencode->getFileList()
);
}
public function testEmptyDictionary() {
public function testEmptyDictionary(): void
{
$expected = [
'encoding' => 'UTF8',
'info' => [
'name' => 'test',
'length' => 1213134,
'pieces' => 'fake pieces string'
]
'pieces' => 'fake pieces string',
],
'test' => ''
];
$bencode = new BencodeTorrent();
$bencode->decodeString(
'd8:encoding4:UTF84:infod4:name4:test6:lengthi1213134e'.
'd8:encoding4:UTF84:infod4:name4:test6:lengthi1213134e' .
'6:pieces18:fake pieces stringe4:test0:e'
);
$this->assertEquals($expected, $bencode->getData());
}
public function testClean() {
public function testClean(): void
{
$data = [
'encoding' => 'UTF8',
'info' => [
@@ -165,7 +233,8 @@ class BencodeTorrentTest extends TestCase {
$this->assertEquals($data, $bencode->getData());
}
public function testSetPrivate() {
public function testSetPrivate(): void
{
$data = [
'encoding' => 'UTF8',
'info' => [
@@ -185,7 +254,8 @@ class BencodeTorrentTest extends TestCase {
$this->assertEquals(1, $actual['info']['private']);
}
public function testSetAlreadyPrivate() {
public function testSetAlreadyPrivate(): void
{
$data = [
'encoding' => 'UTF8',
'info' => [
@@ -199,7 +269,8 @@ class BencodeTorrentTest extends TestCase {
$this->assertFalse($bencode->makePrivate());
}
public function testSetSource() {
public function testSetSource(): void
{
$data = [
'encoding' => 'UTF8',
'info' => [
@@ -211,14 +282,17 @@ class BencodeTorrentTest extends TestCase {
];
$bencode = new BencodeTorrent();
$bencode->setData($data);
$this->assertNull($bencode->getSource());
$this->assertTrue($bencode->setSource('APL'));
$actual = $bencode->getData();
$this->assertEquals('APL', $actual['info']['source']);
$this->assertSame('APL', $bencode->getSource());
$this->assertFalse($bencode->setSource('APL'));
$this->assertNotContains('unique', $actual['info']);
$this->assertNotContains('x_cross_seed', $actual['info']);
}
public function testSetAlreadySourced() {
public function testSetAlreadySourced(): void
{
$data = [
'encoding' => 'UTF8',
'info' => [
@@ -236,7 +310,8 @@ class BencodeTorrentTest extends TestCase {
$this->assertEquals($data, $bencode->getData());
}
public function testSetDifferentSource() {
public function testSetDifferentSource(): void
{
$data = [
'encoding' => 'UTF8',
'info' => [
@@ -256,7 +331,8 @@ class BencodeTorrentTest extends TestCase {
$this->assertNotContains('x_cross_seed', $actual['info']);
}
public function testSetValue() {
public function testSetValue(): void
{
$data = [
'encoding' => 'UTF8',
'info' => [
@@ -310,9 +386,11 @@ class BencodeTorrentTest extends TestCase {
['.mp3 s1234s 01 Test??.mp3 ÷', '.mp3 s2345s 03 Test.mp3 ÷'],
$bencode->getGazelleFileList()
);
$this->assertTrue($bencode->hasFiles());
}
public function testGetUtf8Name() {
public function testGetUtf8Name(): void
{
$data = [
'encoding' => 'UTF8',
'info' => [
@@ -326,11 +404,23 @@ class BencodeTorrentTest extends TestCase {
$bencode->setData($data);
$this->assertEquals('test!!', $bencode->getName());
$this->assertEquals(12345, $bencode->getSize());
$this->assertEquals([['name' => 'test!!', 'size' => 12345]], $bencode->getFileList());
$this->assertEquals(
[
'total_size' => 12345,
'files' => [
[
'path' => 'test!!',
'size' => 12345
]
]
],
$bencode->getFileList()
);
$this->assertEquals(['. s12345s test!! ÷'], $bencode->getGazelleFileList());
}
public function testIso88591Name() {
public function testIso88591Name(): void
{
$data = [
'info' => [
'length' => 1234,
@@ -340,21 +430,19 @@ class BencodeTorrentTest extends TestCase {
$bencode = new BencodeTorrent();
$bencode->setData($data);
$this->assertEquals(['.,!?)óÉ s1234s (test.,!?)óÉ ÷'], $bencode->getGazelleFileList());
$this->assertFalse($bencode->hasFiles());
}
/**
* @expectedException \RuntimeException
*/
public function testInvalidSet() {
public function testInvalidSet(): void
{
$data = ['encoding' => 'UTF8', 'announce' => 'http://localhost:8080/announce'];
$bencode = new BencodeTorrent();
$this->expectException(\RuntimeException::class);
$bencode->setData($data);
}
/**
* @expectedException \RuntimeException
*/
public function testInvalidSetValue() {
public function testInvalidSetValue(): void
{
$data = [
'encoding' => 'UTF8',
'info' => [
@@ -366,28 +454,29 @@ class BencodeTorrentTest extends TestCase {
$bencode = new BencodeTorrent();
$bencode->setData($data);
$this->expectException(\RuntimeException::class);
$bencode->setValue(['info' => []]);
}
/**
* @expectedException \RuntimeException
*/
public function testGetEncodeNoData() {
public function testGetEncodeNoData(): void
{
$bencode = new BencodeTorrent();
$this->expectException(\RuntimeException::class);
$bencode->getEncode();
}
/**
* @expectedException \RuntimeException
*/
public function testInvalidPath() {
public function testEmptyPathPart(): void
{
$data = [
'encoding' => 'UTF8',
'info' => [
'files' => [
0 => [
'length' => 1234,
'path' => ['']
'path' => [
'',
'01 - test test',
]
]
],
'name' => 'test',
@@ -397,5 +486,47 @@ class BencodeTorrentTest extends TestCase {
];
$bencode = new BencodeTorrent();
$bencode->setData($data);
$this->assertSame(
[
'total_size' => 1234,
'files' => [
0 => [
'path' => '/01 - test test',
'size' => 1234,
],
],
],
$bencode->getFileList()
);
}
public function testEmptyPath(): void
{
$data = [
'encoding' => 'UTF8',
'info' => [
'files' => [
0 => [
'length' => 1234,
'path' => [
'',
]
]
],
'name' => 'test',
'length' => 1213134,
'pieces' => 'fake pieces string'
]
];
$bencode = new BencodeTorrent();
$this->expectException(\RuntimeException::class);
$bencode->setData($data);
}
public function testValidateEmptyData(): void
{
$bencode = new BencodeTorrent();
$this->expectException(\TypeError::class);
$bencode->validate();
}
}