Skip to content

Commit 030bd55

Browse files
authored
Feature/file hash generator (#131)
* Add command to generate file-hashes for a file on all versions * Update file-hash lookup * fix phpstan issues * fix phpstan issues * Increase minimum version count for match * fix: Add instance after doing matching
1 parent b48d872 commit 030bd55

5 files changed

Lines changed: 293 additions & 5 deletions

File tree

bin/console.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Gared\EtherScan\Console\GenerateFileHashesCommand;
77
use Gared\EtherScan\Console\GenerateRevisionLookupCommand;
88
use Gared\EtherScan\Console\ScanCommand;
9+
use Gared\EtherScan\Console\GenerateFileHashesAllVersionsCommand;
910
use Symfony\Component\Console\Application;
1011

1112
require __DIR__ . '/../vendor/autoload.php';
@@ -15,4 +16,5 @@
1516
$application->add(new GenerateRevisionLookupCommand());
1617
$application->add(new GenerateFileHashesCommand());
1718
$application->add(new CheckFileHashesCommand());
18-
$application->run();
19+
$application->add(new GenerateFileHashesAllVersionsCommand());
20+
$application->run();
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Gared\EtherScan\Console;
5+
6+
use GuzzleHttp\Client;
7+
use GuzzleHttp\Exception\GuzzleException;
8+
use GuzzleHttp\RequestOptions;
9+
use Symfony\Component\Console\Attribute\AsCommand;
10+
use Symfony\Component\Console\Command\Command;
11+
use Symfony\Component\Console\Helper\Table;
12+
use Symfony\Component\Console\Input\InputArgument;
13+
use Symfony\Component\Console\Input\InputInterface;
14+
use Symfony\Component\Console\Output\OutputInterface;
15+
16+
#[AsCommand(
17+
name: 'ether:generate-file-hashes-all-versions',
18+
description: 'Generate file hashes for specific file on all versions of etherpad'
19+
)]
20+
class GenerateFileHashesAllVersionsCommand extends Command
21+
{
22+
protected function configure(): void
23+
{
24+
$this
25+
->addOption('matches-count', 'm', InputArgument::OPTIONAL, 'Minimum count of matches for version to be considered valid', 4)
26+
->addArgument('file', InputArgument::REQUIRED, 'File path to check');
27+
}
28+
29+
protected function execute(InputInterface $input, OutputInterface $output): int
30+
{
31+
$filePath = $input->getArgument('file');
32+
$countVersionsMatch = (int) $input->getOption('matches-count');
33+
34+
$allInstances = $this->getInstances();
35+
$instanceResults = new InstanceResults();
36+
37+
foreach ($this->getAllVersions($allInstances) as $version) {
38+
$this->scanVersionInstances($allInstances, $version, $filePath, $instanceResults, $output, $countVersionsMatch);
39+
}
40+
41+
$listInstances = $instanceResults->getInstancesByVersion();
42+
uksort($listInstances, function ($a, $b) {
43+
return version_compare($a, $b);
44+
});
45+
46+
$versionRanges = [];
47+
48+
$table = new Table($output);
49+
$table->setHeaders(['Version', 'Count Instances', 'File Hashes']);
50+
foreach ($listInstances as $version => $instances) {
51+
$fileHashes = [];
52+
$fileHashForVersion = null;
53+
foreach ($instances as $instance) {
54+
if ($instance->fileHash !== null) {
55+
$fileHashes[] = $instance->fileHash;
56+
}
57+
}
58+
59+
$fileHashesWithCount = array_count_values($fileHashes);
60+
foreach ($fileHashesWithCount as $fileHash => $count) {
61+
if ($count >= $countVersionsMatch) {
62+
$fileHashForVersion = $fileHash;
63+
}
64+
}
65+
66+
if ($fileHashForVersion !== null) {
67+
$versionRanges[$fileHashForVersion][] = $version;
68+
}
69+
70+
71+
$versionString = '<info>' . $version . '</info>';
72+
if (count($instances) < $countVersionsMatch) {
73+
$versionString = '<comment>' . $version . '</comment>';
74+
} else if ($fileHashForVersion === null) {
75+
$versionString = '<error>' . $version . '</error>';
76+
}
77+
78+
$table->addRow([$versionString, count($instances), ...$fileHashes]);
79+
}
80+
81+
$table->render();
82+
83+
84+
$table = new Table($output);
85+
$table->setHeaders(['File Hash', 'Minimum Version', 'Maximum Version']);
86+
87+
foreach ($versionRanges as $fileHash => $versions) {
88+
usort($versions, function ($a, $b) {
89+
return version_compare($a, $b);
90+
});
91+
92+
$minimumVersion = $versions[array_key_first($versions)];
93+
$maximumVersion = $versions[array_key_last($versions)];
94+
95+
$table->addRow([$fileHash, $minimumVersion, $maximumVersion]);
96+
}
97+
98+
$table->render();
99+
100+
return self::SUCCESS;
101+
}
102+
103+
/**
104+
* @param list<array{name: string, scan: array<mixed>}> $allInstances
105+
*/
106+
private function scanVersionInstances(array $allInstances, string $version, string $file, InstanceResults $instanceResults, OutputInterface $output, int $countVersionsMatchNeeded): void
107+
{
108+
$foundMatchesForHash = [];
109+
$scannedInstances = 0;
110+
111+
foreach ($this->getInstancesByVersion($allInstances, $version) as $instance) {
112+
$fileContent = $this->getFile($instance['name'], $file);
113+
$fileHash = $fileContent !== null ? hash('md5', $fileContent) : null;
114+
$scannedInstances++;
115+
116+
$instanceResult = new InstanceResult($instance['name'], $version, $fileHash, $fileContent);
117+
118+
if ($instanceResult->fileHash === null) {
119+
$output->writeln('<error>Could not get hash for instance ' . $instance['name'] . '</error>', OutputInterface::VERBOSITY_VERY_VERBOSE);
120+
121+
if ($scannedInstances > 4 && count($foundMatchesForHash) === 0) {
122+
break;
123+
}
124+
125+
continue;
126+
}
127+
128+
if ($this->matches($instanceResults, $instanceResult, $version)) {
129+
$output->writeln('Match found for version ' . $version . ' and hash ' . $instanceResult->fileHash);
130+
131+
if (!isset($foundMatchesForHash[$instanceResult->fileHash])) {
132+
$foundMatchesForHash[$instanceResult->fileHash] = 0;
133+
}
134+
135+
$foundMatchesForHash[$instanceResult->fileHash]++;
136+
if ($foundMatchesForHash[$instanceResult->fileHash] === $countVersionsMatchNeeded) {
137+
break;
138+
}
139+
}
140+
$instanceResults->add($instanceResult);
141+
}
142+
}
143+
144+
private function matches(InstanceResults $instanceResults, InstanceResult $instanceResult, string $version): bool
145+
{
146+
foreach ($instanceResults->getInstancesForVersion($version) as $instance) {
147+
if ($instance->fileHash === $instanceResult->fileHash) {
148+
return true;
149+
}
150+
}
151+
152+
return false;
153+
}
154+
155+
/**
156+
* @param list<array{name: string, scan: array<mixed>}> $instances
157+
* @param string $version
158+
* @return list<array{name: string, scan: array<mixed>}>
159+
*/
160+
private function getInstancesByVersion(array $instances, string $version): array
161+
{
162+
$filteredInstances = [];
163+
foreach ($instances as $instance) {
164+
if ($instance['scan']['version'] === $version) {
165+
$filteredInstances[] = $instance;
166+
}
167+
}
168+
shuffle($filteredInstances);
169+
return $filteredInstances;
170+
}
171+
172+
/**
173+
* @param list<array{name: string, scan: array<mixed>}> $instances
174+
* @return list<string>
175+
*/
176+
private function getAllVersions(array $instances): array
177+
{
178+
$versions = [];
179+
foreach ($instances as $instance) {
180+
$version = $instance['scan']['version'];
181+
if (!in_array($version, $versions, true)) {
182+
$versions[] = $version;
183+
}
184+
}
185+
shuffle($versions);
186+
return $versions;
187+
}
188+
189+
/**
190+
* @return list<array{name: string, scan: array<mixed>}>
191+
*/
192+
private function getInstances(): array
193+
{
194+
$client = new Client();
195+
$response = $client->get('https://ether-scan.stefans-entwicklerecke.de/api/instances');
196+
197+
$body = (string)$response->getBody();
198+
$data = json_decode($body, true);
199+
return $data['instances'] ?? [];
200+
}
201+
202+
private function getFile(string $url, string $path): ?string
203+
{
204+
try {
205+
$client = new Client([
206+
'base_uri' => $url,
207+
RequestOptions::TIMEOUT => 3.0,
208+
RequestOptions::CONNECT_TIMEOUT => 1.0,
209+
'verify' => false,
210+
// 'debug' => true,
211+
]);
212+
$response = $client->get($path, [
213+
'headers' => ['Accept-Encoding' => 'gzip'],
214+
]);
215+
216+
return (string)$response->getBody();
217+
} catch (GuzzleException) {
218+
}
219+
220+
return null;
221+
}
222+
}

