Passed
Pull Request — master (#214)
by Théo
02:45
created

Configuration::processFinders()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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