Passed
Push — master ( 5dd2f3...e80b48 )
by Théo
02:17
created

Info::showPharInfo()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 28
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 14
dl 0
loc 28
rs 9.7998
c 0
b 0
f 0
cc 3
nc 2
nop 6
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 function array_filter;
18
use function array_flip;
19
use function array_sum;
20
use Assert\Assertion;
21
use function count;
22
use DirectoryIterator;
23
use function end;
24
use function filesize;
25
use function is_array;
26
use function KevinGH\Box\FileSystem\remove;
27
use function KevinGH\Box\format_size;
28
use function KevinGH\Box\get_phar_compression_algorithms;
29
use KevinGH\Box\PharInfo\PharInfo;
30
use function key;
31
use Phar;
32
use PharData;
33
use PharFileInfo;
34
use function realpath;
35
use function round;
36
use function sprintf;
37
use function str_repeat;
38
use function str_replace;
39
use Symfony\Component\Console\Input\InputArgument;
40
use Symfony\Component\Console\Input\InputInterface;
41
use Symfony\Component\Console\Input\InputOption;
42
use Symfony\Component\Console\Output\OutputInterface;
43
use Symfony\Component\Console\Style\SymfonyStyle;
44
use Throwable;
45
46
/**
47
 * @private
48
 */
