- Sponsor
-
Notifications
You must be signed in to change notification settings - Fork 104
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Closes #383
Showing
11 changed files
with
491 additions
and
70 deletions.
There are no files selected for viewing
Binary file not shown.
Empty file.
Empty file.
Binary file not shown.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
/* | ||
* This file is part of the box project. | ||
* | ||
* (c) Kevin Herrera <[email protected]> | ||
* Théo Fidry <[email protected]> | ||
* | ||
* This source file is subject to the MIT license that is bundled | ||
* with this source code in the file LICENSE. | ||
*/ | ||
|
||
namespace KevinGH\Box\Console\Command; | ||
|
||
use KevinGH\Box\Box; | ||
use function KevinGH\Box\bump_open_file_descriptor_limit; | ||
use KevinGH\Box\Console\IO\IO; | ||
use function KevinGH\Box\create_temporary_phar; | ||
use function KevinGH\Box\FileSystem\dump_file; | ||
use function KevinGH\Box\FileSystem\remove; | ||
use PharFileInfo; | ||
use function realpath; | ||
use RuntimeException; | ||
use function sprintf; | ||
use Symfony\Component\Console\Exception\RuntimeException as ConsoleRuntimeException; | ||
use Symfony\Component\Console\Input\InputArgument; | ||
use Throwable; | ||
|
||
/** | ||
* @private | ||
*/ | ||
final class Extract extends BaseCommand | ||
{ | ||
private const PHAR_ARG = 'phar'; | ||
private const OUTPUT_ARG = 'output'; | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
protected function configure(): void | ||
{ | ||
$this->setName('extract'); | ||
$this->setDescription( | ||
'🚚 Extracts a given PHAR into a directory' | ||
); | ||
$this->addArgument( | ||
self::PHAR_ARG, | ||
InputArgument::REQUIRED, | ||
'The PHAR file.' | ||
); | ||
$this->addArgument( | ||
self::OUTPUT_ARG, | ||
InputArgument::REQUIRED, | ||
'The output directory' | ||
); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
protected function executeCommand(IO $io): int | ||
{ | ||
$input = $io->getInput(); | ||
|
||
$file = realpath($input->getArgument(self::PHAR_ARG)); | ||
|
||
if (false === $file) { | ||
$io->error( | ||
sprintf( | ||
'The file "%s" could not be found.', | ||
$input->getArgument(self::PHAR_ARG) | ||
) | ||
); | ||
|
||
return 1; | ||
} | ||
|
||
$tmpFile = create_temporary_phar($file); | ||
|
||
try { | ||
$box = Box::create($tmpFile); | ||
} catch (Throwable $throwable) { | ||
if ($io->isDebug()) { | ||
throw new ConsoleRuntimeException( | ||
'The given file is not a valid PHAR', | ||
0, | ||
$throwable | ||
); | ||
} | ||
|
||
$io->error('The given file is not a valid PHAR'); | ||
|
||
return 1; | ||
} | ||
|
||
$restoreLimit = bump_open_file_descriptor_limit($box, $io); | ||
|
||
$outputDir = $input->getArgument(self::OUTPUT_ARG); | ||
|
||
try { | ||
remove($outputDir); | ||
|
||
foreach ($box->getPhar() as $pharFile) { | ||
/* @var PharFileInfo $pharFile */ | ||
dump_file( | ||
$outputDir.'/'.$pharFile->getFilename(), | ||
(string) $pharFile->getContent() | ||
); | ||
} | ||
} catch (RuntimeException $exception) { | ||
$io->error($exception->getMessage()); | ||
|
||
return 1; | ||
} finally { | ||
$restoreLimit(); | ||
|
||
remove($tmpFile); | ||
} | ||
|
||
$io->success(''); | ||
|
||
return 0; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,295 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
/* | ||
* This file is part of the box project. | ||
* | ||
* (c) Kevin Herrera <kevin@herrera.io> | ||
* Théo Fidry <theo.fidry@gmail.com> | ||
* | ||
* This source file is subject to the MIT license that is bundled | ||
* with this source code in the file LICENSE. | ||
*/ | ||
|
||
namespace KevinGH\Box\Console\Command; | ||
|
||
use KevinGH\Box\Console\DisplayNormalizer; | ||
use function KevinGH\Box\FileSystem\make_path_relative; | ||
use KevinGH\Box\Test\CommandTestCase; | ||
use Phar; | ||
use function preg_replace; | ||
use Symfony\Component\Console\Command\Command; | ||
use Symfony\Component\Console\Exception\RuntimeException; | ||
use Symfony\Component\Console\Output\OutputInterface; | ||
use Symfony\Component\Finder\Finder; | ||
use Symfony\Component\Finder\SplFileInfo; | ||
use UnexpectedValueException; | ||
|
||
/** | ||
* @covers \KevinGH\Box\Console\Command\Extract | ||
* | ||
* @runTestsInSeparateProcesses This is necessary as instantiating a PHAR in memory may load/autoload some stuff which | ||
* can create undesirable side-effects. | ||
*/ | ||
class ExtractTest extends CommandTestCase | ||
{ | ||
private const FIXTURES = __DIR__.'/../../../fixtures/extract'; | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
protected function getCommand(): Command | ||
{ | ||
return new Extract(); | ||
} | ||
|
||
public function test_it_can_extract_a_phar(): void | ||
{ | ||
$pharPath = self::FIXTURES.'/simple-phar.phar'; | ||
|
||
$this->commandTester->execute( | ||
[ | ||
'command' => 'extract', | ||
'phar' => $pharPath, | ||
'output' => $this->tmp, | ||
] | ||
); | ||
|
||
$expectedFiles = [ | ||
'.hidden' => 'baz', | ||
'foo' => 'bar', | ||
]; | ||
|
||
$actualFiles = $this->collectExtractedFiles(); | ||
|
||
$this->assertEqualsCanonicalizing($expectedFiles, $actualFiles); | ||
|
||
$expectedOutput = <<<'OUTPUT' | ||
[OK] | ||
|
||
|
||
OUTPUT; | ||
|
||
$actual = DisplayNormalizer::removeTrailingSpaces($this->commandTester->getDisplay(true)); | ||
|
||
$this->assertSame($expectedOutput, $actual); | ||
$this->assertSame(0, $this->commandTester->getStatusCode()); | ||
} | ||
|
||
public function test_it_can_extract_a_phar_without_the_phar_extension(): void | ||
{ | ||
$pharPath = self::FIXTURES.'/simple-phar'; | ||
|
||
$this->commandTester->execute( | ||
[ | ||
'command' => 'extract', | ||
'phar' => $pharPath, | ||
'output' => $this->tmp, | ||
] | ||
); | ||
|
||
$expectedFiles = [ | ||
'.hidden' => 'baz', | ||
'foo' => 'bar', | ||
]; | ||
|
||
$actualFiles = $this->collectExtractedFiles(); | ||
|
||
$this->assertEqualsCanonicalizing($expectedFiles, $actualFiles); | ||
|
||
$expectedOutput = <<<'OUTPUT' | ||
[OK] | ||
|
||
|
||
OUTPUT; | ||
|
||
$actual = DisplayNormalizer::removeTrailingSpaces($this->commandTester->getDisplay(true)); | ||
|
||
$this->assertSame($expectedOutput, $actual); | ||
$this->assertSame(0, $this->commandTester->getStatusCode()); | ||
} | ||
|
||
public function test_it_can_extract_a_compressed_phar(): void | ||
{ | ||
$pharPath = self::FIXTURES.'/simple-phar.phar'; | ||
|
||
$this->commandTester->execute( | ||
[ | ||
'command' => 'extract', | ||
'phar' => $pharPath, | ||
'output' => $this->tmp, | ||
] | ||
); | ||
|
||
$expectedFiles = [ | ||
'.hidden' => 'baz', | ||
'foo' => 'bar', | ||
]; | ||
|
||
$actualFiles = $this->collectExtractedFiles(); | ||
|
||
$this->assertEqualsCanonicalizing($expectedFiles, $actualFiles); | ||
|
||
$expectedOutput = <<<'OUTPUT' | ||
[OK] | ||
|
||
|
||
OUTPUT; | ||
|
||
$actual = DisplayNormalizer::removeTrailingSpaces($this->commandTester->getDisplay(true)); | ||
|
||
$this->assertSame($expectedOutput, $actual); | ||
$this->assertSame(0, $this->commandTester->getStatusCode()); | ||
} | ||
|
||
public function test_it_cannot_extract_an_invalid_phar(): void | ||
{ | ||
$pharPath = self::FIXTURES.'/invalid.phar'; | ||
|
||
$this->commandTester->execute( | ||
[ | ||
'command' => 'extract', | ||
'phar' => $pharPath, | ||
'output' => $this->tmp, | ||
] | ||
); | ||
|
||
$expectedFiles = []; | ||
|
||
$actualFiles = $this->collectExtractedFiles(); | ||
|
||
$this->assertEqualsCanonicalizing($expectedFiles, $actualFiles); | ||
|
||
$expectedOutput = <<<'OUTPUT' | ||
[ERROR] The given file is not a valid PHAR | ||
|
||
|
||
OUTPUT; | ||
|
||
$actual = DisplayNormalizer::removeTrailingSpaces($this->commandTester->getDisplay(true)); | ||
$actual = preg_replace('/file[\ \n]+"/', 'file "', $actual); | ||
|
||
$this->assertSame($expectedOutput, $actual); | ||
$this->assertSame(1, $this->commandTester->getStatusCode()); | ||
} | ||
|
||
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] | ||
); | ||
|
||
$this->fail('Expected exception to be thrown.'); | ||
} catch (RuntimeException $exception) { | ||
$this->assertSame( | ||
'The given file is not a valid PHAR', | ||
$exception->getMessage() | ||
); | ||
$this->assertSame(0, $exception->getCode()); | ||
$this->assertNotNull($exception->getPrevious()); | ||
|
||
$previous = $exception->getPrevious(); | ||
|
||
$this->assertInstanceOf(UnexpectedValueException::class, $previous); | ||
$this->assertStringStartsWith('internal corruption of phar', $previous->getMessage()); | ||
} | ||
} | ||
|
||
public function test_it_cannot_extract_an_invalid_phar_without_extension(): void | ||
{ | ||
$pharPath = self::FIXTURES.'/invalid'; | ||
|
||
$this->commandTester->execute( | ||
[ | ||
'command' => 'extract', | ||
'phar' => $pharPath, | ||
'output' => $this->tmp, | ||
] | ||
); | ||
|
||
$expectedFiles = []; | ||
|
||
$actualFiles = $this->collectExtractedFiles(); | ||
|
||
$this->assertSame($expectedFiles, $actualFiles); | ||
|
||
$expectedOutput = <<<'OUTPUT' | ||
[ERROR] The given file is not a valid PHAR | ||
|
||
|
||
OUTPUT; | ||
|
||
$actual = DisplayNormalizer::removeTrailingSpaces($this->commandTester->getDisplay(true)); | ||
$actual = preg_replace('/file[\ \n]+"/', 'file "', $actual); | ||
|
||
$this->assertSame($expectedOutput, $actual); | ||
$this->assertSame(1, $this->commandTester->getStatusCode()); | ||
} | ||
|
||
public function test_it_cannot_extract_an_unknown_file(): void | ||
{ | ||
$this->commandTester->execute( | ||
[ | ||
'command' => 'extract', | ||
'phar' => '/unknown', | ||
'output' => $this->tmp, | ||
] | ||
); | ||
|
||
$expectedFiles = []; | ||
|
||
$actualFiles = $this->collectExtractedFiles(); | ||
|
||
$this->assertSame($expectedFiles, $actualFiles); | ||
|
||
$expectedOutput = <<<'OUTPUT' | ||
[ERROR] The file "/unknown" could not be found. | ||
|
||
|
||
OUTPUT; | ||
|
||
$actual = DisplayNormalizer::removeTrailingSpaces($this->commandTester->getDisplay(true)); | ||
$actual = preg_replace('/file[\ \n]+"/', 'file "', $actual); | ||
|
||
$this->assertSame($expectedOutput, $actual); | ||
$this->assertSame(1, $this->commandTester->getStatusCode()); | ||
} | ||
|
||
/** | ||
* @return array<string,string> | ||
*/ | ||
private function collectExtractedFiles(): array | ||
{ | ||
$finder = Finder::create() | ||
->files() | ||
->in($this->tmp) | ||
->ignoreDotFiles(false) | ||
; | ||
|
||
$files = []; | ||
|
||
foreach ($finder as $file) { | ||
/** @var SplFileInfo $file */ | ||
$filePath = make_path_relative($file->getPathname(), $this->tmp); | ||
|
||
$files[$filePath] = $file->getContents(); | ||
} | ||
|
||
return $files; | ||
} | ||
} |