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

Configuration::isPrivateKeyPrompt()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 0
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
            ->exclude('doc')
1082
            ->exclude('docs')
1083
            ->exclude('documentation')
1084
            // Remove backup files
1085
            ->notName('*~')
1086
            ->notName('*.back')
1087
            ->notName('*.swp')
1088
            // Remove tests
1089
            ->notName('*Test.php')
1090
            ->exclude('test')
1091
            ->exclude('tests')
1092
            ->notName('/phpunit.*\.xml(.dist)?/')
1093
            ->notName('/behat.*\.yml(.dist)?/')
1094
            ->exclude('spec')
1095
            ->exclude('specs')
1096
            ->exclude('features')
1097
            // Remove CI config
1098
            ->exclude('travis')
1099
            ->notName('travis.yml')
1100
            ->notName('appveyor.yml')
1101
            ->notName('build.xml*')
1102
        ;
1103
1104
        $finder->append($files);
1105
        $finder->in($directories);
1106
1107
        $excludedPaths = array_unique(
1108
            array_filter(
1109
                array_map(
1110
                    function (string $path) use ($basePath): string {
1111
                        return make_path_relative($path, $basePath);
1112
                    },
1113
                    $excludedPaths
1114
                ),
1115
                function (string $path): bool {
1116
                    return '..' !== substr($path, 0, 2);
1117
                }
1118
            )
1119
        );
1120
1121
        foreach ($excludedPaths as $excludedPath) {
1122
            $finder->notPath($excludedPath);
1123
        }
1124
1125
        return array_unique(
1126
            toArray(
1127
                map(
1128
                    method('getRealPath'),
1129
                    $finder
1130
                )
1131
            )
1132
        );
1133
    }
1134
1135
    /**
1136
     * @param stdClass $raw
1137
     * @param string   $key      Config property name
1138
     * @param string   $basePath
1139
     *
1140
     * @return string[]
1141
     */
1142
    private static function retrieveDirectoryPaths(stdClass $raw, string $key, string $basePath): array
1143
    {
1144
        if (false === isset($raw->{$key})) {
1145
            return [];
1146
        }
1147
1148
        $directories = $raw->{$key};
1149
1150
        $normalizeDirectory = function (string $directory) use ($basePath, $key): string {
1151
            $directory = self::normalizePath($directory, $basePath);
1152
1153
            if (is_link($directory)) {
1154
                // TODO: add this to baberlei/assert
1155
                throw new InvalidArgumentException(
1156
                    sprintf(
1157
                        'Cannot add the link "%s": links are not supported.',
1158
                        $directory
1159
                    )
1160
                );
1161
            }
1162
1163
            Assertion::directory(
1164
                $directory,
1165
                sprintf(
1166
                    '"%s" must contain a list of existing directories. Could not find "%%s".',
1167
                    $key
1168
                )
1169
            );
1170
1171
            return $directory;
1172
        };
1173
1174
        return array_map($normalizeDirectory, $directories);
1175
    }
1176
1177
    private static function normalizePath(string $file, string $basePath): string
1178
    {
1179
        return make_path_absolute(trim($file), $basePath);
1180
    }
1181
1182
    /**
1183
     * @return Compactor[]
1184
     */
1185
    private static function retrieveCompactors(stdClass $raw, string $basePath): array
1186
    {
1187
        if (false === isset($raw->compactors)) {
1188
            return [];
1189
        }
1190
1191
        $compactorClasses = array_unique((array) $raw->compactors);
1192
1193
        return array_map(
1194
            function (string $class) use ($raw, $basePath): Compactor {
1195
                Assertion::classExists($class, 'The compactor class "%s" does not exist.');
1196
                Assertion::implementsInterface($class, Compactor::class, 'The class "%s" is not a compactor class.');
1197
1198
                if (Php::class === $class || LegacyPhp::class === $class) {
1199
                    return self::createPhpCompactor($raw);
1200
                }
1201
1202
                if (PhpScoperCompactor::class === $class) {
1203
                    $phpScoperConfig = self::retrievePhpScoperConfig($raw, $basePath);
1204
1205
                    return new PhpScoperCompactor(
1206
                        new SimpleScoper(
1207
                            create_scoper(),
1208
                            uniqid('_HumbugBox', false),
1209
                            $phpScoperConfig->getWhitelist(),
1210
                            $phpScoperConfig->getPatchers()
1211
                        )
1212
                    );
1213
                }
1214
1215
                return new $class();
1216
            },
1217
            $compactorClasses
1218
        );
1219
    }
