Passed
Pull Request — master (#282)
by Théo
05:10 queued 01:50
created

Configuration::collectFiles()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 49
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 23
dl 0
loc 49
rs 9.2408
c 0
b 0
f 0
cc 5
nc 4
nop 10

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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;
16
17
use Assert\Assertion;
18
use Closure;
19
use DateTimeImmutable;
20
use DateTimeZone;
21
use Herrera\Annotations\Tokenizer;
22
use Herrera\Box\Compactor\Php as LegacyPhp;
23
use Humbug\PhpScoper\Configuration as PhpScoperConfiguration;
24
use Humbug\PhpScoper\Console\ApplicationFactory;
25
use Humbug\PhpScoper\Scoper;
26
use InvalidArgumentException;
27
use KevinGH\Box\Compactor\Php;
28
use KevinGH\Box\Compactor\PhpScoper as PhpScoperCompactor;
29
use KevinGH\Box\Composer\ComposerConfiguration;
30
use KevinGH\Box\Json\Json;
31
use KevinGH\Box\PhpScoper\SimpleScoper;
32
use Phar;
33
use RuntimeException;
34
use Seld\JsonLint\ParsingException;
35
use SplFileInfo;
36
use stdClass;
37
use function strtoupper;
38
use Symfony\Component\Finder\Finder;
39
use Symfony\Component\Process\Process;
40
use const E_USER_DEPRECATED;
41
use function array_column;
42
use function array_diff;
43
use function array_filter;
44
use function array_key_exists;
45
use function array_keys;
46
use function array_map;
47
use function array_merge;
48
use function array_unique;
49
use function constant;
50
use function defined;
51
use function dirname;
52
use function file_exists;
53
use function in_array;
54
use function intval;
55
use function is_array;
56
use function is_bool;
57
use function is_file;
58
use function is_link;
59
use function is_object;
60
use function is_readable;
61
use function is_string;
62
use function iter\map;
63
use function iter\toArray;
64
use function iter\values;
65
use function KevinGH\Box\FileSystem\canonicalize;
66
use function KevinGH\Box\FileSystem\file_contents;
67
use function KevinGH\Box\FileSystem\is_absolute_path;
68
use function KevinGH\Box\FileSystem\longest_common_base_path;
69
use function KevinGH\Box\FileSystem\make_path_absolute;
70
use function KevinGH\Box\FileSystem\make_path_relative;
71
use function preg_match;
72
use function sprintf;
73
use function substr;
74
use function trigger_error;
75
use function uniqid;
76
77
/**
78
 * @private
79
 */
80
final class Configuration
81
{
82
    private const DEFAULT_ALIAS = 'test.phar';
83
    private const DEFAULT_MAIN_SCRIPT = 'index.php';
84
    private const DEFAULT_DATETIME_FORMAT = 'Y-m-d H:i:s';
85
    private const DEFAULT_REPLACEMENT_SIGIL = '@';
86
    private const DEFAULT_SHEBANG = '#!/usr/bin/env php';
87
    private const DEFAULT_BANNER = <<<'BANNER'
88
Generated by Humbug Box.
89
90
@link https://github.com/humbug/box
91
BANNER;
92
    private const FILES_SETTINGS = [
93
        'directories',
94
        'finder',
95
    ];
96
    private const PHP_SCOPER_CONFIG = 'scoper.inc.php';
97
    private const DEFAULT_SIGNING_ALGORITHM = Phar::SHA1;
98
99
    private $file;
100
    private $fileMode;
101
    private $alias;
102
    private $basePath;
103
    private $composerJson;
104
    private $composerLock;
105
    private $files;
106
    private $binaryFiles;
107
    private $autodiscoveredFiles;
108
    private $dumpAutoload;
109
    private $excludeComposerFiles;
110
    private $compactors;
111
    private $compressionAlgorithm;
112
    private $mainScriptPath;
113
    private $mainScriptContents;
114
    private $map;
115
    private $fileMapper;
116
    private $metadata;
117
    private $tmpOutputPath;
118
    private $outputPath;
119
    private $privateKeyPassphrase;
120
    private $privateKeyPath;
121
    private $promptForPrivateKey;
122
    private $processedReplacements;
123
    private $shebang;
124
    private $signingAlgorithm;
125
    private $stubBannerContents;
126
    private $stubBannerPath;
127
    private $stubPath;
128
    private $isInterceptFileFuncs;
129
    private $isStubGenerated;
130
    private $checkRequirements;
131
    private $warnings;
132
    private $recommendations;
133
134
    public static function create(?string $file, stdClass $raw): self
135
    {
136
        $messages = [
137
            'recommendation' => [],
138
            'warning' => [],
139
        ];
140
141
        $alias = self::retrieveAlias($raw);
142
143
        $basePath = self::retrieveBasePath($file, $raw);
144
145
        $composerFiles = self::retrieveComposerFiles($basePath);
146
147
        $mainScriptPath = self::retrieveMainScriptPath($raw, $basePath, $composerFiles[0][1]);
148
        $mainScriptContents = self::retrieveMainScriptContents($mainScriptPath);
149
150
        [$tmpOutputPath, $outputPath] = self::retrieveOutputPath($raw, $basePath, $mainScriptPath);
151
152
        $composerJson = $composerFiles[0];
153
        $composerLock = $composerFiles[1];
154
155
        $devPackages = ComposerConfiguration::retrieveDevPackages($basePath, $composerJson[1], $composerLock[1]);
156
157
        /**
158
         * @var string[]
159
         * @var Closure  $blacklistFilter
160
         */
161
        [$excludedPaths, $blacklistFilter] = self::retrieveBlacklistFilter($raw, $basePath, $tmpOutputPath, $outputPath, $mainScriptPath);
162
163
        $autodiscoverFiles = self::autodiscoverFiles($file, $raw);
164
        $forceFilesAutodiscovery = self::retrieveForceFilesAutodiscovery($raw);
165
166
        $filesAggregate = self::collectFiles(
167
            $raw,
168
            $basePath,
169
            $mainScriptPath,
170
            $blacklistFilter,
171
            $excludedPaths,
172
            $devPackages,
173
            $composerFiles,
174
            $composerJson,
175
            $autodiscoverFiles,
176
            $forceFilesAutodiscovery
177
        );
178
        $binaryFilesAggregate = self::collectBinaryFiles(
179
            $raw,
180
            $basePath,
181
            $mainScriptPath,
182
            $blacklistFilter,
183
            $excludedPaths,
184
            $devPackages
185
        );
186
187
        $dumpAutoload = self::retrieveDumpAutoload($raw, null !== $composerJson[0], $messages);
188
189
        $excludeComposerFiles = self::retrieveExcludeComposerFiles($raw);
190
191
        $compactors = self::retrieveCompactors($raw, $basePath);
192
        $compressionAlgorithm = self::retrieveCompressionAlgorithm($raw);
193
194
        $fileMode = self::retrieveFileMode($raw);
195
196
        $map = self::retrieveMap($raw);
197
        $fileMapper = new MapFile($basePath, $map);
198
199
        $metadata = self::retrieveMetadata($raw);
200
201
        $signingAlgorithm = self::retrieveSigningAlgorithm($raw);
202
        $promptForPrivateKey = self::retrievePromptForPrivateKey($raw, $signingAlgorithm, $messages);
203
        $privateKeyPath = self::retrievePrivateKeyPath($raw, $basePath, $signingAlgorithm, $messages);
204
        $privateKeyPassphrase = self::retrievePrivateKeyPassphrase($raw, $privateKeyPath, $signingAlgorithm, $messages);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $privateKeyPassphrase is correct as self::retrievePrivateKey...ngAlgorithm, $messages) targeting KevinGH\Box\Configuratio...ePrivateKeyPassphrase() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
205
206
        $replacements = self::retrieveReplacements($raw, $file, $messages);
207
208
        $shebang = self::retrieveShebang($raw);
209
210
        $stubBannerContents = self::retrieveStubBannerContents($raw);
211
        $stubBannerPath = self::retrieveStubBannerPath($raw, $basePath);
212
213
        if (null !== $stubBannerPath) {
214
            $stubBannerContents = file_contents($stubBannerPath);
215
        }
216
217
        $stubBannerContents = self::normalizeStubBannerContents($stubBannerContents);
218
219
        $stubPath = self::retrieveStubPath($raw, $basePath);
220
221
        $isInterceptFileFuncs = self::retrieveIsInterceptFileFuncs($raw);
222
        $isStubGenerated = self::retrieveIsStubGenerated($raw, $stubPath);
223
224
        $checkRequirements = self::retrieveCheckRequirements(
225
            $raw,
226
            null !== $composerJson[0],
227
            null !== $composerLock[0],
228
            $messages
229
        );
230
231
        return new self(
232
            $file,
233
            $alias,
234
            $basePath,
235
            $composerJson,
236
            $composerLock,
237
            $filesAggregate,
238
            $binaryFilesAggregate,
239
            $autodiscoverFiles || $forceFilesAutodiscovery,
240
            $dumpAutoload,
241
            $excludeComposerFiles,
242
            $compactors,
243
            $compressionAlgorithm,
244
            $fileMode,
245
            $mainScriptPath,
246
            $mainScriptContents,
247
            $fileMapper,
248
            $metadata,
249
            $tmpOutputPath,
250
            $outputPath,
251
            $privateKeyPassphrase,
252
            $privateKeyPath,
253
            $promptForPrivateKey,
254
            $replacements,
255
            $shebang,
256
            $signingAlgorithm,
257
            $stubBannerContents,
258
            $stubBannerPath,
259
            $stubPath,
260
            $isInterceptFileFuncs,
261
            $isStubGenerated,
262
            $checkRequirements,
263
            $messages['warning'],
264
            $messages['recommendation']
265
        );
266
    }
267
268
    /**
269
     * @param null|string   $file
270
     * @param null|string   $alias
271
     * @param string        $basePath             Utility to private the base path used and be able to retrieve a
272
     *                                            path relative to it (the base path)
273
     * @param array         $composerJson         The first element is the path to the `composer.json` file as a
274
     *                                            string and the second element its decoded contents as an
275
     *                                            associative array.
276
     * @param array         $composerLock         The first element is the path to the `composer.lock` file as a
277
     *                                            string and the second element its decoded contents as an
278
     *                                            associative array.
279
     * @param SplFileInfo[] $files                List of files
280
     * @param SplFileInfo[] $binaryFiles          List of binary files
281
     * @param bool          $dumpAutoload         Whether or not the Composer autoloader should be dumped
282
     * @param bool          $excludeComposerFiles Whether or not the Composer files composer.json, composer.lock and
283
     *                                            installed.json should be removed from the PHAR
284
     * @param Compactor[]   $compactors           List of file contents compactors
285
     * @param null|int      $compressionAlgorithm Compression algorithm constant value. See the \Phar class constants
286
     * @param null|int      $fileMode             File mode in octal form
287
     * @param string        $mainScriptPath       The main script file path
288
     * @param string        $mainScriptContents   The processed content of the main script file
289
     * @param MapFile       $fileMapper           Utility to map the files from outside and inside the PHAR
290
     * @param mixed         $metadata             The PHAR Metadata
291
     * @param bool          $promptForPrivateKey   If the user should be prompted for the private key passphrase
292
     * @param scalar[]      $replacements         The processed list of replacement placeholders and their values
293
     * @param null|string   $shebang              The shebang line
294
     * @param int           $signingAlgorithm     The PHAR siging algorithm. See \Phar constants
295
     * @param null|string   $stubBannerContents   The stub banner comment
296
     * @param null|string   $stubBannerPath       The path to the stub banner comment file
297
     * @param null|string   $stubPath             The PHAR stub file path
298
     * @param bool          $isInterceptFileFuncs Whether or not Phar::interceptFileFuncs() should be used
299
     * @param bool          $isStubGenerated      Whether or not if the PHAR stub should be generated
300
     * @param bool          $checkRequirements    Whether the PHAR will check the application requirements before
301
     *                                            running
302
     * @param string[]      $warnings
303
     * @param string[]      $recommendations
304
     */
305
    private function __construct(
306
        ?string $file,
307
        string $alias,
308
        string $basePath,
309
        array $composerJson,
310
        array $composerLock,
311
        array $files,
312
        array $binaryFiles,
313
        bool $autodiscoveredFiles,
314
        bool $dumpAutoload,
315
        bool $excludeComposerFiles,
316
        array $compactors,
317
        ?int $compressionAlgorithm,
318
        ?int $fileMode,
319
        ?string $mainScriptPath,
320
        ?string $mainScriptContents,
321
        MapFile $fileMapper,
322
        $metadata,
323
        string $tmpOutputPath,
324
        string $outputPath,
325
        ?string $privateKeyPassphrase,
326
        ?string $privateKeyPath,
327
        bool $promptForPrivateKey,
328
        array $replacements,
329
        ?string $shebang,
330
        int $signingAlgorithm,
331
        ?string $stubBannerContents,
332
        ?string $stubBannerPath,
333
        ?string $stubPath,
334
        bool $isInterceptFileFuncs,
335
        bool $isStubGenerated,
336
        bool $checkRequirements,
337
        array $warnings,
338
        array $recommendations
339
    ) {
340
        Assertion::nullOrInArray(
341
            $compressionAlgorithm,
342
            get_phar_compression_algorithms(),
343
            sprintf(
344
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
345
                implode('", "', array_keys(get_phar_compression_algorithms()))
346
            )
347
        );
348
349
        if (null === $mainScriptPath) {
350
            Assertion::null($mainScriptContents);
351
        } else {
352
            Assertion::notNull($mainScriptContents);
353
        }
354
355
        $this->file = $file;
356
        $this->alias = $alias;
357
        $this->basePath = $basePath;
358
        $this->composerJson = $composerJson;
359
        $this->composerLock = $composerLock;
360
        $this->files = $files;
361
        $this->binaryFiles = $binaryFiles;
362
        $this->autodiscoveredFiles = $autodiscoveredFiles;
363
        $this->dumpAutoload = $dumpAutoload;
364
        $this->excludeComposerFiles = $excludeComposerFiles;
365
        $this->compactors = $compactors;
366
        $this->compressionAlgorithm = $compressionAlgorithm;
367
        $this->fileMode = $fileMode;
368
        $this->mainScriptPath = $mainScriptPath;
369
        $this->mainScriptContents = $mainScriptContents;
370
        $this->fileMapper = $fileMapper;
371
        $this->metadata = $metadata;
372
        $this->tmpOutputPath = $tmpOutputPath;
373
        $this->outputPath = $outputPath;
374
        $this->privateKeyPassphrase = $privateKeyPassphrase;
375
        $this->privateKeyPath = $privateKeyPath;
376
        $this->promptForPrivateKey = $promptForPrivateKey;
377
        $this->processedReplacements = $replacements;
378
        $this->shebang = $shebang;
379
        $this->signingAlgorithm = $signingAlgorithm;
380
        $this->stubBannerContents = $stubBannerContents;
381
        $this->stubBannerPath = $stubBannerPath;
382
        $this->stubPath = $stubPath;
383
        $this->isInterceptFileFuncs = $isInterceptFileFuncs;
384
        $this->isStubGenerated = $isStubGenerated;
385
        $this->checkRequirements = $checkRequirements;
386
        $this->warnings = $warnings;
387
        $this->recommendations = $recommendations;
388
    }
389
390
    public function getConfigurationFile(): ?string
391
    {
392
        return $this->file;
393
    }
394
395
    public function getAlias(): string
396
    {
397
        return $this->alias;
398
    }
399
400
    public function getBasePath(): string
401
    {
402
        return $this->basePath;
403
    }
404
405
    public function getComposerJson(): ?string
406
    {
407
        return $this->composerJson[0];
408
    }
409
410
    public function getDecodedComposerJsonContents(): ?array
411
    {
412
        return $this->composerJson[1];
413
    }
414
415
    public function getComposerLock(): ?string
416
    {
417
        return $this->composerLock[0];
418
    }
419
420
    public function getDecodedComposerLockContents(): ?array
421
    {
422
        return $this->composerLock[1];
423
    }
424
425
    /**
426
     * @return string[]
427
     */
428
    public function getFiles(): array
429
    {
430
        return $this->files;
431
    }
432
433
    /**
434
     * @return string[]
435
     */
436
    public function getBinaryFiles(): array
437
    {
438
        return $this->binaryFiles;
439
    }
440
441
    public function hasAutodiscoveredFiles(): bool
442
    {
443
        return $this->autodiscoveredFiles;
444
    }
445
446
    public function dumpAutoload(): bool
447
    {
448
        return $this->dumpAutoload;
449
    }
450
451
    public function excludeComposerFiles(): bool
452
    {
453
        return $this->excludeComposerFiles;
454
    }
455
456
    /**
457
     * @return Compactor[] the list of compactors
458
     */
459
    public function getCompactors(): array
460
    {
461
        return $this->compactors;
462
    }
463
464
    public function getCompressionAlgorithm(): ?int
465
    {
466
        return $this->compressionAlgorithm;
467
    }
468
469
    public function getFileMode(): ?int
470
    {
471
        return $this->fileMode;
472
    }
473
474
    public function hasMainScript(): bool
475
    {
476
        return null !== $this->mainScriptPath;
477
    }
478
479
    public function getMainScriptPath(): string
480
    {
481
        Assertion::notNull(
482
            $this->mainScriptPath,
483
            'Cannot retrieve the main script path: no main script configured.'
484
        );
485
486
        return $this->mainScriptPath;
487
    }
488
489
    public function getMainScriptContents(): string
490
    {
491
        Assertion::notNull(
492
            $this->mainScriptPath,
493
            'Cannot retrieve the main script contents: no main script configured.'
494
        );
495
496
        return $this->mainScriptContents;
497
    }
498
499
    public function checkRequirements(): bool
500
    {
501
        return $this->checkRequirements;
502
    }
503
504
    public function getTmpOutputPath(): string
505
    {
506
        return $this->tmpOutputPath;
507
    }
508
509
    public function getOutputPath(): string
510
    {
511
        return $this->outputPath;
512
    }
513
514
    public function getFileMapper(): MapFile
515
    {
516
        return $this->fileMapper;
517
    }
518
519
    /**
520
     * @return mixed
521
     */
522
    public function getMetadata()
523
    {
524
        return $this->metadata;
525
    }
526
527
    public function getPrivateKeyPassphrase(): ?string
528
    {
529
        return $this->privateKeyPassphrase;
530
    }
531
532
    public function getPrivateKeyPath(): ?string
533
    {
534
        return $this->privateKeyPath;
535
    }
536
537
    /**
538
     * @deprecated Use promptForPrivateKey() instead
539
     */
540
    public function isPrivateKeyPrompt(): bool
541
    {
542
        return $this->promptForPrivateKey;
543
    }
544
545
    public function promptForPrivateKey(): bool
546
    {
547
        return $this->promptForPrivateKey;
548
    }
549
550
    /**
551
     * @return scalar[]
552
     */
553
    public function getReplacements(): array
554
    {
555
        return $this->processedReplacements;
556
    }
557
558
    public function getShebang(): ?string
559
    {
560
        return $this->shebang;
561
    }
562
563
    public function getSigningAlgorithm(): int
564
    {
565
        return $this->signingAlgorithm;
566
    }
567
568
    public function getStubBannerContents(): ?string
569
    {
570
        return $this->stubBannerContents;
571
    }
572
573
    public function getStubBannerPath(): ?string
574
    {
575
        return $this->stubBannerPath;
576
    }
577
578
    public function getStubPath(): ?string
579
    {
580
        return $this->stubPath;
581
    }
582
583
    public function isInterceptFileFuncs(): bool
584
    {
585
        return $this->isInterceptFileFuncs;
586
    }
587
588
    public function isStubGenerated(): bool
589
    {
590
        return $this->isStubGenerated;
591
    }
592
593
    /**
594
     * @return string[]
595
     */
596
    public function getWarnings(): array
597
    {
598
        return $this->warnings;
599
    }
600
601
    /**
602
     * @return string[]
603
     */
604
    public function getRecommendations(): array
605
    {
606
        return $this->recommendations;
607
    }
608
609
    private static function retrieveAlias(stdClass $raw): string
610
    {
611
        if (false === isset($raw->alias)) {
612
            return uniqid('box-auto-generated-alias-', false).'.phar';
613
        }
614
615
        $alias = trim($raw->alias);
616
617
        Assertion::notEmpty($alias, 'A PHAR alias cannot be empty when provided.');
618
619
        return $alias;
620
    }
621
622
    private static function retrieveBasePath(?string $file, stdClass $raw): string
623
    {
624
        if (null === $file) {
625
            return getcwd();
626
        }
627
628
        if (false === isset($raw->{'base-path'})) {
629
            return realpath(dirname($file));
630
        }
631
632
        $basePath = trim($raw->{'base-path'});
633
634
        Assertion::directory(
635
            $basePath,
636
            'The base path "%s" is not a directory or does not exist.'
637
        );
638
639
        return realpath($basePath);
640
    }
641
642
    private static function autodiscoverFiles(?string $file, stdClass $raw): bool
643
    {
644
        if (null === $file) {
645
            return true;
646
        }
647
648
        // TODO: config should be casted into an array: it is easier to do and we need an array in several places now
649
        $rawConfig = (array) $raw;
650
651
        return self::FILES_SETTINGS === array_diff(self::FILES_SETTINGS, array_keys($rawConfig));
652
    }
653
654
    private static function retrieveForceFilesAutodiscovery(stdClass $raw): bool
655
    {
656
        return $raw->{'auto-discovery'} ?? false;
657
    }
658
659
    private static function retrieveBlacklistFilter(stdClass $raw, string $basePath, ?string ...$excludedPaths): array
660
    {
661
        $blacklist = self::retrieveBlacklist($raw, $basePath, ...$excludedPaths);
662
663
        $blacklistFilter = function (SplFileInfo $file) use ($blacklist): ?bool {
664
            if ($file->isLink()) {
665
                return false;
666
            }
667
668
            if (false === $file->getRealPath()) {
669
                return false;
670
            }
671
672
            if (in_array($file->getRealPath(), $blacklist, true)) {
673
                return false;
674
            }
675
676
            return null;
677
        };
678
679
        return [$blacklist, $blacklistFilter];
680
    }
681
682
    /**
683
     * @param stdClass        $raw
684
     * @param string          $basePath
685
     * @param null[]|string[] $excludedPaths
686
     *
687
     * @return string[]
688
     */
689
    private static function retrieveBlacklist(stdClass $raw, string $basePath, ?string ...$excludedPaths): array
690
    {
691
        /** @var string[] $blacklist */
692
        $blacklist = array_merge(
693
            array_filter($excludedPaths),
694
            $raw->blacklist ?? []
695
        );
696
697
        $normalizedBlacklist = [];
698
699
        foreach ($blacklist as $file) {
700
            $normalizedBlacklist[] = self::normalizePath($file, $basePath);
701
            $normalizedBlacklist[] = canonicalize(make_path_relative(trim($file), $basePath));
702
        }
703
704
        return array_unique($normalizedBlacklist);
705
    }
706
707
    /**
708
     * @param string[] $excludedPaths
709
     * @param string[] $devPackages
710
     *
711
     * @return SplFileInfo[]
712
     */
713
    private static function collectFiles(
714
        stdClass $raw,
715
        string $basePath,
716
        ?string $mainScriptPath,
717
        Closure $blacklistFilter,
718
        array $excludedPaths,
719
        array $devPackages,
720
        array $composerFiles,
721
        array $composerJson,
722
        bool $autodiscoverFiles,
723
        bool $forceFilesAutodiscovery
724
    ): array {
725
        $files = [self::retrieveFiles($raw, 'files', $basePath, $composerFiles, $mainScriptPath)];
726
727
        if ($autodiscoverFiles || $forceFilesAutodiscovery) {
728
            [$filesToAppend, $directories] = self::retrieveAllDirectoriesToInclude(
729
                $basePath,
730
                $composerJson[1],
731
                $devPackages,
732
                array_filter(
733
                    array_column($composerFiles, 0)
734
                ),
735
                $excludedPaths
736
            );
737
738
            $files[] = $filesToAppend;
739
740
            $files[] = self::retrieveAllFiles(
741
                $basePath,
742
                $directories,
743
                $mainScriptPath,
744
                $blacklistFilter,
745
                $excludedPaths,
746
                $devPackages
747
            );
748
        }
749
750
        if (false === $autodiscoverFiles) {
751
            $files[] = self::retrieveDirectories($raw, 'directories', $basePath, $blacklistFilter, $excludedPaths);
752
753
            $filesFromFinders = self::retrieveFilesFromFinders($raw, 'finder', $basePath, $blacklistFilter, $devPackages);
754
755
            foreach ($filesFromFinders as $filesFromFinder) {
756
                // Avoid an array_merge here as it can be quite expansive at this stage depending of the number of files
757
                $files[] = $filesFromFinder;
758
            }
759
        }
760
761
        return self::retrieveFilesAggregate(...$files);
762
    }
763
764
    /**
765
     * @param string[] $excludedPaths
766
     * @param string[] $devPackages
767
     *
768
     * @return SplFileInfo[]
769
     */
770
    private static function collectBinaryFiles(
771
        stdClass $raw,
772
        string $basePath,
773
        ?string $mainScriptPath,
774
        Closure $blacklistFilter,
775
        array $excludedPaths,
776
        array $devPackages
777
    ): array {
778
        $binaryFiles = self::retrieveFiles($raw, 'files-bin', $basePath, [], $mainScriptPath);
779
        $binaryDirectories = self::retrieveDirectories($raw, 'directories-bin', $basePath, $blacklistFilter, $excludedPaths);
780
        $binaryFilesFromFinders = self::retrieveFilesFromFinders($raw, 'finder-bin', $basePath, $blacklistFilter, $devPackages);
781
782
        return self::retrieveFilesAggregate($binaryFiles, $binaryDirectories, ...$binaryFilesFromFinders);
783
    }
784
785
    /**
786
     * @return SplFileInfo[]
787
     */
788
    private static function retrieveFiles(
789
        stdClass $raw,
790
        string $key,
791
        string $basePath,
792
        array $composerFiles,
793
        ?string $mainScriptPath
794
    ): array {
795
        $files = [];
796
797
        if (isset($composerFiles[0][0])) {
798
            $files[] = $composerFiles[0][0];
799
        }
800
801
        if (isset($composerFiles[1][1])) {
802
            $files[] = $composerFiles[1][0];
803
        }
804
805
        if (false === isset($raw->{$key})) {
806
            return $files;
807
        }
808
809
        $files = array_merge((array) $raw->{$key}, $files);
810
811
        Assertion::allString($files);
812
813
        $normalizePath = function (string $file) use ($basePath, $key, $mainScriptPath): ?SplFileInfo {
814
            $file = self::normalizePath($file, $basePath);
815
816
            if (is_link($file)) {
817
                // TODO: add this to baberlei/assert
818
                throw new InvalidArgumentException(
819
                    sprintf(
820
                        'Cannot add the link "%s": links are not supported.',
821
                        $file
822
                    )
823
                );
824
            }
825
826
            Assertion::file(
827
                $file,
828
                sprintf(
829
                    '"%s" must contain a list of existing files. Could not find "%%s".',
830
                    $key
831
                )
832
            );
833
834
            return $mainScriptPath === $file ? null : new SplFileInfo($file);
835
        };
836
837
        return array_filter(array_map($normalizePath, $files));
838
    }
839
840
    /**
841
     * @param string   $key           Config property name
842
     * @param string[] $excludedPaths
843
     *
844
     * @return iterable|SplFileInfo[]
845
     */
846
    private static function retrieveDirectories(
847
        stdClass $raw,
848
        string $key,
849
        string $basePath,
850
        Closure $blacklistFilter,
851
        array $excludedPaths
852
    ): iterable {
853
        $directories = self::retrieveDirectoryPaths($raw, $key, $basePath);
854
855
        if ([] !== $directories) {
856
            $finder = Finder::create()
857
                ->files()
858
                ->filter($blacklistFilter)
859
                ->ignoreVCS(true)
860
                ->in($directories)
861
            ;
862
863
            foreach ($excludedPaths as $excludedPath) {
864
                $finder->notPath($excludedPath);
865
            }
866
867
            return $finder;
868
        }
869
870
        return [];
871
    }
872
873
    /**
874
     * @param string[] $devPackages
875
     *
876
     * @return iterable[]|SplFileInfo[][]
877
     */
878
    private static function retrieveFilesFromFinders(
879
        stdClass $raw,
880
        string $key,
881
        string $basePath,
882
        Closure $blacklistFilter,
883
        array $devPackages
884
    ): array {
885
        if (isset($raw->{$key})) {
886
            return self::processFinders($raw->{$key}, $basePath, $blacklistFilter, $devPackages);
887
        }
888
889
        return [];
890
    }
891
892
    /**
893
     * @param iterable[]|SplFileInfo[][] $fileIterators
894
     *
895
     * @return SplFileInfo[]
896
     */
897
    private static function retrieveFilesAggregate(iterable ...$fileIterators): array
898
    {
899
        $files = [];
900
901
        foreach ($fileIterators as $fileIterator) {
902
            foreach ($fileIterator as $file) {
903
                $files[(string) $file] = $file;
904
            }
905
        }
906
907
        return array_values($files);
908
    }
909
910
    /**
911
     * @param string[] $devPackages
912
     *
913
     * @return Finder[]|SplFileInfo[][]
914
     */
915
    private static function processFinders(
916
        array $findersConfig,
917
        string $basePath,
918
        Closure $blacklistFilter,
919
        array $devPackages
920
    ): array {
921
        $processFinderConfig = function (stdClass $config) use ($basePath, $blacklistFilter, $devPackages) {
922
            return self::processFinder($config, $basePath, $blacklistFilter, $devPackages);
923
        };
924
925
        return array_map($processFinderConfig, $findersConfig);
926
    }
927
928
    /**
929
     * @param string[] $devPackages
930
     *
931
     * @return Finder|SplFileInfo[]
932
     */
933
    private static function processFinder(
934
        stdClass $config,
935
        string $basePath,
936
        Closure $blacklistFilter,
937
        array $devPackages
938
    ): Finder {
939
        $finder = Finder::create()
940
            ->files()
941
            ->filter($blacklistFilter)
942
            ->filter(
943
                function (SplFileInfo $fileInfo) use ($devPackages): bool {
944
                    foreach ($devPackages as $devPackage) {
945
                        if ($devPackage === longest_common_base_path([$devPackage, $fileInfo->getRealPath()])) {
946
                            // File belongs to the dev package
947
                            return false;
948
                        }
949
                    }
950
951
                    return true;
952
                }
953
            )
954
            ->ignoreVCS(true)
955
        ;
956
957
        $normalizedConfig = (function (array $config, Finder $finder): array {
958
            $normalizedConfig = [];
959
960
            foreach ($config as $method => $arguments) {
961
                $method = trim($method);
962
                $arguments = (array) $arguments;
963
964
                Assertion::methodExists(
965
                    $method,
966
                    $finder,
967
                    'The method "Finder::%s" does not exist.'
968
                );
969
970
                $normalizedConfig[$method] = $arguments;
971
            }
972
973
            krsort($normalizedConfig);
974
975
            return $normalizedConfig;
976
        })((array) $config, $finder);
977
978
        $createNormalizedDirectories = function (string $directory) use ($basePath): ?string {
979
            $directory = self::normalizePath($directory, $basePath);
980
981
            if (is_link($directory)) {
982
                // TODO: add this to baberlei/assert
983
                throw new InvalidArgumentException(
984
                    sprintf(
985
                        'Cannot append the link "%s" to the Finder: links are not supported.',
986
                        $directory
987
                    )
988
                );
989
            }
990
991
            Assertion::directory($directory);
992
993
            return $directory;
994
        };
995
996
        $normalizeFileOrDirectory = function (string &$fileOrDirectory) use ($basePath, $blacklistFilter): void {
997
            $fileOrDirectory = self::normalizePath($fileOrDirectory, $basePath);
998
999
            if (is_link($fileOrDirectory)) {
1000
                // TODO: add this to baberlei/assert
1001
                throw new InvalidArgumentException(
1002
                    sprintf(
1003
                        'Cannot append the link "%s" to the Finder: links are not supported.',
1004
                        $fileOrDirectory
1005
                    )
1006
                );
1007
            }
1008
1009
            // TODO: add this to baberlei/assert
1010
            if (false === file_exists($fileOrDirectory)) {
1011
                throw new InvalidArgumentException(
1012
                    sprintf(
1013
                        'Path "%s" was expected to be a file or directory. It may be a symlink (which are unsupported).',
1014
                        $fileOrDirectory
1015
                    )
1016
                );
1017
            }
1018
1019
            // TODO: add fileExists (as file or directory) to Assert
1020
            if (false === is_file($fileOrDirectory)) {
1021
                Assertion::directory($fileOrDirectory);
1022
            } else {
1023
                Assertion::file($fileOrDirectory);
1024
            }
1025
1026
            if (false === $blacklistFilter(new SplFileInfo($fileOrDirectory))) {
1027
                $fileOrDirectory = null;
1028
            }
1029
        };
1030
1031
        foreach ($normalizedConfig as $method => $arguments) {
1032
            if ('in' === $method) {
1033
                $normalizedConfig[$method] = $arguments = array_map($createNormalizedDirectories, $arguments);
1034
            }
1035
1036
            if ('exclude' === $method) {
1037
                $arguments = array_unique(array_map('trim', $arguments));
1038
            }
1039
1040
            if ('append' === $method) {
1041
                array_walk($arguments, $normalizeFileOrDirectory);
1042
1043
                $arguments = [array_filter($arguments)];
1044
            }
1045
1046
            foreach ($arguments as $argument) {
1047
                $finder->$method($argument);
1048
            }
1049
        }
1050
1051
        return $finder;
1052
    }
1053
1054
    /**
1055
     * @param string[] $devPackages
1056
     * @param string[] $filesToAppend
1057
     *
1058
     * @return string[][]
1059
     */
1060
    private static function retrieveAllDirectoriesToInclude(
1061
        string $basePath,
1062
        ?array $decodedJsonContents,
1063
        array $devPackages,
1064
        array $filesToAppend,
1065
        array $excludedPaths
1066
    ): array {
1067
        $toString = function ($file): string {
1068
            // @param string|SplFileInfo $file
1069
            return (string) $file;
1070
        };
1071
1072
        if (null !== $decodedJsonContents && array_key_exists('vendor-dir', $decodedJsonContents)) {
1073
            $vendorDir = self::normalizePath($decodedJsonContents['vendor-dir'], $basePath);
1074
        } else {
1075
            $vendorDir = self::normalizePath('vendor', $basePath);
1076
        }
1077
1078
        if (file_exists($vendorDir)) {
1079
            // The installed.json file is necessary for dumping the autoload correctly. Note however that it will not exists if no
1080
            // dependencies are included in the `composer.json`
1081
            $installedJsonFiles = self::normalizePath($vendorDir.'/composer/installed.json', $basePath);
1082
1083
            if (file_exists($installedJsonFiles)) {
1084
                $filesToAppend[] = $installedJsonFiles;
1085
            }
1086
1087
            $vendorPackages = toArray(values(map(
1088
                $toString,
1089
                Finder::create()
1090
                    ->in($vendorDir)
1091
                    ->directories()
1092
                    ->depth(1)
1093
                    ->ignoreUnreadableDirs()
1094
                    ->filter(
1095
                        function (SplFileInfo $fileInfo): ?bool {
1096
                            if ($fileInfo->isLink()) {
1097
                                return false;
1098
                            }
1099
1100
                            return null;
1101
                        }
1102
                    )
1103
            )));
1104
1105
            $vendorPackages = array_diff($vendorPackages, $devPackages);
1106
1107
            if (null === $decodedJsonContents || false === array_key_exists('autoload', $decodedJsonContents)) {
1108
                $files = toArray(values(map(
1109
                    $toString,
1110
                    Finder::create()
1111
                        ->in($basePath)
1112
                        ->files()
1113
                        ->depth(0)
1114
                )));
1115
1116
                $directories = toArray(values(map(
1117
                    $toString,
1118
                    Finder::create()
1119
                        ->in($basePath)
1120
                        ->notPath('vendor')
1121
                        ->directories()
1122
                        ->depth(0)
1123
                )));
1124
1125
                return [
1126
                    array_merge($files, $filesToAppend),
1127
                    array_merge($directories, $vendorPackages),
1128
                ];
1129
            }
1130
1131
            $paths = $vendorPackages;
1132
        } else {
1133
            $paths = [];
1134
        }
1135
1136
        $autoload = $decodedJsonContents['autoload'] ?? [];
1137
1138
        if (array_key_exists('psr-4', $autoload)) {
1139
            foreach ($autoload['psr-4'] as $path) {
1140
                /** @var string|string[] $path */
1141
                $composerPaths = (array) $path;
1142
1143
                foreach ($composerPaths as $composerPath) {
1144
                    $paths[] = '' !== trim($composerPath) ? $composerPath : $basePath;
1145
                }
1146
            }
1147
        }
1148
1149
        if (array_key_exists('psr-0', $autoload)) {
1150
            foreach ($autoload['psr-0'] as $path) {
1151
                /** @var string|string[] $path */
1152
                $composerPaths = (array) $path;
1153
1154
                foreach ($composerPaths as $composerPath) {
1155
                    if ('' !== trim($composerPath)) {
1156
                        $paths[] = $composerPath;
1157
                    }
1158
                }
1159
            }
1160
        }
1161
1162
        if (array_key_exists('classmap', $autoload)) {
1163
            foreach ($autoload['classmap'] as $path) {
1164
                // @var string $path
1165
                $paths[] = $path;
1166
            }
1167
        }
1168
1169
        $normalizePath = function (string $path) use ($basePath): string {
1170
            return is_absolute_path($path)
1171
                ? canonicalize($path)
1172
                : self::normalizePath(trim($path, '/ '), $basePath)
1173
            ;
1174
        };
1175
1176
        if (array_key_exists('files', $autoload)) {
1177
            foreach ($autoload['files'] as $path) {
1178
                // @var string $path
1179
                $path = $normalizePath($path);
1180
1181
                Assertion::file($path);
1182
                Assertion::false(is_link($path), 'Cannot add the link "'.$path.'": links are not supported.');
1183
1184
                $filesToAppend[] = $path;
1185
            }
1186
        }
1187
1188
        $files = $filesToAppend;
1189
        $directories = [];
1190
1191
        foreach ($paths as $path) {
1192
            $path = $normalizePath($path);
1193
1194
            Assertion::true(file_exists($path), 'File or directory "'.$path.'" was expected to exist.');
1195
            Assertion::false(is_link($path), 'Cannot add the link "'.$path.'": links are not supported.');
1196
1197
            if (is_file($path)) {
1198
                $files[] = $path;
1199
            } else {
1200
                $directories[] = $path;
1201
            }
1202
        }
1203
1204
        [$files, $directories] = [
1205
            array_unique($files),
1206
            array_unique($directories),
1207
        ];
1208
1209
        return [
1210
            array_diff($files, $excludedPaths),
1211
            array_diff($directories, $excludedPaths),
1212
        ];
1213
    }
1214
1215
    /**
1216
     * @param string[] $files
1217
     * @param string[] $directories
1218
     * @param string[] $excludedPaths
1219
     * @param string[] $devPackages
1220
     *
1221
     * @return SplFileInfo[]
1222
     */
1223
    private static function retrieveAllFiles(
1224
        string $basePath,
1225
        array $directories,
1226
        ?string $mainScriptPath,
1227
        Closure $blacklistFilter,
1228
        array $excludedPaths,
1229
        array $devPackages
1230
    ): iterable {
1231
        if ([] === $directories) {
1232
            return [];
1233
        }
1234
1235
        $relativeDevPackages = array_map(
1236
            function (string $packagePath) use ($basePath): string {
1237
                return make_path_relative($packagePath, $basePath);
1238
            },
1239
            $devPackages
1240
        );
1241
1242
        $finder = Finder::create()
1243
            ->files()
1244
            ->filter($blacklistFilter)
1245
            ->exclude($relativeDevPackages)
1246
            ->ignoreVCS(true)
1247
            ->ignoreDotFiles(true)
1248
            // Remove build files
1249
            ->notName('composer.json')
1250
            ->notName('composer.lock')
1251
            ->notName('Makefile')
1252
            ->notName('Vagrantfile')
1253
            ->notName('phpstan*.neon*')
1254
            ->notName('infection*.json*')
1255
            ->notName('humbug*.json*')
1256
            ->notName('easy-coding-standard.neon*')
1257
            ->notName('phpbench.json*')
1258
            ->notName('phpcs.xml*')
1259
            ->notName('psalm.xml*')
1260
            ->notName('scoper.inc*')
1261
            ->notName('box*.json*')
1262
            ->notName('phpdoc*.xml*')
1263
            ->notName('codecov.yml*')
1264
            ->notName('Dockerfile')
1265
            ->exclude('build')
1266
            ->exclude('dist')
1267
            ->exclude('example')
1268
            ->exclude('examples')
1269
            // Remove documentation
1270
            ->notName('*.md')
1271
            ->notName('*.rst')
1272
            ->notName('/^readme(\..*+)?$/i')
1273
            ->notName('/^upgrade(\..*+)?$/i')
1274
            ->notName('/^contributing(\..*+)?$/i')
1275
            ->notName('/^changelog(\..*+)?$/i')
1276
            ->notName('/^authors?(\..*+)?$/i')
1277
            ->notName('/^conduct(\..*+)?$/i')
1278
            ->notName('/^todo(\..*+)?$/i')
1279
            ->exclude('doc')
1280
            ->exclude('docs')
1281
            ->exclude('documentation')
1282
            // Remove backup files
1283
            ->notName('*~')
1284
            ->notName('*.back')
1285
            ->notName('*.swp')
1286
            // Remove tests
1287
            ->notName('*Test.php')
1288
            ->exclude('test')
1289
            ->exclude('Test')
1290
            ->exclude('tests')
1291
            ->exclude('Tests')
1292
            ->notName('/phpunit.*\.xml(.dist)?/')
1293
            ->notName('/behat.*\.yml(.dist)?/')
1294
            ->exclude('spec')
1295
            ->exclude('specs')
1296
            ->exclude('features')
1297
            // Remove CI config
1298
            ->exclude('travis')
1299
            ->notName('travis.yml')
1300
            ->notName('appveyor.yml')
1301
            ->notName('build.xml*')
1302
        ;
1303
1304
        if (null !== $mainScriptPath) {
1305
            $finder->notPath(make_path_relative($mainScriptPath, $basePath));
1306
        }
1307
1308
        $finder->in($directories);
1309
1310
        $excludedPaths = array_unique(
1311
            array_filter(
1312
                array_map(
1313
                    function (string $path) use ($basePath): string {
1314
                        return make_path_relative($path, $basePath);
1315
                    },
1316
                    $excludedPaths
1317
                ),
1318
                function (string $path): bool {
1319
                    return '..' !== substr($path, 0, 2);
1320
                }
1321
            )
1322
        );
1323
1324
        foreach ($excludedPaths as $excludedPath) {
1325
            $finder->notPath($excludedPath);
1326
        }
1327
1328
        return $finder;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $finder returns the type Symfony\Component\Finder\Finder which is incompatible with the documented return type SplFileInfo[].
Loading history...
1329
    }
1330
1331
    /**
1332
     * @param stdClass $raw
1333
     * @param string   $key      Config property name
1334
     * @param string   $basePath
1335
     *
1336
     * @return string[]
1337
     */
1338
    private static function retrieveDirectoryPaths(stdClass $raw, string $key, string $basePath): array
1339
    {
1340
        if (false === isset($raw->{$key})) {
1341
            return [];
1342
        }
1343
1344
        $directories = $raw->{$key};
1345
1346
        $normalizeDirectory = function (string $directory) use ($basePath, $key): string {
1347
            $directory = self::normalizePath($directory, $basePath);
1348
1349
            if (is_link($directory)) {
1350
                // TODO: add this to baberlei/assert
1351
                throw new InvalidArgumentException(
1352
                    sprintf(
1353
                        'Cannot add the link "%s": links are not supported.',
1354
                        $directory
1355
                    )
1356
                );
1357
            }
1358
1359
            Assertion::directory(
1360
                $directory,
1361
                sprintf(
1362
                    '"%s" must contain a list of existing directories. Could not find "%%s".',
1363
                    $key
1364
                )
1365
            );
1366
1367
            return $directory;
1368
        };
1369
1370
        return array_map($normalizeDirectory, $directories);
1371
    }
1372
1373
    private static function normalizePath(string $file, string $basePath): string
1374
    {
1375
        return make_path_absolute(trim($file), $basePath);
1376
    }
1377
1378
    private static function retrieveDumpAutoload(stdClass $raw, bool $composerJson, array &$messages): bool
1379
    {
1380
        $raw = (array) $raw;
1381
1382
        if (false === array_key_exists('dump-autoload', $raw)) {
1383
            return $composerJson;
1384
        }
1385
1386
        $dumpAutoload = $raw['dump-autoload'];
1387
1388
        if ($dumpAutoload) {
1389
            $messages['recommendation'][] = 'The "dump-autoload" setting has been set but is unnecessary since its '
1390
                .'value is the default value.';
1391
        }
1392
1393
        if (false === $composerJson && $dumpAutoload) {
1394
            $messages['warning'][] = 'The "dump-autoload" setting has been set but has been ignored because the '
1395
                .'composer.json file necessary for it could not be found';
1396
1397
            return false;
1398
        }
1399
1400
        return $composerJson && false !== $dumpAutoload;
1401
    }
1402
1403
    private static function retrieveExcludeComposerFiles(stdClass $raw): bool
1404
    {
1405
        return $raw->{'exclude-composer-files'} ?? true;
1406
    }
1407
1408
    /**
1409
     * @return Compactor[]
1410
     */
1411
    private static function retrieveCompactors(stdClass $raw, string $basePath): array
1412
    {
1413
        if (false === isset($raw->compactors)) {
1414
            return [];
1415
        }
1416
1417
        $compactorClasses = array_unique((array) $raw->compactors);
1418
1419
        return array_map(
1420
            function (string $class) use ($raw, $basePath): Compactor {
1421
                Assertion::classExists($class, 'The compactor class "%s" does not exist.');
1422
                Assertion::implementsInterface($class, Compactor::class, 'The class "%s" is not a compactor class.');
1423
1424
                if (Php::class === $class || LegacyPhp::class === $class) {
1425
                    return self::createPhpCompactor($raw);
1426
                }
1427
1428
                if (PhpScoperCompactor::class === $class) {
1429
                    $phpScoperConfig = self::retrievePhpScoperConfig($raw, $basePath);
1430
1431
                    $prefix = null === $phpScoperConfig->getPrefix()
1432
                        ? uniqid('_HumbugBox', false)
1433
                        : $phpScoperConfig->getPrefix()
1434
                    ;
1435
1436
                    return new PhpScoperCompactor(
1437
                        new SimpleScoper(
1438
                            (new class() extends ApplicationFactory {
1439
                                public static function createScoper(): Scoper
1440
                                {
1441
                                    return parent::createScoper();
1442
                                }
1443
                            })::createScoper(),
1444
                            $prefix,
1445
                            $phpScoperConfig->getWhitelist(),
1446
                            $phpScoperConfig->getPatchers()
1447
                        )
1448
                    );
1449
                }
1450
1451
                return new $class();
1452
            },
1453
            $compactorClasses
1454
        );
1455
    }
1456
1457
    private static function retrieveCompressionAlgorithm(stdClass $raw): ?int
1458
    {
1459
        if (false === isset($raw->compression)) {
1460
            return null;
1461
        }
1462
1463
        $knownAlgorithmNames = array_keys(get_phar_compression_algorithms());
1464
1465
        Assertion::inArray(
1466
            $raw->compression,
1467
            $knownAlgorithmNames,
1468
            sprintf(
1469
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
1470
                implode('", "', $knownAlgorithmNames)
1471
            )
1472
        );
1473
1474
        $value = get_phar_compression_algorithms()[$raw->compression];
1475
1476
        // Phar::NONE is not valid for compressFiles()
1477
        if (Phar::NONE === $value) {
1478
            return null;
1479
        }
1480
1481
        return $value;
1482
    }
1483
1484
    private static function retrieveFileMode(stdClass $raw): ?int
1485
    {
1486
        if (isset($raw->chmod)) {
1487
            return intval($raw->chmod, 8);
1488
        }
1489
1490
        return intval(0755, 8);
1491
    }
1492
1493
    private static function retrieveMainScriptPath(stdClass $raw, string $basePath, ?array $decodedJsonContents): ?string
1494
    {
1495
        if (isset($raw->main)) {
1496
            $main = $raw->main;
1497
        } else {
1498
            if (null === $decodedJsonContents
1499
                || false === array_key_exists('bin', $decodedJsonContents)
1500
                || false === $main = current((array) $decodedJsonContents['bin'])
1501
            ) {
1502
                $main = self::DEFAULT_MAIN_SCRIPT;
1503
            }
1504
        }
1505
1506
        if (is_bool($main)) {
1507
            Assertion::false(
1508
                $main,
1509
                'Cannot "enable" a main script: either disable it with `false` or give the main script file path.'
1510
            );
1511
1512
            return null;
1513
        }
1514
1515
        return self::normalizePath($main, $basePath);
1516
    }
1517
1518
    private static function retrieveMainScriptContents(?string $mainScriptPath): ?string
1519
    {
1520
        if (null === $mainScriptPath) {
1521
            return null;
1522
        }
1523
1524
        $contents = file_contents($mainScriptPath);
1525
1526
        // Remove the shebang line: the shebang line in a PHAR should be located in the stub file which is the real
1527
        // PHAR entry point file.
1528
        return preg_replace('/^#!.*\s*/', '', $contents);
1529
    }
1530
1531
    /**
1532
     * @return string|null[][]
1533
     */
1534
    private static function retrieveComposerFiles(string $basePath): array
1535
    {
1536
        $retrieveFileAndContents = function (string $file): array {
1537
            $json = new Json();
1538
1539
            if (false === file_exists($file) || false === is_file($file) || false === is_readable($file)) {
1540
                return [null, null];
1541
            }
1542
1543
            try {
1544
                $contents = $json->decodeFile($file, true);
1545
            } catch (ParsingException $exception) {
1546
                throw new InvalidArgumentException(
1547
                    sprintf(
1548
                        'Expected the file "%s" to be a valid composer.json file but an error has been found: %s',
1549
                        $file,
1550
                        $exception->getMessage()
1551
                    ),
1552
                    0,
1553
                    $exception
1554
                );
1555
            }
1556
1557
            return [$file, $contents];
1558
        };
1559
1560
        [$composerJson, $composerJsonContents] = $retrieveFileAndContents(canonicalize($basePath.'/composer.json'));
1561
        [$composerLock, $composerLockContents] = $retrieveFileAndContents(canonicalize($basePath.'/composer.lock'));
1562
1563
        return [
1564
            [$composerJson, $composerJsonContents],
1565
            [$composerLock, $composerLockContents],
1566
        ];
1567
    }
1568
1569
    /**
1570
     * @return string[][]
1571
     */
1572
    private static function retrieveMap(stdClass $raw): array
1573
    {
1574
        if (false === isset($raw->map)) {
1575
            return [];
1576
        }
1577
1578
        $map = [];
1579
1580
        foreach ((array) $raw->map as $item) {
1581
            $processed = [];
1582
1583
            foreach ($item as $match => $replace) {
1584
                $processed[canonicalize(trim($match))] = canonicalize(trim($replace));
1585
            }
1586
1587
            if (isset($processed['_empty_'])) {
1588
                $processed[''] = $processed['_empty_'];
1589
1590
                unset($processed['_empty_']);
1591
            }
1592
1593
            $map[] = $processed;
1594
        }
1595
1596
        return $map;
1597
    }
1598
1599
    /**
1600
     * @return mixed
1601
     */
1602
    private static function retrieveMetadata(stdClass $raw)
1603
    {
1604
        if (isset($raw->metadata)) {
1605
            if (is_object($raw->metadata)) {
1606
                return (array) $raw->metadata;
1607
            }
1608
1609
            return $raw->metadata;
1610
        }
1611
1612
        return null;
1613
    }
1614
1615
    /**
1616
     * @return string[] The first element is the temporary output path and the second the real one
1617
     */
1618
    private static function retrieveOutputPath(stdClass $raw, string $basePath, ?string $mainScriptPath): array
1619
    {
1620
        if (isset($raw->output)) {
1621
            $path = $raw->output;
1622
        } else {
1623
            if (null !== $mainScriptPath
1624
                && 1 === preg_match('/^(?<main>.*?)(?:\.[\p{L}\d]+)?$/', $mainScriptPath, $matches)
1625
            ) {
1626
                $path = $matches['main'].'.phar';
1627
            } else {
1628
                // Last resort, should not happen
1629
                $path = self::DEFAULT_ALIAS;
1630
            }
1631
        }
1632
1633
        $tmp = $real = self::normalizePath($path, $basePath);
1634
1635
        if ('.phar' !== substr($real, -5)) {
1636
            $tmp .= '.phar';
1637
        }
1638
1639
        return [$tmp, $real];
1640
    }
1641
1642
    private static function retrievePrivateKeyPath(
1643
        stdClass $raw,
1644
        string $basePath,
1645
        int $signingAlgorithm,
1646
        array &$messages
1647
    ): ?string
1648
    {
1649
        $raw = (array) $raw;
1650
1651
        if (array_key_exists('key', $raw) && Phar::OPENSSL !== $signingAlgorithm) {
1652
            if (null === $raw['key']) {
1653
                $messages['recommendation'][] = 'The setting "key" has been set but is unnecessary since the signing algorithm is not "OPENSSL".';
1654
            } else {
1655
                $messages['warning'][] = 'The setting "key" has been set but is ignored since the signing algorithm is not "OPENSSL".';
1656
            }
1657
1658
            return null;
1659
        }
1660
1661
        if (!isset($raw['key'])) {
1662
            Assertion::true(
1663
                Phar::OPENSSL !== $signingAlgorithm,
1664
                'Expected to have a private key for OpenSSL signing but none have been provided.'
1665
            );
1666
1667
            return null;
1668
        }
1669
1670
        $path = self::normalizePath($raw['key'], $basePath);
1671
1672
        Assertion::file($path);
1673
1674
        return $path;
1675
    }
1676
1677
    private static function retrievePrivateKeyPassphrase(
1678
        stdClass $raw,
1679
        ?string $privateKeyPath,
1680
        int $algorithm,
1681
        array &$messages
1682
    ): ?string
1683
    {
1684
        $raw = (array) $raw;
1685
        
1686
        if (array_key_exists('key-pass', $raw) && Phar::OPENSSL !== $algorithm) {
1687
            if (false === $raw['key-pass'] || null === $raw['key-pass']) {
1688
                $messages['recommendation'][] = 'The setting "key-pass" has been set but is unnecessary since the signing algorithm is not "OPENSSL".';
1689
            } else {
1690
                $messages['warning'][] = 'The setting "key-pass" has been set but ignored the signing algorithm is not "OPENSSL".';
1691
            }
1692
1693
            return null;
1694
        }
1695
1696
        return null;
1697
//        if (null === $privateKeyPath) {
1698
//            $messages['warning'][] = 'The setting "key-pass" has been set but not "key".';
1699
//
1700
//            return null;
1701
//        }
1702
//
1703
//        if (isset($raw['key-pass'])
1704
//            && is_string($raw['key-pass'])
1705
//        ) {
1706
//            if (null === $privateKeyPath) {
1707
//                $messages['warning'][] = 'The setting "key-pass" has been set but not "key".';
1708
//            }
1709
//
1710
//            return $raw['key-pass'];
1711
//        }
1712
//
1713
//        return null;
1714
    }
1715
1716
    /**
1717
     * @return scalar[]
1718
     */
1719
    private static function retrieveReplacements(stdClass $raw, ?string $file, array &$messages): array
1720
    {
1721
        if (null === $file) {
1722
            return [];
1723
        }
1724
1725
        $replacements = isset($raw->replacements) ? (array) $raw->replacements : [];
1726
1727
        if (null !== ($git = self::retrievePrettyGitPlaceholder($raw))) {
1728
            $replacements[$git] = self::retrievePrettyGitTag($file);
1729
        }
1730
1731
        if (null !== ($git = self::retrieveGitHashPlaceholder($raw))) {
1732
            $replacements[$git] = self::retrieveGitHash($file);
1733
        }
1734
1735
        if (null !== ($git = self::retrieveGitShortHashPlaceholder($raw))) {
1736
            $replacements[$git] = self::retrieveGitHash($file, true);
1737
        }
1738
1739
        if (null !== ($git = self::retrieveGitTagPlaceholder($raw))) {
1740
            $replacements[$git] = self::retrieveGitTag($file);
1741
        }
1742
1743
        if (null !== ($git = self::retrieveGitVersionPlaceholder($raw))) {
1744
            $replacements[$git] = self::retrieveGitVersion($file);
1745
        }
1746
1747
        $datetimeFormat = self::retrieveDatetimeFormat($raw, $messages);
1748
1749
        if (null !== ($date = self::retrieveDatetimeNowPlaceHolder($raw))) {
1750
            $replacements[$date] = self::retrieveDatetimeNow(
1751
                $datetimeFormat
1752
            );
1753
        }
1754
1755
        $sigil = self::retrieveReplacementSigil($raw);
1756
1757
        foreach ($replacements as $key => $value) {
1758
            unset($replacements[$key]);
1759
            $replacements[$sigil.$key.$sigil] = $value;
1760
        }
1761
1762
        return $replacements;
1763
    }
1764
1765
    private static function retrievePrettyGitPlaceholder(stdClass $raw): ?string
1766
    {
1767
        return isset($raw->{'git'}) ? $raw->{'git'} : null;
1768
    }
1769
1770
    private static function retrieveGitHashPlaceholder(stdClass $raw): ?string
1771
    {
1772
        return isset($raw->{'git-commit'}) ? $raw->{'git-commit'} : null;
1773
    }
1774
1775
    /**
1776
     * @param string $file
1777
     * @param bool   $short Use the short version
1778
     *
1779
     * @return string the commit hash
1780
     */
1781
    private static function retrieveGitHash(string $file, bool $short = false): string
1782
    {
1783
        return self::runGitCommand(
1784
            sprintf(
1785
                'git log --pretty="%s" -n1 HEAD',
1786
                $short ? '%h' : '%H'
1787
            ),
1788
            $file
1789
        );
1790
    }
1791
1792
    private static function retrieveGitShortHashPlaceholder(stdClass $raw): ?string
1793
    {
1794
        return isset($raw->{'git-commit-short'}) ? $raw->{'git-commit-short'} : null;
1795
    }
1796
1797
    private static function retrieveGitTagPlaceholder(stdClass $raw): ?string
1798
    {
1799
        return isset($raw->{'git-tag'}) ? $raw->{'git-tag'} : null;
1800
    }
1801
1802
    private static function retrieveGitTag(string $file): string
1803
    {
1804
        return self::runGitCommand('git describe --tags HEAD', $file);
1805
    }
1806
1807
    private static function retrievePrettyGitTag(string $file): string
1808
    {
1809
        $version = self::retrieveGitTag($file);
1810
1811
        if (preg_match('/^(?<tag>.+)-\d+-g(?<hash>[a-f0-9]{7})$/', $version, $matches)) {
1812
            return sprintf('%s@%s', $matches['tag'], $matches['hash']);
1813
        }
1814
1815
        return $version;
1816
    }
1817
1818
    private static function retrieveGitVersionPlaceholder(stdClass $raw): ?string
1819
    {
1820
        return isset($raw->{'git-version'}) ? $raw->{'git-version'} : null;
1821
    }
1822
1823
    private static function retrieveGitVersion(string $file): ?string
1824
    {
1825
        try {
1826
            return self::retrieveGitTag($file);
1827
        } catch (RuntimeException $exception) {
1828
            try {
1829
                return self::retrieveGitHash($file, true);
1830
            } catch (RuntimeException $exception) {
1831
                throw new RuntimeException(
1832
                    sprintf(
1833
                        'The tag or commit hash could not be retrieved from "%s": %s',
1834
                        dirname($file),
1835
                        $exception->getMessage()
1836
                    ),
1837
                    0,
1838
                    $exception
1839
                );
1840
            }
1841
        }
1842
    }
1843
1844
    private static function retrieveDatetimeNowPlaceHolder(stdClass $raw): ?string
1845
    {
1846
        return isset($raw->{'datetime'}) ? $raw->{'datetime'} : null;
1847
    }
1848
1849
    private static function retrieveDatetimeNow(string $format): string
1850
    {
1851
        $now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
1852
1853
        return $now->format($format);
1854
    }
1855
1856
    private static function retrieveDatetimeFormat(stdClass $raw, array &$messages): string
1857
    {
1858
        if (isset($raw->{'datetime-format'})) {
1859
            $format = $raw->{'datetime-format'};
1860
        } elseif (isset($raw->{'datetime_format'})) {
1861
            @trigger_error(
1862
                'The setting "datetime_format" is deprecated, use "datetime-format" instead.',
1863
                E_USER_DEPRECATED
1864
            );
1865
            $messages['warning'][] = 'The setting "datetime_format" is deprecated, use "datetime-format" instead.';
1866
1867
            $format = $raw->{'datetime_format'};
1868
        }
1869
1870
        if (isset($format)) {
1871
            $formattedDate = (new DateTimeImmutable())->format($format);
1872
1873
            Assertion::false(
1874
                false === $formattedDate || $formattedDate === $format,
1875
                sprintf(
1876
                    'Expected the datetime format to be a valid format: "%s" is not',
1877
                    $format
1878
                )
1879
            );
1880
1881
            return $format;
1882
        }
1883
1884
        return self::DEFAULT_DATETIME_FORMAT;
1885
    }
1886
1887
    private static function retrieveReplacementSigil(stdClass $raw): string
1888
    {
1889
        return isset($raw->{'replacement-sigil'}) ? $raw->{'replacement-sigil'} : self::DEFAULT_REPLACEMENT_SIGIL;
1890
    }
1891
1892
    private static function retrieveShebang(stdClass $raw): ?string
1893
    {
1894
        if (false === array_key_exists('shebang', (array) $raw)) {
1895
            return self::DEFAULT_SHEBANG;
1896
        }
1897
1898
        $shebang = $raw->shebang;
1899
1900
        if (false === $shebang) {
1901
            return null;
1902
        }
1903
1904
        if (null === $shebang) {
1905
            $shebang = self::DEFAULT_SHEBANG;
1906
        }
1907
1908
        Assertion::string($shebang, 'Expected shebang to be either a string, false or null, found true');
1909
1910
        $shebang = trim($shebang);
1911
1912
        Assertion::notEmpty($shebang, 'The shebang should not be empty.');
1913
        Assertion::true(
1914
            '#!' === substr($shebang, 0, 2),
1915
            sprintf(
1916
                'The shebang line must start with "#!". Got "%s" instead',
1917
                $shebang
1918
            )
1919
        );
1920
1921
        return $shebang;
1922
    }
1923
1924
    private static function retrieveSigningAlgorithm(stdClass $raw): int
1925
    {
1926
        if (false === isset($raw->algorithm)) {
1927
            return self::DEFAULT_SIGNING_ALGORITHM;
1928
        }
1929
1930
        $algorithm = strtoupper($raw->algorithm);
1931
1932
        Assertion::inArray($algorithm, array_keys(get_phar_signing_algorithms()));
1933
1934
        Assertion::true(
1935
            defined('Phar::'. $algorithm),
1936
            sprintf(
1937
                'The signing algorithm "%s" is not supported by your current PHAR version.',
1938
                $algorithm
1939
            )
1940
        );
1941
1942
        return constant('Phar::'. $algorithm);
1943
    }
1944
1945
    private static function retrieveStubBannerContents(stdClass $raw): ?string
1946
    {
1947
        if (false === array_key_exists('banner', (array) $raw) || null === $raw->banner) {
1948
            return self::DEFAULT_BANNER;
1949
        }
1950
1951
        $banner = $raw->banner;
1952
1953
        if (false === $banner) {
1954
            return null;
1955
        }
1956
1957
        Assertion::true(is_string($banner) || is_array($banner), 'The banner cannot accept true as a value');
1958
1959
        if (is_array($banner)) {
1960
            $banner = implode("\n", $banner);
1961
        }
1962
1963
        return $banner;
1964
    }
1965
1966
    private static function retrieveStubBannerPath(stdClass $raw, string $basePath): ?string
1967
    {
1968
        if (false === isset($raw->{'banner-file'})) {
1969
            return null;
1970
        }
1971
1972
        $bannerFile = make_path_absolute($raw->{'banner-file'}, $basePath);
1973
1974
        Assertion::file($bannerFile);
1975
1976
        return $bannerFile;
1977
    }
1978
1979
    private static function normalizeStubBannerContents(?string $contents): ?string
1980
    {
1981
        if (null === $contents) {
1982
            return null;
1983
        }
1984
1985
        $banner = explode("\n", $contents);
1986
        $banner = array_map('trim', $banner);
1987
1988
        return implode("\n", $banner);
1989
    }
1990
1991
    private static function retrieveStubPath(stdClass $raw, string $basePath): ?string
1992
    {
1993
        if (isset($raw->stub) && is_string($raw->stub)) {
1994
            $stubPath = make_path_absolute($raw->stub, $basePath);
1995
1996
            Assertion::file($stubPath);
1997
1998
            return $stubPath;
1999
        }
2000
2001
        return null;
2002
    }
2003
2004
    private static function retrieveIsInterceptFileFuncs(stdClass $raw): bool
2005
    {
2006
        if (isset($raw->intercept)) {
2007
            return $raw->intercept;
2008
        }
2009
2010
        return false;
2011
    }
2012
2013
    private static function retrievePromptForPrivateKey(stdClass $raw, int $signingAlgorithm, array &$messages): bool
2014
    {
2015
        if (isset($raw->{'key-pass'}) && true === $raw->{'key-pass'}) {
2016
            if (Phar::OPENSSL !== $signingAlgorithm) {
2017
                $messages['warning'][] = 'A prompt for password for the private key has been requested but ignored '
2018
                    .'since the signing algorithm used is not "OPENSSL.';
2019
2020
                return false;
2021
            }
2022
2023
            return true;
2024
        }
2025
2026
        return false;
2027
    }
2028
2029
    private static function retrieveIsStubGenerated(stdClass $raw, ?string $stubPath): bool
2030
    {
2031
        return null === $stubPath && (false === isset($raw->stub) || false !== $raw->stub);
2032
    }
2033
2034
    private static function retrieveCheckRequirements(
2035
        stdClass $raw,
2036
        bool $hasComposerJson,
2037
        bool $hasComposerLock,
2038
        array &$messages
2039
    ): bool
2040
    {
2041
        $raw = (array) $raw;
2042
2043
        if (false === array_key_exists('check-requirements', $raw)) {
2044
            return $hasComposerJson || $hasComposerLock;
2045
        }
2046
2047
        /** @var bool|null $checkRequirements */
2048
        $checkRequirements = $raw['check-requirements'];
2049
2050
        if ($checkRequirements) {
2051
            $messages['recommendation'][] = 'The "check-requirements" setting has been set but is unnecessary since '
2052
                .'its value is the default value';
2053
        }
2054
2055
        if (null === $checkRequirements) {
2056
            $checkRequirements = true;
2057
        }
2058
2059
        if ($checkRequirements && false === $hasComposerJson && false === $hasComposerLock) {
2060
            $messages['warning'][] = 'The requirement checker could not be used because the composer.json and '
2061
                .'composer.lock file could not be found.';
2062
2063
            return false;
2064
        }
2065
2066
        return $checkRequirements;
2067
    }
2068
2069
    private static function retrievePhpScoperConfig(stdClass $raw, string $basePath): PhpScoperConfiguration
2070
    {
2071
        if (!isset($raw->{'php-scoper'})) {
2072
            $configFilePath = make_path_absolute(self::PHP_SCOPER_CONFIG, $basePath);
2073
2074
            return file_exists($configFilePath)
2075
                ? PhpScoperConfiguration::load($configFilePath)
2076
                : PhpScoperConfiguration::load()
2077
             ;
2078
        }
2079
2080
        $configFile = $raw->{'php-scoper'};
2081
2082
        Assertion::string($configFile);
2083
2084
        $configFilePath = make_path_absolute($configFile, $basePath);
2085
2086
        Assertion::file($configFilePath);
2087
        Assertion::readable($configFilePath);
2088
2089
        return PhpScoperConfiguration::load($configFilePath);
2090
    }
2091
2092
    /**
2093
     * Runs a Git command on the repository.
2094
     *
2095
     * @param string $command the command
2096
     *
2097
     * @return string the trimmed output from the command
2098
     */
2099
    private static function runGitCommand(string $command, string $file): string
2100
    {
2101
        $path = dirname($file);
2102
2103
        $process = new Process($command, $path);
2104
2105
        if (0 === $process->run()) {
2106
            return trim($process->getOutput());
2107
        }
2108
2109
        throw new RuntimeException(
2110
            sprintf(
2111
                'The tag or commit hash could not be retrieved from "%s": %s',
2112
                $path,
2113
                $process->getErrorOutput()
2114
            )
2115
        );
2116
    }
2117
2118
    private static function createPhpCompactor(stdClass $raw): Compactor
2119
    {
2120
        // TODO: false === not set; check & add test/doc
2121
        $tokenizer = new Tokenizer();
2122
2123
        if (false === empty($raw->annotations) && isset($raw->annotations->ignore)) {
2124
            $tokenizer->ignore(
2125
                (array) $raw->annotations->ignore
2126
            );
2127
        }
2128
2129
        return new Php($tokenizer);
2130
    }
2131
}
2132