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

Configuration::collectFiles()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 49
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 23
dl 0
loc 49
rs 9.2408
c 0
b 0
f 0
cc 5
nc 4
nop 10

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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