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 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]; 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; } /**