Passed
Pull Request — master (#235)
by Théo
02:49
created

Configuration::retrieveIsPrivateKeyPrompt()   A

Complexity

Conditions 2
Paths 2

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