171 lines
5.2 KiB
PHP
171 lines
5.2 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
namespace Contractless\Rpc\Protocol;
|
||
|
|
|
||
|
|
use Contractless\Rpc\Exception\ProtocolException;
|
||
|
|
|
||
|
|
final class Binary
|
||
|
|
{
|
||
|
|
public static function u8(int $value): string
|
||
|
|
{
|
||
|
|
self::range($value, 0, 0xff, 'u8');
|
||
|
|
return pack('C', $value);
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function u16(int $value): string
|
||
|
|
{
|
||
|
|
self::range($value, 0, 0xffff, 'u16');
|
||
|
|
return pack('v', $value);
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function u32(int $value): string
|
||
|
|
{
|
||
|
|
self::range($value, 0, 0xffffffff, 'u32');
|
||
|
|
return pack('V', $value);
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function u64(int $value): string
|
||
|
|
{
|
||
|
|
if (PHP_INT_SIZE < 8 || $value < 0) {
|
||
|
|
throw new ProtocolException('u64 values require 64-bit PHP and a non-negative integer.');
|
||
|
|
}
|
||
|
|
|
||
|
|
return pack('V2', $value & 0xffffffff, $value >> 32);
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function i8(int $value): string
|
||
|
|
{
|
||
|
|
self::range($value, -128, 127, 'i8');
|
||
|
|
return pack('c', $value);
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function i16(int $value): string
|
||
|
|
{
|
||
|
|
self::range($value, -32768, 32767, 'i16');
|
||
|
|
return pack('v', $value & 0xffff);
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function i32(int $value): string
|
||
|
|
{
|
||
|
|
self::range($value, -2147483648, 2147483647, 'i32');
|
||
|
|
return pack('V', $value & 0xffffffff);
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function i64(int $value): string
|
||
|
|
{
|
||
|
|
if (PHP_INT_SIZE < 8) {
|
||
|
|
throw new ProtocolException('i64 values require 64-bit PHP.');
|
||
|
|
}
|
||
|
|
return pack('V2', $value & 0xffffffff, $value >> 32);
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function u128(string $value): string
|
||
|
|
{
|
||
|
|
return self::decimalInteger($value, false);
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function i128(string $value): string
|
||
|
|
{
|
||
|
|
return self::decimalInteger($value, true);
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function readU32(string $bytes): int
|
||
|
|
{
|
||
|
|
self::length($bytes, 4, 'u32');
|
||
|
|
return unpack('Vvalue', $bytes)['value'];
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function hex(string $hex, int $bytes, string $field): string
|
||
|
|
{
|
||
|
|
if (strlen($hex) !== $bytes * 2 || !ctype_xdigit($hex)) {
|
||
|
|
throw new ProtocolException("$field must be exactly $bytes bytes of hexadecimal data.");
|
||
|
|
}
|
||
|
|
|
||
|
|
$decoded = hex2bin($hex);
|
||
|
|
if ($decoded === false) {
|
||
|
|
throw new ProtocolException("$field is not valid hexadecimal data.");
|
||
|
|
}
|
||
|
|
return $decoded;
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function fixedText(string $value, int $bytes, string $field): string
|
||
|
|
{
|
||
|
|
if (strlen($value) > $bytes) {
|
||
|
|
throw new ProtocolException("$field must be $bytes bytes or less.");
|
||
|
|
}
|
||
|
|
return str_pad($value, $bytes, ' ');
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function length(string $value, int $bytes, string $field): void
|
||
|
|
{
|
||
|
|
if (strlen($value) !== $bytes) {
|
||
|
|
throw new ProtocolException("$field must be exactly $bytes bytes.");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private static function range(int $value, int $minimum, int $maximum, string $type): void
|
||
|
|
{
|
||
|
|
if ($value < $minimum || $value > $maximum) {
|
||
|
|
throw new ProtocolException("$value is outside the $type range.");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private static function decimalInteger(string $value, bool $signed): string
|
||
|
|
{
|
||
|
|
$value = trim($value);
|
||
|
|
$negative = str_starts_with($value, '-');
|
||
|
|
$digits = $negative ? substr($value, 1) : $value;
|
||
|
|
if ($digits === '' || !ctype_digit($digits) || (!$signed && $negative)) {
|
||
|
|
throw new ProtocolException('Invalid 128-bit decimal integer.');
|
||
|
|
}
|
||
|
|
$digits = ltrim($digits, '0');
|
||
|
|
$digits = $digits === '' ? '0' : $digits;
|
||
|
|
|
||
|
|
$maximum = $signed
|
||
|
|
? ($negative ? '170141183460469231731687303715884105728' : '170141183460469231731687303715884105727')
|
||
|
|
: '340282366920938463463374607431768211455';
|
||
|
|
if (strlen($digits) > strlen($maximum)
|
||
|
|
|| (strlen($digits) === strlen($maximum) && strcmp($digits, $maximum) > 0)
|
||
|
|
) {
|
||
|
|
throw new ProtocolException('128-bit decimal integer is outside its allowed range.');
|
||
|
|
}
|
||
|
|
|
||
|
|
$bytes = '';
|
||
|
|
for ($index = 0; $index < 16; $index++) {
|
||
|
|
[$digits, $remainder] = self::divideDecimal($digits, 256);
|
||
|
|
$bytes .= chr($remainder);
|
||
|
|
}
|
||
|
|
if ($digits !== '0') {
|
||
|
|
throw new ProtocolException('128-bit decimal integer is too large.');
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($negative) {
|
||
|
|
$carry = 1;
|
||
|
|
for ($index = 0; $index < 16; $index++) {
|
||
|
|
$next = ((~ord($bytes[$index])) & 0xff) + $carry;
|
||
|
|
$bytes[$index] = chr($next & 0xff);
|
||
|
|
$carry = $next > 0xff ? 1 : 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return $bytes;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @return array{string, int} */
|
||
|
|
private static function divideDecimal(string $digits, int $divisor): array
|
||
|
|
{
|
||
|
|
$quotient = '';
|
||
|
|
$remainder = 0;
|
||
|
|
for ($index = 0, $length = strlen($digits); $index < $length; $index++) {
|
||
|
|
$number = ($remainder * 10) + (ord($digits[$index]) - 48);
|
||
|
|
$digit = intdiv($number, $divisor);
|
||
|
|
if ($quotient !== '' || $digit !== 0) {
|
||
|
|
$quotient .= (string) $digit;
|
||
|
|
}
|
||
|
|
$remainder = $number % $divisor;
|
||
|
|
}
|
||
|
|
return [$quotient === '' ? '0' : $quotient, $remainder];
|
||
|
|
}
|
||
|
|
}
|