Passed
Pull Request — master (#112)
by Théo
02:25
created

Info::executeShowGlobalInfo()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 15
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 8
nc 1
nop 2
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 DateTimeImmutable;
18
use DirectoryIterator;
19
use Phar;
20
use PharData;
21
use PharFileInfo;
22
use RecursiveIteratorIterator;
23
use Symfony\Component\Console\Command\Command;
24
use Symfony\Component\Console\Input\InputArgument;
25
use Symfony\Component\Console\Input\InputInterface;
26
use Symfony\Component\Console\Input\InputOption;
27
use Symfony\Component\Console\Output\OutputInterface;
28
use Symfony\Component\Console\Style\SymfonyStyle;
29
use Throwable;
30
use UnexpectedValueException;
31
use function array_fill_keys;
32
use function array_filter;
33
use function array_reduce;
34
use function array_sum;
35
use function count;
36
use function end;
37
use function is_array;
38
use function iterator_to_array;
39
use function KevinGH\Box\FileSystem\copy;
40
use function KevinGH\Box\FileSystem\remove;
41
use function KevinGH\Box\formatted_filesize;
42
use function key;
43
use function realpath;
44
use function sprintf;
45
use function sys_get_temp_dir;
46
47
final class Info extends Command
48
{
49
    private const PHAR_ARG = 'phar';
50
    private const LIST_OPT = 'list';
51
    private const METADATA_OPT = 'metadata';
52
    private const MODE_OPT = 'mode';
53
54
    /**
55
     * The list of recognized compression algorithms.
56
     *
57
     * @var array
58
     */
59
    private const ALGORITHMS = [
60
        Phar::BZ2 => 'BZ2',
61
        Phar::GZ => 'GZ',
62
        'NONE' => 'None',
63
    ];
64
65
    /**
66
     * The list of recognized file compression algorithms.
67
     *
68
     * @var array
69
     */
70
    private const FILE_ALGORITHMS = [
71
        Phar::BZ2 => 'BZ2',
72
        Phar::GZ => 'GZ',
73
    ];
74
75
    /**
76
     * {@inheritdoc}
77
     */
78
    protected function configure(): void
79
    {
80
        $this->setName('info');
81
        $this->setDescription(
82
            'Displays information about the PHAR extension or file'
83
        );
84
        $this->setHelp(
85
            <<<'HELP'
86
The <info>%command.name%</info> command will display information about the Phar extension,
87
or the Phar file if specified.
88
89
If the <info>phar</info> argument <comment>(the PHAR file path)</comment> is provided, information
90
about the PHAR file itself will be displayed.
91
92
If the <info>--list|-l</info> option is used, the contents of the PHAR file will
93
be listed. By default, the list is shown as an indented tree. You may
94
instead choose to view a flat listing, by setting the <info>--mode|-m</info> option
95
to <comment>flat</comment>.
96
HELP
97
        );
98
        $this->addArgument(
99
            self::PHAR_ARG,
100
            InputArgument::OPTIONAL,
101
            'The Phar file.'
102
        );
103
        $this->addOption(
104
            self::LIST_OPT,
105
            'l',
106
            InputOption::VALUE_NONE,
107
            'List the contents of the Phar?'
108
        );
109
        $this->addOption(
110
            self::METADATA_OPT,
111
            null,
112
            InputOption::VALUE_NONE,
113
            'Display metadata?'
114
        );
115
        $this->addOption(
116
            self::MODE_OPT,
117
            'm',
118
            InputOption::VALUE_OPTIONAL,
119
            'The listing mode. (default: indent, options: indent, flat)',
120
            'indent'
121
        );
122
    }
123
124
    /**
125
     * @override
126
     */
127
    public function execute(InputInterface $input, OutputInterface $output): int
128
    {
129
        $io = new SymfonyStyle($input, $output);
130
        $io->writeln('');
131
132
        if (null === ($file = $input->getArgument(self::PHAR_ARG))) {
133
            return $this->showGlobalInfo($output, $io);
134
        }
135
136
        $file = realpath($file);
137
138
        if (false === $file) {
139
            $io->error(
140
                sprintf(
141
                    'The file "%s" could not be found.',
142
                    $input->getArgument(self::PHAR_ARG)
143
                )
144
            );
145
146
            return 1;
147
        }
148
149
        if ('' === pathinfo($file, PATHINFO_EXTENSION)) {
150
            // It is likely to be a PHAR without extension
151
            copy($file, $tmpFile = sys_get_temp_dir().'/'.(new DateTimeImmutable())->getTimestamp().$file.'.phar');
152
153
            try {
154
                return $this->showInfo($tmpFile, $file, $input, $output, $io);
155
            } finally {
156
                remove($tmpFile);
157
            }
158
        }
159
160
        return $this->showInfo($file, $file, $input, $output, $io);
161
    }
162
163
    public function showInfo(string $file, string $originalFile, InputInterface $input, OutputInterface $output, SymfonyStyle $io): int
164
    {
165
        try {
166
            try {
167
                $phar = new Phar($file);
168
            } catch (UnexpectedValueException $exception) {
169
                $phar = new PharData($file);
170
            }
171
172
            return $this->showPharInfo(
173
                $phar,
174
                $input->getOption(self::LIST_OPT),
175
                'indent' === $input->getOption(self::MODE_OPT),
176
                $output,
177
                $io
178
            );
179
        } catch (Throwable $throwable) {
180
            $io->error(
181
                sprintf(
182
                    'Could not read the file "%s".',
183
                    $originalFile
184
                )
185
            );
186
187
            return 1;
188
        }
189
    }
190
191
    private function showGlobalInfo(OutputInterface $output, SymfonyStyle $io): int
192
    {
193
        $this->render(
194
            $output,
195
            [
196
                'API Version' => Phar::apiVersion(),
197
                'Supported Compression' => Phar::getSupportedCompression(),
198
                'Supported Signatures' => Phar::getSupportedSignatures(),
199
            ]
200
        );
201
202
        $io->writeln('');
203
        $io->comment('Get a PHAR details by giving its path as an argument.');
204
205
        return 0;
206
    }
207
208
    /**
209
     * @param Phar|PharData $phar
210
     */
211
    private function showPharInfo($phar, bool $content, bool $indent, OutputInterface $output, SymfonyStyle $io): int
212
    {
213
        $signature = $phar->getSignature();
214
215
        $this->showPharGlobalInfo($phar, $io, $signature);
216
217
        if ($content) {
218
            $root = 'phar://'.str_replace('\\', '/', realpath($phar->getPath())).'/';
219
220
            $this->renderContents(
221
                $output,
222
                $phar,
0 ignored issues
show
Bug introduced by
$phar of type Phar|PharData is incompatible with the type iterable expected by parameter $list of KevinGH\Box\Console\Command\Info::renderContents(). ( Ignorable by Annotation )

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

222
                /** @scrutinizer ignore-type */ $phar,
Loading history...
223
                $indent ? 0 : false,
224
                $root,
225
                $phar,
226
                $root
227
            );
228
        } else {
229
            $io->comment('Use the <info>--list|-l</info> option to list the content of the PHAR.');
230
        }
231
232
        return 0;
233
    }
234
235
    /**
236
     * @param Phar|PharData $phar
237
     * @param mixed         $signature
238
     */
239
    private function showPharGlobalInfo($phar, SymfonyStyle $io, $signature): void
240
    {
241
        $io->writeln(
242
            sprintf(
243
                '<comment>API Version:</comment> %s',
244
                '' !== $phar->getVersion() ? $phar->getVersion() : 'No information found'
245
            )
246
        );
247
        $io->writeln('');
248
249
        $count = array_filter($this->retrieveCompressionCount($phar));
250
        $totalCount = array_sum($count);
251
252
        if (1 === count($count)) {
253
            $io->writeln(
254
                sprintf(
255
                    '<comment>Archive Compression:</comment> %s',
256
                    key($count)
257
                )
258
            );
259
        } else {
260
            $io->writeln('<comment>Archive Compression:</comment>');
261
262
            end($count);
263
            $lastAlgorithmName = key($count);
264
265
            $totalPercentage = 100;
266
267
            foreach ($count as $algorithmName => $nbrOfFiles) {
268
                if ($lastAlgorithmName === $algorithmName) {
269
                    $percentage = $totalPercentage;
270
                } else {
271
                    $percentage = $nbrOfFiles * 100 / $totalCount;
272
273
                    $totalPercentage -= $percentage;
274
                }
275
276
                $io->writeln(
277
                    sprintf(
278
                        '  - %s (%0.2f%%)',
279
                        $algorithmName,
280
                        $percentage
281
                    )
282
                );
283
            }
284
        }
285
        $io->writeln('');
286
287
        if (false !== $signature) {
288
            $io->writeln(
289
                sprintf(
290
                    '<comment>Signature:</comment> %s',
291
                    $signature['hash_type']
292
                )
293
            );
294
            $io->writeln(
295
                sprintf(
296
                    '<comment>Signature Hash:</comment> %s',
297
                    $signature['hash']
298
                )
299
            );
300
            $io->writeln('');
301
        }
302
303
        $metadata = var_export($phar->getMetadata(), true);
304
305
        if ('NULL' === $metadata) {
306
            $io->writeln('<comment>Metadata:</comment> None');
307
        } else {
308
            $io->writeln('<comment>Metadata:</comment>');
309
            $io->writeln($metadata);
310
        }
311
        $io->writeln('');
312
313
        $io->writeln(
314
            sprintf(
315
                '<comment>Contents:</comment>%s (%s)',
316
                1 === $totalCount ? ' 1 file' : " $totalCount files",
317
                formatted_filesize($phar->getPath())
318
            )
319
        );
320
    }
321
322
    private function render(OutputInterface $output, array $attributes): void
323
    {
324
        $out = false;
325
326
        foreach ($attributes as $name => $value) {
327
            if ($out) {
328
                $output->writeln('');
329
            }
330
331
            $output->write("<comment>$name:</comment>");
332
333
            if (is_array($value)) {
334
                $output->writeln('');
335
336
                foreach ($value as $v) {
337
                    $output->writeln("  - $v");
338
                }
339
            } else {
340
                $output->writeln(" $value");
341
            }
342
343
            $out = true;
344
        }
345
    }
346
347
    /**
348
     * @param OutputInterface         $output
349
     * @param iterable|PharFileInfo[] $list
350
     * @param bool|int                $indent Nbr of indent or `false`
351
     * @param string                  $base
352
     * @param Phar                    $phar
353
     * @param string                  $root
354
     */
355
    private function renderContents(
356
        OutputInterface $output,
357
        iterable $list,
358
        $indent,
359
        string $base,
360
        Phar $phar,
361
        string $root
362
    ): void {
363
        foreach ($list as $item) {
364
            $item = $phar[str_replace($root, '', $item->getPathname())];
365
366
            if (false !== $indent) {
367
                $output->write(str_repeat(' ', $indent));
0 ignored issues
show
Bug introduced by
It seems like $indent can also be of type true; however, parameter $multiplier of str_repeat() does only seem to accept integer, 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

367
                $output->write(str_repeat(' ', /** @scrutinizer ignore-type */ $indent));
Loading history...
368
369
                $path = $item->getFilename();
0 ignored issues
show
Bug introduced by
The method getFilename() does not exist on integer. ( Ignorable by Annotation )

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

369
                /** @scrutinizer ignore-call */ 
370
                $path = $item->getFilename();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
370
371
                if ($item->isDir()) {
372
                    $path .= '/';
373
                }
374
            } else {
375
                $path = str_replace($base, '', $item->getPathname());
376
            }
377
378
            if ($item->isDir()) {
379
                if (false !== $indent) {
380
                    $output->writeln("<info>$path</info>");
381
                }
382
            } else {
383
                $compression = ' <fg=red>[NONE]</fg=red>';
384
385
                foreach (self::FILE_ALGORITHMS as $code => $name) {
386
                    if ($item->isCompressed($code)) {
387
                        $compression = " <fg=cyan>[$name]</fg=cyan>";
388
                        break;
389
                    }
390
                }
391
392
                $output->writeln($path.$compression);
393
            }
394
395
            if ($item->isDir()) {
396
                $this->renderContents(
397
                    $output,
398
                    new DirectoryIterator($item->getPathname()),
0 ignored issues
show
Bug introduced by
new DirectoryIterator($item->getPathname()) of type DirectoryIterator is incompatible with the type iterable expected by parameter $list of KevinGH\Box\Console\Command\Info::renderContents(). ( Ignorable by Annotation )

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

398
                    /** @scrutinizer ignore-type */ new DirectoryIterator($item->getPathname()),
Loading history...
399
                    (false === $indent) ? $indent : $indent + 2,
400
                    $base,
401
                    $phar,
402
                    $root
403
                );
404
            }
405
        }
406
    }
407
408
    /**
409
     * @param Phar|PharData $phar
410
     */
411
    private function retrieveCompressionCount($phar): array
412
    {
413
        $count = array_fill_keys(
414
           self::ALGORITHMS,
415
            0
416
        );
417
418
        if ($phar instanceof PharData) {
419
            $count[self::ALGORITHMS[$phar->isCompressed()]] = 1;
420
421
            return $count;
422
        }
423
424
        $countFile = function (array $count, PharFileInfo $file) {
425
            if (false === $file->isCompressed()) {
426
                ++$count['None'];
427
428
                return $count;
429
            }
430
431
            foreach (self::ALGORITHMS as $compressionAlgorithmCode => $compressionAlgorithmName) {
432
                if ($file->isCompressed($compressionAlgorithmCode)) {
433
                    ++$count[$compressionAlgorithmName];
434
435
                    return $count;
436
                }
437
            }
438
439
            return $count;
440
        };
441
442
        return array_reduce(
443
            iterator_to_array(new RecursiveIteratorIterator($phar)),
444
            $countFile,
445
            $count
446
        );
447
    }
448
}
449