Passed
Pull Request — master (#279)
by Théo
02:32
created

Configuration.php$0 ➔ retrievePrivateKeyPath()   A

Complexity

Conditions 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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