Initial Commit

This commit is contained in:
narrakas 2019-06-22 00:36:56 -04:00
commit c1801b93a7
20 changed files with 1940 additions and 0 deletions

View File

@ -0,0 +1,22 @@
<?php
namespace ZiProto\Abstracts;
/**
* Class Options
* @package ZiProto\Abstracts
*/
abstract class Options
{
const BIGINT_AS_STR = 0b001;
const BIGINT_AS_GMP = 0b010;
const BIGINT_AS_EXCEPTION = 0b100;
const FORCE_STR = 0b00000001;
const FORCE_BIN = 0b00000010;
const DETECT_STR_BIN = 0b00000100;
const FORCE_ARR = 0b00001000;
const FORCE_MAP = 0b00010000;
const DETECT_ARR_MAP = 0b00100000;
const FORCE_FLOAT32 = 0b01000000;
const FORCE_FLOAT64 = 0b10000000;
}

View File

@ -0,0 +1,21 @@
<?php
namespace ZiProto\Abstracts;
/**
* Class Regex
* @package ZiProto\Abstracts
*/
abstract class Regex
{
const UTF8_REGEX = '/\A(?:
[\x00-\x7F]++ # ASCII
| [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte
| \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs
| [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte
| \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates
| \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3
| [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15
| \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16
)*+\z/x';
}

View File

@ -0,0 +1,791 @@
<?php
namespace ZiProto;
use function gmp_init;
use function ord;
use function sprintf;
use function substr;
use function unpack;
use ZiProto\Exception\InsufficientDataException;
use ZiProto\Exception\IntegerOverflowException;
use ZiProto\Exception\DecodingFailedException;
use ZiProto\Exception\InvalidOptionException;
use ZiProto\TypeTransformer\Extension;
/**
* Class BufferStream
* @package ZiProto
*/
class BufferStream
{
/**
* @var string
*/
private $buffer;
/**
* @var int
*/
private $offset = 0;
/**
* @var bool
*/
private $isBigIntAsStr;
/**
* @var bool
*/
private $isBigIntAsGmp;
/**
* @var Extension[]|null
*/
private $transformers;
/**
* @param string $buffer
* @param DecodingOptions|int|null $options
*
* @throws InvalidOptionException
*/
public function __construct(string $buffer = '', $options = null)
{
if (null === $options)
{
$options = DecodingOptions::fromDefaults();
}
elseif (!$options instanceof EncodingOptions)
{
$options = DecodingOptions::fromBitmask($options);
}
$this->isBigIntAsStr = $options->isBigIntAsStrMode();
$this->isBigIntAsGmp = $options->isBigIntAsGmpMode();
$this->buffer = $buffer;
}
/**
* @param Extension $transformer
* @return BufferStream
*/
public function registerTransformer(Extension $transformer) : self
{
$this->transformers[$transformer->getType()] = $transformer;
return $this;
}
/**
* @param string $data
* @return BufferStream
*/
public function append(string $data) : self
{
$this->buffer .= $data;
return $this;
}
/**
* @param string $buffer
* @return BufferStream
*/
public function reset(string $buffer = '') : self
{
$this->buffer = $buffer;
$this->offset = 0;
return $this;
}
/**
* Clone Method
*/
public function __clone()
{
$this->buffer = '';
$this->offset = 0;
}
/**
* @return array
*/
public function trydecode() : array
{
$data = [];
$offset = $this->offset;
try
{
do
{
$data[] = $this->decode();
$offset = $this->offset;
} while (isset($this->buffer[$this->offset]));
}
catch (InsufficientDataException $e)
{
$this->offset = $offset;
}
if ($this->offset)
{
$this->buffer = isset($this->buffer[$this->offset]) ? substr($this->buffer, $this->offset) : '';
$this->offset = 0;
}
return $data;
}
/**
* @return array|bool|int|mixed|resource|string|Ext|null
*/
public function decode()
{
if (!isset($this->buffer[$this->offset]))
{
throw InsufficientDataException::unexpectedLength($this->buffer, $this->offset, 1);
}
$c = ord($this->buffer[$this->offset]);
++$this->offset;
// fixint
if ($c <= 0x7f)
{
return $c;
}
// fixstr
if ($c >= 0xa0 && $c <= 0xbf)
{
return ($c & 0x1f) ? $this->decodeStrData($c & 0x1f) : '';
}
// fixarray
if ($c >= 0x90 && $c <= 0x9f)
{
return ($c & 0xf) ? $this->decodeArrayData($c & 0xf) : [];
}
// fixmap
if ($c >= 0x80 && $c <= 0x8f)
{
return ($c & 0xf) ? $this->decodeMapData($c & 0xf) : [];
}
// negfixint
if ($c >= 0xe0)
{
return $c - 0x100;
}
switch ($c)
{
case 0xc0: return null;
case 0xc2: return false;
case 0xc3: return true;
// bin
case 0xc4: return $this->decodeStrData($this->decodeUint8());
case 0xc5: return $this->decodeStrData($this->decodeUint16());
case 0xc6: return $this->decodeStrData($this->decodeUint32());
// float
case 0xca: return $this->decodeFloat32();
case 0xcb: return $this->decodeFloat64();
// uint
case 0xcc: return $this->decodeUint8();
case 0xcd: return $this->decodeUint16();
case 0xce: return $this->decodeUint32();
case 0xcf: return $this->decodeUint64();
// int
case 0xd0: return $this->decodeInt8();
case 0xd1: return $this->decodeInt16();
case 0xd2: return $this->decodeInt32();
case 0xd3: return $this->decodeInt64();
// str
case 0xd9: return $this->decodeStrData($this->decodeUint8());
case 0xda: return $this->decodeStrData($this->decodeUint16());
case 0xdb: return $this->decodeStrData($this->decodeUint32());
// array
case 0xdc: return $this->decodeArrayData($this->decodeUint16());
case 0xdd: return $this->decodeArrayData($this->decodeUint32());
// map
case 0xde: return $this->decodeMapData($this->decodeUint16());
case 0xdf: return $this->decodeMapData($this->decodeUint32());
// ext
case 0xd4: return $this->decodeExtData(1);
case 0xd5: return $this->decodeExtData(2);
case 0xd6: return $this->decodeExtData(4);
case 0xd7: return $this->decodeExtData(8);
case 0xd8: return $this->decodeExtData(16);
case 0xc7: return $this->decodeExtData($this->decodeUint8());
case 0xc8: return $this->decodeExtData($this->decodeUint16());
case 0xc9: return $this->decodeExtData($this->decodeUint32());
}
throw DecodingFailedException::unknownCode($c);
}
/**
* @return null
*/
public function decodeNil()
{
if (!isset($this->buffer[$this->offset]))
{
throw InsufficientDataException::unexpectedLength($this->buffer, $this->offset, 1);
}
if ("\xc0" === $this->buffer[$this->offset])
{
++$this->offset;
return null;
}
throw DecodingFailedException::unexpectedCode(ord($this->buffer[$this->offset++]), 'nil');
}
/**
* @return bool
*/
public function decodeBool()
{
if (!isset($this->buffer[$this->offset]))
{
throw InsufficientDataException::unexpectedLength($this->buffer, $this->offset, 1);
}
$c = ord($this->buffer[$this->offset]);
++$this->offset;
if (0xc2 === $c)
{
return false;
}
if (0xc3 === $c)
{
return true;
}
throw DecodingFailedException::unexpectedCode($c, 'bool');
}
/**
* @return int|resource|string
*/
public function decodeInt()
{
if (!isset($this->buffer[$this->offset]))
{
throw InsufficientDataException::unexpectedLength($this->buffer, $this->offset, 1);
}
$c = ord($this->buffer[$this->offset]);
++$this->offset;
// fixint
if ($c <= 0x7f)
{
return $c;
}
// negfixint
if ($c >= 0xe0)
{
return $c - 0x100;
}
switch ($c)
{
// uint
case 0xcc: return $this->decodeUint8();
case 0xcd: return $this->decodeUint16();
case 0xce: return $this->decodeUint32();
case 0xcf: return $this->decodeUint64();
// int
case 0xd0: return $this->decodeInt8();
case 0xd1: return $this->decodeInt16();
case 0xd2: return $this->decodeInt32();
case 0xd3: return $this->decodeInt64();
}
throw DecodingFailedException::unexpectedCode($c, 'int');
}
/**
* @return mixed
*/
public function decodeFloat()
{
if (!isset($this->buffer[$this->offset]))
{
throw InsufficientDataException::unexpectedLength($this->buffer, $this->offset, 1);
}
$c = ord($this->buffer[$this->offset]);
++$this->offset;
if (0xcb === $c)
{
return $this->decodeFloat64();
}
if (0xca === $c)
{
return $this->decodeFloat32();
}
throw DecodingFailedException::unexpectedCode($c, 'float');
}
/**
* @return bool|string
*/
public function decodeStr()
{
if (!isset($this->buffer[$this->offset]))
{
throw InsufficientDataException::unexpectedLength($this->buffer, $this->offset, 1);
}
$c = ord($this->buffer[$this->offset]);
++$this->offset;
if ($c >= 0xa0 && $c <= 0xbf)
{
return ($c & 0x1f) ? $this->decodeStrData($c & 0x1f) : '';
}
if (0xd9 === $c)
{
return $this->decodeStrData($this->decodeUint8());
}
if (0xda === $c)
{
return $this->decodeStrData($this->decodeUint16());
}
if (0xdb === $c)
{
return $this->decodeStrData($this->decodeUint32());
}
throw DecodingFailedException::unexpectedCode($c, 'str');
}
/**
* @return bool|string
*/
public function decodeBin()
{
if (!isset($this->buffer[$this->offset]))
{
throw InsufficientDataException::unexpectedLength($this->buffer, $this->offset, 1);
}
$c = ord($this->buffer[$this->offset]);
++$this->offset;
if (0xc4 === $c)
{
return $this->decodeStrData($this->decodeUint8());
}
if (0xc5 === $c)
{
return $this->decodeStrData($this->decodeUint16());
}
if (0xc6 === $c)
{
return $this->decodeStrData($this->decodeUint32());
}
throw DecodingFailedException::unexpectedCode($c, 'bin');
}
/**
* @return array
*/
public function decodeArray()
{
$size = $this->decodeArrayHeader();
$array = [];
while ($size--)
{
$array[] = $this->decode();
}
return $array;
}
/**
* @return int
*/
public function decodeArrayHeader()
{
if (!isset($this->buffer[$this->offset]))
{
throw InsufficientDataException::unexpectedLength($this->buffer, $this->offset, 1);
}
$c = ord($this->buffer[$this->offset]);
++$this->offset;
if ($c >= 0x90 && $c <= 0x9f)
{
return $c & 0xf;
}
if (0xdc === $c)
{
return $this->decodeUint16();
}
if (0xdd === $c)
{
return $this->decodeUint32();
}
throw DecodingFailedException::unexpectedCode($c, 'array header');
}
/**
* @return array
*/
public function decodeMap()
{
$size = $this->decodeMapHeader();
$map = [];
while ($size--)
{
$map[$this->decode()] = $this->decode();
}
return $map;
}
/**
* @return int
*/
public function decodeMapHeader()
{
if (!isset($this->buffer[$this->offset]))
{
throw InsufficientDataException::unexpectedLength($this->buffer, $this->offset, 1);
}
$c = ord($this->buffer[$this->offset]);
++$this->offset;
if ($c >= 0x80 && $c <= 0x8f)
{
return $c & 0xf;
}
if (0xde === $c)
{
return $this->decodeUint16();
}
if (0xdf === $c)
{
return $this->decodeUint32();
}
throw DecodingFailedException::unexpectedCode($c, 'map header');
}
/**
* @return mixed|Ext
*/
public function decodeExt()
{
if (!isset($this->buffer[$this->offset]))
{
throw InsufficientDataException::unexpectedLength($this->buffer, $this->offset, 1);
}
$c = ord($this->buffer[$this->offset]);
++$this->offset;
switch ($c)
{
case 0xd4: return $this->decodeExtData(1);
case 0xd5: return $this->decodeExtData(2);
case 0xd6: return $this->decodeExtData(4);
case 0xd7: return $this->decodeExtData(8);
case 0xd8: return $this->decodeExtData(16);
case 0xc7: return $this->decodeExtData($this->decodeUint8());
case 0xc8: return $this->decodeExtData($this->decodeUint16());
case 0xc9: return $this->decodeExtData($this->decodeUint32());
}
throw DecodingFailedException::unexpectedCode($c, 'ext header');
}
/**
* @return int
*/
private function decodeUint8()
{
if (!isset($this->buffer[$this->offset]))
{
throw InsufficientDataException::unexpectedLength($this->buffer, $this->offset, 1);
}
return ord($this->buffer[$this->offset++]);
}
/**
* @return int
*/
private function decodeUint16()
{
if (!isset($this->buffer[$this->offset + 1]))
{
throw InsufficientDataException::unexpectedLength($this->buffer, $this->offset, 2);
}
$hi = ord($this->buffer[$this->offset]);
$lo = ord($this->buffer[++$this->offset]);
++$this->offset;
return $hi << 8 | $lo;
}
/**
* @return mixed
*/
private function decodeUint32()
{
if (!isset($this->buffer[$this->offset + 3]))
{
throw InsufficientDataException::unexpectedLength($this->buffer, $this->offset, 4);
}
$num = unpack('N', $this->buffer, $this->offset)[1];
$this->offset += 4;
return $num;
}
/**
* @return resource|string
*/
private function decodeUint64()
{
if (!isset($this->buffer[$this->offset + 7]))
{
throw InsufficientDataException::unexpectedLength($this->buffer, $this->offset, 8);
}
$num = unpack('J', $this->buffer, $this->offset)[1];
$this->offset += 8;
return $num < 0 ? $this->handleIntOverflow($num) : $num;
}
/**
* @return int
*/
private function decodeInt8()
{
if (!isset($this->buffer[$this->offset]))
{
throw InsufficientDataException::unexpectedLength($this->buffer, $this->offset, 1);
}
$num = ord($this->buffer[$this->offset]);
++$this->offset;
return $num > 0x7f ? $num - 0x100 : $num;
}
/**
* @return int
*/
private function decodeInt16()
{
if (!isset($this->buffer[$this->offset + 1]))
{
throw InsufficientDataException::unexpectedLength($this->buffer, $this->offset, 2);
}
$hi = ord($this->buffer[$this->offset]);
$lo = ord($this->buffer[++$this->offset]);
++$this->offset;
return $hi > 0x7f ? $hi << 8 | $lo - 0x10000 : $hi << 8 | $lo;
}
/**
* @return int
*/
private function decodeInt32()
{
if (!isset($this->buffer[$this->offset + 3]))
{
throw InsufficientDataException::unexpectedLength($this->buffer, $this->offset, 4);
}
$num = unpack('N', $this->buffer, $this->offset)[1];
$this->offset += 4;
return $num > 0x7fffffff ? $num - 0x100000000 : $num;
}
/**
* @return mixed
*/
private function decodeInt64()
{
if (!isset($this->buffer[$this->offset + 7]))
{
throw InsufficientDataException::unexpectedLength($this->buffer, $this->offset, 8);
}
$num = unpack('J', $this->buffer, $this->offset)[1];
$this->offset += 8;
return $num;
}
/**
* @return mixed
*/
private function decodeFloat32()
{
if (!isset($this->buffer[$this->offset + 3]))
{
throw InsufficientDataException::unexpectedLength($this->buffer, $this->offset, 4);
}
$num = unpack('G', $this->buffer, $this->offset)[1];
$this->offset += 4;
return $num;
}
/**
* @return mixed
*/
private function decodeFloat64()
{
if (!isset($this->buffer[$this->offset + 7]))
{
throw InsufficientDataException::unexpectedLength($this->buffer, $this->offset, 8);
}
$num = unpack('E', $this->buffer, $this->offset)[1];
$this->offset += 8;
return $num;
}
/**
* @param $length
* @return bool|string
*/
private function decodeStrData($length)
{
if (!isset($this->buffer[$this->offset + $length - 1]))
{
throw InsufficientDataException::unexpectedLength($this->buffer, $this->offset, $length);
}
$str = substr($this->buffer, $this->offset, $length);
$this->offset += $length;
return $str;
}
/**
* @param $size
* @return array
*/
private function decodeArrayData($size)
{
$array = [];
while ($size--)
{
$array[] = $this->decode();
}
return $array;
}
/**
* @param $size
* @return array
*/
private function decodeMapData($size)
{
$map = [];
while ($size--)
{
$map[$this->decode()] = $this->decode();
}
return $map;
}
/**
* @param $length
* @return mixed|Ext
*/
private function decodeExtData($length)
{
if (!isset($this->buffer[$this->offset + $length - 1]))
{
throw InsufficientDataException::unexpectedLength($this->buffer, $this->offset, $length);
}
// int8
$num = ord($this->buffer[$this->offset]);
++$this->offset;
$type = $num > 0x7f ? $num - 0x100 : $num;
if (isset($this->transformers[$type]))
{
return $this->transformers[$type]->decode($this, $length);
}
$data = substr($this->buffer, $this->offset, $length);
$this->offset += $length;
return new Ext($type, $data);
}
/**
* @param $value
* @return resource|string
*/
private function handleIntOverflow($value)
{
if ($this->isBigIntAsStr)
{
return sprintf('%u', $value);
}
if ($this->isBigIntAsGmp)
{
return gmp_init(sprintf('%u', $value));
}
throw new IntegerOverflowException($value);
}
}

View File

@ -0,0 +1,96 @@
<?php
namespace ZiProto;
use ZiProto\Exception\InvalidOptionException;
use ZiProto\Abstracts\Options;
/**
* Class DecodingOptions
* @package ZiProto
*/
final class DecodingOptions
{
/**
* @var
*/
private $bigIntMode;
/**
* DecodingOptions constructor.
*/
private function __construct() {}
/**
* @return DecodingOptions
*/
public static function fromDefaults() : self
{
$self = new self();
$self->bigIntMode = Options::BIGINT_AS_STR;
return $self;
}
/**
* @param int $bitmask
* @return DecodingOptions
*/
public static function fromBitmask(int $bitmask) : self
{
$self = new self();
$self->bigIntMode = self::getSingleOption('bigint', $bitmask,
Options::BIGINT_AS_STR |
Options::BIGINT_AS_GMP |
Options::BIGINT_AS_EXCEPTION
) ?: Options::BIGINT_AS_STR;
return $self;
}
/**
* @return bool
*/
public function isBigIntAsStrMode() : bool
{
return Options::BIGINT_AS_STR === $this->bigIntMode;
}
/**
* @return bool
*/
public function isBigIntAsGmpMode() : bool
{
return Options::BIGINT_AS_GMP === $this->bigIntMode;
}
/**
* @param string $name
* @param int $bitmask
* @param int $validBitmask
* @return int
*/
private static function getSingleOption(string $name, int $bitmask, int $validBitmask) : int
{
$option = $bitmask & $validBitmask;
if ($option === ($option & -$option))
{
return $option;
}
static $map = [
Options::BIGINT_AS_STR => 'BIGINT_AS_STR',
Options::BIGINT_AS_GMP => 'BIGINT_AS_GMP',
Options::BIGINT_AS_EXCEPTION => 'BIGINT_AS_EXCEPTION',
];
$validOptions = [];
for ($i = $validBitmask & -$validBitmask; $i <= $validBitmask; $i <<= 1)
{
$validOptions[] = __CLASS__.'::'.$map[$i];
}
throw InvalidOptionException::outOfRange($name, $validOptions);
}
}

View File

@ -0,0 +1,171 @@
<?php
namespace ZiProto;
use ZiProto\Exception\InvalidOptionException;
use ZiProto\Abstracts\Options;
/**
* Class EncodingOptions
* @package ZiProto
*/
final class EncodingOptions
{
/**
* @var mixed
*/
private $strBinMode;
/**
* @var mixed
*/
private $arrMapMode;
/**
* @var mixed
*/
private $floatMode;
/**
* EncodingOptions constructor.
*/
private function __construct() {}
/**
* @return EncodingOptions
*/
public static function fromDefaults() : self
{
$self = new self();
$self->strBinMode = Options::DETECT_STR_BIN;
$self->arrMapMode = Options::DETECT_ARR_MAP;
$self->floatMode = Options::FORCE_FLOAT64;
return $self;
}
/**
* @param int $bitmask
* @return EncodingOptions
*/
public static function fromBitmask(int $bitmask) : self
{
$self = new self();
if (self::getSingleOption('str/bin', $bitmask, Options::FORCE_STR | Options::FORCE_BIN | Options::DETECT_STR_BIN))
{
$self->strBinMode = self::getSingleOption('str/bin', $bitmask,
Options::FORCE_STR |
Options::FORCE_BIN |
Options::DETECT_STR_BIN
);
}
else
{
$self->strBinMode = Options::DETECT_STR_BIN;
}
if (self::getSingleOption('arr/map', $bitmask, Options::FORCE_ARR | Options::FORCE_MAP | Options::DETECT_ARR_MAP))
{
$self->arrMapMode = self::getSingleOption('arr/map', $bitmask,
Options::FORCE_ARR |
Options::FORCE_MAP |
Options::DETECT_ARR_MAP
);
}
else
{
$self->arrMapMode = Options::DETECT_ARR_MAP;
}
if (self::getSingleOption('float', $bitmask, Options::FORCE_FLOAT32 | Options::FORCE_FLOAT64))
{
$self->floatMode = self::getSingleOption('float', $bitmask,
Options::FORCE_FLOAT32 |
Options::FORCE_FLOAT64
);
}
else
{
$self->floatMode = Options::FORCE_FLOAT64;
}
return $self;
}
/**
* @return bool
*/
public function isDetectStrBinMode() : bool
{
return Options::DETECT_STR_BIN === $this->strBinMode;
}
/**
* @return bool
*/
public function isForceStrMode() : bool
{
return Options::FORCE_STR === $this->strBinMode;
}
/**
* @return bool
*/
public function isDetectArrMapMode() : bool
{
return Options::DETECT_ARR_MAP === $this->arrMapMode;
}
/**
* @return bool
*/
public function isForceArrMode() : bool
{
return Options::FORCE_ARR === $this->arrMapMode;
}
/**
* @return bool
*/
public function isForceFloat32Mode() : bool
{
return Options::FORCE_FLOAT32 === $this->floatMode;
}
/**
* @param string $name
* @param int $bitmask
* @param int $validBitmask
* @return int
*/
private static function getSingleOption(string $name, int $bitmask, int $validBitmask) : int
{
$option = $bitmask & $validBitmask;
if ($option === ($option & -$option))
{
return $option;
}
static $map = [
Options::FORCE_STR => 'FORCE_STR',
Options::FORCE_BIN => 'FORCE_BIN',
Options::DETECT_STR_BIN => 'DETECT_STR_BIN',
Options::FORCE_ARR => 'FORCE_ARR',
Options::FORCE_MAP => 'FORCE_MAP',
Options::DETECT_ARR_MAP => 'DETECT_ARR_MAP',
Options::FORCE_FLOAT32 => 'FORCE_FLOAT32',
Options::FORCE_FLOAT64 => 'FORCE_FLOAT64',
];
$validOptions = [];
for ($i = $validBitmask & -$validBitmask; $i <= $validBitmask; $i <<= 1)
{
$validOptions[] = __CLASS__.'::'.$map[$i];
}
throw InvalidOptionException::outOfRange($name, $validOptions);
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace ZiProto\Exception;
use RuntimeException;
use function sprintf;
/**
* Class DecodingFailedException
* @package ZiProto\Exception
*/
class DecodingFailedException extends RuntimeException
{
/**
* @param int $code
* @return DecodingFailedException
*/
public static function unknownCode(int $code) : self
{
return new self(sprintf('Unknown code: 0x%x.', $code));
}
/**
* @param int $code
* @param string $type
* @return DecodingFailedException
*/
public static function unexpectedCode(int $code, string $type) : self
{
return new self(sprintf('Unexpected %s code: 0x%x.', $type, $code));
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace ZiProto\Exception;
use function get_class;
use function gettype;
use function is_object;
use RuntimeException;
use function sprintf;
use Throwable;
/**
* Class EncodingFailedException
* @package ZiProto\Exception
*/
class EncodingFailedException extends RuntimeException
{
/**
* @var mixed
*/
private $value;
/**
* EncodingFailedException constructor.
* @param $value
* @param string $message
* @param Throwable|null $previous
*/
public function __construct($value, string $message = '', Throwable $previous = null)
{
parent::__construct($message, 0, $previous);
$this->value = $value;
}
/**
* @return mixed
*/
public function getValue()
{
return $this->value;
}
/**
* @param $value
* @return EncodingFailedException
*/
public static function unsupportedType($value) : self
{
$message = sprintf('Unsupported type: %s.',
is_object($value) ? get_class($value) : gettype($value)
);
return new self($value, $message);
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace ZiProto\Exception;
use function strlen;
/**
* Class InsufficientDataException
* @package ZiProto\Exception
*/
class InsufficientDataException extends DecodingFailedException
{
/**
* @param string $buffer
* @param int $offset
* @param int $expectedLength
* @return InsufficientDataException
*/
public static function unexpectedLength(string $buffer, int $offset, int $expectedLength) : self
{
$actualLength = strlen($buffer) - $offset;
$message = "Not enough data to unpack: expected $expectedLength, got $actualLength.";
return new self($message);
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace ZiProto\Exception;
use function sprintf;
/**
* Class IntegerOverflowException
* @package ZiProto\Exception
*/
class IntegerOverflowException extends DecodingFailedException
{
/**
* @var int
*/
private $value;
/**
* IntegerOverflowException constructor.
* @param int $value
*/
public function __construct(int $value)
{
parent::__construct(sprintf('The value is too big: %u.', $value));
$this->value = $value;
}
/**
* @return int
*/
public function getValue() : int
{
return $this->value;
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace ZiProto\Exception;
use function array_pop;
use function count;
use function implode;
use InvalidArgumentException;
use function sprintf;
/**
* Class InvalidOptionException
* @package ZiProto\Exception
*/
class InvalidOptionException extends InvalidArgumentException
{
/**
* @param string $invalidOption
* @param array $validOptions
* @return InvalidOptionException
*/
public static function outOfRange(string $invalidOption, array $validOptions) : self
{
$use = count($validOptions) > 2
? sprintf('one of %2$s or %1$s', array_pop($validOptions), implode(', ', $validOptions))
: implode(' or ', $validOptions);
return new self("Invalid option $invalidOption, use $use.");
}
}

30
src/ZiProto/Ext.php Normal file
View File

@ -0,0 +1,30 @@
<?php
namespace ZiProto;
/**
* Class Ext
* @package ZiProto
*/
final class Ext
{
/**
* @var int
*/
public $type;
/**
* @var string
*/
public $data;
/**
* Ext constructor.
* @param int $type
* @param string $data
*/
public function __construct(int $type, string $data)
{
$this->type = $type;
$this->data = $data;
}
}

419
src/ZiProto/Packet.php Normal file
View File

@ -0,0 +1,419 @@
<?php
namespace ZiProto;
use function array_values;
use function chr;
use function count;
use function is_array;
use function is_bool;
use function is_float;
use function is_int;
use function is_string;
use function pack;
use function preg_match;
use function strlen;
use ZiProto\Abstracts\Regex;
use ZiProto\Exception\InvalidOptionException;
use ZiProto\Exception\EncodingFailedException;
use ZiProto\TypeTransformer\Validator;
/**
* Class Packet
* @package ZiProto
*/
class Packet
{
/**
* @var bool
*/
private $isDetectStrBin;
/**
* @var bool
*/
private $isForceStr;
/**
* @var bool
*/
private $isDetectArrMap;
/**
* @var bool
*/
private $isForceArr;
/**
* @var bool
*/
private $isForceFloat32;
/**
* @var Validator[]|null
*/
private $transformers;
/**
* @param EncodingOptions|int|null $options
*
* @throws InvalidOptionException
*/
public function __construct($options = null)
{
if (null === $options)
{
$options = EncodingOptions::fromDefaults();
}
elseif (!$options instanceof EncodingOptions)
{
$options = EncodingOptions::fromBitmask($options);
}
$this->isDetectStrBin = $options->isDetectStrBinMode();
$this->isForceStr = $options->isForceStrMode();
$this->isDetectArrMap = $options->isDetectArrMapMode();
$this->isForceArr = $options->isForceArrMode();
$this->isForceFloat32 = $options->isForceFloat32Mode();
}
/**
* @param Validator $transformer
* @return Packet
*/
public function registerTransformer(Validator $transformer) : self
{
$this->transformers[] = $transformer;
return $this;
}
/**
* @param $value
* @return false|string
*/
public function encode($value)
{
if (is_int($value))
{
return $this->encodeInt($value);
}
if (is_string($value))
{
if ($this->isForceStr)
{
return $this->encodeStr($value);
}
if ($this->isDetectStrBin)
{
return preg_match(Regex::UTF8_REGEX, $value)
? $this->encodeStr($value)
: $this->encodeBin($value);
}
return $this->encodeBin($value);
}
if (is_array($value))
{
if ($this->isDetectArrMap)
{
return array_values($value) === $value
? $this->encodeArray($value)
: $this->encodeMap($value);
}
return $this->isForceArr ? $this->encodeArray($value) : $this->encodeMap($value);
}
if (null === $value)
{
return "\xc0";
}
if (is_bool($value))
{
return $value ? "\xc3" : "\xc2";
}
if (is_float($value))
{
return $this->encodeFloat($value);
}
if ($value instanceof Ext)
{
return $this->encodeExt($value->type, $value->data);
}
if ($this->transformers)
{
foreach ($this->transformers as $transformer)
{
if (null !== $encoded = $transformer->check($this, $value))
{
return $encoded;
}
}
}
throw EncodingFailedException::unsupportedType($value);
}
/**
* @return string
*/
public function encodeNil()
{
return "\xc0";
}
/**
* @param $bool
* @return string
*/
public function encodeBool($bool)
{
return $bool ? "\xc3" : "\xc2";
}
/**
* @param $int
* @return false|string
*/
public function encodeInt($int)
{
if ($int >= 0)
{
if ($int <= 0x7f)
{
return chr($int);
}
if ($int <= 0xff)
{
return "\xcc". chr($int);
}
if ($int <= 0xffff)
{
return "\xcd". chr($int >> 8). chr($int);
}
if ($int <= 0xffffffff)
{
return pack('CN', 0xce, $int);
}
return pack('CJ', 0xcf, $int);
}
if ($int >= -0x20)
{
return chr(0xe0 | $int);
}
if ($int >= -0x80)
{
return "\xd0". chr($int);
}
if ($int >= -0x8000)
{
return "\xd1". chr($int >> 8). chr($int);
}
if ($int >= -0x80000000)
{
return pack('CN', 0xd2, $int);
}
return pack('CJ', 0xd3, $int);
}
/**
* @param $float
* @return string
*/
public function encodeFloat($float)
{
return $this->isForceFloat32
? "\xca". pack('G', $float)
: "\xcb". pack('E', $float);
}
/**
* @param $str
* @return string
*/
public function encodeStr($str)
{
$length = strlen($str);
if ($length < 32)
{
return chr(0xa0 | $length).$str;
}
if ($length <= 0xff)
{
return "\xd9". chr($length).$str;
}
if ($length <= 0xffff)
{
return "\xda". chr($length >> 8). chr($length).$str;
}
return pack('CN', 0xdb, $length).$str;
}
/**
* @param $str
* @return string
*/
public function encodeBin($str)
{
$length = strlen($str);
if ($length <= 0xff)
{
return "\xc4". chr($length).$str;
}
if ($length <= 0xffff)
{
return "\xc5". chr($length >> 8). chr($length).$str;
}
return pack('CN', 0xc6, $length).$str;
}
/**
* @param $array
* @return false|string
*/
public function encodeArray($array)
{
$data = $this->encodeArrayHeader(count($array));
foreach ($array as $val)
{
$data .= $this->encode($val);
}
return $data;
}
/**
* @param $size
* @return false|string
*/
public function encodeArrayHeader($size)
{
if ($size <= 0xf)
{
return chr(0x90 | $size);
}
if ($size <= 0xffff)
{
return "\xdc". chr($size >> 8). chr($size);
}
return pack('CN', 0xdd, $size);
}
/**
* @param $map
* @return false|string
*/
public function encodeMap($map)
{
$data = $this->encodeMapHeader(count($map));
if ($this->isForceStr)
{
foreach ($map as $key => $val)
{
$data .= is_string($key) ? $this->encodeStr($key) : $this->encodeInt($key);
$data .= $this->encode($val);
}
return $data;
}
if ($this->isDetectStrBin)
{
foreach ($map as $key => $val)
{
$data .= is_string($key)
? (preg_match(Regex::UTF8_REGEX, $key) ? $this->encodeStr($key) : $this->encodeBin($key))
: $this->encodeInt($key);
$data .= $this->encode($val);
}
return $data;
}
foreach ($map as $key => $val)
{
$data .= is_string($key) ? $this->encodeBin($key) : $this->encodeInt($key);
$data .= $this->encode($val);
}
return $data;
}
/**
* @param $size
* @return false|string
*/
public function encodeMapHeader($size)
{
if ($size <= 0xf)
{
return chr(0x80 | $size);
}
if ($size <= 0xffff)
{
return "\xde". chr($size >> 8). chr($size);
}
return pack('CN', 0xdf, $size);
}
/**
* @param $type
* @param $data
* @return string
*/
public function encodeExt($type, $data)
{
$length = strlen($data);
switch ($length)
{
case 1: return "\xd4". chr($type).$data;
case 2: return "\xd5". chr($type).$data;
case 4: return "\xd6". chr($type).$data;
case 8: return "\xd7". chr($type).$data;
case 16: return "\xd8". chr($type).$data;
}
if ($length <= 0xff)
{
return "\xc7". chr($length). chr($type).$data;
}
if ($length <= 0xffff)
{
return pack('CnC', 0xc8, $length, $type).$data;
}
return pack('CNC', 0xc9, $length, $type).$data;
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace ZiProto\Type;
/**
* Class Binary
* @package ZiProto\Type
*/
final class Binary
{
/**
* @var string
*/
public $data;
/**
* Binary constructor.
* @param string $data
*/
public function __construct(string $data)
{
$this->data = $data;
}
}

24
src/ZiProto/Type/Map.php Normal file
View File

@ -0,0 +1,24 @@
<?php
namespace ZiProto\Type;
/**
* Class Map
* @package ZiProto\Type
*/
final class Map
{
/**
* @var array
*/
public $map;
/**
* Map constructor.
* @param array $map
*/
public function __construct(array $map)
{
$this->map = $map;
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace ZiProto\TypeTransformer;
use ZiProto\Packet;
use ZiProto\Type\Binary;
/**
* Class BinaryTransformer
* @package ZiProto\TypeTransformer
*/
abstract class BinaryTransformer
{
/**
* @param Packet $packer
* @param $value
* @return string
*/
public function pack(Packet $packer, $value): string
{
if ($value instanceof Binary)
{
return $packer->encodeBin($value->data);
}
else
{
return null;
}
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace ZiProto\TypeTransformer;
use ZiProto\BufferStream;
/**
* Interface Extension
* @package ZiProto\TypeTransformer
*/
interface Extension
{
/**
* @return int
*/
public function getType() : int;
/**
* @param BufferStream $stream
* @param int $extLength
* @return mixed
*/
public function decode(BufferStream $stream, int $extLength);
}

View File

@ -0,0 +1,29 @@
<?php
namespace ZiProto\TypeTransformer;
use ZiProto\Packet;
use ZiProto\Type\Map;
/**
* Class MapTransformer
* @package ZiProto\TypeTransformer
*/
abstract class MapTransformer
{
/**
* @param Packet $packer
* @param $value
* @return string
*/
public function encode(Packet $packer, $value): string
{
if ($value instanceof Map)
{
return $packer->encodeMap($value->map);
}
else
{
return null;
}
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace ZiProto\TypeTransformer;
use ZiProto\Packet;
/**
* Interface Validator
* @package ZiProto\TypeTransformer
*/
interface Validator
{
/**
* @param Packet $packer
* @param $value
* @return string
*/
public function check(Packet $packer, $value) :string;
}

63
src/ZiProto/ZiProto.php Normal file
View File

@ -0,0 +1,63 @@
<?php
namespace ZiProto;
use ZiProto\Exception\DecodingFailedException;
use ZiProto\Exception\EncodingFailedException;
use ZiProto\Exception\InvalidOptionException;
include_once(__DIR__ . DIRECTORY_SEPARATOR . 'Abstracts' . DIRECTORY_SEPARATOR . 'Options.php');
include_once(__DIR__ . DIRECTORY_SEPARATOR . 'Abstracts' . DIRECTORY_SEPARATOR . 'Regex.php');
include_once(__DIR__ . DIRECTORY_SEPARATOR . 'Exception' . DIRECTORY_SEPARATOR . 'DecodingFailedException.php');
include_once(__DIR__ . DIRECTORY_SEPARATOR . 'Exception' . DIRECTORY_SEPARATOR . 'EncodingFailedException.php');
include_once(__DIR__ . DIRECTORY_SEPARATOR . 'Exception' . DIRECTORY_SEPARATOR . 'InsufficientDataException.php');
include_once(__DIR__ . DIRECTORY_SEPARATOR . 'Exception' . DIRECTORY_SEPARATOR . 'IntegerOverflowException.php');
include_once(__DIR__ . DIRECTORY_SEPARATOR . 'Exception' . DIRECTORY_SEPARATOR . 'InvalidOptionException.php');
include_once(__DIR__ . DIRECTORY_SEPARATOR . 'Type' . DIRECTORY_SEPARATOR . 'Binary.php');
include_once(__DIR__ . DIRECTORY_SEPARATOR . 'Type' . DIRECTORY_SEPARATOR . 'Map.php');
include_once(__DIR__ . DIRECTORY_SEPARATOR . 'TypeTransformer' . DIRECTORY_SEPARATOR . 'BinaryTransformer.php');
include_once(__DIR__ . DIRECTORY_SEPARATOR . 'TypeTransformer' . DIRECTORY_SEPARATOR . 'Extension.php');
include_once(__DIR__ . DIRECTORY_SEPARATOR . 'TypeTransformer' . DIRECTORY_SEPARATOR . 'MapTransformer.php');
include_once(__DIR__ . DIRECTORY_SEPARATOR . 'TypeTransformer' . DIRECTORY_SEPARATOR . 'Validator.php');
include_once(__DIR__ . DIRECTORY_SEPARATOR . 'BufferStream.php');
include_once(__DIR__ . DIRECTORY_SEPARATOR . 'DecodingOptions.php');
include_once(__DIR__ . DIRECTORY_SEPARATOR . 'EncodingOptions.php');
include_once(__DIR__ . DIRECTORY_SEPARATOR . 'Ext.php');
include_once(__DIR__ . DIRECTORY_SEPARATOR . 'Packet.php');
/**
* ZiProto Class
*
* Class ZiProto
* @package ZiProto
*/
class ZiProto
{
/**
* @param mixed $value
* @param EncodingOptions|int|null $options
*
* @throws InvalidOptionException
* @throws EncodingFailedException
*
* @return string
*/
public static function encode($value, $options = null) : string
{
return (new Packet($options))->encode($value);
}
/**
* @param string $data
* @param DecodingOptions|int|null $options
*
* @throws InvalidOptionException
* @throws DecodingFailedException
*
* @return mixed
*/
public static function decode(string $data, $options = null)
{
return (new BufferStream($data, $options))->decode();
}
}

3
src/ZiProto/ziproto.json Normal file
View File

@ -0,0 +1,3 @@
{
"version": "1.0.0.1"
}