Passed
Pull Request — master (#201)
by Théo
03:03
created

Configuration::retrieveDumpAutoload()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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