Passed
Pull Request — master (#302)
by Théo
02:24
created

Configuration   A

Complexity

Total Complexity 11

Size/Duplication

Total Lines 2408
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 11
eloc 1096
dl 0
loc 2408
rs 9.216
c 0
b 0
f 0

105 Methods

Rating   Name   Duplication   Size   Complexity  
B retrieveFiles() 0 55 6
A collectFiles() 0 64 5
A collectBinaryFiles() 0 30 1
A retrieveDirectories() 0 26 3
A getWarnings() 0 3 1
A getFileMapper() 0 3 1
A getStubBannerContents() 0 3 1
A getConfigurationFile() 0 3 1
A getTmpOutputPath() 0 3 1
A getShebang() 0 3 1
A checkRequirements() 0 3 1
A getBinaryFiles() 0 3 1
A retrieveForceFilesAutodiscovery() 0 5 1
A getComposerJson() 0 3 1
A normalizePath() 0 3 1
A promptForPrivateKey() 0 3 1
A retrieveDumpAutoload() 0 20 5
A excludeComposerFiles() 0 3 1
A retrieveFilesFromFinders() 0 17 2
A getStubBannerPath() 0 3 1
A dumpAutoload() 0 3 1
A retrieveBlacklistFilter() 0 25 4
A isInterceptFileFuncs() 0 3 1
A getFileMode() 0 3 1
A getRecommendations() 0 3 1
A retrieveAlias() 0 22 3
A getMainScriptPath() 0 8 1
A retrieveBasePath() 0 25 4
A getAlias() 0 3 1
A getReplacements() 0 3 1
B retrieveAllFiles() 0 106 4
A getStubPath() 0 3 1
A getBasePath() 0 3 1
A retrieveBlacklist() 0 22 2
A retrieveExcludeComposerFiles() 0 5 1
A getComposerLock() 0 3 1
A isStubGenerated() 0 3 1
A retrieveFilesAggregate() 0 11 3
A getSigningAlgorithm() 0 3 1
A getCompressionAlgorithm() 0 3 1
A getPrivateKeyPassphrase() 0 3 1
F retrieveAllDirectoriesToInclude() 0 152 23
A getFiles() 0 3 1
A hasMainScript() 0 3 1
C processFinder() 0 112 11
A getMetadata() 0 3 1
A getOutputPath() 0 3 1
A processFinders() 0 11 1
A retrieveDirectoryPaths() 0 37 2
A __construct() 0 83 2
A autodiscoverFiles() 0 9 2
A getPrivateKeyPath() 0 3 1
A isPrivateKeyPrompt() 0 3 1
A getDecodedComposerJsonContents() 0 3 1
A hasAutodiscoveredFiles() 0 3 1
A getDecodedComposerLockContents() 0 3 1
B create() 0 144 6
A getMainScriptContents() 0 8 1
A getCompactors() 0 3 1
A retrieveDatetimeFormat() 0 34 5
A retrieveMainScriptContents() 0 13 2
A retrievePlaceholder() 0 5 1
A retrieveMetadata() 0 11 3
A retrieveGitTag() 0 3 1
A retrieveGitHashPlaceholder() 0 3 1
A retrieveInterceptsFileFuncs() 0 23 4
A retrieveDatetimeNow() 0 5 1
B retrieveCompactors() 0 50 9
B retrieveMainScriptPath() 0 43 9
A createPhpCompactor() 0 11 3
B retrieveStubBannerContents() 0 40 7
A hp$0 ➔ createScoper() 0 3 1
A retrievePromptForPrivateKey() 0 19 4
B retrieveOutputPath() 0 34 7
A retrieveGitTagPlaceholder() 0 3 1
addRecommendationForDefaultValue() 0 6 ?
A retrieveReplacementSigil() 0 3 1
A retrievePrivateKeyPassphrase() 0 36 6
A retrieveGitHash() 0 8 2
A retrieveMap() 0 28 5
A retrieveStubBannerPath() 0 26 3
D retrieveReplacements() 0 56 11
A retrievePhpScoperConfig() 0 23 3
B retrieveCheckRequirements() 0 36 8
A retrieveIsStubGenerated() 0 5 3
A retrievePrettyGitTag() 0 9 2
A retrieveSigningAlgorithm() 0 29 5
A retrieveShebang() 0 48 5
A hp$0 ➔ addRecommendationForDefaultValue() 0 6 1
A runGitCommand() 0 15 2
checkIfDefaultValue() 0 20 ?
createPhpScoperCompactor() 0 22 ?
A normalizeStubBannerContents() 0 10 2
A retrievePrettyGitPlaceholder() 0 3 1
A retrieveGitVersionPlaceholder() 0 3 1
A retrievePrivateKeyPath() 0 34 5
A retrieveGitVersion() 0 16 3
A retrieveCompressionAlgorithm() 0 27 3
A hp$0 ➔ createPhpScoperCompactor() 0 22 2
A retrieveFileMode() 0 19 5
A retrieveComposerFiles() 0 32 5
A retrieveStubPath() 0 13 3
A retrieveGitShortHashPlaceholder() 0 3 1
A retrieveDatetimeNowPlaceHolder() 0 3 1
B hp$0 ➔ checkIfDefaultValue() 0 20 7
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 T';
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
                    return self::createPhpScoperCompactor($raw, $basePath, $logger);
1558
                }
