Passed
Pull Request — master (#146)
by Théo
02:11
created

Configuration::retrieveGitTagPlaceholder()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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