instant-0chan/lib/getid3/module.misc.torrent.php

248 lines
8.6 KiB
PHP

<?php
/////////////////////////////////////////////////////////////////
/// getID3() by James Heinrich <info@getid3.org> //
// available at https://github.com/JamesHeinrich/getID3 //
// or https://www.getid3.org //
// or http://getid3.sourceforge.net //
// see readme.txt for more details //
/////////////////////////////////////////////////////////////////
// //
// module.misc.torrent.php //
// module for analyzing .torrent files //
// dependencies: NONE //
// ///
/////////////////////////////////////////////////////////////////
if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers
exit;
}
class getid3_torrent extends getid3_handler
{
/**
* Assume all .torrent files are less than 1MB and just read entire thing into memory for easy processing.
* Override this value if you need to process files larger than 1MB
*
* @var int
*/
public $max_torrent_filesize = 1048576;
/**
* calculated InfoHash (SHA1 of the entire "info" Dictionary)
*
* @var string
*/
private $infohash = '';
const PIECE_HASHLENGTH = 20; // number of bytes the SHA1 hash is for each piece
/**
* @return bool
*/
public function Analyze() {
$info = &$this->getid3->info;
$filesize = $info['avdataend'] - $info['avdataoffset'];
if ($filesize > $this->max_torrent_filesize) { //
$this->error('File larger ('.number_format($filesize).' bytes) than $max_torrent_filesize ('.number_format($this->max_torrent_filesize).' bytes), increase getid3_torrent->max_torrent_filesize if needed');
return false;
}
$this->fseek($info['avdataoffset']);
$TORRENT = $this->fread($filesize);
$offset = 0;
if (!preg_match('#^(d8\\:announce|d7\\:comment)#', $TORRENT)) {
$this->error('Expecting "d8:announce" or "d7:comment" at '.$info['avdataoffset'].', found "'.substr($TORRENT, $offset, 12).'" instead.');
return false;
}
$info['fileformat'] = 'torrent';
$info['torrent'] = $this->NextEntity($TORRENT, $offset);
if ($this->infohash) {
$info['torrent']['infohash'] = $this->infohash;
}
if (empty($info['torrent']['info']['length']) && !empty($info['torrent']['info']['files'][0]['length'])) {
$info['torrent']['info']['length'] = 0;
foreach ($info['torrent']['info']['files'] as $key => $filedetails) {
$info['torrent']['info']['length'] += $filedetails['length'];
}
}
if (!empty($info['torrent']['info']['length']) && !empty($info['torrent']['info']['piece length']) && !empty($info['torrent']['info']['pieces'])) {
$num_pieces_size = ceil($info['torrent']['info']['length'] / $info['torrent']['info']['piece length']);
$num_pieces_hash = strlen($info['torrent']['info']['pieces']) / getid3_torrent::PIECE_HASHLENGTH; // should be concatenated 20-byte SHA1 hashes
if ($num_pieces_hash == $num_pieces_size) {
$info['torrent']['info']['piece_hash'] = array();
for ($i = 0; $i < $num_pieces_size; $i++) {
$info['torrent']['info']['piece_hash'][$i] = '';
for ($j = 0; $j < getid3_torrent::PIECE_HASHLENGTH; $j++) {
$info['torrent']['info']['piece_hash'][$i] .= sprintf('%02x', ord($info['torrent']['info']['pieces'][(($i * getid3_torrent::PIECE_HASHLENGTH) + $j)]));
}
}
unset($info['torrent']['info']['pieces']);
} else {
$this->warning('found '.$num_pieces_size.' pieces based on file/chunk size; found '.$num_pieces_hash.' pieces in hash table');
}
}
if (!empty($info['torrent']['info']['name']) && !empty($info['torrent']['info']['length']) && !isset($info['torrent']['info']['files'])) {
// single-file torrent
$info['torrent']['files'] = array($info['torrent']['info']['name'] => $info['torrent']['info']['length']);
} elseif (!empty($info['torrent']['info']['files'])) {
// multi-file torrent
$info['torrent']['files'] = array();
foreach ($info['torrent']['info']['files'] as $key => $filedetails) {
$info['torrent']['files'][implode('/', $filedetails['path'])] = $filedetails['length'];
}
} else {
$this->warning('no files found');
}
return true;
}
/**
* @return string|array|int|bool
*/
public function NextEntity(&$TORRENT, &$offset) {
// https://fileformats.fandom.com/wiki/Torrent_file
// https://en.wikipedia.org/wiki/Torrent_file
// https://en.wikipedia.org/wiki/Bencode
if ($offset >= strlen($TORRENT)) {
$this->error('cannot read beyond end of file '.$offset);
return false;
}
$type = $TORRENT[$offset++];
if ($type == 'i') {
// Integers are stored as i<integer>e:
// i90e
$value = $this->ReadSequentialDigits($TORRENT, $offset, true);
if ($TORRENT[$offset++] == 'e') {
//echo '<li>int: '.$value.'</li>';
return (int) $value;
}
$this->error('unexpected('.__LINE__.') input "'.$value.'" at offset '.($offset - 1));
return false;
} elseif ($type == 'd') {
// Dictionaries are stored as d[key1][value1][key2][value2][...]e. Keys and values appear alternately.
// Keys must be strings and must be ordered alphabetically.
// For example, {apple-red, lemon-yellow, violet-blue, banana-yellow} is stored as:
// d5:apple3:red6:banana6:yellow5:lemon6:yellow6:violet4:bluee
$values = array();
//echo 'DICTIONARY @ '.$offset.'<ul>';
$info_dictionary_start = null; // dummy declaration to prevent "Variable might not be defined" warnings
while (true) {
if ($TORRENT[$offset] === 'e') {
break;
}
$thisentry = array();
$key = $this->NextEntity($TORRENT, $offset);
if ($key == 'info') {
$info_dictionary_start = $offset;
}
if ($key === false) {
$this->error('unexpected('.__LINE__.') input at offset '.$offset);
return false;
}
$value = $this->NextEntity($TORRENT, $offset);
if ($key == 'info') {
$info_dictionary_end = $offset;
$this->infohash = sha1(substr($TORRENT, $info_dictionary_start, $info_dictionary_end - $info_dictionary_start));
}
if ($value === false) {
$this->error('unexpected('.__LINE__.') input at offset '.$offset);
return false;
}
$values[$key] = $value;
}
if ($TORRENT[$offset++] == 'e') {
//echo '</ul>';
return $values;
}
$this->error('unexpected('.__LINE__.') input "'.$TORRENT[($offset - 1)].'" at offset '.($offset - 1));
return false;
} elseif ($type == 'l') {
//echo 'LIST @ '.$offset.'<ul>';
// Lists are stored as l[value 1][value2][value3][...]e. For example, {spam, eggs, cheeseburger} is stored as:
// l4:spam4:eggs12:cheeseburgere
$values = array();
while (true) {
if ($TORRENT[$offset] === 'e') {
break;
}
$NextEntity = $this->NextEntity($TORRENT, $offset);
if ($NextEntity === false) {
$this->error('unexpected('.__LINE__.') input at offset '.($offset - 1));
return false;
}
$values[] = $NextEntity;
}
if ($TORRENT[$offset++] == 'e') {
//echo '</ul>';
return $values;
}
$this->error('unexpected('.__LINE__.') input "'.$TORRENT[($offset - 1)].'" at offset '.($offset - 1));
return false;
} elseif (ctype_digit($type)) {
// Strings are stored as <length of string>:<string>:
// 4:wiki
$length = $type;
while (true) {
$char = $TORRENT[$offset++];
if ($char == ':') {
break;
} elseif (!ctype_digit($char)) {
$this->error('unexpected('.__LINE__.') input "'.$char.'" at offset '.($offset - 1));
return false;
}
$length .= $char;
}
if (($offset + $length) > strlen($TORRENT)) {
$this->error('string at offset '.$offset.' claims to be '.$length.' bytes long but only '.(strlen($TORRENT) - $offset).' bytes of data left in file');
return false;
}
$string = substr($TORRENT, $offset, $length);
$offset += $length;
//echo '<li>string: '.$string.'</li>';
return (string) $string;
} else {
$this->error('unexpected('.__LINE__.') input "'.$type.'" at offset '.($offset - 1));
return false;
}
}
/**
* @return string
*/
public function ReadSequentialDigits(&$TORRENT, &$offset, $allow_negative=false) {
$start_offset = $offset;
$value = '';
while (true) {
$char = $TORRENT[$offset++];
if (!ctype_digit($char)) {
if ($allow_negative && ($char == '-') && (strlen($value) == 0)) {
// allow negative-sign if first character and $allow_negative enabled
} else {
$offset--;
break;
}
}
$value .= $char;
}
if (($value[0] === '0') && ($value !== '0')) {
$this->warning('illegal zero-padded number "'.$value.'" at offset '.$start_offset);
}
return $value;
}
}