dd/app/Services/Freenode/NativeValidator.php

73 lines
2.7 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Freenode;
use RuntimeException;
use Symfony\Component\Process\Process;
/** Native core checks run per complete config, with bisection only when a config fails. */
final class NativeValidator
{
public int $checks = 0;
public array $rejected = [];
private string $dir;
public function __construct(private string $singbox, private string $mihomo, private ?array $clashTemplate = null)
{
$this->dir = sys_get_temp_dir().'/freenode-check-'.bin2hex(random_bytes(8));
mkdir($this->dir, 0700, true);
}
public function __destruct()
{
foreach (glob($this->dir.'/*') ?: [] as $p) if (is_file($p)) @unlink($p);
@rmdir($this->dir);
}
public function check(string $format, string $content): bool
{
$binary = $format === 'singbox' ? $this->singbox : $this->mihomo;
if (!is_executable($binary)) throw new RuntimeException('native_validator_not_executable:'.$format);
$file = $this->dir.'/'.bin2hex(random_bytes(6)).($format === 'clash' ? '.yaml' : '.json');
file_put_contents($file, $content); chmod($file, 0600);
$command = $format === 'singbox' ? [$binary, 'check', '-c', $file] : [$binary, '-t', '-d', $this->dir, '-f', $file];
$process = new Process($command);
$process->setTimeout(45);
try {
$process->run();
$this->checks++;
return $process->isSuccessful();
} finally {
@unlink($file);
}
}
public function filter(string $format, array $nodes): array
{
$render = fn (array $rows) => $this->render($format, $rows);
if (!$this->check($format, $render([]))) throw new RuntimeException('invalid_template_or_core:'.$format);
return $this->partition($format, $nodes, $render);
}
public function render(string $format, array $nodes): string
{
return $format === 'singbox' ? Renderer::singbox($nodes) : Renderer::clash($nodes, $this->clashTemplate);
}
private function partition(string $format, array $nodes, callable $render): array
{
if (!$nodes || $this->check($format, $render($nodes))) return $nodes;
if (count($nodes) === 1) {
$this->rejected[$format][] = array_key_first($nodes);
return [];
}
$chunks = array_chunk($nodes, (int) ceil(count($nodes) / 2), true);
$good = [];
foreach ($chunks as $chunk) $good += $this->partition($format, $chunk, $render);
// A final whole-file check also catches cross-node constraints.
if (!$this->check($format, $render($good))) throw new RuntimeException('combined_config_invalid:'.$format);
return $good;
}
}