Passed
Push — master ( a127e3...314cca )
by Théo
02:46
created

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