From fcdc71d4b4debb6c7b82fe036fa9a6eb8d827f49 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Tue, 31 Mar 2026 00:42:12 +0400 Subject: [PATCH 1/3] feat: Support `.gz` archives --- src/Module/Archive/ArchiveFactory.php | 6 ++ src/Module/Archive/Internal/GzArchive.php | 62 +++++++++++++++++++ tests/Acceptance/DLoadTest.php | 73 +++++++++++++++++++---- 3 files changed, 128 insertions(+), 13 deletions(-) create mode 100644 src/Module/Archive/Internal/GzArchive.php diff --git a/src/Module/Archive/ArchiveFactory.php b/src/Module/Archive/ArchiveFactory.php index b45fbdf..def5bed 100644 --- a/src/Module/Archive/ArchiveFactory.php +++ b/src/Module/Archive/ArchiveFactory.php @@ -5,6 +5,7 @@ namespace Internal\DLoad\Module\Archive; use Closure as ArchiveMatcher; +use Internal\DLoad\Module\Archive\Internal\GzArchive; use Internal\DLoad\Module\Archive\Internal\NullArchive; use Internal\DLoad\Module\Archive\Internal\PharArchive; use Internal\DLoad\Module\Archive\Internal\TarPharArchive; @@ -116,6 +117,11 @@ private function bootDefaultMatchers(): void static fn(\SplFileInfo $info): Archive => new ZipPharArchive($info), ), ['zip']); + $this->extend($this->matcher( + 'gz', + static fn(\SplFileInfo $info): Archive => new GzArchive($info), + ), ['gz']); + $this->extend($this->matcher( 'tar.gz', static fn(\SplFileInfo $info): Archive => new TarPharArchive($info), diff --git a/src/Module/Archive/Internal/GzArchive.php b/src/Module/Archive/Internal/GzArchive.php new file mode 100644 index 0000000..acac3ad --- /dev/null +++ b/src/Module/Archive/Internal/GzArchive.php @@ -0,0 +1,62 @@ +asset->getRealPath() ?: $this->asset->getPathname(); + + $gz = \gzopen($sourcePath, 'rb'); + $gz !== false or throw new ArchiveException( + \sprintf('Could not open "%s" for reading.', $this->asset->getPathname()), + ); + + try { + // Derive output filename by stripping .gz extension + $outputName = \preg_replace('/\.gz$/i', '', $this->asset->getFilename()); + $tempPath = \sys_get_temp_dir() . \DIRECTORY_SEPARATOR . $outputName; + + $out = \fopen($tempPath, 'wb'); + $out !== false or throw new ArchiveException( + \sprintf('Could not create temporary file "%s".', $tempPath), + ); + + try { + while (!\gzeof($gz)) { + $chunk = \gzread($gz, 8192); + if ($chunk === false) { + break; + } + \fwrite($out, $chunk); + } + } finally { + \fclose($out); + } + + $fileInfo = new \SplFileInfo($tempPath); + + /** @var \SplFileInfo|null $fileTo */ + $fileTo = yield $fileInfo->getPathname() => $fileInfo; + + if ($fileTo instanceof \SplFileInfo) { + \copy($tempPath, $fileTo->getRealPath() ?: $fileTo->getPathname()); + } + } finally { + \gzclose($gz); + } + } +} diff --git a/tests/Acceptance/DLoadTest.php b/tests/Acceptance/DLoadTest.php index c26e96d..4a69070 100644 --- a/tests/Acceptance/DLoadTest.php +++ b/tests/Acceptance/DLoadTest.php @@ -88,6 +88,31 @@ public function testDownloadsTrapPharSuccessfullyWithForceOption(): void } } + public function testDownloadsTomlTestGzBinary(): void + { + // Arrange + $dload = $this->buildDLoad($this->createTomlTestXmlConfig()); + $downloadConfig = new DownloadConfig(); + $downloadConfig->software = 'toml-test'; + $downloadConfig->version = '2.1.0'; + $downloadConfig->type = Type::Binary; + $downloadConfig->extractPath = (string) $this->destinationDir; + + // Act + $dload->addTask($downloadConfig); + $dload->run(); + + // Assert - Check that toml-test binary was downloaded and extracted from .gz + $os = OperatingSystem::fromGlobals(); + $expectedPath = (string) $this->destinationDir->join('toml-test' . $os->getBinaryExtension()); + self::assertFileExists($expectedPath, 'toml-test binary should be downloaded and extracted from .gz archive'); + self::assertGreaterThan(1024, \filesize($expectedPath), 'Downloaded binary should have substantial size'); + + if (\PHP_OS_FAMILY !== 'Windows') { + self::assertTrue(\is_executable($expectedPath), 'Binary file should be executable'); + } + } + public function testDownloadsTrapBinary(): void { // Arrange @@ -120,29 +145,51 @@ protected function setUp(): void $this->tempDir = $this->testRuntimeDir->join('temp'); $this->destinationDir = $this->testRuntimeDir; - // Initialize DLoad through Bootstrap + $this->dload = $this->buildDLoad($this->createTrapXmlConfig()); + } + + protected function tearDown(): void + { + // Clean up test directories + if ($this->testRuntimeDir->isDir()) { + $this->removeDirectory($this->testRuntimeDir); + } + } + + /** + * @return non-empty-string + */ + private function buildDLoad(string $xmlConfig): DLoad + { $container = Bootstrap::init() - ->withConfig( - $this->createTrapXmlConfig(), - [], - [], - \getenv(), - ) + ->withConfig($xmlConfig, [], [], \getenv()) ->finish(); $container->set($input = new ArgvInput(), InputInterface::class); $container->set($output = new BufferedOutput(), OutputInterface::class); $container->set(new SymfonyStyle($input, $output), StyleInterface::class); $container->set(new Logger($output)); - $this->dload = $container->get(DLoad::class); + return $container->get(DLoad::class); } - protected function tearDown(): void + /** + * @return non-empty-string + */ + private function createTomlTestXmlConfig(): string { - // Clean up test directories - if ($this->testRuntimeDir->isDir()) { - $this->removeDirectory($this->testRuntimeDir); - } + return << + + + + + + + + + XML; } /** From ad36f62bd5136c03fd8ca733e68535ddb52dcddc Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Tue, 31 Mar 2026 01:00:28 +0400 Subject: [PATCH 2/3] ci: Add PHP 8.5 --- .github/workflows/testing.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 4ea43b1..06b34c7 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -76,6 +76,7 @@ jobs: - '8.2' - '8.3' - '8.4' + - '8.5' dependencies: - lowest - locked From 7bf11e189d600fc650f57b251a9a05e06934e3e5 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Tue, 31 Mar 2026 01:09:23 +0400 Subject: [PATCH 3/3] feat: Validate file extensions in renaming logic --- src/DLoad.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/DLoad.php b/src/DLoad.php index 8132474..5bb9e55 100644 --- a/src/DLoad.php +++ b/src/DLoad.php @@ -307,10 +307,16 @@ private function shouldBeExtracted(\SplFileInfo $source, array $mapping, Path $p { foreach ($mapping as $conf) { if (\preg_match($conf->pattern, $source->getFilename())) { + $extension = $source->getExtension(); + // Validate that the "extension" looks like a real file extension + // (short, alphanumeric only — e.g. "exe", "phar", "gz") + // and not a version/platform artifact like "0-linux-amd64" + $hasRealExtension = $extension !== '' && \preg_match('/^(?=.*[a-zA-Z])[a-zA-Z0-9]{1,10}$/', $extension) === 1; + $newName = match (true) { $conf->rename === null => $source->getFilename(), - $source->getExtension() === '' => $conf->rename, - default => $conf->rename . '.' . $source->getExtension(), + !$hasRealExtension => $conf->rename, + default => $conf->rename . '.' . $extension, }; return [new \SplFileInfo((string) $path->join($newName)), $conf];