1220
1221
    private static function retrieveCompressionAlgorithm(stdClass $raw): ?int
1222
    {
1223
        if (false === isset($raw->compression)) {
1224
            return null;
1225
        }
1226
1227
        $knownAlgorithmNames = array_keys(get_phar_compression_algorithms());
1228
1229
        Assertion::inArray(
1230
            $raw->compression,
1231
            $knownAlgorithmNames,
1232
            sprintf(
1233
                'Invalid compression algorithm "%%s", use one of "%s" instead.',
1234
                implode('", "', $knownAlgorithmNames)
1235
            )
1236
        );
1237
1238
        $value = get_phar_compression_algorithms()[$raw->compression];
1239
1240
        // Phar::NONE is not valid for compressFiles()
1241
        if (Phar::NONE === $value) {
1242
            return null;
1243
        }
1244
1245
        return $value;
1246
    }
1247
1248
    private static function retrieveFileMode(stdClass $raw): ?int
1249
    {
1250
        if (isset($raw->chmod)) {
1251
            return intval($raw->chmod, 8);
1252
        }
1253
1254
        return null;
1255
    }
1256
1257
    private static function retrieveMainScriptPath(stdClass $raw, string $basePath, ?array $decodedJsonContents): string
1258
    {
1259
        if (isset($raw->main)) {
1260
            $main = $raw->main;
1261
        } else {
1262
            if (null === $decodedJsonContents
1263
                || false === array_key_exists('bin', $decodedJsonContents)
1264
                || false === $main = current($decodedJsonContents['bin'])
1265
            ) {
1266
                $main = self::DEFAULT_MAIN_SCRIPT;
1267
            }
1268
        }
1269
1270
        return self::normalizePath($main, $basePath);
1271
    }
1272
1273
    private static function retrieveMainScriptContents(string $mainScriptPath): string
1274
    {
1275
        $contents = file_contents($mainScriptPath);
1276
1277
        // Remove the shebang line: the shebang line in a PHAR should be located in the stub file which is the real
1278
        // PHAR entry point file.
1279
        return preg_replace('/^#!.*\s*/', '', $contents);
1280
    }
1281
1282
    private static function retrieveComposerFiles(string $basePath): array
1283
    {
1284
        $retrieveFileAndContents = function (string $file): array {
1285
            $json = new Json();
1286
1287
            if (false === file_exists($file) || false === is_file($file) || false === is_readable($file)) {
1288
                return [null, null];
1289
            }
1290
1291
            try {
1292
                $contents = $json->decodeFile($file, true);
1293
            } catch (ParsingException $exception) {
1294
                throw new InvalidArgumentException(
1295
                    sprintf(
1296
                        'Expected the file "%s" to be a valid composer.json file but an error has been found: %s',
1297
                        $file,
1298
                        $exception->getMessage()
1299
                    ),
1300
                    0,
1301
                    $exception
1302
                );
1303
            }
1304
1305
            return [$file, $contents];
1306
        };
1307
1308
        [$composerJson, $composerJsonContents] = $retrieveFileAndContents(canonicalize($basePath.'/composer.json'));
1309
        [$composerLock, $composerLockContents] = $retrieveFileAndContents(canonicalize($basePath.'/composer.lock'));
1310
1311
        return [
1312
            [$composerJson, $composerJsonContents],
1313
            [$composerLock, $composerLockContents],
1314
        ];
1315
    }
