index/src/Serialisation/BencodeSerialiser.php
2023-01-01 19:53:27 +00:00

172 lines
5.9 KiB
PHP

<?php
// BencodeSerialiser.php
// Created: 2022-01-13
// Updated: 2023-01-01
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) {
$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 '';
}
}
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;
}
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[] = $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;
}
return $input->read((int)$length);
}
}
}