Passed
Push — master ( d57ea4...c78585 )
by Théo
02:18
created

Configuration::retrieveForceFilesAutodiscovery()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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