index/src/Serialisation/Bencode.php

163 lines
5.7 KiB
PHP

<?php
// Bencode.php
// Created: 2022-01-13
// Updated: 2023-09-15
namespace Index\Serialisation;
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);
}
}
}