Issues (224)

src/Console/Command/Extract.php (4 issues)

Labels
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the box project.
7
 *
8
 * (c) Kevin Herrera <[email protected]>
9
 *     Théo Fidry <[email protected]>
10
 *
11
 * This source file is subject to the MIT license that is bundled
12
 * with this source code in the file LICENSE.
13
 */
14
15
namespace KevinGH\Box\Console\Command;
16
17
use Fidry\Console\Command\Command;
18
use Fidry\Console\Command\Configuration;
19
use Fidry\Console\ExitCode;
20
use Fidry\Console\IO;
0 ignored issues
show
The type Fidry\Console\IO was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
21
use Fidry\FileSystem\FS;
22
use KevinGH\Box\Console\Php\PhpSettingsChecker;
23
use KevinGH\Box\Phar\PharFactory;
24
use KevinGH\Box\Phar\PharMeta;
0 ignored issues
show
The type KevinGH\Box\Phar\PharMeta was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
25
use KevinGH\Box\Phar\Throwable\InvalidPhar;
26
use Symfony\Component\Console\Input\InputArgument;
27
use Symfony\Component\Console\Input\InputOption;
28
use Symfony\Component\Console\Question\ConfirmationQuestion;
29
use Throwable;
30
use function bin2hex;
31
use function file_exists;
32
use function realpath;
33
use function sprintf;
34
use const DIRECTORY_SEPARATOR;
35
36
/**
37
 * @private
38
 */
39
final class Extract implements Command
40
{
41
    public const STUB_PATH = '.phar/stub.php';
42
    public const PHAR_META_PATH = '.phar/meta.json';
43
44
    private const PHAR_ARG = 'phar';
45
    private const OUTPUT_ARG = 'output';
46
    private const INTERNAL_OPT = 'internal';
47
48
    public function getConfiguration(): Configuration
49
    {
50
        return new Configuration(
51
            'extract',
52
            '🚚  Extracts a given PHAR into a directory',
53
            '',
54
            [
55
                new InputArgument(
56
                    self::PHAR_ARG,
57
                    InputArgument::REQUIRED,
58
                    'The path to the PHAR file',
59
                ),
60
                new InputArgument(
61
                    self::OUTPUT_ARG,
62
                    InputArgument::REQUIRED,
63
                    'The output directory',
64
                ),
65
            ],
66
            [
67
                new InputOption(
68
                    self::INTERNAL_OPT,
69
                    null,
70
                    InputOption::VALUE_NONE,
71
                    'Internal option; Should not be used.',
72
                ),
73
            ],
74
        );
75
    }
76
77
    public function execute(IO $io): int
78
    {
79
        PhpSettingsChecker::check($io);
80
81
        $pharPath = self::getPharFilePath($io);
82
        $outputDir = $io->getTypedArgument(self::OUTPUT_ARG)->asNonEmptyString();
83
        $internal = $io->getTypedOption(self::INTERNAL_OPT)->asBoolean();
84
85
        if (null === $pharPath) {
86
            return ExitCode::FAILURE;
87
        }
88
89
        if (file_exists($outputDir)) {
90
            $canDelete = $io->askQuestion(
91
                new ConfirmationQuestion(
92
                    'The output directory already exists. Do you want to delete its current content?',
93
                    // If is interactive, we want the prompt to default to false since it can be an error made by the user.
94
                    // Otherwise, this is likely launched by a script or Pharaoh in which case we do not care.
95
                    $internal,
96
                ),
97
            );
98
99
            if ($canDelete) {
100
                FS::remove($outputDir);
101
            // Continue
102
            } else {
103
                // Do nothing
104
                return ExitCode::FAILURE;
105
            }
106
        }
107
108
        FS::mkdir($outputDir);
109
110
        try {
111
            self::dumpPhar($pharPath, $outputDir);
112
        } catch (InvalidPhar $invalidPhar) {
113
            if (!$internal) {
114
                throw $invalidPhar;
115
            }
116
117
            $io->getErrorIO()->write($invalidPhar->getMessage());
118
119
            return ExitCode::FAILURE;
120
        }
121
122
        return ExitCode::SUCCESS;
123
    }
124
125
    private static function getPharFilePath(IO $io): ?string
126
    {
127
        $filePath = realpath($io->getTypedArgument(self::PHAR_ARG)->asString());
128
129
        if (false !== $filePath) {
130
            return $filePath;
131
        }
132
133
        $io->error(
134
            sprintf(
135
                'The file "%s" could not be found.',
136
                $io->getTypedArgument(self::PHAR_ARG)->asRaw(),
137
            ),
138
        );
139
140
        return null;
141
    }
142
143
    private static function dumpPhar(string $file, string $tmpDir): string
144
    {
145
        // We have to give every one a different alias, or it pukes.
146
        $alias = self::generateAlias($file);
147
148
        // Create a temporary PHAR: this is because the extension might be
149
        // missing in which case we would not be able to create a Phar instance
150
        // as it requires the .phar extension.
151
        $tmpFile = $tmpDir.DIRECTORY_SEPARATOR.$alias;
152
        $pubKey = $file.'.pubkey';
153
        $pubKeyContent = null;
154
        $tmpPubKey = $tmpFile.'.pubkey';
155
        $stub = $tmpDir.DIRECTORY_SEPARATOR.self::STUB_PATH;
156
157
        try {
158
            FS::copy($file, $tmpFile, true);
159
160
            if (file_exists($pubKey)) {
161
                FS::copy($pubKey, $tmpPubKey, true);
162
                $pubKeyContent = FS::getFileContents($pubKey);
163
            }
164
165
            $phar = PharFactory::create($tmpFile, $file);
166
            $pharMeta = PharMeta::fromPhar($phar, $pubKeyContent);
167
168
            $phar->extractTo($tmpDir);
169
            FS::dumpFile($stub, $phar->getStub());
170
        } catch (Throwable $throwable) {
171
            FS::remove([$tmpFile, $tmpPubKey]);
172
173
            throw $throwable;
174
        }
175
176
        FS::dumpFile(
177
            $tmpDir.DIRECTORY_SEPARATOR.self::PHAR_META_PATH,
178
            $pharMeta->toJson(),
179
        );
180
181
        // Cleanup the temporary PHAR.
182
        FS::remove([$tmpFile, $tmpPubKey]);
183
184
        return $tmpDir;
185
    }
186
187
    private static function generateAlias(string $file): string
188
    {
189
        $extension = self::getExtension($file);
190
191
        return bin2hex(random_bytes(16)).$extension;
192
    }
193
194
    private static function getExtension(string $file): string
195
    {
196
        $lastExtension = pathinfo($file, PATHINFO_EXTENSION);
197
        $extension = '';
198
199
        while ('' !== $lastExtension) {
200
            $extension = '.'.$lastExtension.$extension;
0 ignored issues
show
Are you sure $lastExtension of type array|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

200
            $extension = '.'./** @scrutinizer ignore-type */ $lastExtension.$extension;
Loading history...
201
            $file = mb_substr($file, 0, -(mb_strlen($lastExtension) + 1));
0 ignored issues
show
It seems like $lastExtension can also be of type array; however, parameter $string of mb_strlen() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

201
            $file = mb_substr($file, 0, -(mb_strlen(/** @scrutinizer ignore-type */ $lastExtension) + 1));
Loading history...
202
            $lastExtension = pathinfo($file, PATHINFO_EXTENSION);
203
        }
204
205
        return '' === $extension ? '.phar' : $extension;
206
    }
207
}
208