420 lines
10 KiB
PHP
420 lines
10 KiB
PHP
<?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;
|
|
}
|
|
}
|