From e62194eeeb470105d69fee962327eb8b16105f9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Sat, 21 Feb 2026 17:58:14 +0100 Subject: [PATCH 1/4] Add command to generate file-hashes for a file on all versions --- bin/console.php | 4 +- .../GenerateFileHashesAllVersionsCommand.php | 219 ++++++++++++++++++ src/Console/InstanceResult.php | 15 ++ src/Console/InstanceResults.php | 46 ++++ 4 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 src/Console/GenerateFileHashesAllVersionsCommand.php create mode 100644 src/Console/InstanceResult.php create mode 100644 src/Console/InstanceResults.php diff --git a/bin/console.php b/bin/console.php index 6ec3aab..b6989d9 100755 --- a/bin/console.php +++ b/bin/console.php @@ -6,6 +6,7 @@ use Gared\EtherScan\Console\GenerateFileHashesCommand; use Gared\EtherScan\Console\GenerateRevisionLookupCommand; use Gared\EtherScan\Console\ScanCommand; +use Gared\EtherScan\Console\GenerateFileHashesAllVersionsCommand; use Symfony\Component\Console\Application; require __DIR__ . '/../vendor/autoload.php'; @@ -15,4 +16,5 @@ $application->add(new GenerateRevisionLookupCommand()); $application->add(new GenerateFileHashesCommand()); $application->add(new CheckFileHashesCommand()); -$application->run(); \ No newline at end of file +$application->add(new GenerateFileHashesAllVersionsCommand()); +$application->run(); diff --git a/src/Console/GenerateFileHashesAllVersionsCommand.php b/src/Console/GenerateFileHashesAllVersionsCommand.php new file mode 100644 index 0000000..4ecceff --- /dev/null +++ b/src/Console/GenerateFileHashesAllVersionsCommand.php @@ -0,0 +1,219 @@ +addOption('matches-count', null, InputArgument::OPTIONAL, 'Minimum count of matches for version to be considered valid', 3) + ->addArgument('file', InputArgument::REQUIRED, 'File path to check'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $filePath = $input->getArgument('file'); + $countVersionsMatch = $input->getOption('matches-count'); + + $allInstances = $this->getInstances(); + $instanceResults = new InstanceResults(); + + foreach ($this->getAllVersions($allInstances) as $version) { + $this->scanVersionInstances($allInstances, $version, $filePath, $instanceResults, $output, $countVersionsMatch); + } + + $listInstances = $instanceResults->getInstancesByVersion(); + uksort($listInstances, function ($a, $b) { + return version_compare($a, $b); + }); + + $versionRanges = []; + + $table = new Table($output); + $table->setHeaders(['Version', 'Count Instances', 'File Hashes']); + foreach ($listInstances as $version => $instances) { + $fileHashes = []; + $fileHashForVersion = null; + foreach ($instances as $instance) { + if ($instance->fileHash !== null) { + $fileHashes[] = $instance->fileHash; + } + } + + $fileHashesWithCount = array_count_values($fileHashes); + foreach ($fileHashesWithCount as $fileHash => $count) { + if ($count >= $countVersionsMatch) { + $fileHashForVersion = $fileHash; + } + } + + if ($fileHashForVersion !== null) { + $versionRanges[$fileHashForVersion][] = $version; + } + + + $versionString = '' . $version . ''; + if (count($instances) < $countVersionsMatch) { + $versionString = '' . $version . ''; + } else if ($fileHashForVersion === null) { + $versionString = '' . $version . ''; + } + + $table->addRow([$versionString, count($instances), ...$fileHashes]); + } + + $table->render(); + + + $table = new Table($output); + $table->setHeaders(['File Hash', 'Minimum Version', 'Maximum Version']); + + foreach ($versionRanges as $fileHash => $versions) { + usort($versions, function ($a, $b) { + return version_compare($a, $b); + }); + + $minimumVersion = $versions[array_key_first($versions)]; + $maximumVersion = $versions[array_key_last($versions)]; + + $table->addRow([$fileHash, $minimumVersion, $maximumVersion]); + } + + $table->render(); + + return self::SUCCESS; + } + + private function scanVersionInstances(array $allInstances, string $version, string $file, InstanceResults $instanceResults, OutputInterface $output, int $countVersionsMatchNeeded): void + { + $foundMatchesForHash = []; + $scannedInstances = 0; + + foreach ($this->getInstancesByVersion($allInstances, $version) as $instance) { + $fileContent = $this->getFile($instance['name'], $file); + $fileHash = $fileContent !== null ? hash('md5', $fileContent) : null; + $scannedInstances++; + + $instanceResult = new InstanceResult($instance['name'], $version, $fileHash, $fileContent); + $instanceResults->add($instanceResult); + + if ($fileHash === null) { + $output->writeln('Could not get hash for instance ' . $instance['name'] . '', OutputInterface::VERBOSITY_VERY_VERBOSE); + + if ($scannedInstances > 4 && count($foundMatchesForHash) === 0) { + break; + } + + continue; + } + + if ($this->matches($instanceResults, $instanceResult, $version)) { + $output->writeln('Match found for version ' . $version . ' and hash ' . $instanceResult->fileHash); + + if (!isset($foundMatchesForHash[$instanceResult->fileHash])) { + $foundMatchesForHash[$instanceResult->fileHash] = 0; + } + + $foundMatchesForHash[$instanceResult->fileHash]++; + if ($foundMatchesForHash[$instanceResult->fileHash] === $countVersionsMatchNeeded) { + break; + } + } else { +// $output->writeln('Mismatch found for version ' . $version . ' and hashes ' . $fileHash . ' != ' . $lastInstanceResult->fileHash . ' for servers: ' . $instanceResult->name . ', ' . $lastInstanceResult->name . ''); +// $output->writeln($lastInstanceResult->name . ': (' . mb_strlen($lastInstanceResult->fileContent) . ') ' . mb_substr($lastInstanceResult->fileContent, 0, 500), OutputInterface::VERBOSITY_DEBUG); +// $output->writeln($instanceResult->name . ': (' . mb_strlen($instanceResult->fileContent) . ') ' . mb_substr($instanceResult->fileContent, 0, 500), OutputInterface::VERBOSITY_DEBUG); + } + } + } + + private function matches(InstanceResults $instanceResults, InstanceResult $instanceResult, string $version): bool + { + foreach ($instanceResults->getInstancesForVersion($version) as $instance) { + if ($instance?->fileHash === $instanceResult->fileHash) { + return true; + } + } + + return false; + } + + /** + * @param list $instances + * @param string $version + * @return list + */ + private function getInstancesByVersion(array $instances, string $version): array + { + $filteredInstances = []; + foreach ($instances as $instance) { + if ($instance['scan']['version'] === $version) { + $filteredInstances[] = $instance; + } + } + shuffle($filteredInstances); + return $filteredInstances; + } + + private function getAllVersions(array $instances): array + { + $versions = []; + foreach ($instances as $instance) { + $version = $instance['scan']['version']; + if (!in_array($version, $versions, true)) { + $versions[] = $version; + } + } + shuffle($versions); + return $versions; + } + + /** + * @return list + */ + private function getInstances(): array + { + $client = new Client(); + $response = $client->get('https://ether-scan.stefans-entwicklerecke.de/api/instances'); + + $body = (string)$response->getBody(); + $data = json_decode($body, true); + return $data['instances'] ?? []; + } + + private function getFile(string $url, string $path): ?string + { + try { + $client = new Client([ + 'base_uri' => $url, + RequestOptions::TIMEOUT => 3.0, + RequestOptions::CONNECT_TIMEOUT => 1.0, + 'verify' => false, +// 'debug' => true, + ]); + $response = $client->get($path, [ + 'headers' => ['Accept-Encoding' => 'gzip'], + ]); + + return (string)$response->getBody(); + } catch (GuzzleException) { + } + + return null; + } +} diff --git a/src/Console/InstanceResult.php b/src/Console/InstanceResult.php new file mode 100644 index 0000000..282491c --- /dev/null +++ b/src/Console/InstanceResult.php @@ -0,0 +1,15 @@ + + */ + private array $instances = []; + + public function add(InstanceResult $instanceResult): void + { + $this->instances[] = $instanceResult; + } + + /** + * @return array> + */ + public function getInstancesByVersion(): array + { + $result = []; + foreach ($this->instances as $instance) { + $version = $instance->version; + if (!isset($result[$version])) { + $result[$version] = []; + } + $result[$version][] = $instance; + } + return $result; + } + + /** + * @return list + */ + public function getInstancesForVersion(string $version): array + { + $result = []; + foreach ($this->instances as $instance) { + if ($instance->version === $version) { + $result[] = $instance; + } + } + return $result; + } +} From a586e4cc54a96d9046bc3e9281412f0ea23ed2a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Sat, 21 Feb 2026 17:58:30 +0100 Subject: [PATCH 2/4] Update file-hash lookup --- src/Service/FileHashLookupService.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Service/FileHashLookupService.php b/src/Service/FileHashLookupService.php index 21ccd88..b21aaa0 100644 --- a/src/Service/FileHashLookupService.php +++ b/src/Service/FileHashLookupService.php @@ -10,7 +10,7 @@ class FileHashLookupService /** * @var array>> */ - private const FILE_HASH_VERSIONS = [ + private const array FILE_HASH_VERSIONS = [ 'static/js/AttributePool.js' => [ '4edf12f374e005bfa7d0fc6681caa67f' => [null, '1.8.0'], '64ac4ec21f716d36d37a4b1a9aa0debe' => ['1.8.3', '1.8.4'], @@ -71,15 +71,15 @@ class FileHashLookupService 'fc1965c84113e78fb5b29b68c8fc84f8' => ['1.9.1', '1.9.1'], 'e1d8c5fc1e4fcfe28b527828543a4729' => ['1.9.2', '2.1.0'], '96fd880e3e348fe4b45170b7c750a0b1' => ['2.1.1', '2.1.1'], - 'a9aa5b16c8e3ff79933156220cb87dbf' => ['2.2.2', null], + 'a9aa5b16c8e3ff79933156220cb87dbf' => ['2.2.2', '2.2.2'], ], 'static/css/pad.css' => [ '169c79ec1a44c5c45dfce64c0f62c7ef' => [null, '1.9.7'], '2a37d1ffbd906c905fe7f1b42564caa5' => ['2.0.0', '2.1.0'], '8fab111c95434eac9414f0d8ea5d81b8' => ['2.1.1', '2.1.1'], '8ae26862f7716d1bada457fdc92bb1d1' => ['2.2.2', '2.3.2'], - '12ba3a5933f399b882cf847d407c31f0' => ['2.4.1', '2.5.0'], - '53c72fe8218c95773dcfce173dacb3f6' => ['2.5.1', null], + '12ba3a5933f399b882cf847d407c31f0' => ['2.4.1', '2.5.1'], + '53c72fe8218c95773dcfce173dacb3f6' => ['2.5.2', null], ], 'static/skins/colibris/index.js' => [ 'eb3857ee08d0c2217649dcb61b1c8e73' => ['2.1.1', '2.2.7'], From dcf7b4075d0e41f995450ab7f6fd5f736c5f787e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Sat, 21 Feb 2026 18:04:48 +0100 Subject: [PATCH 3/4] fix phpstan issues --- .../GenerateFileHashesAllVersionsCommand.php | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Console/GenerateFileHashesAllVersionsCommand.php b/src/Console/GenerateFileHashesAllVersionsCommand.php index 4ecceff..7fa5374 100644 --- a/src/Console/GenerateFileHashesAllVersionsCommand.php +++ b/src/Console/GenerateFileHashesAllVersionsCommand.php @@ -100,6 +100,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int return self::SUCCESS; } + /** + * @param list}> $allInstances + */ private function scanVersionInstances(array $allInstances, string $version, string $file, InstanceResults $instanceResults, OutputInterface $output, int $countVersionsMatchNeeded): void { $foundMatchesForHash = []; @@ -145,7 +148,7 @@ private function scanVersionInstances(array $allInstances, string $version, stri private function matches(InstanceResults $instanceResults, InstanceResult $instanceResult, string $version): bool { foreach ($instanceResults->getInstancesForVersion($version) as $instance) { - if ($instance?->fileHash === $instanceResult->fileHash) { + if ($instance->fileHash === $instanceResult->fileHash) { return true; } } @@ -154,9 +157,9 @@ private function matches(InstanceResults $instanceResults, InstanceResult $insta } /** - * @param list $instances + * @param list}> $instances * @param string $version - * @return list + * @return list}> */ private function getInstancesByVersion(array $instances, string $version): array { @@ -170,6 +173,10 @@ private function getInstancesByVersion(array $instances, string $version): array return $filteredInstances; } + /** + * @param list}> $instances + * @return list + */ private function getAllVersions(array $instances): array { $versions = []; @@ -184,7 +191,7 @@ private function getAllVersions(array $instances): array } /** - * @return list + * @return list}> */ private function getInstances(): array { From 0349d210984c88b6a4156acbfcbe6880feccb933 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Sat, 21 Feb 2026 19:07:02 +0100 Subject: [PATCH 4/4] fix phpstan issues --- src/Console/GenerateFileHashesAllVersionsCommand.php | 2 +- src/Console/InstanceResult.php | 1 + src/Console/InstanceResults.php | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Console/GenerateFileHashesAllVersionsCommand.php b/src/Console/GenerateFileHashesAllVersionsCommand.php index 7fa5374..d30f3d1 100644 --- a/src/Console/GenerateFileHashesAllVersionsCommand.php +++ b/src/Console/GenerateFileHashesAllVersionsCommand.php @@ -116,7 +116,7 @@ private function scanVersionInstances(array $allInstances, string $version, stri $instanceResult = new InstanceResult($instance['name'], $version, $fileHash, $fileContent); $instanceResults->add($instanceResult); - if ($fileHash === null) { + if ($instanceResult->fileHash === null) { $output->writeln('Could not get hash for instance ' . $instance['name'] . '', OutputInterface::VERBOSITY_VERY_VERBOSE); if ($scannedInstances > 4 && count($foundMatchesForHash) === 0) { diff --git a/src/Console/InstanceResult.php b/src/Console/InstanceResult.php index 282491c..d4ab438 100644 --- a/src/Console/InstanceResult.php +++ b/src/Console/InstanceResult.php @@ -1,4 +1,5 @@