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

Configuration::retrieveAllDirectoriesToInclude()   D

Complexity

Conditions 22
Paths 194

Size

Total Lines 129
Code Lines 71

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 129
rs 4.3966
c 0
b 0
f 0
cc 22
eloc 71
nc 194
nop 5

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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