PocketMine-MP 5.15.1 git-be6754494fdbbb9dd57c058ba0e33a4a78c4581f
Utils.php
1<?php
2
3/*
4 *
5 * ____ _ _ __ __ _ __ __ ____
6 * | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
7 * | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
8 * | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
9 * |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
10 *
11 * This program is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU Lesser General Public License as published by
13 * the Free Software Foundation, either version 3 of the License, or
14 * (at your option) any later version.
15 *
16 * @author PocketMine Team
17 * @link http://www.pocketmine.net/
18 *
19 *
20 */
21
22declare(strict_types=1);
23
28namespace pocketmine\utils;
29
35use Ramsey\Uuid\Uuid;
36use Ramsey\Uuid\UuidInterface;
37use function array_combine;
38use function array_map;
39use function array_reverse;
40use function array_values;
41use function bin2hex;
42use function chunk_split;
43use function class_exists;
44use function count;
45use function debug_zval_dump;
46use function dechex;
47use function exec;
48use function explode;
49use function file;
50use function file_exists;
51use function file_get_contents;
52use function function_exists;
53use function get_class;
54use function get_current_user;
55use function get_loaded_extensions;
56use function getenv;
57use function gettype;
58use function implode;
59use function interface_exists;
60use function is_a;
61use function is_array;
62use function is_bool;
63use function is_float;
64use function is_infinite;
65use function is_int;
66use function is_nan;
67use function is_object;
68use function is_string;
69use function mb_check_encoding;
70use function ob_end_clean;
71use function ob_get_contents;
72use function ob_start;
73use function opcache_get_status;
74use function ord;
75use function php_uname;
76use function phpversion;
77use function preg_grep;
78use function preg_match;
79use function preg_match_all;
80use function preg_replace;
81use function shell_exec;
82use function spl_object_id;
83use function str_ends_with;
84use function str_pad;
85use function str_split;
86use function str_starts_with;
87use function stripos;
88use function strlen;
89use function substr;
90use function sys_get_temp_dir;
91use function trim;
92use function xdebug_get_function_stack;
93use const PHP_EOL;
94use const PHP_INT_MAX;
95use const PHP_INT_SIZE;
96use const PHP_MAXPATHLEN;
97use const STR_PAD_LEFT;
98use const STR_PAD_RIGHT;
99
103final class Utils{
104 public const OS_WINDOWS = "win";
105 public const OS_IOS = "ios";
106 public const OS_MACOS = "mac";
107 public const OS_ANDROID = "android";
108 public const OS_LINUX = "linux";
109 public const OS_BSD = "bsd";
110 public const OS_UNKNOWN = "other";
111
112 private static ?string $os = null;
113 private static ?UuidInterface $serverUniqueId = null;
114 private static ?int $cpuCores = null;
115
122 public static function getNiceClosureName(\Closure $closure) : string{
123 $func = new \ReflectionFunction($closure);
124 if(!str_ends_with($func->getName(), '{closure}')){
125 //closure wraps a named function, can be done with reflection or fromCallable()
126 //isClosure() is useless here because it just tells us if $func is reflecting a Closure object
127
128 $scope = $func->getClosureScopeClass();
129 if($scope !== null){ //class method
130 return
131 $scope->getName() .
132 ($func->getClosureThis() !== null ? "->" : "::") .
133 $func->getName(); //name doesn't include class in this case
134 }
135
136 //non-class function
137 return $func->getName();
138 }
139 $filename = $func->getFileName();
140
141 return "closure@" . ($filename !== false ?
142 Filesystem::cleanPath($filename) . "#L" . $func->getStartLine() :
143 "internal"
144 );
145 }
146
152 public static function getNiceClassName(object $obj) : string{
153 $reflect = new \ReflectionClass($obj);
154 if($reflect->isAnonymous()){
155 $filename = $reflect->getFileName();
156
157 return "anonymous@" . ($filename !== false ?
158 Filesystem::cleanPath($filename) . "#L" . $reflect->getStartLine() :
159 "internal"
160 );
161 }
162
163 return $reflect->getName();
164 }
165
169 public static function cloneCallback() : \Closure{
170 return static function(object $o){
171 return clone $o;
172 };
173 }
174
185 public static function cloneObjectArray(array $array) : array{
187 $callback = self::cloneCallback();
188 return array_map($callback, $array);
189 }
190
199 public static function getMachineUniqueId(string $extra = "") : UuidInterface{
200 if(self::$serverUniqueId !== null && $extra === ""){
201 return self::$serverUniqueId;
202 }
203
204 $machine = php_uname("a");
205 $cpuinfo = @file("/proc/cpuinfo");
206 if($cpuinfo !== false){
207 $cpuinfoLines = preg_grep("/(model name|Processor|Serial)/", $cpuinfo);
208 if($cpuinfoLines === false){
209 throw new AssumptionFailedError("Pattern is valid, so this shouldn't fail ...");
210 }
211 $machine .= implode("", $cpuinfoLines);
212 }
213 $machine .= sys_get_temp_dir();
214 $machine .= $extra;
215 $os = Utils::getOS();
216 if($os === Utils::OS_WINDOWS){
217 @exec("ipconfig /ALL", $mac);
218 $mac = implode("\n", $mac);
219 if(preg_match_all("#Physical Address[. ]{1,}: ([0-9A-F\\-]{17})#", $mac, $matches) > 0){
220 foreach($matches[1] as $i => $v){
221 if($v == "00-00-00-00-00-00"){
222 unset($matches[1][$i]);
223 }
224 }
225 $machine .= implode(" ", $matches[1]); //Mac Addresses
226 }
227 }elseif($os === Utils::OS_LINUX){
228 if(file_exists("/etc/machine-id")){
229 $machine .= file_get_contents("/etc/machine-id");
230 }else{
231 @exec("ifconfig 2>/dev/null", $mac);
232 $mac = implode("\n", $mac);
233 if(preg_match_all("#HWaddr[ \t]{1,}([0-9a-f:]{17})#", $mac, $matches) > 0){
234 foreach($matches[1] as $i => $v){
235 if($v == "00:00:00:00:00:00"){
236 unset($matches[1][$i]);
237 }
238 }
239 $machine .= implode(" ", $matches[1]); //Mac Addresses
240 }
241 }
242 }elseif($os === Utils::OS_ANDROID){
243 $machine .= @file_get_contents("/system/build.prop");
244 }elseif($os === Utils::OS_MACOS){
245 $machine .= shell_exec("system_profiler SPHardwareDataType | grep UUID");
246 }
247 $data = $machine . PHP_MAXPATHLEN;
248 $data .= PHP_INT_MAX;
249 $data .= PHP_INT_SIZE;
250 $data .= get_current_user();
251 foreach(get_loaded_extensions() as $ext){
252 $data .= $ext . ":" . phpversion($ext);
253 }
254
255 //TODO: use of NIL as namespace is a hack; it works for now, but we should have a proper namespace UUID
256 $uuid = Uuid::uuid3(Uuid::NIL, $data);
257
258 if($extra === ""){
259 self::$serverUniqueId = $uuid;
260 }
261
262 return $uuid;
263 }
264
275 public static function getOS(bool $recalculate = false) : string{
276 if(self::$os === null || $recalculate){
277 $uname = php_uname("s");
278 if(stripos($uname, "Darwin") !== false){
279 if(str_starts_with(php_uname("m"), "iP")){
280 self::$os = self::OS_IOS;
281 }else{
282 self::$os = self::OS_MACOS;
283 }
284 }elseif(stripos($uname, "Win") !== false || $uname === "Msys"){
285 self::$os = self::OS_WINDOWS;
286 }elseif(stripos($uname, "Linux") !== false){
287 if(@file_exists("/system/build.prop")){
288 self::$os = self::OS_ANDROID;
289 }else{
290 self::$os = self::OS_LINUX;
291 }
292 }elseif(stripos($uname, "BSD") !== false || $uname === "DragonFly"){
293 self::$os = self::OS_BSD;
294 }else{
295 self::$os = self::OS_UNKNOWN;
296 }
297 }
298
299 return self::$os;
300 }
301
302 public static function getCoreCount(bool $recalculate = false) : int{
303 if(self::$cpuCores !== null && !$recalculate){
304 return self::$cpuCores;
305 }
306
307 $processors = 0;
308 switch(Utils::getOS()){
309 case Utils::OS_LINUX:
310 case Utils::OS_ANDROID:
311 if(($cpuinfo = @file('/proc/cpuinfo')) !== false){
312 foreach($cpuinfo as $l){
313 if(preg_match('/^processor[ \t]*:[ \t]*[0-9]+$/m', $l) > 0){
314 ++$processors;
315 }
316 }
317 }elseif(($cpuPresent = @file_get_contents("/sys/devices/system/cpu/present")) !== false){
318 if(preg_match("/^([0-9]+)\\-([0-9]+)$/", trim($cpuPresent), $matches) > 0){
319 $processors = ((int) $matches[2]) - ((int) $matches[1]);
320 }
321 }
322 break;
323 case Utils::OS_BSD:
324 case Utils::OS_MACOS:
325 $processors = (int) shell_exec("sysctl -n hw.ncpu");
326 break;
327 case Utils::OS_WINDOWS:
328 $processors = (int) getenv("NUMBER_OF_PROCESSORS");
329 break;
330 }
331 return self::$cpuCores = $processors;
332 }
333
337 public static function hexdump(string $bin) : string{
338 $output = "";
339 $bin = str_split($bin, 16);
340 foreach($bin as $counter => $line){
341 $hex = chunk_split(chunk_split(str_pad(bin2hex($line), 32, " ", STR_PAD_RIGHT), 2, " "), 24, " ");
342 $ascii = preg_replace('#([^\x20-\x7E])#', ".", $line);
343 $output .= str_pad(dechex($counter << 4), 4, "0", STR_PAD_LEFT) . " " . $hex . " " . $ascii . PHP_EOL;
344 }
345
346 return $output;
347 }
348
352 public static function printable(mixed $str) : string{
353 if(!is_string($str)){
354 return gettype($str);
355 }
356
357 return preg_replace('#([^\x20-\x7E])#', '.', $str);
358 }
359
360 public static function javaStringHash(string $string) : int{
361 $hash = 0;
362 for($i = 0, $len = strlen($string); $i < $len; $i++){
363 $ord = ord($string[$i]);
364 if(($ord & 0x80) !== 0){
365 $ord -= 0x100;
366 }
367 $hash = 31 * $hash + $ord;
368 $hash &= 0xFFFFFFFF;
369 }
370 return $hash;
371 }
372
373 public static function getReferenceCount(object $value, bool $includeCurrent = true) : int{
374 ob_start();
375 debug_zval_dump($value);
376 $contents = ob_get_contents();
377 if($contents === false) throw new AssumptionFailedError("ob_get_contents() should never return false here");
378 $ret = explode("\n", $contents);
379 ob_end_clean();
380
381 if(preg_match('/^.* refcount\\(([0-9]+)\\)\\{$/', trim($ret[0]), $m) > 0){
382 return ((int) $m[1]) - ($includeCurrent ? 3 : 4); //$value + zval call + extra call
383 }
384 return -1;
385 }
386
387 private static function printableExceptionMessage(\Throwable $e) : string{
388 $errstr = preg_replace('/\s+/', ' ', trim($e->getMessage()));
389
390 $errno = $e->getCode();
391 if(is_int($errno)){
392 try{
393 $errno = ErrorTypeToStringMap::get($errno);
394 }catch(\InvalidArgumentException $ex){
395 //pass
396 }
397 }
398
399 $errfile = Filesystem::cleanPath($e->getFile());
400 $errline = $e->getLine();
401
402 return get_class($e) . ": \"$errstr\" ($errno) in \"$errfile\" at line $errline";
403 }
404
409 public static function printableExceptionInfo(\Throwable $e, $trace = null) : array{
410 if($trace === null){
411 $trace = $e->getTrace();
412 }
413
414 $lines = [self::printableExceptionMessage($e)];
415 $lines[] = "--- Stack trace ---";
416 foreach(Utils::printableTrace($trace) as $line){
417 $lines[] = " " . $line;
418 }
419 for($prev = $e->getPrevious(); $prev !== null; $prev = $prev->getPrevious()){
420 $lines[] = "--- Previous ---";
421 $lines[] = self::printableExceptionMessage($prev);
422 foreach(Utils::printableTrace($prev->getTrace()) as $line){
423 $lines[] = " " . $line;
424 }
425 }
426 $lines[] = "--- End of exception information ---";
427 return $lines;
428 }
429
430 private static function stringifyValueForTrace(mixed $value, int $maxStringLength) : string{
431 return match(true){
432 is_object($value) => "object " . self::getNiceClassName($value) . "#" . spl_object_id($value),
433 is_array($value) => "array[" . count($value) . "]",
434 is_string($value) => "string[" . strlen($value) . "] " . substr(Utils::printable($value), 0, $maxStringLength),
435 is_bool($value) => $value ? "true" : "false",
436 is_int($value) => "int " . $value,
437 is_float($value) => "float " . $value,
438 $value === null => "null",
439 default => gettype($value) . " " . Utils::printable((string) $value)
440 };
441 }
442
449 public static function printableTrace(array $trace, int $maxStringLength = 80) : array{
450 $messages = [];
451 for($i = 0; isset($trace[$i]); ++$i){
452 $params = "";
453 if(isset($trace[$i]["args"]) || isset($trace[$i]["params"])){
454 if(isset($trace[$i]["args"])){
455 $args = $trace[$i]["args"];
456 }else{
457 $args = $trace[$i]["params"];
458 }
461 $paramsList = [];
462 $offset = 0;
463 foreach($args as $argId => $value){
464 $paramsList[] = ($argId === $offset ? "" : "$argId: ") . self::stringifyValueForTrace($value, $maxStringLength);
465 $offset++;
466 }
467 $params = implode(", ", $paramsList);
468 }
469 $messages[] = "#$i " . (isset($trace[$i]["file"]) ? Filesystem::cleanPath($trace[$i]["file"]) : "") . "(" . (isset($trace[$i]["line"]) ? $trace[$i]["line"] : "") . "): " . (isset($trace[$i]["class"]) ? $trace[$i]["class"] . (($trace[$i]["type"] === "dynamic" || $trace[$i]["type"] === "->") ? "->" : "::") : "") . $trace[$i]["function"] . "(" . Utils::printable($params) . ")";
470 }
471 return $messages;
472 }
473
483 public static function printableTraceWithMetadata(array $rawTrace, int $maxStringLength = 80) : array{
484 $printableTrace = self::printableTrace($rawTrace, $maxStringLength);
485 $safeTrace = [];
486 foreach($printableTrace as $frameId => $printableFrame){
487 $rawFrame = $rawTrace[$frameId];
488 $safeTrace[$frameId] = new ThreadCrashInfoFrame(
489 $printableFrame,
490 $rawFrame["file"] ?? "unknown",
491 $rawFrame["line"] ?? 0
492 );
493 }
494
495 return $safeTrace;
496 }
497
502 public static function currentTrace(int $skipFrames = 0) : array{
503 ++$skipFrames; //omit this frame from trace, in addition to other skipped frames
504 if(function_exists("xdebug_get_function_stack") && count($trace = @xdebug_get_function_stack()) !== 0){
505 $trace = array_reverse($trace);
506 }else{
507 $e = new \Exception();
508 $trace = $e->getTrace();
509 }
510 for($i = 0; $i < $skipFrames; ++$i){
511 unset($trace[$i]);
512 }
513 return array_values($trace);
514 }
515
519 public static function printableCurrentTrace(int $skipFrames = 0) : array{
520 return self::printableTrace(self::currentTrace(++$skipFrames));
521 }
522
528 public static function parseDocComment(string $docComment) : array{
529 $rawDocComment = substr($docComment, 3, -2); //remove the opening and closing markers
530 preg_match_all('/(*ANYCRLF)^[\t ]*(?:\* )?@([a-zA-Z\-]+)(?:[\t ]+(.+?))?[\t ]*$/m', $rawDocComment, $matches);
531
532 return array_combine($matches[1], $matches[2]);
533 }
534
539 public static function testValidInstance(string $className, string $baseName) : void{
540 $baseInterface = false;
541 if(!class_exists($baseName)){
542 if(!interface_exists($baseName)){
543 throw new \InvalidArgumentException("Base class $baseName does not exist");
544 }
545 $baseInterface = true;
546 }
547 if(!class_exists($className)){
548 throw new \InvalidArgumentException("Class $className does not exist or is not a class");
549 }
550 if(!is_a($className, $baseName, true)){
551 throw new \InvalidArgumentException("Class $className does not " . ($baseInterface ? "implement" : "extend") . " $baseName");
552 }
553 $class = new \ReflectionClass($className);
554 if(!$class->isInstantiable()){
555 throw new \InvalidArgumentException("Class $className cannot be constructed");
556 }
557 }
558
571 public static function validateCallableSignature(callable|CallbackType $signature, callable $subject) : void{
572 if(!($signature instanceof CallbackType)){
573 $signature = CallbackType::createFromCallable($signature);
574 }
575 if(!$signature->isSatisfiedBy($subject)){
576 throw new \TypeError("Declaration of callable `" . CallbackType::createFromCallable($subject) . "` must be compatible with `" . $signature . "`");
577 }
578 }
579
585 public static function validateArrayValueType(array $array, \Closure $validator) : void{
586 foreach($array as $k => $v){
587 try{
588 $validator($v);
589 }catch(\TypeError $e){
590 throw new \TypeError("Incorrect type of element at \"$k\": " . $e->getMessage(), 0, $e);
591 }
592 }
593 }
594
605 public static function stringifyKeys(array $array) : \Generator{
606 foreach($array as $key => $value){ // @phpstan-ignore-line - this is where we fix the stupid bullshit with array keys :)
607 yield (string) $key => $value;
608 }
609 }
610
611 public static function checkUTF8(string $string) : void{
612 if(!mb_check_encoding($string, 'UTF-8')){
613 throw new \InvalidArgumentException("Text must be valid UTF-8");
614 }
615 }
616
623 public static function assumeNotFalse(mixed $value, \Closure|string $context = "This should never be false") : mixed{
624 if($value === false){
625 throw new AssumptionFailedError("Assumption failure: " . (is_string($context) ? $context : $context()) . " (THIS IS A BUG)");
626 }
627 return $value;
628 }
629
630 public static function checkFloatNotInfOrNaN(string $name, float $float) : void{
631 if(is_nan($float)){
632 throw new \InvalidArgumentException("$name cannot be NaN");
633 }
634 if(is_infinite($float)){
635 throw new \InvalidArgumentException("$name cannot be infinite");
636 }
637 }
638
639 public static function checkVector3NotInfOrNaN(Vector3 $vector3) : void{
640 if($vector3 instanceof Location){ //location could be masquerading as vector3
641 self::checkFloatNotInfOrNaN("yaw", $vector3->yaw);
642 self::checkFloatNotInfOrNaN("pitch", $vector3->pitch);
643 }
644 self::checkFloatNotInfOrNaN("x", $vector3->x);
645 self::checkFloatNotInfOrNaN("y", $vector3->y);
646 self::checkFloatNotInfOrNaN("z", $vector3->z);
647 }
648
649 public static function checkLocationNotInfOrNaN(Location $location) : void{
650 self::checkVector3NotInfOrNaN($location);
651 }
652
657 public static function getOpcacheJitMode() : ?int{
658 if(
659 function_exists('opcache_get_status') &&
660 ($opcacheStatus = opcache_get_status(false)) !== false &&
661 isset($opcacheStatus["jit"]["on"])
662 ){
663 $jit = $opcacheStatus["jit"];
664 if($jit["on"] === true){
665 return (($jit["opt_flags"] >> 2) * 1000) +
666 (($jit["opt_flags"] & 0x03) * 100) +
667 ($jit["kind"] * 10) +
668 $jit["opt_level"];
669 }
670
671 //jit available, but disabled
672 return 0;
673 }
674
675 //jit not available
676 return null;
677 }
678}
static printableExceptionInfo(\Throwable $e, $trace=null)
Definition: Utils.php:409
static parseDocComment(string $docComment)
Definition: Utils.php:528
static assumeNotFalse(mixed $value, \Closure|string $context="This should never be false")
Definition: Utils.php:623
static validateArrayValueType(array $array, \Closure $validator)
Definition: Utils.php:585
static validateCallableSignature(callable|CallbackType $signature, callable $subject)
Definition: Utils.php:571
static getMachineUniqueId(string $extra="")
Definition: Utils.php:199
static stringifyKeys(array $array)
Definition: Utils.php:605
static hexdump(string $bin)
Definition: Utils.php:337
static getNiceClosureName(\Closure $closure)
Definition: Utils.php:122
static getOS(bool $recalculate=false)
Definition: Utils.php:275
static currentTrace(int $skipFrames=0)
Definition: Utils.php:502
static printable(mixed $str)
Definition: Utils.php:352
static printableTraceWithMetadata(array $rawTrace, int $maxStringLength=80)
Definition: Utils.php:483
static testValidInstance(string $className, string $baseName)
Definition: Utils.php:539
static getOpcacheJitMode()
Definition: Utils.php:657
static getNiceClassName(object $obj)
Definition: Utils.php:152
static cloneCallback()
Definition: Utils.php:169
static cloneObjectArray(array $array)
Definition: Utils.php:185
static printableTrace(array $trace, int $maxStringLength=80)
Definition: Utils.php:449
static printableCurrentTrace(int $skipFrames=0)
Definition: Utils.php:519