src/Console/InstanceResult.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Gared\EtherScan\Console;
5+
6+
class InstanceResult
7+
{
8+
public function __construct(
9+
public readonly string $name,
10+
public readonly string $version,
11+
public readonly ?string $fileHash = null,
12+
public readonly ?string $fileContent = null,
13+
)
14+
{
15+
}
16+
}

src/Console/InstanceResults.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Gared\EtherScan\Console;
5+
6+
class InstanceResults
7+
{
8+
/**
9+
* @var list<InstanceResult>
10+
*/
11+
private array $instances = [];
12+
13+
public function add(InstanceResult $instanceResult): void
14+
{
15+
$this->instances[] = $instanceResult;
16+
}
17+
18+
/**
19+
* @return array<string, list<InstanceResult>>
20+
*/
21+
public function getInstancesByVersion(): array
22+
{
23+
$result = [];
24+
foreach ($this->instances as $instance) {
25+
$version = $instance->version;
26+
if (!isset($result[$version])) {
27+
$result[$version] = [];
28+
}
29+
$result[$version][] = $instance;
30+
}
31+
return $result;
32+
}
33+
34+
/**
35+
* @return list<InstanceResult>
36+
*/
37+
public function getInstancesForVersion(string $version): array
38+
{
39+
$result = [];
40+
foreach ($this->instances as $instance) {
41+
if ($instance->version === $version) {
42+
$result[] = $instance;
43+
}
44+
}
45+
return $result;
46+
}
47+
}

