TypePHP 编译器
https://swoole.com/aot/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
140 lines
5.8 KiB
140 lines
5.8 KiB
<?php
|
|
|
|
namespace TypePhp\Config;
|
|
|
|
use Closure;
|
|
use Symfony\Component\Yaml\Yaml;
|
|
|
|
final class ProjectYamlLoader
|
|
{
|
|
/** @param Closure(string): never $error */
|
|
public function __construct(
|
|
private string $phpVersion,
|
|
private readonly Closure $error,
|
|
private readonly string $osFamily = PHP_OS_FAMILY,
|
|
) {
|
|
}
|
|
|
|
public function setPhpVersion(string $phpVersion): void
|
|
{
|
|
$this->phpVersion = $phpVersion;
|
|
}
|
|
|
|
public function load(string $path): array
|
|
{
|
|
$config = Yaml::parseFile($path);
|
|
if (!is_array($config)) {
|
|
($this->error)('Project YAML root must be a map');
|
|
}
|
|
return $config;
|
|
}
|
|
|
|
/** @return array{0: string, 1: string|null} */
|
|
public function parseSourceEntry(mixed $entry): array
|
|
{
|
|
if (is_string($entry)) {
|
|
return [$entry, null];
|
|
}
|
|
if (!is_array($entry)) {
|
|
($this->error)('Each `sources` entry must be a string or map');
|
|
}
|
|
$path = $entry['path'] ?? $entry['source'] ?? $entry['file'] ?? null;
|
|
if (!is_string($path) || trim($path) === '') {
|
|
($this->error)('Conditional `sources` entries must include a non-empty `path`');
|
|
}
|
|
$condition = $entry['if'] ?? $entry['when'] ?? null;
|
|
if ($condition !== null && !is_string($condition)) {
|
|
($this->error)('Source condition must be a string');
|
|
}
|
|
return [$path, $condition];
|
|
}
|
|
|
|
public function evaluateCondition(string $condition): bool
|
|
{
|
|
$condition = trim($condition);
|
|
if ($condition === '') {
|
|
($this->error)('Source condition must not be empty');
|
|
}
|
|
$expr = $this->replacePhpVersionComparisons($condition);
|
|
$expr = $this->replaceOsFamilyComparisons($expr, $condition);
|
|
if (preg_match('/[A-Za-z_]/', $expr)
|
|
|| !preg_match('/^[0-9\s<>=!&|().+-]+$/', $expr)
|
|
|| preg_match('/(?<![&])&(?!&)|(?<![|])\|(?!\|)/', $expr)) {
|
|
($this->error)('Unsupported source condition: `' . $condition . '`');
|
|
}
|
|
try {
|
|
return (bool) eval('return (' . $expr . ');');
|
|
} catch (\Throwable) {
|
|
($this->error)('Invalid source condition: `' . $condition . '`');
|
|
}
|
|
}
|
|
|
|
private function replacePhpVersionComparisons(string $condition): string
|
|
{
|
|
$literal = '"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'';
|
|
$operator = '(>=|<=|==|!=|<>|=|>|<|lt|le|gt|ge|eq|ne)';
|
|
$patterns = [
|
|
'/\bPHP_VERSION_ID\b\s*' . $operator . '\s*([0-9]+)/i' => fn(array $m): bool => version_compare($this->phpVersion, $this->versionIdToString((int) $m[2]), strtolower($m[1])),
|
|
'/([0-9]+)\s*' . $operator . '\s*\bPHP_VERSION_ID\b/i' => fn(array $m): bool => version_compare($this->versionIdToString((int) $m[1]), $this->phpVersion, strtolower($m[2])),
|
|
'/\bPHP_VERSION\b\s*' . $operator . '\s*(' . $literal . ')/i' => function (array $m): bool {
|
|
$version = stripcslashes(($m[3] ?? '') !== '' ? $m[3] : $m[4]);
|
|
$this->assertVersion($version);
|
|
return version_compare($this->phpVersion, $version, strtolower($m[1]));
|
|
},
|
|
'/(' . $literal . ')\s*' . $operator . '\s*\bPHP_VERSION\b/i' => function (array $m): bool {
|
|
$version = stripcslashes($m[2] !== '' ? $m[2] : $m[3]);
|
|
$this->assertVersion($version);
|
|
return version_compare($version, $this->phpVersion, strtolower($m[4]));
|
|
},
|
|
];
|
|
$expr = $condition;
|
|
foreach ($patterns as $pattern => $compare) {
|
|
$expr = preg_replace_callback($pattern, fn(array $m): string => $compare($m) ? '1' : '0', $expr);
|
|
if ($expr === null) {
|
|
($this->error)('Invalid source condition: `' . $condition . '`');
|
|
}
|
|
}
|
|
if (preg_match('/\bPHP_VERSION(?:_ID)?\b/', $expr)) {
|
|
($this->error)('Unsupported source condition: `' . $condition . '`');
|
|
}
|
|
return $expr;
|
|
}
|
|
|
|
private function replaceOsFamilyComparisons(string $expr, string $condition): string
|
|
{
|
|
$literal = '"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'';
|
|
$expr = preg_replace_callback('/\bPHP_OS_FAMILY\b\s*(==|!=)\s*(' . $literal . ')/i', function (array $m): string {
|
|
$expected = stripcslashes(($m[3] ?? '') !== '' ? $m[3] : $m[4]);
|
|
$this->assertOsFamily($expected);
|
|
return (($this->osFamily === $expected) xor ($m[1] === '!=')) ? '1' : '0';
|
|
}, $expr);
|
|
$expr = preg_replace_callback('/(' . $literal . ')\s*(==|!=)\s*\bPHP_OS_FAMILY\b/i', function (array $m): string {
|
|
$expected = stripcslashes($m[2] !== '' ? $m[2] : $m[3]);
|
|
$this->assertOsFamily($expected);
|
|
return (($expected === $this->osFamily) xor ($m[4] === '!=')) ? '1' : '0';
|
|
}, $expr ?? '');
|
|
if ($expr === null || preg_match('/\bPHP_OS_FAMILY\b/', $expr)) {
|
|
($this->error)('Unsupported source condition: `' . $condition . '`');
|
|
}
|
|
return $expr;
|
|
}
|
|
|
|
private function assertVersion(string $version): void
|
|
{
|
|
if ($version === '' || !preg_match('/^[0-9A-Za-z_.+\-]+$/', $version)) {
|
|
($this->error)('Invalid PHP_VERSION literal: `' . $version . '`');
|
|
}
|
|
}
|
|
|
|
private function assertOsFamily(string $osFamily): void
|
|
{
|
|
if (!in_array($osFamily, ['Windows', 'BSD', 'Darwin', 'Solaris', 'Linux', 'Unknown'], true)) {
|
|
($this->error)('Invalid PHP_OS_FAMILY literal: `' . $osFamily . '`');
|
|
}
|
|
}
|
|
|
|
private function versionIdToString(int $versionId): string
|
|
{
|
|
return intdiv($versionId, 10000) . '.' . intdiv($versionId % 10000, 100) . '.' . ($versionId % 100);
|
|
}
|
|
}
|
|
|