Passed
Pull Request — master (#274)
by Théo
02:33
created

Configuration.php$0 ➔ retrieveStubBannerContents()   A

Complexity

Conditions 6

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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