Passed
Push — master ( f0b518...ee2495 )
by Théo
04:08 queued 02:07
created

Configuration::getPrivateKeyPath()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 0
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 as PhpCompactor;
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 Symfony\Component\Finder\Finder;
38
use Symfony\Component\Process\Process;
39
use const E_USER_DEPRECATED;
40
use function array_column;
41
use function array_diff;
42
use function array_filter;
43
use function array_key_exists;
44
use function array_keys;
45
use function array_map;
46
use function array_merge;
47
use function array_unique;
48
use function constant;
49
use function defined;
50
use function dirname;
51
use function file_exists;
52
use function in_array;
53
use function intval;
54
use function is_array;
55
use function is_bool;
56
use function is_file;
57
use function is_link;
58
use function is_object;
59
use function is_readable;
60
use function is_string;
61
use function iter\map;
62
use function iter\toArray;
63
use function iter\values;
64
use function KevinGH\Box\FileSystem\canonicalize;
65
use function KevinGH\Box\FileSystem\file_contents;
66
use function KevinGH\Box\FileSystem\is_absolute_path;
67
use function KevinGH\Box\FileSystem\longest_common_base_path;
68
use function KevinGH\Box\FileSystem\make_path_absolute;
69
use function KevinGH\Box\FileSystem\make_path_relative;
70
use function preg_match;
71
use function property_exists;
72
use function realpath;
73
use function sprintf;
74
use function strtoupper;
75
use function substr;
76
use function trigger_error;
77
78
/**
79
 * @private
80
 */