1316
1317
    /**
1318
     * @return string[][]
1319
     */
1320
    private static function retrieveMap(stdClass $raw): array
1321
    {
1322
        if (false === isset($raw->map)) {
1323
            return [];
1324
        }
1325
1326
        $map = [];
1327
1328
        foreach ((array) $raw->map as $item) {
1329
            $processed = [];
1330
1331
            foreach ($item as $match => $replace) {
1332
                $processed[canonicalize(trim($match))] = canonicalize(trim($replace));
1333
            }
1334
1335
            if (isset($processed['_empty_'])) {
1336
                $processed[''] = $processed['_empty_'];
1337
1338
                unset($processed['_empty_']);
1339
            }
1340
1341
            $map[] = $processed;
1342
        }
1343
1344
        return $map;
1345
    }
1346
1347
    /**
1348
     * @return mixed
1349
     */
1350
    private static function retrieveMetadata(stdClass $raw)
1351
    {
1352
        if (isset($raw->metadata)) {
1353
            if (is_object($raw->metadata)) {
1354
                return (array) $raw->metadata;
1355
            }
1356
1357
            return $raw->metadata;
1358
        }
1359
1360
        return null;
1361
    }
1362
1363
    /**
1364
     * @return string[] The first element is the temporary output path and the second the real one
1365
     */
1366
    private static function retrieveOutputPath(stdClass $raw, string $basePath, string $mainScriptPath): array
1367
    {
1368
        if (isset($raw->output)) {
1369
            $path = $raw->output;
1370
        } else {
1371
            if (1 === preg_match('/^(?<main>.*?)(?:\.[\p{L}\d]+)?$/', $mainScriptPath, $matches)) {
1372
                $path = $matches['main'].'.phar';
1373
            } else {
1374
                // Last resort, should not happen
1375
                $path = self::DEFAULT_ALIAS;
1376
            }
1377
        }
1378
1379
        $tmp = $real = self::normalizePath($path, $basePath);
1380
1381
        if ('.phar' !== substr($real, -5)) {
1382
            $tmp .= '.phar';
1383
        }
1384
1385
        return [$tmp, $real];
1386
    }
1387
1388
    private static function retrievePrivateKeyPassphrase(stdClass $raw): ?string
1389
    {
1390
        // TODO: add check to not allow this setting without the private key path
1391
        if (isset($raw->{'key-pass'})
1392
            && is_string($raw->{'key-pass'})
1393
        ) {
1394
            return $raw->{'key-pass'};
1395
        }
1396
1397
        return null;
1398
    }
1399
1400
    private static function retrievePrivateKeyPath(stdClass $raw): ?string
1401
    {
1402
        // TODO: If passed need to check its existence
1403
        // Also need
1404
1405
        if (isset($raw->key)) {
1406
            return $raw->key;
1407
        }
1408
1409
        return null;
1410
    }
1411
1412
    private static function retrieveReplacements(stdClass $raw): array
1413
    {
1414
        // TODO: add exmample in the doc
1415
        // Add checks against the values
1416
        if (isset($raw->replacements)) {
1417
            return (array) $raw->replacements;
1418
        }
1419
1420
        return [];
1421
    }
