Completed
Pull Request — master (#237)
by Théo
04:25 queued 01:49
created

Configuration::normalizeStubBannerContents()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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