1559
1560
                return new $class();
1561
            },
1562
            $compactorClasses
1563
        );
1564
1565
        $scoperCompactor = false;
1566
1567
        foreach ($compators as $compactor) {
1568
            if ($compactor instanceof PhpScoperCompactor) {
1569
                $scoperCompactor = true;
1570
            }
1571
1572
            if ($compactor instanceof PhpCompactor) {
1573
                if (true === $scoperCompactor) {
1574
                    $logger->addRecommendation(
1575
                        sprintf(
1576
                            'The PHP compactor has been registered after the PhpScoper compactor. It is '
1577
                            .'recommended to register the PHP compactor before for a clearer code and faster processing.'
1578
                        )
1579
                    );
1580
                }
1581
1582
                break;
1583
            }
1584
        }
1585
1586
        return $compators;
1587
    }
1588
1589
    private static function retrieveCompressionAlgorithm(stdClass $raw, ConfigurationLogger $logger): ?int
1590
    {
1591
        self::checkIfDefaultValue($logger, $raw, self::COMPRESSION_KEY, 'NONE');
1592
1593
        if (false === isset($raw->{self::COMPRESSION_KEY})) {
1594
            return null;
1595
        }
1596
1597
        $knownAlgorithmNames = array_keys(get_phar_compression_algorithms());
1598
1599
        Assertion::inArray(
1600
            $raw->{self::COMPRESSION_KEY},
1601
            $knownAlgorithmNames,
1602
            sprintf(
1603
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
1604
                implode('", "', $knownAlgorithmNames)
1605
            )
1606
        );
1607
1608
        $value = get_phar_compression_algorithms()[$raw->{self::COMPRESSION_KEY}];
1609
1610
        // Phar::NONE is not valid for compressFiles()
1611
        if (Phar::NONE === $value) {
1612
            return null;
1613
        }
1614
1615
        return $value;
1616
    }
1617
1618
    private static function retrieveFileMode(stdClass $raw, ConfigurationLogger $logger): ?int
1619
    {
1620
        if (property_exists($raw, self::CHMOD_KEY) && null === $raw->{self::CHMOD_KEY}) {
1621
            self::addRecommendationForDefaultValue($logger, self::CHMOD_KEY);
1622
        }
1623
1624
        $defaultChmod = intval(0755, 8);
1625
1626
        if (isset($raw->{self::CHMOD_KEY})) {
1627
            $chmod = intval($raw->{self::CHMOD_KEY}, 8);
1628
1629
            if ($defaultChmod === $chmod) {
1630
                self::addRecommendationForDefaultValue($logger, self::CHMOD_KEY);
1631
            }
1632
1633
            return $chmod;
1634
        }
1635
1636
        return $defaultChmod;
1637
    }
1638
1639
    private static function retrieveMainScriptPath(
1640
        stdClass $raw,
1641
        string $basePath,
1642
        ?array $decodedJsonContents,
1643
        ConfigurationLogger $logger
1644
    ): ?string {
1645
        $firstBin = false;
1646
1647
        if (null !== $decodedJsonContents && array_key_exists('bin', $decodedJsonContents)) {
1648
            /** @var false|string $firstBin */
1649
            $firstBin = current((array) $decodedJsonContents['bin']);
1650
1651
            if (false !== $firstBin) {
1652
                $firstBin = self::normalizePath($firstBin, $basePath);
1653
            }
1654
        }
1655
1656
        if (isset($raw->{self::MAIN_KEY})) {
1657
            $main = $raw->{self::MAIN_KEY};
1658
1659
            if (is_string($main)) {
1660
                $main = self::normalizePath($main, $basePath);
1661
1662
                if ($main === $firstBin) {
1663
                    $logger->addRecommendation('The "main" setting can be omitted since is set to its default value');
1664
                }
1665
            }
1666
        } else {
1667
            $main = false !== $firstBin ? $firstBin : self::normalizePath(self::DEFAULT_MAIN_SCRIPT, $basePath);
1668
        }
1669
1670
        if (is_bool($main)) {
1671
            Assertion::false(
1672
                $main,
1673
                'Cannot "enable" a main script: either disable it with `false` or give the main script file path.'
1674
            );
1675
1676
            return null;
1677
        }
1678
1679
        Assertion::file($main);
1680
1681
        return $main;
1682
    }
1683
1684
    private static function retrieveMainScriptContents(?string $mainScriptPath): ?string
