Skip to content

Commit 5c26a5c

Browse files
author
itismadness
committed
0.9.0 release
1 parent 7453306 commit 5c26a5c

File tree

3 files changed

+448
-141
lines changed

3 files changed

+448
-141
lines changed

.travis.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ php:
66
- '7.0'
77
- '7.1'
88
- '7.2'
9+
- nightly
910

1011
cache:
1112
directories:
@@ -16,4 +17,5 @@ install:
1617

1718
script:
1819
- vendor/bin/phpunit
19-
- vendor/bin/phpcs src -s
20+
- vendor/bin/phpcs src
21+
- vendor/bin/phpcs tests

src/BencodeTorrent.php

Lines changed: 103 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -46,33 +46,45 @@ private function setDelim() {
4646
}
4747

4848
/**
49+
* Sets the internal data array
4950
* @param array $data
50-
* @throws \Exception
51+
* @throws \RuntimeException
5152
*/
52-
public function setData($data) {
53+
public function setData(array $data) {
5354
$this->data = $data;
5455
$this->validate();
5556
}
5657

5758
/**
59+
* Given a BEncoded string and decode it
5860
* @param string $data
59-
* @throws \Exception
61+
* @throws \RuntimeException
6062
*/
61-
public function decodeData(string $data) {
63+
public function decodeString(string $data) {
6264
$this->data = $this->decode($data);
6365
$this->validate();
6466
}
6567

6668
/**
69+
* Given a path to a file, decode the contents of it
70+
*
6771
* @param string $path
68-
* @throws \Exception
72+
* @throws \RuntimeException
6973
*/
7074
public function decodeFile(string $path) {
7175
$this->data = $this->decode(file_get_contents($path, FILE_BINARY));
7276
$this->validate();
7377
}
7478

7579
/**
80+
* Decodes a BEncoded string to the following values:
81+
* - Dictionary (starts with d, ends with e)
82+
* - List (starts with l, ends with e
83+
* - Integer (starts with i, ends with e
84+
* - String (starts with number denoting number of characters followed by : and then the string)
85+
*
86+
* @see https://wiki.theory.org/index.php/BitTorrentSpecification
87+
*
7688
* @param string $data
7789
* @param int $pos
7890
* @return array|bool|float|string
@@ -89,6 +101,7 @@ private function decode(string $data, int &$pos = 0) {
89101
}
90102
$return[$key] = $value;
91103
}
104+
ksort($return);
92105
$pos++;
93106
}
94107
elseif ($data[$pos] === 'l') {
@@ -116,16 +129,37 @@ private function decode(string $data, int &$pos = 0) {
116129
return $return;
117130
}
118131

119-
public function getData() {
132+
/**
133+
* Get the internal data array
134+
* @return array
135+
*/
136+
public function getData() : array {
120137
return $this->data;
121138
}
122139

123140
/**
124-
* @throws \Exception
141+
* Validates that the internal data array
142+
* @throws \RuntimeException
125143
*/
126144
public function validate() {
127145
if (empty($this->data['info'])) {
128-
throw new \Exception("Torrent dictionary doesn't have info key");
146+
throw new \RuntimeException("Torrent dictionary doesn't have info key");
147+
}
148+
if (isset($this->data['info']['files'])) {
149+
foreach ($this->data['info']['files'] as $file) {
150+
$path_key = isset($file['path.utf-8']) ? 'path.utf-8' : 'path';
151+
if (isset($file[$path_key])) {
152+
$filter = array_filter(
153+
$file[$path_key],
154+
function ($element) {
155+
return strlen($element) === 0;
156+
}
157+
);
158+
if (count($filter) > 0) {
159+
throw new \RuntimeException('Cannot have empty path for a file');
160+
}
161+
}
162+
}
129163
}
130164
}
131165

@@ -141,7 +175,7 @@ private function hasData() {
141175
/**
142176
* @return string
143177
*/
144-
public function getEncode() {
178+
public function getEncode() : string {
145179
$this->hasData();
146180
return $this->encodeVal($this->data);
147181
}
@@ -150,7 +184,7 @@ public function getEncode() {
150184
* @param mixed $data
151185
* @return string
152186
*/
153-
private function encodeVal($data) {
187+
private function encodeVal($data) : string {
154188
if (is_array($data)) {
155189
$return = '';
156190
$check = -1;
@@ -194,7 +228,7 @@ private function encodeVal($data) {
194228
*
195229
* @return bool flag to indicate if we altered the info dictionary
196230
*/
197-
public function clean() {
231+
public function clean() : bool {
198232
$this->cleanDataDictionary();
199233
return $this->cleanInfoDictionary();
200234
}
@@ -206,9 +240,9 @@ public function clean() {
206240
*/
207241
public function cleanDataDictionary() {
208242
$allowed_keys = array('encoding', 'info');
209-
foreach ($this->data['info'] as $key => $value) {
243+
foreach ($this->data as $key => $value) {
210244
if (!in_array($key, $allowed_keys)) {
211-
unset($this->data['info'][$key]);
245+
unset($this->data[$key]);
212246
}
213247
}
214248
}
@@ -227,7 +261,7 @@ public function cleanDataDictionary() {
227261
*
228262
* @return bool
229263
*/
230-
public function cleanInfoDictionary() {
264+
public function cleanInfoDictionary() : bool {
231265
$cleaned = false;
232266
$allowed_keys = array('files', 'name', 'piece length', 'pieces', 'private', 'length',
233267
'name.utf8', 'name.utf-8', 'md5sum', 'sha1', 'source',
@@ -247,7 +281,7 @@ public function cleanInfoDictionary() {
247281
*
248282
* @return bool
249283
*/
250-
public function isPrivate() {
284+
public function isPrivate() : bool {
251285
$this->hasData();
252286
return isset($this->data['info']['private']) && $this->data['info']['private'] === 1;
253287
}
@@ -261,7 +295,7 @@ public function isPrivate() {
261295
*
262296
* @return bool
263297
*/
264-
public function makePrivate() {
298+
public function makePrivate() : bool {
265299
$this->hasData();
266300
if ($this->isPrivate()) {
267301
return false;
@@ -282,58 +316,48 @@ public function makePrivate() {
282316
*
283317
* @return bool true if the source was set/changed, false if no change
284318
*/
285-
public function setSource(string $source) {
319+
public function setSource(string $source) : bool {
286320
$this->hasData();
287321
if (isset($this->data['info']['source']) && $this->data['info']['source'] === $source) {
288322
return false;
289323
}
290-
// Set we've set the source and will require a download, we might as well clean
324+
// Since we've set the source and will require a re-download, we might as well clean
291325
// these out as well
292326
unset($this->data['info']['x_cross_seed']);
293327
unset($this->data['info']['unique']);
294-
$this->data['info']['source'] = $source;
295-
ksort($this->data['info']);
328+
$this->setValue(['info.source' => $source]);
296329
return true;
297330
}
298331

299332
/**
300-
* Function to allow you set any number of keys and values in the data dictionary.
333+
* Function to allow you set any number of keys and values in the data dictionary. You can
334+
* set the value in a dictionary by concatenating the keys into a string with a period
335+
* separator (ex: info.name will set name field in the info dictionary) so that the rest
336+
* of the dictionary is unaffected.
337+
*
301338
* @param array $array
302339
*/
303-
public function set(array $array) {
340+
public function setValue(array $array) {
304341
foreach ($array as $key => $value) {
305-
$this->data[$key] = $value;
306342
if (is_array($value)) {
307-
ksort($this->data[$key]);
343+
ksort($value);
344+
}
345+
$keys = explode('.', $key);
346+
$data = &$this->data;
347+
for ($i = 0; $i < count($keys); $i++) {
348+
$data = &$data[$keys[$i]];
349+
}
350+
$data = $value;
351+
$data = &$this->data;
352+
for ($i = 0; $i < count($keys); $i++) {
353+
$data = &$data[$keys[$i]];
354+
if (is_array($data)) {
355+
ksort($data);
356+
}
308357
}
309358
}
310359
ksort($this->data);
311-
}
312-
313-
/**
314-
* Sets the announce URL for current data. This URL forms the base of the GET request that
315-
* the torrent client will send to a tracker so that a client can get peer lists as well as
316-
* tell the tracker the stats on the download. The announce URL should probably also end with
317-
* /announce which allows for the more efficient scrape to happen on an initial handshake by
318-
* the client and when getting just the peer list.
319-
*
320-
* @see https://wiki.theory.org/index.php/BitTorrentSpecification#Tracker_HTTP.2FHTTPS_Protocol
321-
* @see https://wiki.theory.org/index.php/BitTorrentSpecification#Tracker_.27scrape.27_Convention
322-
*
323-
* @param string $announce_url
324-
*/
325-
public function setAnnounceUrl(string $announce_url) {
326-
$this->hasData();
327-
$this->set(['announce' => $announce_url]);
328-
}
329-
330-
/**
331-
* Sets the comment string for the current data. This does not affect the info_hash.
332-
* @param string $comment
333-
*/
334-
public function setComment(string $comment) {
335-
$this->hasData();
336-
$this->set(['comment' => $comment]);
360+
$this->validate();
337361
}
338362

339363
/**
@@ -346,11 +370,15 @@ public function setComment(string $comment) {
346370
*
347371
* @return string
348372
*/
349-
public function getInfoHash() {
373+
public function getInfoHash() : string {
350374
$this->hasData();
351375
return sha1($this->encodeVal($this->data['info']));
352376
}
353377

378+
public function getHexInfoHash(): string {
379+
return pack('H*', $this->getInfoHash());
380+
}
381+
354382
/**
355383
* @return string
356384
*/
@@ -368,7 +396,7 @@ public function getName() {
368396
*
369397
* @return int
370398
*/
371-
public function getSize() {
399+
public function getSize() : int {
372400
$cur_size = 0;
373401
if (!isset($this->data['info']['files'])) {
374402
$cur_size = $this->data['info']['length'];
@@ -390,7 +418,7 @@ public function getSize() {
390418
*
391419
* @return array
392420
*/
393-
public function getFileList() {
421+
public function getFileList() : array {
394422
$files = [];
395423
if (!isset($this->data['info']['files'])) {
396424
// Single-file torrent
@@ -431,12 +459,12 @@ function ($a, $b) {
431459
*
432460
* @return array
433461
*/
434-
public function getGazelleFileList() {
462+
public function getGazelleFileList() : array {
435463
$files = [];
436464
foreach ($this->getFileList() as $file) {
437465
$name = $file['name'];
438-
$size = $file['length'];
439-
$name = self::makeUTF8(strtr($name, "\n\r\t", ' '));
466+
$size = $file['size'];
467+
$name = $this->makeUTF8(strtr($name, "\n\r\t", ' '));
440468
$ext_pos = strrpos($name, '.');
441469
// Should not be $ExtPos !== false. Extension-less files that start with a .
442470
// should not get extensions
@@ -449,52 +477,28 @@ public function getGazelleFileList() {
449477
/**
450478
* Given a string, convert it to UTF-8 format, if it's not already in UTF-8.
451479
*
452-
* @param string $Str input to convert to utf-8 format
480+
* @param string $str input to convert to utf-8 format
453481
*
454482
* @return string
455483
*/
456-
private static function makeUTF8($Str) {
457-
if ($Str != '') {
458-
if (self::isUTF8($Str)) {
459-
$Encoding = 'UTF-8';
460-
}
461-
if (empty($Encoding)) {
462-
$Encoding = mb_detect_encoding($Str, 'UTF-8, ISO-8859-1');
463-
}
464-
if (empty($Encoding)) {
465-
$Encoding = 'ISO-8859-1';
466-
}
467-
if ($Encoding == 'UTF-8') {
468-
return $Str;
469-
}
470-
else {
471-
return @mb_convert_encoding($Str, 'UTF-8', $Encoding);
472-
}
484+
private function makeUTF8(string $str) : string {
485+
if (preg_match('//u', $str)) {
486+
$encoding = 'UTF-8';
487+
}
488+
if (empty($encoding)) {
489+
$encoding = mb_detect_encoding($str, 'UTF-8, ISO-8859-1');
490+
}
491+
// Legacy thing for Gazelle, leaving it in, but not going to bother testing
492+
// @codeCoverageIgnoreStart
493+
if (empty($encoding)) {
494+
$encoding = 'ISO-8859-1';
495+
}
496+
// @codeCoverageIgnoreEnd
497+
if ($encoding === 'UTF-8') {
498+
return $str;
499+
}
500+
else {
501+
return @mb_convert_encoding($str, 'UTF-8', $encoding);
473502
}
474-
return $Str;
475-
}
476-
477-
/**
478-
* Given a string, determine if that string is encoded in UTF-8 via regular expressions,
479-
* so that we don't have to rely on mb_detect_encoding which isn't quite as accurate
480-
*
481-
* @param string $Str input to check encoding of
482-
*
483-
* @return false|int
484-
*/
485-
private static function isUTF8($Str) {
486-
return preg_match(
487-
'%^(?:
488-
[\x09\x0A\x0D\x20-\x7E] // ASCII
489-
| [\xC2-\xDF][\x80-\xBF] // non-overlong 2-byte
490-
| \xE0[\xA0-\xBF][\x80-\xBF] // excluding overlongs
491-
| [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} // straight 3-byte
492-
| \xED[\x80-\x9F][\x80-\xBF] // excluding surrogates
493-
| \xF0[\x90-\xBF][\x80-\xBF]{2} // planes 1-3
494-
| [\xF1-\xF3][\x80-\xBF]{3} // planes 4-15
495-
| \xF4[\x80-\x8F][\x80-\xBF]{2} // plane 16
496-
)*$%xs',
497-
$Str
498-
);
499503
}
500504
}

0 commit comments

Comments
 (0)