1422
1423
    private static function retrieveProcessedReplacements(
1424
        array $replacements,
1425
        stdClass $raw,
1426
        ?string $file
1427
    ): array {
1428
        if (null === $file) {
1429
            return [];
1430
        }
1431
1432
        if (null !== ($git = self::retrieveGitHashPlaceholder($raw))) {
1433
            $replacements[$git] = self::retrieveGitHash($file);
1434
        }
1435
1436
        if (null !== ($git = self::retrieveGitShortHashPlaceholder($raw))) {
1437
            $replacements[$git] = self::retrieveGitHash($file, true);
1438
        }
1439
1440
        if (null !== ($git = self::retrieveGitTagPlaceholder($raw))) {
1441
            $replacements[$git] = self::retrieveGitTag($file);
1442
        }
1443
1444
        if (null !== ($git = self::retrieveGitVersionPlaceholder($raw))) {
1445
            $replacements[$git] = self::retrieveGitVersion($file);
1446
        }
1447
1448
        if (null !== ($date = self::retrieveDatetimeNowPlaceHolder($raw))) {
1449
            $replacements[$date] = self::retrieveDatetimeNow(
1450
                self::retrieveDatetimeFormat($raw)
1451
            );
1452
        }
1453
1454
        $sigil = self::retrieveReplacementSigil($raw);
1455
1456
        foreach ($replacements as $key => $value) {
1457
            unset($replacements[$key]);
1458
            $replacements["$sigil$key$sigil"] = $value;
1459
        }
1460
1461
        return $replacements;
1462
    }
1463
1464
    private static function retrieveGitHashPlaceholder(stdClass $raw): ?string
1465
    {
1466
        if (isset($raw->{'git-commit'})) {
1467
            return $raw->{'git-commit'};
1468
        }
1469
1470
        return null;
1471
    }
1472
1473
    /**
1474
     * @param string $file
1475
     * @param bool   $short Use the short version
1476
     *
1477
     * @return string the commit hash
1478
     */
1479
    private static function retrieveGitHash(string $file, bool $short = false): string
1480
    {
1481
        return self::runGitCommand(
1482
            sprintf(
1483
                'git log --pretty="%s" -n1 HEAD',
1484
                $short ? '%h' : '%H'
1485
            ),
1486
            $file
1487
        );
1488
    }
1489
1490
    private static function retrieveGitShortHashPlaceholder(stdClass $raw): ?string
1491
    {
1492
        if (isset($raw->{'git-commit-short'})) {
1493
            return $raw->{'git-commit-short'};
1494
        }
1495
1496
        return null;
1497
    }
1498
1499
    private static function retrieveGitTagPlaceholder(stdClass $raw): ?string
1500
    {
1501
        if (isset($raw->{'git-tag'})) {
1502
            return $raw->{'git-tag'};
1503
        }
1504
1505
        return null;
1506
    }
1507
1508
    private static function retrieveGitTag(string $file): ?string
1509
    {
1510
        return self::runGitCommand('git describe --tags HEAD', $file);
1511
    }
1512
1513
    private static function retrieveGitVersionPlaceholder(stdClass $raw): ?string
1514
    {
1515
        if (isset($raw->{'git-version'})) {
1516
            return $raw->{'git-version'};
1517
        }
1518
1519
        return null;
1520
    }
1521
1522
    private static function retrieveGitVersion(string $file): ?string
1523
    {
1524
        // TODO: check if is still relevant as IMO we are better off using OcramiusVersionPackage
1525
        // to avoid messing around with that
1526
1527
        try {
1528
            return self::retrieveGitTag($file);
1529
        } catch (RuntimeException $exception) {
1530
            try {
1531
                return self::retrieveGitHash($file, true);
1532
            } catch (RuntimeException $exception) {
1533
                throw new RuntimeException(
1534
                    sprintf(
1535
                        'The tag or commit hash could not be retrieved from "%s": %s',
1536
                        dirname($file),
1537
                        $exception->getMessage()
1538
                    ),
1539
                    0,
1540
                    $exception
1541
                );
1542
            }
1543
        }
1544
    }
1545
1546
    private static function retrieveDatetimeNowPlaceHolder(stdClass $raw): ?string
1547
    {
1548
        // TODO: double check why this is done and how it is used it's not completely clear to me.
1549
        // Also make sure the documentation is up to date after.
1550
        // Instead of having two sistinct doc entries for `datetime` and `datetime-format`, it would
1551
        // be better to have only one element IMO like:
1552
        //
1553
        // "datetime": {
1554
        //   "value": "val",
1555
        //   "format": "Y-m-d"
1556
        // }
1557
        //
1558
        // Also add a check that one cannot be provided without the other. Or maybe it should? I guess
1559
        // if the datetime format is the default one it's ok; but in any case the format should not
1560
        // be added without the datetime value...
1561
1562
        if (isset($raw->{'datetime'})) {
1563
            return $raw->{'datetime'};
1564
        }
1565
1566
        return null;
1567
    }
