Passed
Pull Request — master (#285)
by Théo
02:14
created

Configuration.php$0 ➔ retrievePlaceholder()   A

Complexity

Conditions 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 10
c 0
b 0
f 0
cc 1
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 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
use function uniqid;
78
79
/**
80
 * @private
81
 */
82
final class Configuration
83
{
84
    private const DEFAULT_ALIAS = 'test.phar';
85
    private const DEFAULT_MAIN_SCRIPT = 'index.php';
86
    private const DEFAULT_DATETIME_FORMAT = 'Y-m-d H:i:s';
87
    private const DEFAULT_REPLACEMENT_SIGIL = '@';
88
    private const DEFAULT_SHEBANG = '#!/usr/bin/env php';
89
    private const DEFAULT_BANNER = <<<'BANNER'
90
Generated by Humbug Box.
91
92
@link https://github.com/humbug/box
93
BANNER;
94
    private const FILES_SETTINGS = [
95
        'directories',
96
        'finder',
97
    ];
98
    private const PHP_SCOPER_CONFIG = 'scoper.inc.php';
99
    private const DEFAULT_SIGNING_ALGORITHM = Phar::SHA1;
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 $map;
158
    private $fileMapper;
159
    private $metadata;
160
    private $tmpOutputPath;
161
    private $outputPath;
162
    private $privateKeyPassphrase;
163
    private $privateKeyPath;
164
    private $promptForPrivateKey;
165
    private $processedReplacements;
166
    private $shebang;
167
    private $signingAlgorithm;
168
    private $stubBannerContents;
169
    private $stubBannerPath;
170
    private $stubPath;
171
    private $isInterceptFileFuncs;
172
    private $isStubGenerated;
173
    private $checkRequirements;
174
    private $warnings;
175
    private $recommendations;
176
177
    public static function create(?string $file, stdClass $raw): self
178
    {
179
        $logger = new ConfigurationLogger();
180
181
        $alias = self::retrieveAlias($raw);
182
183
        $basePath = self::retrieveBasePath($file, $raw, $logger);
184
185
        $composerFiles = self::retrieveComposerFiles($basePath);
186
187
        $mainScriptPath = self::retrieveMainScriptPath($raw, $basePath, $composerFiles[0][1], $logger);
188
        $mainScriptContents = self::retrieveMainScriptContents($mainScriptPath);
189
190
        [$tmpOutputPath, $outputPath] = self::retrieveOutputPath($raw, $basePath, $mainScriptPath, $logger);
191
192
        /** @var (string|null)[] $composerJson */
193
        $composerJson = $composerFiles[0];
194
        /** @var (string|null)[] $composerJson */
195
        $composerLock = $composerFiles[1];
196
197
        $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

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