Passed
Pull Request — master (#236)
by Théo
02:54
created

Info::showPharInfo()   B

Complexity

Conditions 3
Paths 2

Size

Total Lines 30
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 30
rs 8.8571
c 0
b 0
f 0
cc 3
eloc 15
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 Assert\Assertion;
18
use DateTimeImmutable;
19
use DirectoryIterator;
20
use Phar;
21
use PharData;
22
use PharFileInfo;
23
use RecursiveIteratorIterator;
24
use Symfony\Component\Console\Command\Command;
25
use Symfony\Component\Console\Input\InputArgument;
26
use Symfony\Component\Console\Input\InputInterface;
27
use Symfony\Component\Console\Input\InputOption;
28
use Symfony\Component\Console\Output\OutputInterface;
29
use Symfony\Component\Console\Style\SymfonyStyle;
30
use Throwable;
31
use UnexpectedValueException;
32
use function array_fill_keys;
33
use function array_filter;
34
use function array_reduce;
35
use function array_sum;
36
use function count;
37
use function end;
38
use function filesize;
39
use function is_array;
40
use function iterator_to_array;
41
use function KevinGH\Box\FileSystem\copy;
42
use function KevinGH\Box\FileSystem\remove;
43
use function KevinGH\Box\format_size;
44
use function key;
45
use function realpath;
46
use function sprintf;
47
use function str_repeat;
48
use function str_replace;
49
use function sys_get_temp_dir;
50
use function var_export;
51
52
/**
53
 * @private
54
 */
55
final class Info extends Command
56
{
57
    private const PHAR_ARG = 'phar';
58
    private const LIST_OPT = 'list';
59
    private const METADATA_OPT = 'metadata';
60
    private const MODE_OPT = 'mode';
61
    private const DEPTH_OPT = 'depth';
62
63
    /**
64
     * The list of recognized compression algorithms.
65
     *
66
     * @var array
67
     */
68
    private const ALGORITHMS = [
69
        Phar::BZ2 => 'BZ2',
70
        Phar::GZ => 'GZ',
71
        Phar::NONE => 'None',
72
    ];
73
74
    /**
75
     * The list of recognized file compression algorithms.
76
     *
77
     * @var array
78
     */
79
    private const FILE_ALGORITHMS = [
80
        Phar::BZ2 => 'BZ2',
81
        Phar::GZ => 'GZ',
82
    ];
83
84
    /**
85
     * {@inheritdoc}
86
     */
87
    protected function configure(): void
88
    {
89
        $this->setName('info');
90
        $this->setDescription(
91
            'Displays information about the PHAR extension or file'
92
        );
93
        $this->setHelp(
94
            <<<'HELP'
95
The <info>%command.name%</info> command will display information about the Phar extension,
96
or the Phar file if specified.
97
98
If the <info>phar</info> argument <comment>(the PHAR file path)</comment> is provided, information
99
about the PHAR file itself will be displayed.
100
101
If the <info>--list|-l</info> option is used, the contents of the PHAR file will
102
be listed. By default, the list is shown as an indented tree. You may
103
instead choose to view a flat listing, by setting the <info>--mode|-m</info> option
104
to <comment>flat</comment>.
105
HELP
106
        );
107
        $this->addArgument(
108
            self::PHAR_ARG,
109
            InputArgument::OPTIONAL,
110
            'The Phar file.'
111
        );
112
        $this->addOption(
113
            self::LIST_OPT,
114
            'l',
115
            InputOption::VALUE_NONE,
116
            'List the contents of the Phar?'
117
        );
118
        $this->addOption(
119
            self::METADATA_OPT,
120
            null,
121
            InputOption::VALUE_NONE,
122
            'Display metadata?'
123
        );
124
        $this->addOption(
125
            self::MODE_OPT,
126
            'm',
127
            InputOption::VALUE_REQUIRED,
128
            'The listing mode. (default: indent, options: indent, flat)',
129
            'indent'
130
        );
131
        $this->addOption(
132
            self::DEPTH_OPT,
133
            'd',
134
            InputOption::VALUE_REQUIRED,
135
            'The depth of the tree displayed',
136
            -1
137
        );
138
    }
139
140
    /**
141
     * {@inheritdoc}
142
     */
143
    public function execute(InputInterface $input, OutputInterface $output): int
144
    {
145
        $io = new SymfonyStyle($input, $output);
146
        $io->writeln('');
147
148
        if (null === ($file = $input->getArgument(self::PHAR_ARG))) {
149
            return $this->showGlobalInfo($output, $io);
150
        }
151
152
        $file = realpath($file);
153
154
        if (false === $file) {
155
            $io->error(
156
                sprintf(
157
                    'The file "%s" could not be found.',
158
                    $input->getArgument(self::PHAR_ARG)
159
                )
160
            );
161
162
            return 1;
163
        }
164
165
        if ('' === pathinfo($file, PATHINFO_EXTENSION)) {
166
            // It is likely to be a PHAR without extension
167
            copy($file, $tmpFile = sys_get_temp_dir().'/'.(new DateTimeImmutable())->getTimestamp().$file.'.phar');
168
169
            try {
170
                return $this->showInfo($tmpFile, $file, $input, $output, $io);
171
            } finally {
172
                remove($tmpFile);
173
            }
174
        }
175
176
        return $this->showInfo($file, $file, $input, $output, $io);
177
    }
178
179
    public function showInfo(string $file, string $originalFile, InputInterface $input, OutputInterface $output, SymfonyStyle $io): int
180
    {
181
        $depth = (int) $input->getOption(self::DEPTH_OPT);
182
183
        Assertion::greaterOrEqualThan($depth, -1, 'Expected the depth to be a positive integer or -1, got "%d"');
184
185
        try {
186
            try {
187
                $phar = new Phar($file);
188
            } catch (UnexpectedValueException $exception) {
189
                $phar = new PharData($file);
190
            }
191
192
            return $this->showPharInfo(
193
                $phar,
194
                $input->getOption(self::LIST_OPT),
195
                $depth,
196
                'indent' === $input->getOption(self::MODE_OPT),
197
                $output,
198
                $io
199
            );
200
        } catch (Throwable $throwable) {
201
            if ($output->isDebug()) {
202
                throw $throwable;
203
            }
204
205
            $io->error(
206
                sprintf(
207
                    'Could not read the file "%s".',
208
                    $originalFile
209
                )
210
            );
211
212
            return 1;
213
        }
214
    }
215
216
    private function showGlobalInfo(OutputInterface $output, SymfonyStyle $io): int
217
    {
218
        $this->render(
219
            $output,
220
            [
221
                'API Version' => Phar::apiVersion(),
222
                'Supported Compression' => Phar::getSupportedCompression(),
223
                'Supported Signatures' => Phar::getSupportedSignatures(),
224
            ]
225
        );
226
227
        $io->writeln('');
228
        $io->comment('Get a PHAR details by giving its path as an argument.');
229
230
        return 0;
231
    }
232
233
    /**
234
     * @param Phar|PharData $phar
235
     */
236
    private function showPharInfo(
237
        $phar,
238
        bool $content,
239
        int $depth,
240
        bool $indent,
241
        OutputInterface $output,
242
        SymfonyStyle $io
243
    ): int {
244
        $signature = $phar->getSignature();
245
246
        $this->showPharGlobalInfo($phar, $io, $signature);
247
248
        if ($content) {
249
            $root = 'phar://'.str_replace('\\', '/', realpath($phar->getPath())).'/';
250
251
            $this->renderContents(
252
                $output,
253
                $phar,
254
                0,
255
                $depth,
256
                $indent ? 0 : false,
257
                $root,
258
                $phar,
259
                $root
260
            );
261
        } else {
262
            $io->comment('Use the <info>--list|-l</info> option to list the content of the PHAR.');
263
        }
264
265
        return 0;
266
    }
267
268
    /**
269
     * @param Phar|PharData $phar
270
     * @param mixed         $signature
271
     */
272
    private function showPharGlobalInfo($phar, SymfonyStyle $io, $signature): void
273
    {
274
        $io->writeln(
275
            sprintf(
276
                '<comment>API Version:</comment> %s',
277
                '' !== $phar->getVersion() ? $phar->getVersion() : 'No information found'
278
            )
279
        );
280
        $io->writeln('');
281
282
        $count = array_filter($this->retrieveCompressionCount($phar));
283
        $totalCount = array_sum($count);
284
285
        if (1 === count($count)) {
286
            $io->writeln(
287
                sprintf(
288
                    '<comment>Archive Compression:</comment> %s',
289
                    key($count)
290
                )
291
            );
292
        } else {
293
            $io->writeln('<comment>Archive Compression:</comment>');
294
295
            end($count);
296
            $lastAlgorithmName = key($count);
297
298
            $totalPercentage = 100;
299
300
            foreach ($count as $algorithmName => $nbrOfFiles) {
301
                if ($lastAlgorithmName === $algorithmName) {
302
                    $percentage = $totalPercentage;
303
                } else {
304
                    $percentage = $nbrOfFiles * 100 / $totalCount;
305
306
                    $totalPercentage -= $percentage;
307
                }
308
309
                $io->writeln(
310
                    sprintf(
311
                        '  - %s (%0.2f%%)',
312
                        $algorithmName,
313
                        $percentage
314
                    )
315
                );
316
            }
317
        }
318
        $io->writeln('');
319
320
        if (false !== $signature) {
321
            $io->writeln(
322
                sprintf(
323
                    '<comment>Signature:</comment> %s',
324
                    $signature['hash_type']
325
                )
326
            );
327
            $io->writeln(
328
                sprintf(
329
                    '<comment>Signature Hash:</comment> %s',
330
                    $signature['hash']
331
                )
332
            );
333
            $io->writeln('');
334
        }
335
336
        $metadata = var_export($phar->getMetadata(), true);
337
338
        if ('NULL' === $metadata) {
339
            $io->writeln('<comment>Metadata:</comment> None');
340
        } else {
341
            $io->writeln('<comment>Metadata:</comment>');
342
            $io->writeln($metadata);
343
        }
344
        $io->writeln('');
345
346
        $io->writeln(
347
            sprintf(
348
                '<comment>Contents:</comment>%s (%s)',
349
                1 === $totalCount ? ' 1 file' : " $totalCount files",
350
                format_size(
351
                    filesize($phar->getPath())
352
                )
353
            )
354
        );
355
    }
356
357
    private function render(OutputInterface $output, array $attributes): void
358
    {
359
        $out = false;
360
361
        foreach ($attributes as $name => $value) {
362
            if ($out) {
363
                $output->writeln('');
364
            }
365
366
            $output->write("<comment>$name:</comment>");
367
368
            if (is_array($value)) {
369
                $output->writeln('');
370
371
                foreach ($value as $v) {
372
                    $output->writeln("  - $v");
373
                }
374
            } else {
375
                $output->writeln(" $value");
376
            }
377
378
            $out = true;
379
        }
380
    }
381
382
    /**
383
     * @param OutputInterface         $output
384
     * @param iterable|PharFileInfo[] $list
385
     * @param false|int               $indent Nbr of indent or `false`
386
     * @param string                  $base
387
     * @param Phar|PharData           $phar
388
     * @param string                  $root
389
     */
390
    private function renderContents(
391
        OutputInterface $output,
392
        iterable $list,
393
        int $depth,
394
        int $maxDepth,
395
        $indent,
396
        string $base,
397
        $phar,
398
        string $root
399
    ): void {
400
        if (-1 !== $maxDepth && $depth > $maxDepth) {
401
            return;
402
        }
403
404
        foreach ($list as $item) {
405
            $item = $phar[str_replace($root, '', $item->getPathname())];
406
407
            if (false !== $indent) {
408
                $output->write(str_repeat(' ', $indent));
409
410
                $path = $item->getFilename();
411
412
                if ($item->isDir()) {
413
                    $path .= '/';
414
                }
415
            } else {
416
                $path = str_replace($base, '', $item->getPathname());
417
            }
418
419
            if ($item->isDir()) {
420
                if (false !== $indent) {
421
                    $output->writeln("<info>$path</info>");
422
                }
423
            } else {
424
                $compression = '<fg=red>[NONE]</fg=red>';
425
426
                foreach (self::FILE_ALGORITHMS as $code => $name) {
427
                    if ($item->isCompressed($code)) {
428
                        $compression = "<fg=cyan>[$name]</fg=cyan>";
429
                        break;
430
                    }
431
                }
432
433
                $fileSize = format_size($item->getCompressedSize());
434
435
                $output->writeln(
436
                    sprintf(
437
                        '%s %s - %s',
438
                        $path,
439
                        $compression,
440
                        $fileSize
441
                    )
442
                );
443
            }
444
445
            if ($item->isDir()) {
446
                $this->renderContents(
447
                    $output,
448
                    new DirectoryIterator($item->getPathname()),
449
                    $depth + 1,
450
                    $maxDepth,
451
                    (false === $indent) ? $indent : $indent + 2,
452
                    $base,
453
                    $phar,
454
                    $root
455
                );
456
            }
457
        }
458
    }
459
460
    /**
461
     * @param Phar|PharData $phar
462
     */
463
    private function retrieveCompressionCount($phar): array
464
    {
465
        $count = array_fill_keys(
466
           self::ALGORITHMS,
467
            0
468
        );
469
470
        if ($phar instanceof PharData) {
471
            $count[self::ALGORITHMS[$phar->isCompressed()]] = 1;
472
473
            return $count;
474
        }
475
476
        $countFile = function (array $count, PharFileInfo $file): array {
477
            if (false === $file->isCompressed()) {
478
                ++$count['None'];
479
480
                return $count;
481
            }
482
483
            foreach (self::ALGORITHMS as $compressionAlgorithmCode => $compressionAlgorithmName) {
484
                if ($file->isCompressed($compressionAlgorithmCode)) {
485
                    ++$count[$compressionAlgorithmName];
486
487
                    return $count;
488
                }
489
            }
490
491
            return $count;
492
        };
493
494
        return array_reduce(
495
            iterator_to_array(new RecursiveIteratorIterator($phar)),
496
            $countFile,
497
            $count
498
        );
499
    }
500
}
501