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); } } }