Passed
Pull Request — master (#251)
by Théo
03:01
created

Configuration::retrieveDatetimeNow()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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