<?php const STREAM_OPEN_FOR_INCLUDE = 128; final class HardCoreDebugLogger { public static function register(string $output = 'php://stdout') { register_tick_function(function () use ($output) { $bt = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1); $last = reset($bt); $info = sprintf("%.4f %s +%d\n", microtime(true), $last['file'], $last['line']); file_put_contents($output, $info, FILE_APPEND); }); HardCoreFilter::register(); HardCore::register(); } } final class HardCore { const PROTOCOLS = ['file', 'phar']; public static function register() { foreach (self::PROTOCOLS as $protocol) { stream_wrapper_unregister($protocol); stream_wrapper_register($protocol, self::class); } } public static function unregister() { foreach (self::PROTOCOLS as $protocol) { set_error_handler(function () { }); stream_wrapper_restore($protocol); restore_error_handler(); } } public function __construct() { } public function dir_closedir(): bool { closedir($this->resource); return true; } public function dir_opendir(string $path, int $options): bool { $this->resource = $this->wrapCallWithContext('opendir', $path); return false !== $this->resource; } public function dir_readdir() { return readdir($this->resource); } public function dir_rewinddir(): bool { rewinddir($this->resource); return true; } public function mkdir(string $path, int $mode, int $options): bool { $recursive = (bool) ($options & STREAM_MKDIR_RECURSIVE); return $this->wrapCallWithContext('mkdir', $path, $mode, $recursive); } public function rename(string $path_from, string $path_to): bool { return $this->wrapCallWithContext('rename', $path_from, $path_to); } public function rmdir(string $path, int $options): bool { return $this->wrapCallWithContext('rmdir', $path); } public function stream_cast(int $cast_as) { return $this->resource; } public function stream_close() { fclose($this->resource); } public function stream_eof(): bool { return feof($this->resource); } public function stream_flush(): bool { return fflush($this->resource); } public function stream_lock(int $operation): bool { return flock($this->resource, $operation); } public function stream_metadata(string $path, int $option, $value): bool { return $this->wrapCall(function (string $path, int $option, $value) { $result = false; switch ($option) { case STREAM_META_TOUCH: if (empty($value)) { $result = touch($path); } else { $result = touch($path, $value[0], $value[1]); } break; case STREAM_META_OWNER_NAME: case STREAM_META_OWNER: $result = chown($path, $value); break; case STREAM_META_GROUP_NAME: case STREAM_META_GROUP: $result = chgrp($path, $value); break; case STREAM_META_ACCESS: $result = chmod($path, $value); break; } return $result; }, $path, $option, $value); } public function stream_open(string $path, string $mode, int $options, string &$opened_path = null): bool { $useIncludePath = (bool) ($options & STREAM_USE_PATH); $this->resource = $this->wrapCallWithContext('fopen', $path, $mode, $useIncludePath); $including = (bool) ($options & STREAM_OPEN_FOR_INCLUDE); if ($including && false !== $this->resource) { HardCoreFilter::append($this->resource, $path); } return false !== $this->resource; } public function stream_read(int $count): string { return fread($this->resource, $count); } public function stream_seek(int $offset, int $whence = SEEK_SET): bool { return fseek($this->resource, $offset, $whence); } public function stream_set_option(int $option, int $arg1, int $arg2): bool { switch ($option) { case STREAM_OPTION_BLOCKING: return stream_set_blocking($this->resource, $arg1); case STREAM_OPTION_READ_TIMEOUT: return stream_set_timeout($this->resource, $arg1, $arg2); case STREAM_OPTION_WRITE_BUFFER: return stream_set_write_buffer($this->resource, $arg1); case STREAM_OPTION_READ_BUFFER: return stream_set_read_buffer($this->resource, $arg1); } } public function stream_stat(): array { $stats = fstat($this->resource); unset($stats['size']); return $stats; } public function stream_tell(): int { return ftell($this->resource); } public function stream_truncate(int $new_size): bool { return ftruncate($this->resource, $new_size); } public function stream_write(string $data): int { return fwrite($this->resource, $data); } public function unlink(string $path): bool { return $this->wrapCallWithContext('unlink', $path); } public function url_stat(string $path, int $flags) { $result = @$this->wrapCall('stat', $path); if (false === $result) { $result = null; } return $result; } private function wrapCallWithContext(callable $function, ...$args) { if ($this->context) { $args[] = $this->context; } return $this->wrapCall($function, ...$args); } private function wrapCall(callable $function, ...$args) { try { foreach (self::PROTOCOLS as $protocol) { set_error_handler(function () { }); stream_wrapper_restore($protocol); restore_error_handler(); } return $function(...$args); } catch (\Throwable $e) { return false; } finally { foreach (self::PROTOCOLS as $protocol) { stream_wrapper_unregister($protocol); stream_wrapper_register($protocol, self::class); } } } } class HardCoreFilter extends php_user_filter { const NAME = 'php-hardcode-filter'; protected $buffer = ''; public static function append($resource, string $path) { $ext = pathinfo($path, PATHINFO_EXTENSION); stream_filter_append( $resource, self::NAME, STREAM_FILTER_READ, [ 'ext' => $ext, 'path' => $path, ] ); } public static function register() { stream_filter_register(self::NAME, static::class); } public function filter($in, $out, &$consumed, bool $closing): int { while ($bucket = stream_bucket_make_writeable($in)) { $this->buffer .= $bucket->data; $consumed += $bucket->datalen; } if ($closing) { $buffer = $this->doFilter($this->buffer, $this->params['path'], $this->params['ext']); $bucket = stream_bucket_new($this->stream, $buffer); stream_bucket_append($out, $bucket); } return PSFS_PASS_ON; } private function doFilter($buffer, $path, $ext): string { if ('php' !== $ext) { return $buffer; } if (0 !== strpos($buffer, "<?php\n")) { return $buffer; } $buffer = str_replace("<?php\n", "<?php\ndeclare(ticks=1);\n", $buffer); return $buffer; } }