1568
1569
    private static function retrieveDatetimeNow(string $format)
1570
    {
1571
        $now = new DateTimeImmutable('now');
1572
1573
        $datetime = $now->format($format);
1574
1575
        if (!$datetime) {
1576
            throw new InvalidArgumentException(
1577
                sprintf(
1578
                    '""%s" is not a valid PHP date format',
1579
                    $format
1580
                )
1581
            );
1582
        }
1583
1584
        return $datetime;
1585
    }
1586
1587
    private static function retrieveDatetimeFormat(stdClass $raw): string
1588
    {
1589
        if (isset($raw->{'datetime_format'})) {
1590
            return $raw->{'datetime_format'};
1591
        }
1592
1593
        return self::DEFAULT_DATETIME_FORMAT;
1594
    }
1595
1596
    private static function retrieveReplacementSigil(stdClass $raw)
1597
    {
1598
        if (isset($raw->{'replacement-sigil'})) {
1599
            return $raw->{'replacement-sigil'};
1600
        }
1601
1602
        return self::DEFAULT_REPLACEMENT_SIGIL;
1603
    }
1604
1605
    private static function retrieveShebang(stdClass $raw): ?string
1606
    {
1607
        if (false === array_key_exists('shebang', (array) $raw)) {
1608
            return self::DEFAULT_SHEBANG;
1609
        }
1610
1611
        if (null === $raw->shebang) {
1612
            return null;
1613
        }
1614
1615
        $shebang = trim($raw->shebang);
1616
1617
        Assertion::notEmpty($shebang, 'The shebang should not be empty.');
1618
        Assertion::true(
1619
            '#!' === substr($shebang, 0, 2),
1620
            sprintf(
1621
                'The shebang line must start with "#!". Got "%s" instead',
1622
                $shebang
1623
            )
1624
        );
1625
1626
        return $shebang;
1627
    }
1628
1629
    private static function retrieveSigningAlgorithm(stdClass $raw): int
1630
    {
1631
        // TODO: trigger warning: if no signing algorithm is given provided we are not in dev mode
1632
        // TODO: trigger a warning if the signing algorithm used is weak
1633
        // TODO: no longer accept strings & document BC break
1634
        if (false === isset($raw->algorithm)) {
1635
            return Phar::SHA1;
1636
        }
1637
1638
        if (false === defined('Phar::'.$raw->algorithm)) {
1639
            throw new InvalidArgumentException(
1640
                sprintf(
1641
                    'The signing algorithm "%s" is not supported.',
1642
                    $raw->algorithm
1643
                )
1644
            );
1645
        }
1646
1647
        return constant('Phar::'.$raw->algorithm);
1648
    }
1649
1650
    private static function retrieveStubBannerContents(stdClass $raw): ?string
1651
    {
1652
        if (false === array_key_exists('banner', (array) $raw)) {
1653
            return self::DEFAULT_BANNER;
1654
        }
1655
1656
        if (null === $raw->banner) {
1657
            return null;
1658
        }
1659
1660
        $banner = $raw->banner;
1661
1662
        if (is_array($banner)) {
1663
            $banner = implode("\n", $banner);
1664
        }
1665
1666
        return $banner;
1667
    }
1668
1669
    private static function retrieveStubBannerPath(stdClass $raw, string $basePath): ?string
1670
    {
1671
        if (false === isset($raw->{'banner-file'})) {
1672
            return null;
1673
        }
1674
1675
        $bannerFile = make_path_absolute($raw->{'banner-file'}, $basePath);
1676
1677
        Assertion::file($bannerFile);
1678
1679
        return $bannerFile;
1680
    }
1681
1682
    private static function normalizeStubBannerContents(?string $contents): ?string
