Passed
Pull Request — master (#274)
by Théo
02:33
created

Configuration::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 77
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 40
dl 0
loc 77
rs 9.28
c 0
b 0
f 0
cc 2
nc 2
nop 30

How to fix   Long Method    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

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