Passed
Pull Request — master (#245)
by Théo
02:36
created

Configuration::getReplacements()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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