1685
    {
1686
        if (null === $mainScriptPath) {
1687
            return null;
1688
        }
1689
1690
        $contents = file_contents($mainScriptPath);
1691
1692
        // Remove the shebang line: the shebang line in a PHAR should be located in the stub file which is the real
1693
        // PHAR entry point file.
1694
        // If one needs the shebang, then the main file should act as the stub and be registered as such and in which
1695
        // case the main script can be ignored or disabled.
1696
        return preg_replace('/^#!.*\s*/', '', $contents);
1697
    }
1698
1699
    /**
1700
     * @return string|null[][]
1701
     */
1702
    private static function retrieveComposerFiles(string $basePath): array
1703
    {
1704
        $retrieveFileAndContents = function (string $file): array {
1705
            $json = new Json();
1706
1707
            if (false === file_exists($file) || false === is_file($file) || false === is_readable($file)) {
1708
                return [null, null];
1709
            }
1710
1711
            try {
1712
                $contents = $json->decodeFile($file, true);
1713
            } catch (ParsingException $exception) {
1714
                throw new InvalidArgumentException(
1715
                    sprintf(
1716
                        'Expected the file "%s" to be a valid composer.json file but an error has been found: %s',
1717
                        $file,
1718
                        $exception->getMessage()
1719
                    ),
1720
                    0,
1721
                    $exception
1722
                );
1723
            }
1724
1725
            return [$file, $contents];
1726
        };
1727
1728
        [$composerJson, $composerJsonContents] = $retrieveFileAndContents(canonicalize($basePath.'/composer.json'));
1729
        [$composerLock, $composerLockContents] = $retrieveFileAndContents(canonicalize($basePath.'/composer.lock'));
1730
1731
        return [
1732
            [$composerJson, $composerJsonContents],
1733
            [$composerLock, $composerLockContents],
1734
        ];
1735
    }
1736
1737
    /**
1738
     * @return string[][]
1739
     */
1740
    private static function retrieveMap(stdClass $raw, ConfigurationLogger $logger): array
1741
    {
1742
        self::checkIfDefaultValue($logger, $raw, self::MAP_KEY, []);
1743
1744
        if (false === isset($raw->{self::MAP_KEY})) {
1745
            return [];
1746
        }
1747
1748
        $map = [];
1749
        $rawMap = (array) $raw->{self::MAP_KEY};
1750
1751
        foreach ($rawMap as $item) {
1752
            $processed = [];
1753
1754
            foreach ($item as $match => $replace) {
1755
                $processed[canonicalize(trim($match))] = canonicalize(trim($replace));
1756
            }
1757
1758
            if (isset($processed['_empty_'])) {
1759
                $processed[''] = $processed['_empty_'];
1760
1761
                unset($processed['_empty_']);
1762
            }
1763
1764
            $map[] = $processed;
1765
        }
1766
1767
        return $map;
1768
    }
1769
1770
    /**
1771
     * @return mixed
1772
     */
1773
    private static function retrieveMetadata(stdClass $raw, ConfigurationLogger $logger)
1774
    {
1775
        self::checkIfDefaultValue($logger, $raw, self::METADATA_KEY);
1776
1777
        if (false === isset($raw->{self::METADATA_KEY})) {
1778
            return null;
1779
        }
1780
1781
        $metadata = $raw->{self::METADATA_KEY};
1782
1783
        return is_object($metadata) ? (array) $metadata : $metadata;
1784
    }
1785
1786
    /**
1787
     * @return string[] The first element is the temporary output path and the second the final one
1788
     */
1789
    private static function retrieveOutputPath(
1790
        stdClass $raw,
1791
        string $basePath,
1792
        ?string $mainScriptPath,
1793
        ConfigurationLogger $logger
1794
    ): array {
1795
        $defaultPath = null;
1796
1797
        if (null !== $mainScriptPath
1798
            && 1 === preg_match('/^(?<main>.*?)(?:\.[\p{L}\d]+)?$/u', $mainScriptPath, $matches)
1799
        ) {
1800
            $defaultPath = $matches['main'].'.phar';
1801
        }
1802
1803
        if (isset($raw->{self::OUTPUT_KEY})) {
1804
            $path = self::normalizePath($raw->{self::OUTPUT_KEY}, $basePath);
1805
1806
            if ($path === $defaultPath) {
1807
                self::addRecommendationForDefaultValue($logger, self::OUTPUT_KEY);
1808
            }
1809
        } elseif (null !== $defaultPath) {
1810
            $path = $defaultPath;
1811
        } else {
1812
            // Last resort, should not happen
1813
            $path = self::normalizePath(self::DEFAULT_OUTPUT_FALLBACK, $basePath);
1814
        }
1815
1816
        $tmp = $real = $path;
1817
1818
        if ('.phar' !== substr($real, -5)) {
1819
            $tmp .= '.phar';
1820
        }
1821
1822
        return [$tmp, $real];
1823
    }