49
final class Info extends Command
50
{
51
    use CreateTemporaryPharFile;
52
53
    private const PHAR_ARG = 'phar';
54
    private const LIST_OPT = 'list';
55
    private const METADATA_OPT = 'metadata';
56
    private const MODE_OPT = 'mode';
57
    private const DEPTH_OPT = 'depth';
58
59
    private static $FILE_ALGORITHMS;
60
61
    /**
62
     * {@inheritdoc}
63
     */
64
    public function __construct(?string $name = null)
65
    {
66
        parent::__construct($name);
67
68
        if (null === self::$FILE_ALGORITHMS) {
69
            self::$FILE_ALGORITHMS = array_flip(array_filter(get_phar_compression_algorithms()));
70
        }
71
    }
72
73
    /**
74
     * {@inheritdoc}
75
     */
76
    protected function configure(): void
77
    {
78
        $this->setName('info');
79
        $this->setDescription(
80
            '🔍  Displays information about the PHAR extension or file'
81
        );
82
        $this->setHelp(
83
            <<<'HELP'
84
The <info>%command.name%</info> command will display information about the Phar extension,
85
or the Phar file if specified.
86
87
If the <info>phar</info> argument <comment>(the PHAR file path)</comment> is provided, information
88
about the PHAR file itself will be displayed.
89
90
If the <info>--list|-l</info> option is used, the contents of the PHAR file will
91
be listed. By default, the list is shown as an indented tree. You may
92
instead choose to view a flat listing, by setting the <info>--mode|-m</info> option
93
to <comment>flat</comment>.
94
HELP
95
        );
96
        $this->addArgument(
97
            self::PHAR_ARG,
98
            InputArgument::OPTIONAL,
99
            'The Phar file.'
100
        );
101
        $this->addOption(
102
            self::LIST_OPT,
103
            'l',
104
            InputOption::VALUE_NONE,
105
            'List the contents of the Phar?'
106
        );
107
        $this->addOption(
108
            self::METADATA_OPT,
109
            null,
110
            InputOption::VALUE_NONE,
111
            'Display metadata?'
112
        );
113
        $this->addOption(
114
            self::MODE_OPT,
115
            'm',
116
            InputOption::VALUE_REQUIRED,
117
            'The listing mode. (default: indent, options: indent, flat)',
118
            'indent'
119
        );
120
        $this->addOption(
121
            self::DEPTH_OPT,
122
            'd',
123
            InputOption::VALUE_REQUIRED,
124
            'The depth of the tree displayed',
125
            -1
126
        );
127
    }
128
129
    /**
130
     * {@inheritdoc}
131
     */
132
    public function execute(InputInterface $input, OutputInterface $output): int
133
    {
134
        $io = new SymfonyStyle($input, $output);
135
        $io->newLine();
136
137
        if (null === ($file = $input->getArgument(self::PHAR_ARG))) {
138
            return $this->showGlobalInfo($output, $io);
139
        }
140
141
        $file = realpath($file);
0 ignored issues
show
Bug introduced by
It seems like $file can also be of type string[]; however, parameter $path of realpath() 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

141
        $file = realpath(/** @scrutinizer ignore-type */ $file);
Loading history...
142
143
        if (false === $file) {
144
            $io->error(
145
                sprintf(
146
                    'The file "%s" could not be found.',
147
                    $input->getArgument(self::PHAR_ARG)
0 ignored issues
show
Bug introduced by
It seems like $input->getArgument(self::PHAR_ARG) can also be of type string[]; however, parameter $args of sprintf() 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

147
                    /** @scrutinizer ignore-type */ $input->getArgument(self::PHAR_ARG)
Loading history...
148
                )
149
            );
150
151
            return 1;
152
        }
153
154
        $tmpFile = $this->createTemporaryPhar($file);
155
156
        try {
157
            return $this->showInfo($tmpFile, $file, $input, $output, $io);
158
        } finally {
159
            if ($file !== $tmpFile) {
160
                remove($tmpFile);
161
            }
162
        }
163
    }
164
165
    public function showInfo(string $file, string $originalFile, InputInterface $input, OutputInterface $output, SymfonyStyle $io): int
166
    {
167
        $depth = (int) $input->getOption(self::DEPTH_OPT);
168
169
        Assertion::greaterOrEqualThan($depth, -1, 'Expected the depth to be a positive integer or -1, got "%d"');
170
171
        try {
172
            $pharInfo = new PharInfo($file);
173
174
            return $this->showPharInfo(
175
                $pharInfo,
176
                $input->getOption(self::LIST_OPT),
177
                $depth,
178
                'indent' === $input->getOption(self::MODE_OPT),
179
                $output,
180
                $io
181
            );
182
        } catch (Throwable $throwable) {
183
            if ($output->isDebug()) {
184
                throw $throwable;
185
            }
186
187
            $io->error(
188
                sprintf(
189
                    'Could not read the file "%s".',
190
                    $originalFile
191
                )
192
            );
193
194
            return 1;
195
        }
196
    }
197
198
    private function showGlobalInfo(OutputInterface $output, SymfonyStyle $io): int
199
    {
200
        $this->render(
201
            $output,
202
            [
203
                'API Version' => Phar::apiVersion(),
204
                'Supported Compression' => Phar::getSupportedCompression(),
205
                'Supported Signatures' => Phar::getSupportedSignatures(),
206
            ]
207
        );
208
209
        $io->newLine();
210
        $io->comment('Get a PHAR details by giving its path as an argument.');
211
212
        return 0;
213
    }
214
215
    private function showPharInfo(
216
        PharInfo $pharInfo,
217
        bool $content,
218
        int $depth,
219
        bool $indent,
220
        OutputInterface $output,
221
        SymfonyStyle $io
222
    ): int {
223
        $signature = $pharInfo->getPhar()->getSignature();
224
225
        $this->showPharGlobalInfo($pharInfo, $io, $signature);
226
227
        if ($content) {
228
            $this->renderContents(
229
                $output,
230
                $pharInfo->getPhar(),
231
                0,
232
                $depth,
233
                $indent ? 0 : false,
234
                $pharInfo->getRoot(),
235
                $pharInfo->getPhar(),
236
                $pharInfo->getRoot()
237
            );
238
        } else {
239
            $io->comment('Use the <info>--list|-l</info> option to list the content of the PHAR.');
240
        }
241
242
        return 0;
243
    }
244
245
    /**
246
     * @param mixed $signature
247
     */
248
    private function showPharGlobalInfo(PharInfo $pharInfo, SymfonyStyle $io, $signature): void
249
    {
250
        $io->writeln(
251
            sprintf(
252
                '<comment>API Version:</comment> %s',
253
                $pharInfo->getVersion()
254
            )
255
        );
256
        $io->newLine();
257
258
        $count = array_filter($pharInfo->retrieveCompressionCount());
259
        $totalCount = array_sum($count);
260
261
        if (1 === count($count)) {
262
            $io->writeln(
263
                sprintf(
264
                    '<comment>Archive Compression:</comment> %s',
265
                    key($count)
266
                )
267
            );
268
        } else {
269
            $io->writeln('<comment>Archive Compression:</comment>');
270
271
            end($count);
272
            $lastAlgorithmName = key($count);
273
274
            $totalPercentage = 100;
275
276
            foreach ($count as $algorithmName => $nbrOfFiles) {
277
                if ($lastAlgorithmName === $algorithmName) {
278
                    $percentage = $totalPercentage;
279
                } else {
280
                    $percentage = round($nbrOfFiles * 100 / $totalCount, 2);
281
282
                    $totalPercentage -= $percentage;
283
                }
284
285
                $io->writeln(
286
                    sprintf(
287
                        '  - %s (%0.2f%%)',
288
                        $algorithmName,
289
                        $percentage
290
                    )
291
                );
292
            }
293
        }
294
        $io->newLine();
295
296
        if (false !== $signature) {
297
            $io->writeln(
298
                sprintf(
299
                    '<comment>Signature:</comment> %s',
300
                    $signature['hash_type']
301
                )
302
            );
303
            $io->writeln(
304
                sprintf(
305
                    '<comment>Signature Hash:</comment> %s',
306
                    $signature['hash']
307
                )
308
            );
309
            $io->newLine();
310
        }
311
312
        $metadata = $pharInfo->getNormalizedMetadata();
313
314
        if (null === $metadata) {
315
            $io->writeln('<comment>Metadata:</comment> None');
316
        } else {
317
            $io->writeln('<comment>Metadata:</comment>');
318
            $io->writeln($metadata);
319
        }
320
        $io->newLine();
321
322
        $io->writeln(
323
            sprintf(
324
                '<comment>Contents:</comment>%s (%s)',
325
                1 === $totalCount ? ' 1 file' : " $totalCount files",
326
                format_size(
327
                    filesize($pharInfo->getPhar()->getPath())
328
                )
329
            )
330
        );
331
    }
332
333
    private function render(OutputInterface $output, array $attributes): void
334
    {
335
        $out = false;
336
337
        foreach ($attributes as $name => $value) {
338
            if ($out) {
339
                $output->writeln('');
340
            }
341
342
            $output->write("<comment>$name:</comment>");
343
344
            if (is_array($value)) {
345
                $output->writeln('');
346
347
                foreach ($value as $v) {
348
                    $output->writeln("  - $v");
349
                }
350
            } else {
351
                $output->writeln(" $value");
352
            }
353
354
            $out = true;
355
        }
356
    }
357
358
    /**
359
     * @param iterable|PharFileInfo[] $list
360
     * @param false|int               $indent Nbr of indent or `false`
361
     * @param Phar|PharData           $phar
362
     */
363
    private function renderContents(
364
        OutputInterface $output,
365
        iterable $list,
366
        int $depth,
367
        int $maxDepth,
368
        $indent,
369
        string $base,
370
        $phar,
371
        string $root
372
    ): void {
373
        if (-1 !== $maxDepth && $depth > $maxDepth) {
374
            return;
375
        }
376
377
        foreach ($list as $item) {
378
            $item = $phar[str_replace($root, '', $item->getPathname())];
379
380
            if (false !== $indent) {
381
                $output->write(str_repeat(' ', $indent));
382
383
                $path = $item->getFilename();
384
385
                if ($item->isDir()) {
386
                    $path .= '/';
387
                }
388
            } else {
389
                $path = str_replace($base, '', $item->getPathname());
390
            }
391
392
            if ($item->isDir()) {
393
                if (false !== $indent) {
394
                    $output->writeln("<info>$path</info>");
395
                }
396
            } else {
397
                $compression = '<fg=red>[NONE]</fg=red>';
398
399
                foreach (self::$FILE_ALGORITHMS as $code => $name) {
400
                    if ($item->isCompressed($code)) {
401
                        $compression = "<fg=cyan>[$name]</fg=cyan>";
402
                        break;
403
                    }
404
                }
405
406
                $fileSize = format_size($item->getCompressedSize());
407
408
                $output->writeln(
409
                    sprintf(
410
                        '%s %s - %s',
411
                        $path,
412
                        $compression,
413
                        $fileSize
414
                    )
415
                );
416
            }
417
418
            if ($item->isDir()) {
419
                $this->renderContents(
420
                    $output,
421
                    new DirectoryIterator($item->getPathname()),
422
                    $depth + 1,
423
                    $maxDepth,
424
                    false === $indent ? $indent : $indent + 2,
425
                    $base,
426
                    $phar,
427
                    $root
428
                );
429
            }
430
        }
431
    }
432
}
433