Passed
Push — master ( c78585...0e87fa )
by Théo
02:47
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 strtoupper;
73
use function substr;
74
use function trigger_error;
75
use function uniqid;
76
77
/**
78
 * @private
79
 */
80
final class Configuration
81
{
82
    private const DEFAULT_ALIAS = 'test.phar';
83
    private const DEFAULT_MAIN_SCRIPT = 'index.php';
84
    private const DEFAULT_DATETIME_FORMAT = 'Y-m-d H:i:s';
85
    private const DEFAULT_REPLACEMENT_SIGIL = '@';
86
    private const DEFAULT_SHEBANG = '#!/usr/bin/env php';
87
    private const DEFAULT_BANNER = <<<'BANNER'
88
Generated by Humbug Box.
89
90
@link https://github.com/humbug/box
91
BANNER;
92
    private const FILES_SETTINGS = [
93
        'directories',
94
        'finder',
95
    ];
96
    private const PHP_SCOPER_CONFIG = 'scoper.inc.php';
97
    private const DEFAULT_SIGNING_ALGORITHM = Phar::SHA1;
98
99
    private $file;
100
    private $fileMode;
101
    private $alias;
102
    private $basePath;
103
    private $composerJson;
104
    private $composerLock;
105
    private $files;
106
    private $binaryFiles;
107
    private $autodiscoveredFiles;
108
    private $dumpAutoload;
109
    private $excludeComposerFiles;
110
    private $compactors;
111
    private $compressionAlgorithm;
112
    private $mainScriptPath;
113
    private $mainScriptContents;
114
    private $map;
115
    private $fileMapper;
116
    private $metadata;
117
    private $tmpOutputPath;
118
    private $outputPath;
119
    private $privateKeyPassphrase;
120
    private $privateKeyPath;
121
    private $promptForPrivateKey;
122
    private $processedReplacements;
123
    private $shebang;
124
    private $signingAlgorithm;
125
    private $stubBannerContents;
126
    private $stubBannerPath;
127
    private $stubPath;
128
    private $isInterceptFileFuncs;
129
    private $isStubGenerated;
130
    private $checkRequirements;
131
    private $warnings;
132
    private $recommendations;
133
134
    public static function create(?string $file, stdClass $raw): self
135
    {
136
        $logger = new ConfigurationLogger();
137
138
        $alias = self::retrieveAlias($raw);
139
140
        $basePath = self::retrieveBasePath($file, $raw);
141
142
        $composerFiles = self::retrieveComposerFiles($basePath);
143
144
        $mainScriptPath = self::retrieveMainScriptPath($raw, $basePath, $composerFiles[0][1]);
145
        $mainScriptContents = self::retrieveMainScriptContents($mainScriptPath);
146
147
        [$tmpOutputPath, $outputPath] = self::retrieveOutputPath($raw, $basePath, $mainScriptPath);
148
149
        $composerJson = $composerFiles[0];
150
        $composerLock = $composerFiles[1];
151
152
        $devPackages = ComposerConfiguration::retrieveDevPackages($basePath, $composerJson[1], $composerLock[1]);
153
154
        /**
155
         * @var string[]
156
         * @var Closure  $blacklistFilter
157
         */
158
        [$excludedPaths, $blacklistFilter] = self::retrieveBlacklistFilter($raw, $basePath, $tmpOutputPath, $outputPath, $mainScriptPath);
159
160
        $autodiscoverFiles = self::autodiscoverFiles($file, $raw);
161
        $forceFilesAutodiscovery = self::retrieveForceFilesAutodiscovery($raw);
162
163
        $filesAggregate = self::collectFiles(
164
            $raw,
165
            $basePath,
166
            $mainScriptPath,
167
            $blacklistFilter,
168
            $excludedPaths,
169
            $devPackages,
170
            $composerFiles,
171
            $composerJson,
172
            $autodiscoverFiles,
173
            $forceFilesAutodiscovery
174
        );
175
        $binaryFilesAggregate = self::collectBinaryFiles(
176
            $raw,
177
            $basePath,
178
            $mainScriptPath,
179
            $blacklistFilter,
180
            $excludedPaths,
181
            $devPackages
182
        );
183
184
        $dumpAutoload = self::retrieveDumpAutoload($raw, null !== $composerJson[0], $logger);
185
186
        $excludeComposerFiles = self::retrieveExcludeComposerFiles($raw);
187
188
        $compactors = self::retrieveCompactors($raw, $basePath);
189
        $compressionAlgorithm = self::retrieveCompressionAlgorithm($raw);
190
191
        $fileMode = self::retrieveFileMode($raw);
192
193
        $map = self::retrieveMap($raw);
194
        $fileMapper = new MapFile($basePath, $map);
195
196
        $metadata = self::retrieveMetadata($raw);
197
198
        $signingAlgorithm = self::retrieveSigningAlgorithm($raw);
199
        $promptForPrivateKey = self::retrievePromptForPrivateKey($raw, $signingAlgorithm, $logger);
200
        $privateKeyPath = self::retrievePrivateKeyPath($raw, $basePath, $signingAlgorithm, $logger);
201
        $privateKeyPassphrase = self::retrievePrivateKeyPassphrase($raw, $signingAlgorithm, $logger);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $privateKeyPassphrase is correct as self::retrievePrivateKey...ningAlgorithm, $logger) targeting KevinGH\Box\Configuratio...ePrivateKeyPassphrase() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

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