1824
1825
    private static function retrievePrivateKeyPath(
1826
        stdClass $raw,
1827
        string $basePath,
1828
        int $signingAlgorithm,
1829
        ConfigurationLogger $logger
1830
    ): ?string {
1831
        if (property_exists($raw, self::KEY_KEY) && Phar::OPENSSL !== $signingAlgorithm) {
1832
            if (null === $raw->{self::KEY_KEY}) {
1833
                $logger->addRecommendation(
1834
                    'The setting "key" has been set but is unnecessary since the signing algorithm is not "OPENSSL".'
1835
                );
1836
            } else {
1837
                $logger->addWarning(
1838
                    'The setting "key" has been set but is ignored since the signing algorithm is not "OPENSSL".'
1839
                );
1840
            }
1841
1842
            return null;
1843
        }
1844
1845
        if (!isset($raw->{self::KEY_KEY})) {
1846
            Assertion::true(
1847
                Phar::OPENSSL !== $signingAlgorithm,
1848
                'Expected to have a private key for OpenSSL signing but none have been provided.'
1849
            );
1850
1851
            return null;
1852
        }
1853
1854
        $path = self::normalizePath($raw->{self::KEY_KEY}, $basePath);
1855
1856
        Assertion::file($path);
1857
1858
        return $path;
1859
    }
1860
1861
    private static function retrievePrivateKeyPassphrase(
1862
        stdClass $raw,
1863
        int $algorithm,
1864
        ConfigurationLogger $logger
1865
    ): ?string {
1866
        self::checkIfDefaultValue($logger, $raw, self::KEY_PASS_KEY);
1867
1868
        if (false === property_exists($raw, self::KEY_PASS_KEY)) {
1869
            return null;
1870
        }
1871
1872
        /** @var null|false|string $keyPass */
1873
        $keyPass = $raw->{self::KEY_PASS_KEY};
1874
1875
        if (Phar::OPENSSL !== $algorithm) {
1876
            if (false === $keyPass || null === $keyPass) {
1877
                $logger->addRecommendation(
1878
                    sprintf(
1879
                        'The setting "%s" has been set but is unnecessary since the signing algorithm is '
1880
                        .'not "OPENSSL".',
1881
                        self::KEY_PASS_KEY
1882
                    )
1883
                );
1884
            } else {
1885
                $logger->addWarning(
1886
                    sprintf(
1887
                    'The setting "%s" has been set but ignored the signing algorithm is not "OPENSSL".',
1888
                        self::KEY_PASS_KEY
1889
                    )
1890
                );
1891
            }
1892
1893
            return null;
1894
        }
1895
1896
        return is_string($keyPass) ? $keyPass : null;
1897
    }
1898
1899
    /**
1900
     * @return scalar[]
1901
     */
1902
    private static function retrieveReplacements(stdClass $raw, ?string $file, ConfigurationLogger $logger): array
1903
    {
1904
        self::checkIfDefaultValue($logger, $raw, self::REPLACEMENTS_KEY, new stdClass());
1905
1906
        if (null === $file) {
1907
            return [];
1908
        }
1909
1910
        $replacements = isset($raw->{self::REPLACEMENTS_KEY}) ? (array) $raw->{self::REPLACEMENTS_KEY} : [];
1911
1912
        if (null !== ($git = self::retrievePrettyGitPlaceholder($raw, $logger))) {
1913
            $replacements[$git] = self::retrievePrettyGitTag($file);
1914
        }
1915
1916
        if (null !== ($git = self::retrieveGitHashPlaceholder($raw, $logger))) {
1917
            $replacements[$git] = self::retrieveGitHash($file);
1918
        }
1919
1920
        if (null !== ($git = self::retrieveGitShortHashPlaceholder($raw, $logger))) {
1921
            $replacements[$git] = self::retrieveGitHash($file, true);
1922
        }
1923
1924
        if (null !== ($git = self::retrieveGitTagPlaceholder($raw, $logger))) {
1925
            $replacements[$git] = self::retrieveGitTag($file);
1926
        }
1927
1928
        if (null !== ($git = self::retrieveGitVersionPlaceholder($raw, $logger))) {
1929
            $replacements[$git] = self::retrieveGitVersion($file);
1930
        }
1931
1932
        /**
1933
         * @var string
1934
         * @var bool   $valueSetByUser
1935
         */
1936
        [$datetimeFormat, $valueSetByUser] = self::retrieveDatetimeFormat($raw, $logger);
1937
1938
        if (null !== ($date = self::retrieveDatetimeNowPlaceHolder($raw, $logger))) {
1939
            $replacements[$date] = self::retrieveDatetimeNow($datetimeFormat);
1940
        } elseif ($valueSetByUser) {
1941
            $logger->addRecommendation(
1942
                sprintf(
1943
                    'The setting "%s" has been set but is unnecessary because the setting "%s" is not set.',
1944
                    self::DATETIME_FORMAT_KEY,
1945
                    self::DATETIME_KEY
1946
                )
1947
            );
1948
        }
1949
1950
        $sigil = self::retrieveReplacementSigil($raw, $logger);
1951
1952
        foreach ($replacements as $key => $value) {
1953
            unset($replacements[$key]);
1954
            $replacements[$sigil.$key.$sigil] = $value;
1955
        }
1956
1957
        return $replacements;
1958
    }
