Contractless-PHP-RPC/src/Protocol/Binary.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];
}
}