src/Service/FileHashLookupService.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class FileHashLookupService
1010
/**
1111
* @var array<string, array<string, array<string|null>>>
1212
*/
13-
private const FILE_HASH_VERSIONS = [
13+
private const array FILE_HASH_VERSIONS = [
1414
'static/js/AttributePool.js' => [
1515
'4edf12f374e005bfa7d0fc6681caa67f' => [null, '1.8.0'],
1616
'64ac4ec21f716d36d37a4b1a9aa0debe' => ['1.8.3', '1.8.4'],
@@ -71,15 +71,16 @@ class FileHashLookupService
7171
'fc1965c84113e78fb5b29b68c8fc84f8' => ['1.9.1', '1.9.1'],
7272
'e1d8c5fc1e4fcfe28b527828543a4729' => ['1.9.2', '2.1.0'],
7373
'96fd880e3e348fe4b45170b7c750a0b1' => ['2.1.1', '2.1.1'],
74-
'a9aa5b16c8e3ff79933156220cb87dbf' => ['2.2.2', null],
74+
'a9aa5b16c8e3ff79933156220cb87dbf' => ['2.2.2', '2.2.2'],
7575
],
7676
'static/css/pad.css' => [
7777
'169c79ec1a44c5c45dfce64c0f62c7ef' => [null, '1.9.7'],
7878
'2a37d1ffbd906c905fe7f1b42564caa5' => ['2.0.0', '2.1.0'],
7979
'8fab111c95434eac9414f0d8ea5d81b8' => ['2.1.1', '2.1.1'],
8080
'8ae26862f7716d1bada457fdc92bb1d1' => ['2.2.2', '2.3.2'],
81-
'12ba3a5933f399b882cf847d407c31f0' => ['2.4.1', '2.5.0'],
82-
'53c72fe8218c95773dcfce173dacb3f6' => ['2.5.1', null],
81+
'12ba3a5933f399b882cf847d407c31f0' => ['2.4.1', '2.5.1'],
82+
'53c72fe8218c95773dcfce173dacb3f6' => ['2.5.2', '2.6.1'],
83+
'38cff1ae26208862112021f063b361a4' => ['2.7.0', null],
8384
],
8485
'static/skins/colibris/index.js' => [
8586
'eb3857ee08d0c2217649dcb61b1c8e73' => ['2.1.1', '2.2.7'],

0 commit comments

Comments
 (0)