1959
1960
    private static function retrievePrettyGitPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
1961
    {
1962
        return self::retrievePlaceholder($raw, $logger, self::GIT_KEY);
1963
    }
1964
1965
    private static function retrieveGitHashPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
1966
    {
1967
        return self::retrievePlaceholder($raw, $logger, self::GIT_COMMIT_KEY);
1968
    }
1969
1970
    /**
1971
     * @param string $file
1972
     * @param bool   $short Use the short version
1973
     *
1974
     * @return string the commit hash
1975
     */
1976
    private static function retrieveGitHash(string $file, bool $short = false): string
1977
    {
1978
        return self::runGitCommand(
1979
            sprintf(
1980
                'git log --pretty="%s" -n1 HEAD',
1981
                $short ? '%h' : '%H'
1982
            ),
1983
            $file
1984
        );
1985
    }
1986
1987
    private static function retrieveGitShortHashPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
1988
    {
1989
        return self::retrievePlaceholder($raw, $logger, self::GIT_COMMIT_SHORT_KEY);
1990
    }
1991
1992
    private static function retrieveGitTagPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
1993
    {
1994
        return self::retrievePlaceholder($raw, $logger, self::GIT_TAG_KEY);
1995
    }
1996
1997
    private static function retrievePlaceholder(stdClass $raw, ConfigurationLogger $logger, string $key): ?string
1998
    {
1999
        self::checkIfDefaultValue($logger, $raw, $key);
2000
2001
        return $raw->{$key} ?? null;
2002
    }
2003
2004
    private static function retrieveGitTag(string $file): string
2005
    {
2006
        return self::runGitCommand('git describe --tags HEAD', $file);
2007
    }
2008
2009
    private static function retrievePrettyGitTag(string $file): string
2010
    {
2011
        $version = self::retrieveGitTag($file);
2012
2013
        if (preg_match('/^(?<tag>.+)-\d+-g(?<hash>[a-f0-9]{7})$/', $version, $matches)) {
2014
            return sprintf('%s@%s', $matches['tag'], $matches['hash']);
2015
        }
2016
2017
        return $version;
2018
    }
2019
2020
    private static function retrieveGitVersionPlaceholder(stdClass $raw, ConfigurationLogger $logger): ?string
2021
    {
2022
        return self::retrievePlaceholder($raw, $logger, self::GIT_VERSION_KEY);
2023
    }
2024
2025
    private static function retrieveGitVersion(string $file): ?string
2026
    {
2027
        try {
2028
            return self::retrieveGitTag($file);
2029
        } catch (RuntimeException $exception) {
2030
            try {
2031
                return self::retrieveGitHash($file, true);
2032
            } catch (RuntimeException $exception) {
2033
                throw new RuntimeException(
2034
                    sprintf(
2035
                        'The tag or commit hash could not be retrieved from "%s": %s',
2036
                        dirname($file),
2037
                        $exception->getMessage()
2038
                    ),
2039
                    0,
2040
                    $exception
2041
                );
2042
            }
2043
        }
2044
    }
2045
2046
    private static function retrieveDatetimeNowPlaceHolder(stdClass $raw, ConfigurationLogger $logger): ?string
2047
    {
2048
        return self::retrievePlaceholder($raw, $logger, self::DATETIME_KEY);
2049
    }
2050
2051
    private static function retrieveDatetimeNow(string $format): string
2052
    {
2053
        $now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
2054
2055
        return $now->format($format);
2056
    }
2057
2058
    private static function retrieveDatetimeFormat(stdClass $raw, ConfigurationLogger $logger): array
2059
    {
2060
        self::checkIfDefaultValue($logger, $raw, self::DATETIME_FORMAT_KEY, self::DEFAULT_DATETIME_FORMAT);
2061
        self::checkIfDefaultValue($logger, $raw, self::DATETIME_FORMAT_KEY, self::DATETIME_FORMAT_DEPRECATED_KEY);
2062
2063
        if (isset($raw->{self::DATETIME_FORMAT_KEY})) {
2064
            $format = $raw->{self::DATETIME_FORMAT_KEY};
2065
        } elseif (isset($raw->{self::DATETIME_FORMAT_DEPRECATED_KEY})) {
2066
            @trigger_error(
2067
                'The "datetime_format" is deprecated, use "datetime-format" setting instead.',
2068
                E_USER_DEPRECATED
2069
            );
2070
            $logger->addWarning('The "datetime_format" is deprecated, use "datetime-format" setting instead.');
2071
2072
            $format = $raw->{self::DATETIME_FORMAT_DEPRECATED_KEY};
2073
        } else {
2074
            $format = null;
2075
        }
2076
2077
        if (null !== $format) {
2078
            $formattedDate = (new DateTimeImmutable())->format($format);
2079
2080
            Assertion::false(
2081
                false === $formattedDate || $formattedDate === $format,
2082
                sprintf(
2083
                    'Expected the datetime format to be a valid format: "%s" is not',
2084
                    $format
2085
                )
2086
            );
2087
2088
            return [$format, true];
2089
        }
2090
2091
        return [self::DEFAULT_DATETIME_FORMAT, false];
2092
    }
