* @license https://www.gnu.org/licenses/agpl-3.0.en.html/ GNU Affero General Public License v3.0 */ namespace App\Helpers; use function theodorejb\polycast\safe_int; class Bencode { public static function parse_integer($s, &$pos) { $len = \strlen((string) $s); if ($len === 0 || $s[$pos] != 'i') { return; } $pos++; $result = ''; while ($pos < $len && $s[$pos] != 'e') { if (is_numeric($s[$pos]) || $s[$pos] === '-') { $result .= $s[$pos]; } else { // We have an invalid character in the string. return; } $pos++; } if ($pos >= $len) { // No end marker, hence we return null. return; } $pos++; if (safe_int($result)) { return (int) $result; } } public static function parse_string($s, &$pos) { $len = \strlen((string) $s); $lengthStr = ''; while ($pos < $len && $s[$pos] != ':') { if (is_numeric($s[$pos])) { $lengthStr .= $s[$pos]; } else { // Non-numeric character, we return null in this case. return; } $pos++; } if ($pos >= $len) { // We need a colon here, but there's none. return; } $pos++; if (!safe_int($lengthStr)) { return; } $length = (int) $lengthStr; $result = ''; while ($pos < $len && $length > 0) { $result .= $s[$pos]; $length--; $pos++; } if ($length > 0) { // Input ended, but the string is longer than that. return; } return $result; } public static function bdecode($s, &$pos = 0) { $len = \strlen((string) $s); if ($pos >= $len) { return; } $c = $s[$pos]; if ($c == 'i') { return self::parse_integer($s, $pos); } if (is_numeric($c)) { return self::parse_string($s, $pos); } if ($c == 'd') { $dict = []; $pos++; while ($pos < $len && $s[$pos] != 'e') { $key = self::bdecode($s, $pos); $value = self::bdecode($s, $pos); if (null === $key || null === $value) { return; } $dict[$key] = $value; } if ($pos >= $len) { // We need a end marker here return; } $pos++; return $dict; } if ($c == 'l') { $list = []; $pos++; while ($pos < $len && $s[$pos] != 'e') { $next = self::bdecode($s, $pos); if (null !== $next) { $list[] = $next; } else { return; } } if ($pos >= $len) { // We need a end marker here return; } $pos++; return $list; } } public static function bencode($d) { if (\is_array($d)) { $ret = 'l'; $isDict = !array_is_list($d); if ($isDict) { $ret = 'd'; // this is required by the specs, and BitTornado actually chokes on unsorted dictionaries ksort($d, SORT_STRING); } foreach ($d as $key => $value) { if ($isDict) { $ret .= \strlen((string) $key).':'.$key; } if (\is_int($value) || \is_float($value)) { $ret .= \sprintf('i%de', $value); } elseif (\is_string($value)) { $ret .= \strlen($value).':'.$value; } else { $ret .= self::bencode($value); } } return $ret.'e'; } if (\is_string($d)) { return \strlen($d).':'.$d; } if (\is_int($d) || \is_float($d)) { return \sprintf('i%de', $d); } } public static function bdecode_file($filename) { $f = file_get_contents($filename); return self::bdecode($f); } public static function get_infohash($t): string { return sha1((string) self::bencode($t['info']), true); } public static function get_meta($t): array { $result = []; $size = 0; $count = 0; // Multi-file if (isset($t['info']['files']) && \is_array($t['info']['files'])) { foreach ($t['info']['files'] as $file) { $count++; $size += $file['length']; } } else { $size = $t['info']['length']; $count = 1; $t['info']['files'][0]['path'] = $t['info']['name']; $t['info']['files'][0]['length'] = $t['info']['length']; } $result['count'] = $count; $result['size'] = $size; return $result; } public static function get_name($t): ?string { $name = null; if ( \array_key_exists('info', $t) && \is_array($t['info']) && \array_key_exists('name', $t['info']) && \is_string($t['info']['name']) && \array_key_exists('files', $t['info']) ) { $name = $t['info']['name']; } return $name; } public static function is_v2_or_hybrid($t): bool { return isset($t['piece layers']); } }