Skip to content

Commit

Permalink
refactor: Directly dump the PHAR content in the Extract command (#966)
Browse files Browse the repository at this point in the history
Remove the usage of `Box` or `Pharaoh`. This currently creates some redundancy with the implementation of `Pharaoh`, but the latter will be refactored to leverage the extract command instead of the other way around.
  • Loading branch information
theofidry authored Mar 31, 2023
1 parent c7fc30e commit 6737acd
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 95 deletions.
113 changes: 53 additions & 60 deletions src/Console/Command/Extract.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,18 @@
use Fidry\Console\Command\Configuration;
use Fidry\Console\ExitCode;
use Fidry\Console\Input\IO;
use KevinGH\Box\Box;
use KevinGH\Box\Pharaoh\Pharaoh;
use RecursiveIteratorIterator;
use Symfony\Component\Console\Exception\RuntimeException as ConsoleRuntimeException;
use KevinGH\Box\Pharaoh\InvalidPhar;
use ParagonIE\ConstantTime\Hex;
use Phar;
use PharData;
use Symfony\Component\Console\Input\InputArgument;
use Throwable;
use function count;
use function KevinGH\Box\bump_open_file_descriptor_limit;
use function KevinGH\Box\FileSystem\dump_file;
use UnexpectedValueException;
use function KevinGH\Box\FileSystem\copy;
use function KevinGH\Box\FileSystem\remove;
use function realpath;
use function sprintf;
use const DIRECTORY_SEPARATOR;

/**
* @private
Expand Down Expand Up @@ -69,20 +69,7 @@ public function execute(IO $io): int
return ExitCode::FAILURE;
}

[$box, $cleanUpTmpPhar] = $this->getBox($filePath);

if (null === $box) {
return ExitCode::FAILURE;
}

$restoreLimit = bump_open_file_descriptor_limit(count($box), $io);

$cleanUp = static function () use ($cleanUpTmpPhar, $restoreLimit): void {
$cleanUpTmpPhar();
$restoreLimit();
};

self::dumpPhar($outputDir, $box, $cleanUp);
self::dumpPhar($filePath, $outputDir);

return ExitCode::SUCCESS;
}
Expand All @@ -105,55 +92,61 @@ private static function getPharFilePath(IO $io): ?string
return null;
}

/**
* @return array{Box, callable(): void}|array{null, null}
*/
private function getBox(string $filePath): ?array
private static function dumpPhar(string $file, string $tmpDir): string
{
$cleanUp = static fn () => null;
// We have to give every one a different alias, or it pukes.
$alias = self::generateAlias($file);

// Create a temporary PHAR: this is because the extension might be
// missing in which case we would not be able to create a Phar instance
// as it requires the .phar extension.
$tmpFile = $tmpDir.DIRECTORY_SEPARATOR.$alias;

try {
$pharaoh = new Pharaoh($filePath);
// The cleanup is still necessary. Indeed, without it, we would loose
// the reference of the pharaoh instance immediately which would result
// in the encapsulated PHAR to be unliked too early.
$cleanUp = static function () use ($pharaoh): void {
unset($pharaoh);
};

return [
Box::createFromPharaoh($pharaoh),
$cleanUp,
];
copy($file, $tmpFile, true);

$phar = self::createPhar($file, $tmpFile);

$phar->extractTo($tmpDir);
} catch (Throwable $throwable) {
$cleanUp();
remove($tmpFile);

throw new ConsoleRuntimeException(
'The given file is not a valid PHAR.',
0,
$throwable,
);
throw $throwable;
}

// Cleanup the temporary PHAR.
remove($tmpFile);

return $tmpDir;
}

private static function generateAlias(string $file): string
{
$extension = self::getExtension($file);

return Hex::encode(random_bytes(16)).$extension;
}

private static function getExtension(string $file): string
{
$lastExtension = pathinfo($file, PATHINFO_EXTENSION);
$extension = '';

while ('' !== $lastExtension) {
$extension = '.'.$lastExtension.$extension;
$file = mb_substr($file, 0, -(mb_strlen($lastExtension) + 1));
$lastExtension = pathinfo($file, PATHINFO_EXTENSION);
}

return '' === $extension ? '.phar' : $extension;
}

/**
* @param callable(): void $cleanUp
*/
private static function dumpPhar(string $outputDir, Box $box, callable $cleanUp): void
private static function createPhar(string $file, string $tmpFile): Phar|PharData
{
try {
remove($outputDir);

$rootLength = mb_strlen('phar://'.$box->getPhar()->getPath()) + 1;

foreach (new RecursiveIteratorIterator($box->getPhar()) as $filePath) {
dump_file(
$outputDir.'/'.mb_substr($filePath->getPathname(), $rootLength),
(string) $filePath->getContent(),
);
}
} finally {
$cleanUp();
return new Phar($tmpFile);
} catch (UnexpectedValueException $cannotCreatePhar) {
throw InvalidPhar::create($file, $cannotCreatePhar);
}
}
}
38 changes: 3 additions & 35 deletions tests/Console/Command/ExtractTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@
use KevinGH\Box\Pharaoh\InvalidPhar;
use KevinGH\Box\Test\CommandTestCase;
use KevinGH\Box\Test\RequiresPharReadonlyOff;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
use function KevinGH\Box\FileSystem\make_path_relative;
Expand Down Expand Up @@ -127,18 +125,17 @@ public function test_it_cannot_extract_an_invalid_phar(
);

self::fail('Expected exception to be thrown.');
} catch (RuntimeException $exception) {
} catch (InvalidPhar $exception) {
// Continue
$innerException = $exception->getPrevious();
}

self::assertSame(
$exceptionClassName,
$innerException::class,
$exception::class,
);
self::assertMatchesRegularExpression(
$expectedExceptionMessage,
$innerException->getMessage(),
$exception->getMessage(),
);

self::assertSame([], $this->collectExtractedFiles());
Expand All @@ -165,35 +162,6 @@ public static function invalidPharPath(): iterable
];
}

public function test_it_provides_the_original_exception_in_debug_mode_when_cannot_extract_an_invalid_phar(): void
{
$pharPath = self::FIXTURES.'/invalid.phar';

try {
$this->commandTester->execute(
[
'command' => 'extract',
'phar' => $pharPath,
'output' => $this->tmp,
],
['verbosity' => OutputInterface::VERBOSITY_DEBUG],
);

self::fail('Expected exception to be thrown.');
} catch (RuntimeException $exception) {
self::assertSame(
'The given file is not a valid PHAR.',
$exception->getMessage(),
);
self::assertSame(0, $exception->getCode());
self::assertNotNull($exception->getPrevious());

$previous = $exception->getPrevious();

self::assertInstanceOf(InvalidPhar::class, $previous);
}
}

/**
* @return array<string,string>
*/
Expand Down

0 comments on commit 6737acd

Please sign in to comment.