2093
2094
    private static function retrieveReplacementSigil(stdClass $raw, ConfigurationLogger $logger): string
2095
    {
2096
        return self::retrievePlaceholder($raw, $logger, self::REPLACEMENT_SIGIL_KEY) ?? self::DEFAULT_REPLACEMENT_SIGIL;
2097
    }
2098
2099
    private static function retrieveShebang(stdClass $raw, bool $stubIsGenerated, ConfigurationLogger $logger): ?string
2100
    {
2101
        self::checkIfDefaultValue($logger, $raw, self::SHEBANG_KEY, self::DEFAULT_SHEBANG);
2102
2103
        if (false === isset($raw->{self::SHEBANG_KEY})) {
2104
            return self::DEFAULT_SHEBANG;
2105
        }
2106
2107
        $shebang = $raw->{self::SHEBANG_KEY};
2108
2109
        if (false === $shebang) {
2110
            if (false === $stubIsGenerated) {
2111
                $logger->addRecommendation(
2112
                    sprintf(
2113
                        'The "%s" has been set to `false` but is unnecessary since the Box built-in stub is not'
2114
                        .' being used',
2115
                        self::SHEBANG_KEY
2116
                    )
2117
                );
2118
            }
2119
2120
            return null;
2121
        }
2122
2123
        Assertion::string($shebang, 'Expected shebang to be either a string, false or null, found true');
2124
2125
        $shebang = trim($shebang);
2126
2127
        Assertion::notEmpty($shebang, 'The shebang should not be empty.');
2128
        Assertion::true(
2129
            '#!' === substr($shebang, 0, 2),
2130
            sprintf(
2131
                'The shebang line must start with "#!". Got "%s" instead',
2132
                $shebang
2133
            )
2134
        );
2135
2136
        if (false === $stubIsGenerated) {
2137
            $logger->addWarning(
2138
                sprintf(
2139
                    'The "%s" has been set but ignored since it is used only with the Box built-in stub which is not'
2140
                    .' used',
2141
                    self::SHEBANG_KEY
2142
                )
2143
            );
2144
        }
2145
2146
        return $shebang;
2147
    }
2148
2149
    private static function retrieveSigningAlgorithm(stdClass $raw, ConfigurationLogger $logger): int
2150
    {
2151
        if (property_exists($raw, self::ALGORITHM_KEY) && null === $raw->{self::ALGORITHM_KEY}) {
2152
            self::addRecommendationForDefaultValue($logger, self::ALGORITHM_KEY);
2153
        }
2154
2155
        if (false === isset($raw->{self::ALGORITHM_KEY})) {
2156
            return self::DEFAULT_SIGNING_ALGORITHM;
2157
        }
2158
2159
        $algorithm = strtoupper($raw->{self::ALGORITHM_KEY});
2160
2161
        Assertion::inArray($algorithm, array_keys(get_phar_signing_algorithms()));
2162
2163
        Assertion::true(
2164
            defined('Phar::'.$algorithm),
2165
            sprintf(
2166
                'The signing algorithm "%s" is not supported by your current PHAR version.',
2167
                $algorithm
2168
            )
2169
        );
2170
2171
        $algorithm = constant('Phar::'.$algorithm);
2172
2173
        if (self::DEFAULT_SIGNING_ALGORITHM === $algorithm) {
2174
            self::addRecommendationForDefaultValue($logger, self::ALGORITHM_KEY);
2175
        }
2176
2177
        return $algorithm;
2178
    }
2179
2180
    private static function retrieveStubBannerContents(stdClass $raw, bool $stubIsGenerated, ConfigurationLogger $logger): ?string
2181
    {
2182
        self::checkIfDefaultValue($logger, $raw, self::BANNER_KEY, self::DEFAULT_BANNER);
2183
2184
        if (false === isset($raw->{self::BANNER_KEY})) {
2185
            return self::DEFAULT_BANNER;
2186
        }
2187
2188
        $banner = $raw->{self::BANNER_KEY};
2189
2190
        if (false === $banner) {
2191
            if (false === $stubIsGenerated) {
2192
                $logger->addRecommendation(
2193
                    sprintf(
2194
                        'The "%s" setting has been set but is unnecessary since the Box built-in stub is not '
2195
                        .'being used',
2196
                        self::BANNER_KEY
2197
                    )
2198
                );
2199
            }
2200
2201
            return null;
2202
        }
2203
2204
        Assertion::true(is_string($banner) || is_array($banner), 'The banner cannot accept true as a value');
2205
2206
        if (is_array($banner)) {
2207
            $banner = implode("\n", $banner);
2208
        }
2209
2210
        if (false === $stubIsGenerated) {
2211
            $logger->addWarning(
2212
                sprintf(
2213
                    'The "%s" setting has been set but is ignored since the Box built-in stub is not being used',
2214
                    self::BANNER_KEY
2215
                )
2216
            );
2217
        }
2218
2219
        return $banner;
2220
    }
