@@ -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