81
final class Configuration
82
{
83
    private const DEFAULT_OUTPUT_FALLBACK = 'test.phar';
84
    private const DEFAULT_MAIN_SCRIPT = 'index.php';
85
    private const DEFAULT_DATETIME_FORMAT = 'Y-m-d H:i:s';
86
    private const DEFAULT_REPLACEMENT_SIGIL = '@';
87
    private const DEFAULT_SHEBANG = '#!/usr/bin/env php';
88
    private const DEFAULT_BANNER = <<<'BANNER'
89
Generated by Humbug Box.
90
91
@link https://github.com/humbug/box
92
BANNER;
93
    private const FILES_SETTINGS = [
94
        'directories',
95
        'finder',
96
    ];
97
    private const PHP_SCOPER_CONFIG = 'scoper.inc.php';
98
    private const DEFAULT_SIGNING_ALGORITHM = Phar::SHA1;
99
    private const DEFAULT_ALIAS_PREFIX = 'box-auto-generated-alias-';
100
101
    private const ALGORITHM_KEY = 'algorithm';
102
    private const ALIAS_KEY = 'alias';
103
    private const ANNOTATIONS_KEY = 'annotations';
104
    private const AUTO_DISCOVERY_KEY = 'force-autodiscovery';
105
    private const BANNER_KEY = 'banner';
106
    private const BANNER_FILE_KEY = 'banner-file';
107
    private const BASE_PATH_KEY = 'base-path';
108
    private const BLACKLIST_KEY = 'blacklist';
109
    private const CHECK_REQUIREMENTS_KEY = 'check-requirements';
110
    private const CHMOD_KEY = 'chmod';
111
    private const COMPACTORS_KEY = 'compactors';
112
    private const COMPRESSION_KEY = 'compression';
113
    private const DATETIME_KEY = 'datetime';
114
    private const DATETIME_FORMAT_KEY = 'datetime-format';
115
    private const DATETIME_FORMAT_DEPRECATED_KEY = 'datetime_format';
116
    private const DIRECTORIES_KEY = 'directories';
117
    private const DIRECTORIES_BIN_KEY = 'directories-bin';
118
    private const DUMP_AUTOLOAD_KEY = 'dump-autoload';
119
    private const EXCLUDE_COMPOSER_FILES_KEY = 'exclude-composer-files';
120
    private const FILES_KEY = 'files';
121
    private const FILES_BIN_KEY = 'files-bin';
122
    private const FINDER_KEY = 'finder';
123
    private const FINDER_BIN_KEY = 'finder-bin';
124
    private const GIT_KEY = 'git';
125
    private const GIT_COMMIT_KEY = 'git-commit';
126
    private const GIT_COMMIT_SHORT_KEY = 'git-commit-short';
127
    private const GIT_TAG_KEY = 'git-tag';
128
    private const GIT_VERSION_KEY = 'git-version';
129
    private const INTERCEPT_KEY = 'intercept';
130
    private const KEY_KEY = 'key';
131
    private const KEY_PASS_KEY = 'key-pass';
132
    private const MAIN_KEY = 'main';
133
    private const MAP_KEY = 'map';
134
    private const METADATA_KEY = 'metadata';
135
    private const OUTPUT_KEY = 'output';
136
    private const PHP_SCOPER_KEY = 'php-scoper';
137
    private const REPLACEMENT_SIGIL_KEY = 'replacement-sigil';
138
    private const REPLACEMENTS_KEY = 'replacements';
139
    private const SHEBANG_KEY = 'shebang';
140
    private const STUB_KEY = 'stub';
141
142
    private $file;
143
    private $fileMode;
144
    private $alias;
145
    private $basePath;
146
    private $composerJson;
147
    private $composerLock;
148
    private $files;
149
    private $binaryFiles;
150
    private $autodiscoveredFiles;
151
    private $dumpAutoload;
152
    private $excludeComposerFiles;
153
    private $compactors;
154
    private $compressionAlgorithm;
155
    private $mainScriptPath;
156
    private $mainScriptContents;
157
    private $fileMapper;
158
    private $metadata;
159
    private $tmpOutputPath;
160
    private $outputPath;
161
    private $privateKeyPassphrase;
162
    private $privateKeyPath;
163
    private $promptForPrivateKey;
164
    private $processedReplacements;
165
    private $shebang;
166
    private $signingAlgorithm;
167
    private $stubBannerContents;
168
    private $stubBannerPath;
169
    private $stubPath;
170
    private $isInterceptFileFuncs;
171
    private $isStubGenerated;
172
    private $checkRequirements;
173
    private $warnings;
174
    private $recommendations;
175
176
    public static function create(?string $file, stdClass $raw): self
177
    {
178
        $logger = new ConfigurationLogger();
179
180
        $basePath = self::retrieveBasePath($file, $raw, $logger);
181
182
        $composerFiles = self::retrieveComposerFiles($basePath);
183
184
        $mainScriptPath = self::retrieveMainScriptPath($raw, $basePath, $composerFiles[0][1], $logger);
185
        $mainScriptContents = self::retrieveMainScriptContents($mainScriptPath);
186
187
        [$tmpOutputPath, $outputPath] = self::retrieveOutputPath($raw, $basePath, $mainScriptPath, $logger);
188
189
        /** @var (string|null)[] $composerJson */
190
        $composerJson = $composerFiles[0];
191
        /** @var (string|null)[] $composerJson */
192
        $composerLock = $composerFiles[1];
193
194
        $stubPath = self::retrieveStubPath($raw, $basePath, $logger);
195
        $isStubGenerated = self::retrieveIsStubGenerated($raw, $stubPath, $logger);
196
197
        $alias = self::retrieveAlias($raw, null !== $stubPath, $logger);
198
199
        $shebang = self::retrieveShebang($raw, $isStubGenerated, $logger);
200
201
        $stubBannerContents = self::retrieveStubBannerContents($raw, $isStubGenerated, $logger);
202
        $stubBannerPath = self::retrieveStubBannerPath($raw, $basePath, $isStubGenerated, $logger);
203
204
        if (null !== $stubBannerPath) {
205
            $stubBannerContents = file_contents($stubBannerPath);
206
        }
207
208
        $stubBannerContents = self::normalizeStubBannerContents($stubBannerContents);
209
210
        if (null !== $stubBannerPath && self::DEFAULT_BANNER === $stubBannerContents) {
211
            self::addRecommendationForDefaultValue($logger, self::BANNER_FILE_KEY);
212
        }
213
214
        $isInterceptsFileFuncs = self::retrieveInterceptsFileFuncs($raw, $isStubGenerated, $logger);
215
216
        $checkRequirements = self::retrieveCheckRequirements(
217
            $raw,
218
            null !== $composerJson[0],
219
            null !== $composerLock[0],
220
            false === $isStubGenerated && null === $stubPath,
221
            $logger
222
        );
223
224
        $devPackages = ComposerConfiguration::retrieveDevPackages($basePath, $composerJson[1], $composerLock[1]);
0 ignored issues
show
Bug introduced by
It seems like $composerJson[1] can also be of type string; however, parameter $composerJsonDecodedContents of KevinGH\Box\Composer\Com...::retrieveDevPackages() does only seem to accept null|array, maybe add an additional type check? ( Ignorable by Annotation )

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

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