2221
2222
    private static function retrieveStubBannerPath(
2223
        stdClass $raw,
2224
        string $basePath,
2225
        bool $stubIsGenerated,
2226
        ConfigurationLogger $logger
2227
    ): ?string {
2228
        self::checkIfDefaultValue($logger, $raw, self::BANNER_FILE_KEY);
2229
2230
        if (false === isset($raw->{self::BANNER_FILE_KEY})) {
2231
            return null;
2232
        }
2233
2234
        $bannerFile = make_path_absolute($raw->{self::BANNER_FILE_KEY}, $basePath);
2235
2236
        Assertion::file($bannerFile);
2237
2238
        if (false === $stubIsGenerated) {
2239
            $logger->addWarning(
2240
                sprintf(
2241
                    'The "%s" setting has been set but is ignored since the Box built-in stub is not being used',
2242
                    self::BANNER_FILE_KEY
2243
                )
2244
            );
2245
        }
2246
2247
        return $bannerFile;
2248
    }
2249
2250
    private static function normalizeStubBannerContents(?string $contents): ?string
2251
    {
2252
        if (null === $contents) {
2253
            return null;
2254
        }
2255
2256
        $banner = explode("\n", $contents);
2257
        $banner = array_map('trim', $banner);
2258
2259
        return implode("\n", $banner);
2260
    }
2261
2262
    private static function retrieveStubPath(stdClass $raw, string $basePath, ConfigurationLogger $logger): ?string
2263
    {
2264
        self::checkIfDefaultValue($logger, $raw, self::STUB_KEY);
2265
2266
        if (isset($raw->{self::STUB_KEY}) && is_string($raw->{self::STUB_KEY})) {
2267
            $stubPath = make_path_absolute($raw->{self::STUB_KEY}, $basePath);
2268
2269
            Assertion::file($stubPath);
2270
2271
            return $stubPath;
2272
        }
2273
2274
        return null;
2275
    }
2276
2277
    private static function retrieveInterceptsFileFuncs(
2278
        stdClass $raw,
2279
        bool $stubIsGenerated,
2280
        ConfigurationLogger $logger
2281
    ): bool {
2282
        self::checkIfDefaultValue($logger, $raw, self::INTERCEPT_KEY, false);
2283
2284
        if (false === isset($raw->{self::INTERCEPT_KEY})) {
2285
            return false;
2286
        }
2287
2288
        $intercept = $raw->{self::INTERCEPT_KEY};
2289
2290
        if ($intercept && false === $stubIsGenerated) {
2291
            $logger->addWarning(
2292
                sprintf(
2293
                    'The "%s" setting has been set but is ignored since the Box built-in stub is not being used',
2294
                    self::INTERCEPT_KEY
2295
                )
2296
            );
2297
        }
2298
2299
        return $intercept;
2300
    }
2301
2302
    private static function retrievePromptForPrivateKey(
2303
        stdClass $raw,
2304
        int $signingAlgorithm,
2305
        ConfigurationLogger $logger
2306
    ): bool {
2307
        if (isset($raw->{self::KEY_PASS_KEY}) && true === $raw->{self::KEY_PASS_KEY}) {
2308
            if (Phar::OPENSSL !== $signingAlgorithm) {
2309
                $logger->addWarning(
2310
                    'A prompt for password for the private key has been requested but ignored since the signing '
2311
                    .'algorithm used is not "OPENSSL.'
2312
                );
2313
2314
                return false;
2315
            }
2316
2317
            return true;
2318
        }
2319
2320
        return false;
2321
    }
2322
2323
    private static function retrieveIsStubGenerated(stdClass $raw, ?string $stubPath, ConfigurationLogger $logger): bool
2324
    {
2325
        self::checkIfDefaultValue($logger, $raw, self::STUB_KEY, true);
2326
2327
        return null === $stubPath && (false === isset($raw->{self::STUB_KEY}) || false !== $raw->{self::STUB_KEY});
2328
    }
2329
2330
    private static function retrieveCheckRequirements(
2331
        stdClass $raw,
2332
        bool $hasComposerJson,
2333
        bool $hasComposerLock,
2334
        bool $pharStubUsed,
2335
        ConfigurationLogger $logger
2336
    ): bool {
2337
        self::checkIfDefaultValue($logger, $raw, self::CHECK_REQUIREMENTS_KEY, true);
2338
2339
        if (false === property_exists($raw, self::CHECK_REQUIREMENTS_KEY)) {
2340
            return $hasComposerJson || $hasComposerLock;
2341
        }
2342
2343
        /** @var bool $checkRequirements */
2344
        $checkRequirements = $raw->{self::CHECK_REQUIREMENTS_KEY} ?? true;
2345
2346
        if ($checkRequirements && false === $hasComposerJson && false === $hasComposerLock) {
2347
            $logger->addWarning(
2348
                'The requirement checker could not be used because the composer.json and composer.lock file could not '
2349
                .'be found.'
2350
            );
2351
2352
            return false;
2353
        }
2354
2355
        if ($checkRequirements && $pharStubUsed) {
2356
            $logger->addWarning(
2357
                sprintf(
2358
                    'The "%s" setting has been set but has been ignored since the PHAR built-in stub is being '
2359
                    .'used.',
2360
                    self::CHECK_REQUIREMENTS_KEY
2361
                )
2362
            );
2363
        }
2364
2365
        return $checkRequirements;
2366
    }
