162 lines
5.7 KiB
PHP
162 lines
5.7 KiB
PHP
<?php
|
|
// Bencode.php
|
|
// Created: 2022-01-13
|
|
// Updated: 2024-07-31
|
|
|
|
namespace Index\Bencode;
|
|
|
|
use InvalidArgumentException;
|
|
use RuntimeException;
|
|
use Index\IO\GenericStream;
|
|
use Index\IO\Stream;
|
|
use Index\IO\TempFileStream;
|
|
|
|
/**
|
|
* Provides a Bencode serialiser.
|
|
*/
|
|
final class Bencode {
|
|
public const DEFAULT_DEPTH = 512;
|
|
|
|
public static function encode(mixed $input, int $depth = self::DEFAULT_DEPTH): string {
|
|
if($depth < 1)
|
|
throw new RuntimeException('Maximum depth reached, structure is too dense.');
|
|
|
|
switch(gettype($input)) {
|
|
case 'string':
|
|
return sprintf('%d:%s', strlen($input), $input);
|
|
|
|
case 'integer':
|
|
return sprintf('i%de', $input);
|
|
|
|
case 'array':
|
|
if(array_is_list($input)) {
|
|
$output = 'l';
|
|
foreach($input as $item)
|
|
$output .= self::encode($item, $depth - 1);
|
|
} else {
|
|
$output = 'd';
|
|
foreach($input as $key => $value) {
|
|
$output .= self::encode((string)$key, $depth - 1);
|
|
$output .= self::encode($value, $depth - 1);
|
|
}
|
|
}
|
|
|
|
return $output . 'e';
|
|
|
|
case 'object':
|
|
if($input instanceof IBencodeSerialisable)
|
|
return self::encode($input->bencodeSerialise(), $depth - 1);
|
|
|
|
$input = get_object_vars($input);
|
|
$output = 'd';
|
|
|
|
foreach($input as $key => $value) {
|
|
$output .= self::encode((string)$key, $depth - 1);
|
|
$output .= self::encode($value, $depth - 1);
|
|
}
|
|
|
|
return $output . 'e';
|
|
|
|
default:
|
|
return '';
|
|
}
|
|
}
|
|
|
|
public static function decode(mixed $input, int $depth = self::DEFAULT_DEPTH, bool $dictAsObject = false): mixed {
|
|
if(is_string($input)) {
|
|
$input = TempFileStream::fromString($input);
|
|
$input->seek(0);
|
|
} elseif(is_resource($input))
|
|
$input = new GenericStream($input);
|
|
elseif(!($input instanceof Stream))
|
|
throw new InvalidArgumentException('$input must be a string, an Index Stream or a file resource.');
|
|
|
|
if($depth < 1)
|
|
throw new RuntimeException('Maximum depth reached, structure is too dense.');
|
|
|
|
$char = $input->readChar();
|
|
if($char === null)
|
|
throw new RuntimeException('Unexpected end of stream in $input.');
|
|
|
|
switch($char) {
|
|
case 'i':
|
|
$number = '';
|
|
|
|
for(;;) {
|
|
$char = $input->readChar();
|
|
if($char === null)
|
|
throw new RuntimeException('Unexpected end of stream while parsing integer.');
|
|
if($char === 'e')
|
|
break;
|
|
if($char === '-' && $number !== '')
|
|
throw new RuntimeException('Unexpected - (minus) while parsing integer.');
|
|
if($char === '0' && $number === '-')
|
|
throw new RuntimeException('Negative integer zero.');
|
|
if($char === '0' && $number === '0')
|
|
throw new RuntimeException('Integer double zero.');
|
|
if(!ctype_digit($char))
|
|
throw new RuntimeException('Unexpected character while parsing integer.');
|
|
$number .= $char;
|
|
}
|
|
|
|
return (int)$number;
|
|
|
|
case 'l':
|
|
$list = [];
|
|
|
|
for(;;) {
|
|
$char = $input->readChar();
|
|
if($char === null)
|
|
throw new RuntimeException('Unexpected end of stream while parsing list.');
|
|
if($char === 'e')
|
|
break;
|
|
$input->seek(-1, Stream::CURRENT);
|
|
$list[] = self::decode($input, $depth - 1, $dictAsObject);
|
|
}
|
|
|
|
return $list;
|
|
|
|
case 'd':
|
|
$dict = [];
|
|
|
|
for(;;) {
|
|
$char = $input->readChar();
|
|
if($char === null)
|
|
throw new RuntimeException('Unexpected end of stream while parsing dictionary');
|
|
if($char === 'e')
|
|
break;
|
|
if(!ctype_digit($char))
|
|
throw new RuntimeException('Unexpected dictionary key type, expected a string.');
|
|
$input->seek(-1, Stream::CURRENT);
|
|
$dict[self::decode($input, $depth - 1, $dictAsObject)] = self::decode($input, $depth - 1, $dictAsObject);
|
|
}
|
|
|
|
if($dictAsObject)
|
|
$dict = (object)$dict;
|
|
|
|
return $dict;
|
|
|
|
default:
|
|
if(!ctype_digit($char))
|
|
throw new RuntimeException('Unexpected character while parsing string.');
|
|
|
|
$length = $char;
|
|
|
|
for(;;) {
|
|
$char = $input->readChar();
|
|
if($char === null)
|
|
throw new RuntimeException('Unexpected end of character while parsing string length.');
|
|
if($char === ':')
|
|
break;
|
|
if($char === '0' && $length === '0')
|
|
throw new RuntimeException('Integer double zero while parsing string length.');
|
|
if(!ctype_digit($char))
|
|
throw new RuntimeException('Unexpected character while parsing string length.');
|
|
|
|
$length .= $char;
|
|
}
|
|
|
|
return $input->read((int)$length);
|
|
}
|
|
}
|
|
}
|