2022-09-13 15:13:11 +02:00
|
|
|
<?php
|
|
|
|
// BencodeSerialiser.php
|
|
|
|
// Created: 2022-01-13
|
2023-01-01 19:00:00 +00:00
|
|
|
// Updated: 2023-01-01
|
2022-09-13 15:13:11 +02:00
|
|
|
|
|
|
|
namespace Index\Serialisation;
|
|
|
|
|
|
|
|
use RuntimeException;
|
|
|
|
use Index\IO\Stream;
|
|
|
|
use Index\IO\TempFileStream;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Provides a Bencode serialiser.
|
|
|
|
*/
|
|
|
|
class BencodeSerialiser extends Serialiser {
|
|
|
|
private BencodeSerialiserSettings $settings;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a new Bencode serialiser.
|
|
|
|
*
|
|
|
|
* @param BencodeSerialiserSettings|null $settings Settings for the serialisation behaviour.
|
|
|
|
*/
|
|
|
|
public function __construct(?BencodeSerialiserSettings $settings = null) {
|
|
|
|
$this->settings = $settings ?? new BencodeSerialiserSettings;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function serialise(mixed $input): string {
|
|
|
|
return self::encode((string)$input, $this->settings->getMaxDepth());
|
|
|
|
}
|
|
|
|
|
|
|
|
public function deserialise(Stream|string $input): mixed {
|
|
|
|
if(!($input instanceof Stream))
|
|
|
|
$input = TempFileStream::fromString($input);
|
|
|
|
return $this->decode($input, $this->settings->getMaxDepth());
|
|
|
|
}
|
|
|
|
|
|
|
|
private static function encode(mixed $input, int $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) {
|
2023-01-01 19:53:27 +00:00
|
|
|
$output .= self::encode((string)$key, $depth - 1);
|
2022-09-13 15:13:11 +02:00
|
|
|
$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) {
|
2023-01-01 19:53:27 +00:00
|
|
|
$output .= self::encode((string)$key, $depth - 1);
|
2022-09-13 15:13:11 +02:00
|
|
|
$output .= self::encode($value, $depth - 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $output . 'e';
|
|
|
|
|
|
|
|
default:
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private function decode(Stream $input, int $depth): mixed {
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2023-01-01 19:00:00 +00:00
|
|
|
return (int)$number;
|
2022-09-13 15:13:11 +02:00
|
|
|
|
|
|
|
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[] = $this->decode($input, $depth - 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
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[$this->decode($input, $depth - 1)] = $this->decode($input, $depth - 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
if($this->settings->shouldDecodeDictAsObject())
|
|
|
|
$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;
|
|
|
|
}
|
|
|
|
|
2023-01-01 19:00:00 +00:00
|
|
|
return $input->read((int)$length);
|
2022-09-13 15:13:11 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|