2367
2368
    private static function retrievePhpScoperConfig(stdClass $raw, string $basePath, ConfigurationLogger $logger): PhpScoperConfiguration
2369
    {
2370
        self::checkIfDefaultValue($logger, $raw, self::PHP_SCOPER_KEY, self::PHP_SCOPER_CONFIG);
2371
2372
        if (!isset($raw->{self::PHP_SCOPER_KEY})) {
2373
            $configFilePath = make_path_absolute(self::PHP_SCOPER_CONFIG, $basePath);
2374
2375
            return file_exists($configFilePath)
2376
                ? PhpScoperConfiguration::load($configFilePath)
2377
                : PhpScoperConfiguration::load()
2378
             ;
2379
        }
2380
2381
        $configFile = $raw->{self::PHP_SCOPER_KEY};
2382
2383
        Assertion::string($configFile);
2384
2385
        $configFilePath = make_path_absolute($configFile, $basePath);
2386
2387
        Assertion::file($configFilePath);
2388
        Assertion::readable($configFilePath);
2389
2390
        return PhpScoperConfiguration::load($configFilePath);
2391
    }
2392
2393
    /**
2394
     * Runs a Git command on the repository.
2395
     *
2396
     * @param string $command the command
2397
     *
2398
     * @return string the trimmed output from the command
2399
     */
2400
    private static function runGitCommand(string $command, string $file): string
2401
    {
2402
        $path = dirname($file);
2403
2404
        $process = new Process($command, $path);
2405
2406
        if (0 === $process->run()) {
2407
            return trim($process->getOutput());
2408
        }
2409
2410
        throw new RuntimeException(
2411
            sprintf(
2412
                'The tag or commit hash could not be retrieved from "%s": %s',
2413
                $path,
2414
                $process->getErrorOutput()
2415
            )
2416
        );
2417
    }
2418
2419
    private static function createPhpCompactor(stdClass $raw): Compactor
2420
    {
2421
        $tokenizer = new Tokenizer();
2422
2423
        if (false === empty($raw->{self::ANNOTATIONS_KEY}) && isset($raw->{self::ANNOTATIONS_KEY}->ignore)) {
2424
            $tokenizer->ignore(
2425
                (array) $raw->{self::ANNOTATIONS_KEY}->ignore
2426
            );
2427
        }
2428
2429
        return new PhpCompactor($tokenizer);
2430
    }
2431
2432
    private static function createPhpScoperCompactor(stdClass $raw, string $basePath, ConfigurationLogger $logger): Compactor
2433
    {
2434
        $phpScoperConfig = self::retrievePhpScoperConfig($raw, $basePath, $logger);
2435
2436
        $phpScoper = (new class() extends ApplicationFactory {
2437
            public static function createScoper(): Scoper
2438
            {
2439
                return parent::createScoper();
2440
            }
2441
        })::createScoper();
2442
2443
        $prefix = null === $phpScoperConfig->getPrefix()
2444
            ? unique_id('_HumbugBox')
2445
            : $phpScoperConfig->getPrefix()
2446
        ;
2447
2448
        return new PhpScoperCompactor(
2449
            new SimpleScoper(
2450
                $phpScoper,
2451
                $prefix,
2452
                $phpScoperConfig->getWhitelist(),
2453
                $phpScoperConfig->getPatchers()
2454
            )
2455
        );
2456
    }
2457
2458
    private static function checkIfDefaultValue(
2459
        ConfigurationLogger $logger,
2460
        stdClass $raw,
2461
        string $key,
2462
        $defaultValue = null
2463
    ): void {
2464
        if (false === property_exists($raw, $key)) {
2465
            return;
2466
        }
2467
2468
        $value = $raw->{$key};
2469
2470
        if (null === $value
2471
            || (false === is_object($defaultValue) && $defaultValue === $value)
2472
            || (is_object($defaultValue) && $defaultValue == $value)
2473
        ) {
2474
            $logger->addRecommendation(
2475
                sprintf(
2476
                    'The "%s" setting can be omitted since is set to its default value',
2477
                    $key
2478
                )
2479
            );
2480
        }
2481
    }
2482
2483
    private static function addRecommendationForDefaultValue(ConfigurationLogger $logger, string $key): void
2484
    {
2485
        $logger->addRecommendation(
2486
            sprintf(
2487
                'The "%s" setting can be omitted since is set to its default value',
2488
                $key
2489
            )
2490
        );
2491
    }
2492
}
2493