1683
    {
1684
        if (null === $contents) {
1685
            return null;
1686
        }
1687
1688
        $banner = explode("\n", $contents);
1689
        $banner = array_map('trim', $banner);
1690
1691
        return implode("\n", $banner);
1692
    }
1693
1694
    private static function retrieveStubPath(stdClass $raw, string $basePath): ?string
1695
    {
1696
        if (isset($raw->stub) && is_string($raw->stub)) {
1697
            $stubPath = make_path_absolute($raw->stub, $basePath);
1698
1699
            Assertion::file($stubPath);
1700
1701
            return $stubPath;
1702
        }
1703
1704
        return null;
1705
    }
1706
1707
    private static function retrieveIsInterceptFileFuncs(stdClass $raw): bool
1708
    {
1709
        if (isset($raw->intercept)) {
1710
            return $raw->intercept;
1711
        }
1712
1713
        return false;
1714
    }
1715
1716
    private static function retrieveIsPrivateKeyPrompt(stdClass $raw): bool
1717
    {
1718
        return isset($raw->{'key-pass'}) && (true === $raw->{'key-pass'});
1719
    }
1720
1721
    private static function retrieveIsStubGenerated(stdClass $raw, ?string $stubPath): bool
1722
    {
1723
        return null === $stubPath && (false === isset($raw->stub) || false !== $raw->stub);
1724
    }
1725
1726
    private static function retrieveCheckRequirements(stdClass $raw, bool $hasComposerLock, bool $generateStub): bool
1727
    {
1728
        // TODO: emit warning when stub is not generated and check requirements is explicitly set to true
1729
        // TODO: emit warning when no composer lock is found but check requirements is explicitely set to true
1730
        if (false === $hasComposerLock) {
1731
            return false;
1732
        }
1733
1734
        return $raw->{'check-requirements'} ?? true;
1735
    }
1736
1737
    private static function retrievePhpScoperConfig(stdClass $raw, string $basePath): PhpScoperConfiguration
1738
    {
1739
        if (!isset($raw->{'php-scoper'})) {
1740
            $configFilePath = make_path_absolute(self::PHP_SCOPER_CONFIG, $basePath);
1741
1742
            return file_exists($configFilePath)
1743
                ? PhpScoperConfiguration::load($configFilePath)
1744
                : PhpScoperConfiguration::load()
1745
             ;
1746
        }
1747
1748
        $configFile = $raw->phpScoper;
1749
1750
        Assertion::string($configFile);
1751
1752
        $configFilePath = make_path_absolute($configFile, $basePath);
1753
1754
        Assertion::file($configFilePath);
1755
        Assertion::readable($configFilePath);
1756
1757
        return PhpScoperConfiguration::load($configFilePath);
1758
    }
1759
1760
    /**
1761
     * Runs a Git command on the repository.
1762
     *
1763
     * @param string $command the command
1764
     *
1765
     * @return string the trimmed output from the command
1766
     */
1767
    private static function runGitCommand(string $command, string $file): string
1768
    {
1769
        $path = dirname($file);
1770
1771
        $process = new Process($command, $path);
1772
1773
        if (0 === $process->run()) {
1774
            return trim($process->getOutput());
1775
        }
1776
1777
        throw new RuntimeException(
1778
            sprintf(
1779
                'The tag or commit hash could not be retrieved from "%s": %s',
1780
                $path,
1781
                $process->getErrorOutput()
1782
            )
1783
        );
1784
    }
1785
1786
    private static function createPhpCompactor(stdClass $raw): Compactor
1787
    {
1788
        // TODO: false === not set; check & add test/doc
1789
        $tokenizer = new Tokenizer();
1790
1791
        if (false === empty($raw->annotations) && isset($raw->annotations->ignore)) {
1792
            $tokenizer->ignore(
1793
                (array) $raw->annotations->ignore
1794
            );
1795
        }
1796
1797
        return new Php($tokenizer);
1798
    }
1799
}
1800