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